PHP反序列化学习笔记
PHP反序列化
概念
php程序为了保存和转储对象,提供了序列化的方法。php序列化是为了在程序运行的过程中对对象进行转储而产生的。序列化可以将对象转换成字符串,但仅保留对象里的成员变量,不保留函数方法。
php序列化的函数为serialize
,可以将对象中的成员变量转换成字符串。
反序列化的函数为unserilize
,可以将serialize
生成的字符串重新还原为对象中的成员变量。
将用户可控的数据进行了反序列化,就是PHP反序列化漏洞。
序列化的目的是方便数据的传输和存储。
在PHP应用中,序列化和反序列化一般用作缓存,比如session缓存,cookie等。通过序列化与反序列化我们可以很方便的在PHP中进行对象的传递。
前置知识
序列化的字母标识
1 | a - array |
序列化后的字符串格式
每一个序列化后的小段都由;
隔开, 使用{}
表示层级关系
数据类型 | 提示符 | 格式 |
---|---|---|
字符串 | s |
s:长度:”内容” |
已转义字符串 | S |
s:长度:”转义后的内容” |
整数 | i |
i:数值 |
布尔值 | b |
b:1 => true / b:0 => false |
空值 | N |
N; |
数组 | a |
a:大小:{键序列段;值序列段;<重复多次>} |
对象 | O |
O:类型名长度:”类型名称”:成员数:{成员名称序列段;成员值序列段:} |
引用 | R |
R:反序列化变量的序号, 从1开始 |
魔术方法
概念
魔术方法是一种特殊的方法,当对对象执行某些操作时会覆盖 PHP 的默认操作。
常用魔术方法
__construct
构造函数, 在对应对象实例化时自动被调用. 子类中的构造函数不会隐式调用父类的构造函数.
在 PHP 8 以前, 与类名同名的方法可以作为 __constuct
调用但 __construct
方法优先
__sleep()
此方法在对象被序列化时会调用。
如果方法存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。
__wakeup
与__sleep()
相反地,此方法在对象被反序列化时会调用
__toString
此方法在对象转化成字符串时会被调用。
当然, 因为 PHP 是一个弱类型语言, 很多情况对象会被隐式转换成字符串, 比如
==
与字符串比较时会被隐式转换- 字符串操作 (str系列函数), 字符串拼接,
addslashes
- 一些参数需要为字符串的参数:
class_exists
,in_array
(第一个参数), SQL 预编译语句,md5
,sha1
等 print
,echo
函数
简单示例
1 |
|
以上示例会输出:
1 | Hello |
1 | __get() //在读取某些不可访问或者不存在的字段时触发, 传入参数为字段名称 |
执行顺序
不同的情况下会有不同的顺序。
- 一个对象在其生命周期中一定会走过
destruct
, 只有当对象没有被任何变量指向时才会被回收。
2.当使用 new
关键字来创建一个对象时会调用 construct
对于序列化/反序列化时的情况
序列化时会先调用 sleep
再调用 destruct
, 故而完整的调用顺序为: sleep
-> (变量存在)
-> destruct
反序列化时如果有 __wakeup
则会调用 __wakeUp
而不是 __construct
, 故而逻辑为 __wakeUp/__construct
-> (变量存在)
反序列化绕过
绕过__wakeup
(CVE-2016-7124)
1 | PHP5 < 5.6.25 |
利用方式:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup()的执行
对于下面这样一个自定义类
1 | <?php |
如果执行unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');
输出结果为666
而把对象属性个数的值增大执行unserialize('O:4:"test":2:{s:1:"a";s:3:"abc";}');
输出结果为abc
绕过部分正则
preg_match('/^O:\d+/')
匹配序列化字符串是否是对象字符串开头
有些时候我们会看到^O:\d+
这种的正则表达式, 要求开头不能为对象反序列化
这种情况我们有以下绕过手段
- 由于
\d
只判断了是否为数字, 则可以在个数前添加+
号来绕过正则表达式 - 将这个对象嵌套在其他类型的反序列化之中, 例如数组
利用加号绕过
(注意在url里传参时+要编码为%2B)
serialize(array(a)) ;
//a为要反序列化的对象
1 | <?php |
利用引用
<?php
class test{
public $a;
public $b;
public function __construct(){
$this->a = 'abc';
$this->b= &$this->a;
}
public function __destruct(){
if($this->a===$this->b){
echo 666;
}
}
}
$a = serialize(new test());
这个例子将$b设置为$a的引用,可以使$a永远与$b相等(相当于指针)
16进制绕过字符的过滤
1 | O:4:"test":2:{s:4:"%00*%00a";s:3:"abc";s:7:"%00test%00b";s:3:"def";} |
我们可以使用十六进制搭配上已转义字符串来绕过对某些字符的检测,例如:
1 |
|
这里检测了是否包含 flag
字符, 可以尝试使用 flag
的十六进制 \66\6c\61\67
来绕过, 构造以下:
1 | 'O:4:"Read":1:{s:4:"name";S:4:"\66\6c\61\67";}' |
Python 脚本可以将字符串转换为 Hex
1 | str = input('Enter a string: ') |
字符逃逸
情况1:过滤后字符变多
本地的php代码,把反序列化后的一个x替换成为两个
1 |
|
正常情况,传入name=mao
1 | 反序列化字符串: string(38)"a:2:{i:0;s:3:"mao";i:1;s:7:"I am 11";}" |
如果此时多传入一个x,反序列化失败,由于溢出(s本来是4,多了一个字符出来),我们可以利用这一点实现字符串逃逸。
1 | 反序列化字符串: string(39) "a:2:{i:O;s:4:"maox";i:1;s:7:"I am 11";}" |
1 | 反序列化字符串:string(79)"a:2:0i:0;s:43:"maoxxxxxx00x0xxoxxxxx";i:1;s:6:"woaini7";}";i:1;s:7:"l am 11";)" |
传入
1 | name=maoxxxxxxxxxxxxxxxxxxxx";i:1;s:6:"woaini";} |
这一部分一共二十个字符,由于一个x会被替换为两个,一共输入了20个x,现在是40个,多出来的20个x取代了这二十个字符";i:1;s:6:"woaini";}
,从而造成";i:1;s:6:"woaini";}
的溢出,而"
闭合了前串,使字符串成功逃逸,可以被反序列化,输出woaini
。
最后的;}
闭合反序列化全过程导致原来的";i:1;s:7:"I am 11";}"
被舍弃,不影响反序列化过程
情况2:过滤后字符变少
把反序列化后的两个x替换成为一个
1 | <?php |
正常情况传入name=mao&age=11
1 | 反序列化字符串:string(46)"a:2:{s:4:"name";s:3:"mao";s:3:"age";s:2:"11"";)" |
前面少了一半,导致后面的字符覆盖,从而执行了后面的代码;
这部分是age序列化后的结果
s:3:"age";s:28:"11";s:3:"age";s:6:"woaini";}"
由于前面是40个x所以导致少了20个字符,所以需要后面来补上,";s:3:"age";s:28:"11
这一部分刚好20个,后面由于有"
闭合了前面因此后面的参数就可以由我们自定义执行了
利用不完整类绕过序列化
当存在 serialize(unserialize($x)) != $x
时, 可以利用不完整类 __PHP_Incomplete_Class
来进行处理。
当我们尝试反序列化到一个不存在的类时, PHP 会使用 __PHP_Incomplete_Class_Name
这个追加的字段来进行存储。我们于是可以尝试自己构造一个不完整类
1 |
|
绕过。
更进一步, 我们可以通过这个让一个对象被调用后凭空消失, 只需要手动构造无__PHP_Incomplete_Class_Name
的不完整对象
PHP 会先把他的属性给创建好, 但是在创建好最后一个属性后并未发现 __PHP_Incomplete_Class_Name
, 于是会将前面创建的所有的属性回收并引发 __destruct
。
反序列化利用
原生类反序列化利用*
Error
Error类是php的一个内置类,用于自动自定义一个Error,在php7的环境下可能会造成一个xss漏洞,因为它内置有一个 __toString()
的方法,常用于PHP 反序列化中。
如果有个POP链走到一半就走不通了,不如尝试利用这个来做一个xss,也可以直接使用 echo <Object>
的写法,当 PHP 对象被当作一个字符串输出或使用时候(如echo
的时候)会触发__toString
方法,这是一种挖洞的新思路。
类摘要
1 | Error implements Throwable { |
类属性
- message:错误消息内容
- code:错误代码
- file:抛出错误的文件名
- line:抛出错误在该文件中的行数
类方法
Error::__construct
— 初始化 error 对象Error::getMessage
— 获取错误信息Error::getPrevious
— 返回先前的 ThrowableError::getCode
— 获取错误代码Error::getFile
— 获取错误发生时的文件Error::getLine
— 获取错误发生时的行号Error::getTrace
— 获取调用栈(stack trace)Error::getTraceAsString
— 获取字符串形式的调用栈(stack trace)Error::__toString
— error 的字符串表达Error::__clone
— 克隆 error
使用 Error 内置类构造XSS
1 |
|
//一个反序列化函数,但是没有进行反序列化的类,反序列化但没有POP链的情况只能找到PHP内置类来进行反序列化
POC
1 |
|
Exception
类摘要
1 | Exception { |
类属性
- message:异常消息内容
- code:异常代码
- file:抛出异常的文件名
- line:抛出异常在该文件中的行号
类方法
Exception::__construct
— 异常构造函数Exception::getMessage
— 获取异常消息内容Exception::getPrevious
— 返回异常链中的前一个异常Exception::getCode
— 获取异常代码Exception::getFile
— 创建异常时的程序文件名称Exception::getLine
— 获取创建的异常所在文件中的行号Exception::getTrace
— 获取异常追踪信息Exception::getTraceAsString
— 获取字符串类型的异常追踪信息Exception::__toString
— 将异常对象转换为字符串Exception::__clone
— 异常克隆
代码
1 |
|
POC
1 |
|
DirectoryIterator
DirectoryIterator 类提供了一个用于查看文件系统目录内容的简单接口,该类是在 PHP 5 中增加的一个类。DirectoryIterator与glob://协议结合将无视open_basedir对目录的限制,可以用来列举出指定目录下的文件。
代码
1 |
|
输入 /?whoami=glob:///*
可列出根目录下的文件。
只能列根目录和open_basedir指定的目录的文件,不能列出其他目录中的文件,且不能读取文件内容。
SoapClient
php
在安装php-soap
拓展后,可以反序列化原生类SoapClient
,来发送http post
请求。
SoapClient
可以进行 HTTP/HTTPS
的请求, 但是不会输出服务端输出的内容. 不过, 我们仍然可以利用这个来进行内网渗透。
我们通过上面的脚本可以找到 SoapClient
类中存在 SoapClient::__call
, 当我们调用一个不存在的方法时会转发到此方法, 同时请求给服务端
对于 SoapClient
的反序列化, 我们可以控制很多地方的参数,
location
(SoapClientlocation
),这样就可以发送请求到指定服务器uri
(SoapClienturi
), 由于这一串最后会到 Header 里的SOAPAction
, 我们可以在这里注入换行来新建 Header 项, 注意这里的会自动给传入的内容包裹上双引号useragent
(SoapClient_user_agent
), 由于User-Agent
段在Content-Type
的上方, 我们可以通过对useragent
换行来覆盖掉默认的text/xml
的请求类型. 由于默认是 POST 请求, 结合起来我们就可以对指定服务器发送任意 POST 请求。
Phar 反序列化
phar
文件
phar
文件本质上是一种压缩文件,会以序列化的形式存储用户自定义的meta-data
。当受影响的文件操作函数调用phar
文件时,会自动反序列化meta-data
内的内容。
在软件中,PHAR
(PHP
归档)文件是一种打包格式,通过将许多PHP
代码文件和其他资源(例如图像,样式表等)捆绑到一个归档文件中来实现应用程序和库的分发
php通过用户定义和内置的“流包装器”实现复杂的文件处理功能。内置包装器可用于文件系统函数,如fopen()
,copy()
,file_exists()
和filesize()
。 phar://
就是一种内置的流包装器。
常见的流包装器
1 | file:// — 访问本地文件系统,在用文件系统函数时默认就使用该包装器 |
phar文件结构
1 | stub:phar文件的标志,必须以 xxx __HALT_COMPILER(); 结尾,否则无法识别。xxx可以为自定义内容。 |
生成一个phar文件
1 |
|
漏洞利用条件
phar文件要能够上传到服务器端。
要有可用的魔术方法作为“跳板”。
文件操作函数的参数可控,且:
、/
、phar
等特殊字符没有被过滤。
绕过方式
当环境限制了phar
不能出现在前面的字符里。可以使用compress.bzip2://
和compress.zlib://
等绕过
1 | compress.bzip://phar:///test.phar/test.txt |
当环境限制了phar
不能出现在前面的字符里,还可以配合其他协议进行利用。php://filter/read=convert.base64-encode/resource=phar://phar.phar
GIF格式验证可以通过在文件头部添加GIF89a
绕过
1、$phar->setStub(“GIF89a”.“<?php __HALT_COMPILER(); ?>”);
//设置stub
2、生成一个phar.phar
,修改后缀名为phar.gif
session反序列化
session.upload_progress
来进行利用
如果没有特别配置的话, session 通常存储在服务器上的某个文件夹中, 并且文件名通常为 sess_{你的SESSION_ID}
由于存储时时通过反序列化, 所以原本的字符串会被保留。于是我们可以注入 PHP 代码, 再通过文件包含执行他
利用条件:
- 可以进行任意文件包含 (或允许包含 session 存储文件)
- 知道session文件存放路径,可以尝试默认路径
- 具有读取和写入session文件的权限
若服务器存在文件 test.php:
1 |
|
我们可以使用类似条件竞争的方法来进行(python示例)
1 | import io |
如果是反序列化的话, 我们也可以进行反序列化注入
如果我们的文件名可控, 我们在之前放上 |
表示前面的是键名, 后再写入恶意代码。注意引号要进行转义。
解析session文件时直接对’|’后的值进行反序列化处理说明:
当会话自动开始或者通过
session_start()
手动开始的时候,PHP
内部会调用会话管理器的 open 和 read 回调函数。 会话管理器可能是PHP
默认的, 也可能是扩展提供的(SQLite
或者Memcached
扩展), 也可能是通过 session_set_save_handler() 设定的用户自定义会话管理器。 通过 read 回调函数返回的现有会话数据(使用特殊的序列化格式存储),PHP
会自动反序列化数据并且填充$_SESSION
超级全局变量。
*注:此处感谢2月22日凌晨0:48:59以后土豆学长的纠正和指导,向他半夜还在改作业的敬业程度致敬!