PHP反序列化漏洞总结
PHP反序列化漏洞总结
基础知识
序列化和反序列化
序列化
序列化就是将 对象object、字符串string、数组array、变量转换成具有一定格式的字符串,方便保持稳定的格式在文件中传输,以便还原为原来的内容。
1 |
|
serialize() 返回字符串,此字符串包含了表示 value
的字节流,可以存储于任何地方。
example:
1 |
|
这里面O代表对象;4代表对象名长度;Test是对象名;3是对象里面的成员变量的数量;同时注意到类里面的方法并不会序列化。
类型 | 结构 |
---|---|
String | s:size:value; |
Integer | i:value; |
Boolean | b:value;(保存1或0) |
Null | N; |
Array | a:size:{key definition;value definition;(repeated per element)} |
Object | O:strlen(object name):object name:object size:{s:strlen(property name):property name:property definition;(repeated per property)} |
注意:当访问控制修饰符(public、protected、private)不同时,序列化后的结果也不同,当我们做题的时候需要注意这一点,
%00
虽然不会显示,但是提交还是要加上去。public : 被序列化的时候属性名 不会更改
protected : 被序列化的时候属性名 会变成
%00*%00属性名
private : 被序列化的时候属性名 会变成
%00类名%00属性名
反序列化
反序列化就是序列化的逆过程。
1 |
|
unserialize() 对单一的已序列化的变量进行操作,将其转换回 PHP 的值。
example:
1 |
|
魔法方法
反序列化漏洞里面会涉及到一些魔法方法
__consruct()
和__destruct()
1 |
|
PHP 允许开发者在一个类中定义一个方法作为构造函数。具有构造函数的类会在每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作。
1 |
|
析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。
example:
1 |
|
以上例程会先输出__construct()!!!
然后再输出__destruct()!!!
。同时我们要注意到,当我们对一个类对象进行实例化的时候,是不会触发__construct
方法的。
__sleep()
和 __wakeup()
1 |
|
serialize()
函数会检查类中是否存在一个魔术方法 __sleep()
。如果存在,该方法会先被调用,然后才执行序列化操作。与之相反,unserialize()
会检查是否存在一个 __wakeup()
方法。如果存在,则会先调用 __wakeup()
方法,预先准备对象需要的资源。
example:
1 |
|
__toString()
1 |
|
__toString()
方法用于一个类被当成字符串时应怎样回应。例如 echo $obj;
应该显示些什么。此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR
级别的致命错误。
example:
1 |
|
以上例程会输出Hello
__toString()
触发方式比较多:
- echo (
$obj
) / print($obj
) 打印时会触发 - 反序列化对象与字符串连接时
- 反序列化对象参与格式化字符串时
- 反序列化对象与字符串进行比较时(PHP进行比较的时候会转换参数类型)
- 反序列化对象参与格式化SQL语句,绑定参数时
- 反序列化对象在经过php字符串函数,如
strlen()
、addslashes()
时 - 在
in_array()
方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用 - 反序列化的对象作为
class_exists()
的参数的时候
属性重载
1 |
|
在给不可访问属性赋值时,__set()
会被调用。
读取不可访问属性的值时,__get()
会被调用。
当对不可访问属性调用 isset()
或 empty()
时,__isset()
会被调用。
当对不可访问属性调用 unset()
时,__unset()
会被调用。
example:
1 |
|
__call()
1 |
|
在对象中调用一个不可访问方法时,__call()
会被调用。
example:
1 |
|
上述例程会输出Calling object method 'runTest' in object context
__invoke()
1 |
|
当尝试以调用函数的方式调用一个对象时,__invoke()
方法会被自动调用。
example:
1 |
|
上述例程会输出int(5)
反序列化漏洞
反序列化漏洞的成因在于代码中的 unserialize()
接收的参数可控,从上面的例子看,这个函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用对对象属性的篡改实现最终的攻击。
魔法方法的作用
从上面的序列化和反序列化的知识我们可以知道,对象的序列化和反序列化只能是里面的属性,也就是说我们通过篡改反序列化的字符串只能获取或控制其他类的属性,这样一来利用面就很窄,因为属性的值都是已经预先设置好的,如果我们想利用类里面的方法呢?这时候魔法方法就派上用场了,魔法正如上面介绍的,魔法方法的调用是在该类序列化或者反序列化的同时自动完成的,不需要人工干预,这就非常符合我们的想法,因此只要魔法方法中出现了一些我们能利用的函数,我们就能通过反序列化中对其对象属性的操控来实现对这些函数的操控,进而达到我们发动攻击的目的。
魔法方法的简单利用
example:
1 |
|
首先我们能看到unserialize()
函数的参数我们是可以控制的,也就是说我们能通过这个接口反序列化任何类的对象(但只有在当前作用域的类才对我们有用),那我们看一下当前这三个类,我们看到后面两个类反序列化以后对我们没有任何意义,因为我们根本没法调用其中的方法,但是第一个类就不一样了,虽然我们也没有什么代码能实现调用其中的方法的,但是我们发现他有一个魔法函数__destruct()
,这就非常有趣了,因为这个函数能在对象销毁的时候自动调用,不用我们人工的干预,接下来让我们看一下怎么利用。
我们看到__destruct()
里面只用到了一个属性test
,再观察一下哪些地方调用了action()
函数,看看这个函数的调用中有没有存在执行命令或者是其他我们能利用的点的,果然在 Evil
这个类中发现他的 action()
函数调用了eval()
,那我们的想法就很明确了,只需要将demo
这个类中的test
属性篡改为 Evil
这个类的对象,然后为了eval
能执行命令,我们还要篡改Evil
对象的test2
属性,将其改成要执行的命令。
payload:
1 |
|
以上脚本输出:
结果:
这样就完成了一个简单的PHP反序列化漏洞的利用。
通过这个简单的例子总结一下寻找 PHP 反序列化漏洞的方法或者流程:
- 寻找
unserialize()
函数的参数是否有我们的可控点; - 寻找我们的反序列化的目标,重点寻找存在
wakeup()
或destruct()
魔法函数的类; - 一层一层地研究该类在魔法方法中使用的属性和属性调用的方法,看看是否有可控的属性能实现在当前调用的过程中触发的;
- 找到我们要控制的属性了以后我们就将要用到的代码部分复制下来,然后构造序列化,发起攻击。
PHP反序列化POP链
POP链介绍
POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的
说的再具体一点就是 ROP 是通过栈溢出实现控制指令的执行流程,而我们的反序列化是通过控制对象的属性从而实现控制程序的执行流程,进而达成利用本身无害的代码进行有害操作的目的。
POP链demo
1 |
|
寻找POP链过程:
- 首先找到
unserialize()
,发现里面的参数可控; - 接着寻找能够利用的魔方方法,一般是
__wakeup()
或者__destruct()
,这里发现Show类里面有__wakeup()
; __wakeup()
里面使用了preg_match()
函数对传进去的参数进行字符匹配,这里如果我们传进去的参数是对象的时候,就能够触发__toString()
魔法方法;__toString()
方法中试图获取属性$str
中的key为str的值,如果我们传进去的$str['str']
是一个类对象中不可访问的属性时,就能够触发__get()
魔法方法;- 接着寻找有魔法方法
__get()
的类,发现Test类里面有这个魔法方法; - Test类里面的
__get()
方法对参数$p
作为函数名字进行调用,如果这时候的$p
是一个类对象的话,就会触发__invoke()
魔法方法; - 寻找存在魔法方法
__invoke()
的类,发现Read类里面有这个魔法方法; - Read类里面的
__invoke()
方法会读取参数$var
里面的内容,并输出;
1 |
|
输出:
1 |
|
这里进行URL编码的原因是私有和保护属性会有%00
字符,直接输出会显示空格
Phar反序列化
Phar原理
phar的本质是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
Phar demo
根据文件结构我们来自己构建一个phar文件,php内置了一个Phar类来处理相关操作。
要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。
phar.php
1 |
|
访问后,会生成一个phar.phar在当前目录下。
可以明显的看到meta-data是以序列化的形式存储的。
有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://
伪协议解析phar文件时,都会将meta-data进行反序列化,知道创宇测试后受影响的函数列表:
就用比较常用的函数file_get_contents()
函数举例:
1 |
|
上述例程会输出hello
将Phar伪造成其他格式的文件
在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>
这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。
1 |
|
采用这种方法可以绕过很大一部分上传检测。
Phar反序列化漏洞利用
漏洞利用条件
- phar文件要能够上传到服务器端。
- 要有可用的魔术方法作为“跳板”。
- 文件操作函数的参数可控,且
:
、/
、phar
等特殊字符没有被过滤。
Phar简单利用
index.html
1 |
|
upload.php
仅允许格式为gif的文件上传。上传成功的文件会存储到upload_file目录下。
1 |
|
evil.php
1 |
|
绕过思路:GIF格式验证可以通过在文件头部添加GIF89a绕过。
用下面的代码生成phar文件:
1 |
|
生成的phar.phar
修改后缀名phar.gif
,再上传该文件,用phar协议解析:
Session反序列化
PHP的Session机制
在学习 session 反序列化之前,我们需要了解这几个参数的含义。
Directive | 含义 |
---|---|
session.save_handler | session保存形式。默认为files |
session.save_path | session保存路径。 |
session.serialize_handler | session序列化存储所用处理器。默认为php |
session.upload_progress.cleanup | 一旦读取了所有POST数据,立即清除进度信息。默认开启 |
session.upload_progress.enabled | 将上传文件的进度信息存在session中。默认开启。 |
在上述的配置中,session.serialize_handler
是用来设置session的序列话引擎的,除了默认的PHP引擎之外,还存在其他引擎,不同的引擎所对应的session的存储方式不相同。
处理器名称 | 存储格式 |
---|---|
php | 键名 + 竖线 + 经过serialize() 函数序列化处理的值 |
php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize() 函数序列化处理的值 |
php_serialize | 经过serialize()函数序列化处理的数组 |
那么具体而言,在默认配置(php)情况下:
1 |
|
SESSION文件的内容是:name|s:5:"ca01h"
,name是键值,s:5:"ca01h";
是serialize("ca01h")
的结果。
在php_serialize引擎下:
1 |
|
SESSION文件的内容是a:1:{s:4:"name";s:5:"ca01h";}
。a:1
是使用php_serialize进行序列话都会加上。同时使用php_serialize会将session中的key和value都会进行序列化。
在php_binary引擎下:
1 |
|
这里我输出的内容和别人的不一样,求解?
Session反序列化的漏洞原因
PHP中的Session的实现是没有的问题,危害主要是由于程序员的Session使用不当而引起的。
如果在PHP在反序列化存储的$_SESSION
数据时使用的引擎和序列化使用的引擎不一样,会导致数据无法正确第反序列化。通过精心构造的数据包,就可以绕过程序的验证或者是执行一些系统的方法。例如:
1 |
|
上面的 $_SESSION 数据,在存储时使用的序列化处理器为 php_serialize,存储的格式如下:
1 |
|
在读取数据时如果用的反序列化处理器不是 php_serialize,而是 php 的话,那么反序列化后的数据将会变成:
1 |
|
这是因为当使用php引擎的时候,php引擎会以|
作为作为key和value的分隔符,那么就会将a:1:{s:5:"hello";s:20:"
作为SESSION的key,将O:8:"stdClass":0:{}
作为value,然后进行反序列化,最后就会得到stdClass这个类。
实际利用的话一般分为两种:
- session.auto_start=On
当配置选项 session.auto_start=On,会自动注册 Session 会话(相当于执行了session_start()
),因为该过程是发生在脚本代码执行前,所以在脚本中设定的包括序列化处理器在内的 session 相关配选项的设置是不起作用的。因此一些需要在脚本中设置序列化处理器配置的程序会在 session.auto_start=On 时,销毁自动生成的 Session 会话。然后设置需要的序列化处理器,再调用 session_start() 函数注册会话,这时如果脚本中设置的序列化处理器与 php.ini 中设置的不同,就会出现安全问题。
1 |
|
访问http://172.31.171.100/tmp/foo1.php?test=|O:8:%22stdClass%22:0:{}
1 |
|
php.ini配置中session_use_trans_sid = 1才能跨页面访问SESSION
- session.auto_start=Off
当配置选项 session.auto_start=Off,两个脚本注册 Session 会话时使用的序列化处理器不同,就会出现安全问题,如下面的代码:
1 |
|
访问连接:http://172.31.171.100/tmp/foo1.php?test=|O:4:"test":1:{s:2:"hi";s:4:John";}
再访问foo2.php
就会发生反序列化漏洞
session.upload_progress
当 session.upload_progress.enabled
INI 选项开启时,PHP 能够在每一个文件上传时监测上传进度。 这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态
当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name
同名变量时,上传进度可以在$_SESSION
中获得。 当PHP检测到这种POST请求时,它会在$_SESSION
中添加一组数据, 索引是 session.upload_progress.prefix
与session.upload_progress.name
连接在一起的值。并且当文件上传完成的时候,这个session会被立即删除。
也就是说,我们通过构造一个上传文件的表单,将其中一个参数的名字设置为session.upload_progress.name
的值(这个值能在phpinfo看到),PHP检测到这种POST请求的时候就会往$_SESSION
里面填入这个参数的值,从而能够用来设置session。然后通过条件竞争来读取session文件的内容。
example:
1 |
|
这里的session.upload_progress.name
的值为PHP_SESSION_UPLOAD_PROGRESS
,这样就把123写入了session里面。当PHP环境存在session反序列化漏洞,但是又没有直接控制session值的方法时,可以利用这个方法。
原生类反序列化利用
SoapClient
SOAP是webService三要素(SOAP、WSDL(WebServicesDescriptionLanguage)、UDDI(UniversalDescriptionDiscovery andIntegration))之一:WSDL 用来描述如何访问具体的接口, UDDI用来管理,分发,查询webService ,SOAP(简单对象访问协议)是连接或Web服务或客户端和Web服务之间的接口。
其采用HTTP作为底层通讯协议,XML作为数据传送的格式。
php中的SoapClient
类可以创建soap数据报文,与wsdl接口进行交互。
1 |
|
第一个参数用来指明是否是wsdl模式。
第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location
和uri
选项,其中location
是要将请求发送到的SOAP服务器的URL,而uri
是SOAP服务的目标命名空间。
其中$options
数组下有个user_agent
选项,我们可以利用该选项来自定义User-Agent
。而在HTTP协议中,HTTP Header与HTTP Body是用两个CRLF分隔的,浏览器就是根据这两个CRLF来取出HTTP 内容并显示出来。所以,一旦我们能够控制HTTP 消息头中的字符,注入一些恶意的换行,这样我们就能注入一些会话Cookie或者HTML代码。
还有一点就是SoapClient
类的__call
魔法方法,当调用这个方法时能够对内网进行访问,构成SSRF攻击。
POC:
1 |
|
Error/Exception
Error类就是php的一个内置类用于自动自定义一个Error
,在php7的环境下可能会造成一个xss
漏洞,因为它内置有一个toString
的方法。
Exception类跟Error类原理一样,但是也适用于PHP5
example:
1 |
|
POC
1 |
|
得到编码后的反序列化结果:
1 |
|
效果:
反序列化字符逃逸
PHP在反序列化时,底层代码是以;
作为字段的分隔,以}
作为结尾(字符串除外),并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化 。
字符逃逸的本质其实也是闭合,但是它分为两种情况,一是字符变多,二是字符变少。
字符增多
字符增多就是后端对我们输入的序列化后的字符进行替换称为长度更长的字符
example:
1 |
|
这里通过filter()
函数对我们输入的内容进行检查,将字符p
替换成ww
,再进行反序列化。
正常情况下,我们输入的内容没有字符p
的时候并不会出现问题:
当输入的内容存在p
字符的时候,由于过滤之后的字符数变多了,并不复合序列化的规则,所以进行反序列化的时候会报错。
如果我们年龄修改为其他,比如18,那么可以通过构造username的值来使得age的值改变
- 首先是构造age值得序列化后的字符
";i:1;s:2:"18";}
,前面的"
是为了闭合前一个元素username的值,最后的}
是为了闭合这一个数组,抛弃后面的内容。 - 然后数上面构造的这一串有多少个字符,这里有16个字符,因此需要通过
filter()
函数之后变多16个字符,使得我们构造的这一部分内容能够逃出username的范围,称为独立的一个元素。由于这里一个字符p
会变成2个w
字符,因此每一个p
就会多出一个字符,所以这里需要16个字符p
。
payload:
1 |
|
字符减少
字符减少就是后端对我们输入的序列化后的字符进行替换称为长度更短的字符
example:
1 |
|
还是上面的例子,其中这里的$age
可控,但是是将输入的字符串中的xx
替换为s
,如果我们这里想插入一个新的参数,比如想在第二个参数插入hello
,那么我能可以尝试让前一个参数进行字符减少,把后面的参数的key作为前一个参数的值吞掉,把后一个参数的value作为新的键值对变成新的变量
绕过
利用 16 进制绕过过滤
将示意字符串的s
改为大写S
时,其值会解析 16 进制数据
例如:O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}
可改为:O:4:"Test":1:{S:3:"\63md";S:6:"\77hoami";}
example:
1 |
|
当传入O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}
时,可以发现无法绕过过滤函数
修改为大写S
时,可以看到成功