前言
本文主要总结php反序列化漏洞。为了总结全面一点,文中部分内容直接或间接引用于其他大牛的文章中,我都会标明出处,如有侵权,请发邮箱联系我删除。多有不合理之处,望大佬指点。
反序列化漏洞利用原理
在反序列化时,由于代码中出现了危险函数,导致我们可以通过调用方法来利用危险函数进行恶意攻击,造成代码执行,getshell等一系列不可控的后果。
反序列化
PHP序列化和反序列化主要是通过serialize
、unserialize
两个函数来分别实现,序列化就是将对象转换为可保存或传输的字符串的格式,反序列化就是把字符串格式恢复回原本的对象。在PHP中,序列化和反序列化一般用做缓存,比如session缓存,cookie等。
以下是一个php序列化的实例:
1 | <?php |
运行结果如下:
1 | //序列化第一位的含义: |
访问控制
在面对对象编程的思想中,为了保证安全性,我们是不想让他人可以任意修改我们的类中的属性和方法的。这时就用到了访问控制。
访问控制的定义:
PHP 对属性或方法的访问控制,是通过在前面添加关键字 public(公有),protected(受保护)或 private(私有)来实现的。
- public(公有):公有的类成员可以在任何地方被访问。
- protected(受保护):受保护的类成员则可以被其自身以及其子类和父类访问。
- private(私有):私有的类成员则只能被其定义所在的类访问。
这样,我们就可以限制其他用户,使其在类外无法修改类内的属性或方法。
protected
示例:
1
2//类sofun中的proteced file属性序列化结果
O:5:"sofun":1:{s:7:"\0*\0file";s:8:"flag.php";}特别要注意的是,
\0
只是用来代替ASCII码值为0的字符,该字符是不可见的,想传入它比较麻烦,所以我们一般使用它在url编码中的结果,即%00*%00
,如下:1
2{s:7:"%00*%00file";s:8:"flag.php";}
//注意%00和\00仍然要算长度private同理。
如果必须要用
\0
的话,可以用python来传参。private
示例:
1
2//类sofun中的private file属性
O:5:"sofun":1:{s:15:"\0sofun\0file";s:8:"flag.php";}
魔术方法
如果使用访问控制把类中的属性和方法都一个个限制了,那我们建这个类如何来让别人使用呢?
在php中,有一种叫做”魔术方法“的东西,这些方法在特定的情况下会自动调用,不需要手动从外面调用。我们就可以利用魔术方法和访问控制来保证类的安全。
而在反序列化漏洞中,这些魔术方法也是我们利用的主要突破口。
下面是比较典型的PHP反序列化漏洞中可能会用到的魔法函数:
1 | __destruct(类执行完毕以后调用,其最主要的作用是拿来做垃圾回收机制。) |
反序列化漏洞
这里有一个最简单的实例:
1 |
|
构造:
1 | test=O:1:"A":1:{s:4:"test";s:28:"<img src=1 onerror=alert(1)>";} |
这样就利用__destruct()
造成了XSS。当然结合实际环境可以造成更大的危害。
利用点
__wake up的绕过
当需要跳过对
__wakeup
的执行时,可以使序列化字符串表示对象属性个数的值大于真实个数的属性,从而跳过。例如下面这道题:
由代码可以看出,flag在
flag1.php
里,下面的if else
里如果没有参数tryhackme就会打印本页源码,有的话则打印指定文件内容, 所以我们要利用_wakeup()
绕过把file改成flag1.php
。百度得知绕过的方法:_wakeup()函数漏洞原理:当序列化字符串表示对象属性个数的值大于真实个数的属性时就会跳过__wakeup的执行。
所以我们这么写:
O:5:”SoFun”:2:{}
只有一个对象,但是依然写2,就可以绕过了。
__wakeup() 或__destruct()的利用
在
unserialize()
中会直接调用wakeup()
或destruct()
,中间无需其他过程。因此最理想的情况就是一些漏洞/危害代码直接在wakeup()
或destruct()
中,从而当我们控制序列化字符串时就可以去直接触发它们。假设index源码如下:
1 |
|
基本的思路是,通过 serialize()
得到我们要的序列化字符串,之后再传进去。通过源代码知,把对象中的test值赋为 “<?php phpinfo();?>”,再调用unserialize()
时会通过__wakeup()
把test的写入到shell.php
中。为此我们写个php脚本:
1 | <?php |
结果如下:
O:1:”a”:1:{s:4:”test”;s:18:”<?php phpinfo();?>”;}
当我们传入的时候便触发了_wakeup
,将一句话木马传入了shell.php
中。
__construct()
有时候反序列化一个对象时,由它调用的
__wakeup()
中又去调用了其他的对象,由此我们可以一步步倒推,从尾至头找到漏洞点。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<?php
class CyzCc{
function __construct($test){
$fp = fopen("shell.php","w") ;
fwrite($fp,$test);
fclose($fp);
}
}
class Wc{
var $test = '123';
function __wakeup(){
$obj = new CyzCc($this->test);
}
}
$class5 = $_GET['test'];
print_r($class5);
echo "</br>";
$class5_unser = unserialize($class5);
require "shell.php";
?>我们给test传入构造好的序列化字符串后,进行反序列化时自动调用
_wakeup()
函数,从而在new CyzCc
会自动调用对象CyzCc
中的construct()
方法,从而将phpinfo()
写入到shell.php
中。O:2:”Wc”:1:{s:4:”test”;s:18:”;}
- 同名方法利用
1 | //demo1.php |
这个例子中,class B和class C有一个同名方法action,我们可以构造目标对象,使得析构函数调用class C的action方法,实现任意代码执行。
构造代码:
1 |
|
运行结果:
1 | O:1:"A":1:{s:6:"target";0:1:"C":1{s:4:"test";s:10:"phpinfo();";}} |
exp:
1 | localhost/demo1.php?test=O:1:"A":1:{s:6:"target";0:1:"C":1{s:4:"test";s:10:"phpinfo();";}} |
pop链
pop链,即把魔术方法作为最开始的小组件,然后在魔术方法中调用其他函数,通过寻找相同名字的函数,再与类中的敏感函数和属性相关联,就是POP CHAIN 。此时类中所有的敏感属性都属于可控的。当unserialize()传入的参数可控,便可以通过反序列化漏洞控制POP CHAIN达到利用特定漏洞的效果。
通俗点就是:当敏感函数不在魔术方法中,而是在一个类的普通方法中时,通过以可触发的魔术方法为起点,寻找同名函数或触发调用其他魔术方法,一步步传递,最终传递参数执行敏感函数。
举个简单的例子:
1 |
|
分析这个上述代码:
先找魔术方法__destruct()
,lemon
这个类本来是调用normal
类的,但是现在action
方法在evil
类里面也有,所以可以构造pop链,调用evil
类中的action
方法。
1 |
|
值得注意的是,protected $ClassObj = new evil();
是不行的,还是需要通过__construct
来实例化。
这类反序列化漏洞的利用关键就是找到从可调用的魔术方法到敏感函数的链条,其思路与python继承链进行模板注入很相似。
解题思路
通过例题来理解:
1 |
|
一般我们先大概看一遍代码,找到可以造成漏洞的敏感函数。在这里很明显就是Test2中的Delete方法。我们想要调用Delete,就要靠Test2中的__tostring
了。我们再利用__construct
来给Test2实例化一个对象。至此我们就找到了完整的pop链了,大概这样:
test2.delete—>test1.__tostring()
—>test1.__construct()
new test2
POC:
1 |
|
[MRCTF2020]Ezpop
1 |
|
不是很长,一个一个来分析吧。
1 | class Modifier { |
危险函数include在这里出现,可以通过value访问flag.php
。这里有__invoke
可以调用,要想办法把对象调用为函数。
1 | class Show{ |
这里过滤了一堆,上面有__tostring
,我们可以把$source
赋一个类的值,这样就可以调用__tostring
了。
1 | class Test{ |
这里有__construct
和__get
,__get
我们可以访问一个不存在的成员变量来调用。
大致的思路是:__wakeup
自动调用,可以令source=new Show(),就可以调用__tostring
了。__tostring
又指向str里的source,可以让str=new Test(),这样Test里是没有source属性的,就可以调用__get
。再令p=new Modifier,用函数的方法访问类来调用__invoke
,最后利用var来获取flag.php
中的内容。
POC:
1 |
|
[网鼎杯 2020 青龙组]AreUSerialz
题目:
1 |
|
找到的敏感函数是file_get_contents,在read方法里,可以利用来读取flag.php,然后寻找调用read的方法,process中有,条件是要让op为2,process可以被析构函数执行,但前面会把op赋值为1。这里的方法是利用弱比对,我们把op赋值为数字2,这样op==2返回true而op===2会返回false。以及后面is_valid函数会对传入的字符串进行ascii码范围的检查,32-125刚好是所有可见字符的范围,而上面我们说过,protect属性序列化会有\00
,这是不可见字符,在这里会被return false,这里我们利用PHP7.1以上版本对属性类型不敏感的特性,用public来绕过。
1 |
|
得到base64编码的flag。
字符逃逸
预备知识
- php在反序列化时,对类中不存在的属性也进行反序列化
- php在反序列化时,底层代码是以;作为字段的分隔,以}作为结尾,并且根据长度判断内容
- 长度不对应时会报错
- 不符合序列化规则的代码部分不会被反序列化成功
e.g.
1 | a:2:{i:0;s:7:"purplet";i:1;s:5:"aaaaa";}needless; \\needless就不会被反序列化到 |
原理
本质上就是自己通过构造payload满足了序列化规则,利用前面字符由于str_replace()而字符增多或减少,提前闭合,吞掉了后面的字符。
字符增加的逃逸
1 |
|
假如我们想要把age修改为19,那么我们需要构造的语句是“;i:1;s:2:“19”;},语句的长度为16个字符,当filter()函数执行的时候,会将一个a替换为三个b,这样就造成了字符增多的情况。如果我们构造8个c,那么会产生24个b,减去构造的8个c,就可以让我们逃逸16个字符,也就是我们构造的语句。
payload:
1 |
|
结果:
1 | string(55) "a:2:{i:0;s:24:"cccccccc";i:1;s:2:"19";}";i:1;s:2:"10";}" |
字符减少的逃逸
原理和字符增加的相同,假设有三个参数中有两个参数可控,通过一个参数的长度变短和另一个参数的调节来修改第三个参数的值 。
Session反序列化漏洞
PHP中的Session是在经序列化后存储,读取时再进行反序列化的。
相关配置:
配置 | 作用 |
---|---|
session.save_handler=”” | 设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数。(数据库等方式) |
session.save_path=”” | 设置session的存储路径。 |
session.auto_start boolen | 指定会话模块是否在请求开始时启动一个会话。(默认为0不启动) |
session.serialize_handler string | 定义用来序列化/反序列化的处理器名字。(默认使用php) |
PHP中有三种序列化处理器,如下表所示:
然后session序列化后需要储存在服务器上,默认的方式是储存在文件中,储存路径在session.save_path
中,如果没有规定储存路径,那么默认会在储存在/tmp
中,文件的名称是’sess_’+session名,文件中储存的是序列化后的session。
示例代码:
该内容存储后即为序列化的session:test|s:4:"test";
不同处理器的格式不同,当不同页面使用了不同的处理器时,由于处理的Session序列化格式不同,就可能产生反序列化漏洞。
下面演示漏洞利用:
该页面有类demo3
,我们开启session,并用php处理器来处理session。
1 |
|
通过session.php设置session,通过generate.php构造实例。
由于session.php与demo3.php采用的序列化处理器不同,我们可以构造“误导”处理器,达到漏洞利用的目的。
1 |
|
访问得到实例的序列化:
1 | O:5:"demo3":1:{s:4:"test";s:3:"w2t3rp2dd13r";} |
要在serialize()
的结果前加|
,当使用php处理器时,就会把|
后的内容给反序列化,从而调用demo3.php
中的__wakeup()
方法和__destruct()
方法。
payload:
1 | |O:5:"demo3":1:{s:4:"test";s:3:"w2t3rp2dd13r";} |
访问demo3.php
成功创建了一个类demo3
的实例。
phar实现php反序列化
phar(PHp ARchive)的解释是:like a Java JAR,but for PHP. 所以它本身其实是一个打包文件,是伪协议中的一种,和反序列化好像没有什么联系。
然而在2018年8月,安全研究员 Sam Thomas 在 BlackHat2018
大会 上分享了议题:It’s a PHP unserialization vulnerability Jim, but not as we know it.
其利用phar伪协议会将用户自定义的meta-data序列化的形式存储这一特性,扩展了php反序列化的攻击面。
因此,我们是可以利用phar来实现php的反序列化的。用户自定义的meta-data会以序列化形式存储在phar文件中的manifest部分;当使用phar://协议来解析phar文件时,便会对phar文件中的manifest部分进行反序列化。
攻击原理
phar结构
phar由四个部分组成。
1 | 1.stub |
phar文件生成
我们来自己构建一个phar
文件,php
内置了一个Phar
类。
注意:需要将php.ini
中的phar.readonly
设置成off。
1 | #eval.class.php |
1 | # phar_gen.php |
然后我们用二进制编辑器打开生成文件,如下图:
可以看到其中的一段序列化文本,就是meta-data
序列化的结果。
phar://
反序列化
那么,如何利用phar://
进行反序列化呢?
使用phar://
伪协议读取文件的时候,文件会被解析成phar对象,这个时候,刚才那部分的序列化的信息就会被反序列化。
1 | #test.php |
访问test.php, http://127.0.0.1/test.php?url=phar://vul.phar
,得下图:
漏洞的简单利用
在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,即__HALT_COMPILER();?>
这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。采用这种方法可以绕过很大一部分上传检测。
下面这道题目就利用了这个漏洞:
我们写一个php来建一个phar。
1 |
|
php文件系统大部分函数再通过phar://解析时,会对meta-data反序列化,从而达到我们的目的。
访问该php得到了flag.php。
进linux对phar进行cat发现是乱码,原来是因为phar是一个data文件。其原因是data会被base64解密一次,只要再data加密就可以cat了。
我们再对其进行urlencode,防止加号被吃,然后就可以上传了。
我们得到了txt文件,问了大佬才知道只要是phar://协议解析,后缀是什么都无所谓,我之前甚至都没有想过这个问题。。。
然后我们就拿到flag啦。
我们得到了txt文件,因为这里的后缀.txt
是不会影响反序列化操作的,我们就成功拿到flag啦。
后记
phar的知识现在已经有很多了,也很值得去深挖,自己还只是会了一点表面,需要更加深入的研究。