前言
PHP反序列化漏洞也叫PHP对象注入,是一个非常常见的漏洞,这种类型的漏洞虽然有些难以利用,但一旦利用成功就会造成非常危险的后果。漏洞的形成的根本原因是程序没有对用户输入的反序列化字符串进行检测,导致反序列化过程可以被恶意控制,进而造成代码执行、getshell等一系列不可控的后果。
预备知识点
序列化与反序列化
基本概念
序列化:将对象转换为字节序列的过程
反序列化:把字节序列还原成对象的过程
基本函数
序列化函数serialize()
serialize()函数会检查类中是否存在一个魔术方法__sleep()。如果存在,__sleep()方法会先被调用,然后才执行序列化操作。如果没有则默认序列化所有属性。
1 |
|
反序列化函数unserialize()
unserialize()函数会检查类中是否存在一个__wakeup()魔术方法。如果存在,__wakeup()方法会先被调用。然后才执行反序列化操作。如果没有则默认序列化所有属性。
1 |
|
访问控制修饰符
1 | public(公有的):类中的成员将没有访问限制,所有的外部成员都可以访问(读和写)这个类成员(包括成员属性和成员方法)。如果类的成员没有指定成员访问修饰符,将被视为public。 |
类内部 | 继承关系类内部 | 类外部 | |
---|---|---|---|
public | yes | yes | yes |
protected | yes | yes | no |
private | yes | no | no |
魔术方法
1 | __construct() //当对象创建(new)时会自动调用。但在unserialize()时是不会自动调用的。 |
漏洞原理
在反序列化时,传入参数可控,程序没有对用户输入的反序列化字符串进行检测,且魔术方法中存在危害函数,导致反序列化过程可以被恶意控制,进而造成代码执行,getshell等一系列不可控的后果。
POP链
概念
通过用户可控的反序列化,以可触发的魔术方法为出发点,魔术方法中的函数在其他类中存在同名函数,或者通过传递、关联等可以调用的其他执行敏感操作的函数,然后传递参数执行敏感操作。
利用过程
- 寻找可控反序列化点
- 寻找类中可利用的危险函数
- 寻找可以相互出发从而调用的魔术方法
- 形成一条连续的调用链
例题解析
第一题
源码
1 |
|
源码分析
观察源码时一般来说首先看最下面的代码,操作是反序列化get到的参数data,然后执行echo $user_data。
总体浏览下来我们肯定是要利用test2中的delete方法,那么就需要通过test1中__tostring()魔术方法,并且利用test1中的__construct()魔术方法将$obj变量赋值为test2类的实例化对象。
所以我们的逻辑链就是
test2.delete—>test1.__tostring()—>test1.__construct()new test2
利用POC
1 |
|
第二题 [MRCTF2020]Ezpop
源码
1 | Welcome to index.php |
源码分析
由于flag在flag.php中,所以我们需要利用Modifier类中的append方法包含flag.php。而调用append方法又需要__invoke()魔术方法,它的触发条件是尝试以调用函数的方式调用一个对象,这说的是Test类中的__get()魔术方法,然后这个的触发条件是访问不存在的成员变量,说明是需要使用到show里面的tostring方法(打得我有点晕了),根据tostring触发条件可知我们需要利用show的wakeup函数。
把上面说的整理成一条逻辑链吧
Modifier.__invoke()—>Test.__get()—>Show.__toString()—>Show.__wakeup()
我们的想实现的就是创建一个show对象a,在被序列化的时候会调用wakeup函数,如果a的source属性是一个对象且为show类,那么就会调用show类的方法__tostring(),查看a
->source->str->source属性,如果source属性不存在的话,就会调用__get方法,所以我们可以将a
->source->str设置为test类,因为test类中不存在source属性,所以就会接下来调用__get方法,这是我们将其的属性p设置为一个类Modifier就会触发__invoke魔术方法,调用append方法包含这个$var。
利用POC
1 |
|
第三题 [网鼎杯 2020 青龙组]AreUSerialz
源码
1 |
|
源码分析
东西很多,一个一个来分析。
首先是这个is_valid函数要求我们输入的东西的ascii码在32到125之间。
接着研究一下FileHandler类,顺着慢慢看下去。constuct函数我们先不管,因为我们不需要新建一个对象。顺着执行的顺序我们会来到__destruct()函数。
__destruct()函数执行逻辑是先判断op的值是否强等于“2”,如果是的话就将op的值赋为“1“。不是就跳过。然后将content的值赋为空。然后跳到process()函数。
process()函数需要检测op的值是否弱等于“2”,如果就将read()函数的返回值赋给res,最后输出它。
read()函数检测是否存在filename,将filename的内容赋给res,然后返回。
利用POC
1 |
|
字符逃逸
预备知识点
php在反序列化时,对类中不存在的属性也进行反序列化
php在反序列化时,底层代码是以;作为字段的分隔,以}作为结尾,并且根据长度判断内容
长度不对应时会报错
不符合序列化规则的代码部分不会被反序列化成功
eg:
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个a,那么会产生24个b,减去构造的8个a,就可以让我们逃逸16个字符,也就是我们构造的语句。
这里面利用了预备知识点的第四点,后面的语句会被忽略掉
payload
1 |
|
结果
1 | string(55) "a:2:{i:0;s:24:"cccccccc";i:1;s:2:"19";}";i:1;s:2:"10";}" |
字符减少的逃逸
源码
1 |
|
分析
filter的规则是两个p会变成一个w,如果我们上传pp,序列化后结果为s:2:”w”,这边就会逃逸一个字符,不过跟字符增多的不同,我们需要在下一个变量中进行修改。首先我们需要构造;i:1;s:2:”19”;},不过要注意的是前面有个双引号需要我们闭合,里面随便闭合一些东西就可以。所以我们最终的payload是x”;i:1;s:2:”19”;}。
这边计算起来较为复杂,所以套用网上给的教程分为3步走
利用Demo中的代码将age的值修改为想要修改的数值,得到age处序列化的值为;i:1;s:2:”19”;},那么把这段数值再次传入Demo代码的age处(该值即为最终的逃逸代码),而此时username传递的p的数值无法确定,先可随意构造,查看结果
1
2
3string(32) "a:2:{i:0;s:2:"pp";i:1;s:2:"19";}"
string(31) "a:2:{i:0;s:2:"W";i:1;s:2:"19";}"
bool(false)age处传递一个任意数值和双引号进行闭合,即:再次传入age = A”;i:1;s:2:”20”;},查看结果
1
2string(48) "a:2:{i:0;s:2:"pp";i:1;s:17:"A";i:1;s:2:"19";}";}"
string(47) "a:2:{i:0;s:2:"W ";i:1;s:17:"A ";i:1;s:2:"19";}";}"空格中的字符串是我们不需要的东西,一共有13个字符,需要把它们逃逸掉。所以我们需要构造13*2的p来逃逸
payload
1 |
|
结果
1 | string(73) "a:2:{i:0;s:26:"pppppppppppppppppppppppppp";i:1;s:17:"A";i:1;s:2:"19";}";}" |
例题解析
第一题 [安洵杯 2019]easy_serialize_php
源码
1 |
|
源码分析
这边省去其他的一些步骤,咱们能做的就是利用extract()函数post上数据,利用字符逃逸将我们需要的对象反序列化上去,
还是按照之前总结的来走。首先我们需要构造的东西是s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
,这边我们考虑post上数组的键为flagphp,这样按照过滤的原则会逃逸出7个字符。咱们在自己的vscode反序列化先实验一下。反序列化后结果为s:7:"";s:39:"s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
,显然还缺点东西,我们加上;s:1:"a";
再尝试一下,成功得到说$flag = ‘flag in /d0g3_fllllllag’;。这边base64编码一下跟前面的那个php一毛一样,一样的步骤获得flag。
phar反序列化
预备知识点
phar文件
文件结构
stub
可以理解为一个标志,格式为xxx,前面内容不限(可以伪造文件头啊啥的),但必须以
__HALT_COMPILER();?>
来结尾,否则phar扩展将无法识别这个文件为phar文件。manifest
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data。
content
被压缩文件的内容
signature(可空)
签名,在文件末尾
实例
1 |
|
文件系统函数
php一大部分的文件系统函数在通过phar://
伪协议解析phar文件时,都会将meta-data进行反序列化。
漏洞原理
phar文件会以序列化的形式存储用户自定义的meta-data,在一些文件操作函数执行的参数可控的情况下,参数部分我们利用Phar伪协议,可以不依赖unserialize()直接进行反序列化操作,在读取phar文件的数据时反序列化meta-data,达成我们的操作目的。
漏洞利用
利用条件
- phar文件要能够上传到服务器端。
- 要有可用的魔术方法作为“跳板”。
- 文件操作函数的参数可控,且
:
、/
、phar
等特殊字符没有被过滤。
绕过waf
php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>
这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。
Session反序列化漏洞
预备知识点
PHP处理器不同的序列化方式
处理器 | 对应的存储格式 |
---|---|
php_binary | 键名的长度对应的ASCII字符+键名+经过serialize() 函数反序列处理的值 |
php | 键名+竖线+经过serialize()函数反序列处理的值 |
php_serialize | serialize()函数反序列处理数组方式 |
与session有关的php.ini配置项
1 | session.save_path="" --设置session的存储路径 默认在/tmp |
漏洞原理
不同页面选择的php处理器不同,序列化和反序列化的方式也不同,可以根据不同处理器处理数据的特性构造危险代码。
举个例子
存储session页面
1 | /*session.php*/ |
可利用页面
1 | /*test.php*/ |
利用语句
session.php?smallwolf=|O:9:”smallwolf”:1:{s:1:”a”;s:17:”“;}”;}
此时/tmp目录生成的session文件内容:
1 | a:1:{s:9:"smallwolf";s:54:"|O:9:"smallwolf":1:{s:1:"a";s:17:" phpinfo()";}";} |
然后再访问test.php时反序列化已存储的session,新的php处理方式会把“|”后的值当作KEY值再serialize(),相当于我们实例化了这个页面的hpdoger类,那么就会触发__destruct()函数。
绕过姿势
绕过__wakeup()魔术方法(CVE-2016-7124)
影响版本
PHP5 < 5.6.25
PHP7 < 7.0.10
利用方法
序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
绕过正则表达式
利用方法
根据正则表达式的实际语句进行修改(通常是在类名长度前加上一个+),使其无法匹配到我们构造的语句
举个例子
1 | /[oc]:\d+:/i 如果我们输入O:4则会被匹配到,而如果我们输入O:+4则不会被匹配到 |
绕过关键词过滤
利用原理
反序列化中为了避免信息丢失,使用大写的S支持字符串的编码。
利用方法
用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示
举个例子
如果user是黑名单中的关键词,我们构造s:4:”user”;会被waf掉,于是我们可以构造S:4:”use\72”;来绕过
绕过变量过滤
利用原理
在 php中如果我们使用 & 对变量A的值指向变量B,这个时候是属于浅拷贝,当变量B改变时,变量A也会跟着改变。
适用场景
在被反序列化的对象的某些变量被过滤了(比如在php 7.0.10以上的版本中无法绕过__wakeup()魔术方法时),但是其他变量可控的情况下,就可以利用浅拷贝来绕过过滤。
绕过is_valid()检测
利用原理
php版本7.1+,对属性的类型不敏感。
利用方法
由于is_valid()函数会检测传入的参数ascii码是否在32到125之间,而protected和private属性经过序列化都存在不可打印字符在32到125之外,利用php版本7.1+对类的属性类型不敏感的特点,我们可以将protected类型更改为public以消除不可打印字符