在 Web 安全的语境下,反序列化漏洞的本质是 “越权的数据控制了代码的执行流” 。
开发者原本的意图只是单纯地保存和恢复一个对象的数据(属性),但由于 PHP 引擎在销毁对象或处理特定上下文时会自动调用一系列“魔术方法”,攻击者就可以通过精心构造序列化字符串中的属性值,人为地制造出一条方法调用的链条。这个链条就是 POP(Property-Oriented Programming)链。
构造 POP 链,实际上就是一个“找积木”和“搭桥”的过程。我们需要找到一个必定会执行的入口(起点),一个包含高危函数的出口(终点),以及连接它们的无数个跳板。
1 序列化与反序列化
在php中,序列化与反序列化的函数分别是serialize和unserialize
- 序列化 (
serialize):将复杂的数据结构(如对象、数组)转化为便于存储或传输的连续字符串格式。 - 反序列化 (
unserialize):将上述字符串还原为原始的PHP变量或对象
1.1 类与对象
1.1.1 类的结构
class Class_name{
//成员变量声明
//成员函数声明
}
1.1.2 类的内容
class hero{
var $name;
var $sex;
function jineng($var1){
echo $this->name;
echo $var1;
echo "程咬金释放了技能".$var1;
}
}
1.1.3 实例化
$cyj = new hexo();
$cyj->name="程咬金";
$cyj->sex="男";
$cyj->jineng('跳跳');
1.1.4 类的修饰符介绍
PHP 对属性或方法的访问控制,是通过在前面添加关键字 public(公有),protected(受保护)或 private(私有)来实现的。
public(公有):公有的类成员可以在任何地方被访问。
protected(受保护):受保护的类成员则可以被其自身以及其子类和父类访问。
private(私有):私有的类成员则只能被其定义所在的类访问。
注意:不同修饰符序列化后的值不一样
访问控制修饰符的不同,序列化后属性的长度和属性值会有所不同,如下所示:
public:属性被序列化的时候属性值会变成属性名
protected:属性被序列化的时候属性值会变成\x00*\x00属性名
private:属性被序列化的时候属性值会变成\x00类名\x00属性名
其中:\x00表示空字符,但是还是占用一个字符位置
1.1.4.1 实例
class hero{
pubilc $name='chengyaojin';
private $sex='man';
protected Sshengao='165';
function jineng($var1){
echo $this->name;
echo $var1;
}
}
class hero2 extends hexo{
fuction test(){
echo $this->name;
echo $this->sex;
echo $this->shengao;
}
}
$cyj=new hero();
$cyj2=new hero2();
echo $cyj->name;
echo $cyj2->test();
1.2 序列化基础知识
1.2.1 作用
将对象的状态信息转换为可以存储或传输的形式的过程
1.2.2 表达格式
实例化数据:
O:1:"S":1:{s:4:"test";s:7:"pikachu";}
O:代表object
1:代表对象名字长度为一个字符
S:对象的名称
1:代表对象里面有一个变量
s:数据类型
4:变量名称的长度
test:变量名称
s:数据类型
7:变量值的长度
pikachu:变量值
所有格式:
- 空字符
null→N; - 整形
666→i:666; - 浮点型
66.6→d:66.6; - 布尔型
true→b:1;/false→b:0; - 字符串型
'benben'→s:6:"benben"; - 私有属性 在变量名两边加上
%00
1.3 反序列化基础知识
1.3.1 特性
- 反序列化之后内容为一个对象
- 反序列化生成的对象里的值,由反序列化里的值提供;与原有类预定义的值无关
- 反序列化不触发类的成员方法;需要调用方法后才能触发
2 魔术方法
| 方法名 | 描述 |
|---|---|
| __construct() | 类的构造函数 |
| __destruct() | 类的析构函数 |
| __call(arguments) | 当调用一个未定义或不可达方法时, __call () 方法将被调用。 |
| __callStatic(arguments) | 当调用一个未定义或不可达的静态方法时, __callStatic () 方法将被调用 |
| __get($propertyName) | 当获取一个类的成员变量时, __get () 方法将被调用。 |
| __set(value) | 当赋值一个类的成员变量时, __set () 方法将被调用。 |
| __isset($content) | 当调用 isset () 或 empty () 对一个未定义或不可达的成员赋值时, __isset () 方法将被调用。 |
| __unset($content) | 当调用 reset () 对一个未定义或不可达的成员更新时, __unset () 方法将被调用。 |
| __sleep() | 当执行序列化 serialize () 时,__sleep () 方法将首先被调用。 |
| __wakeup() | 当执行反序列化 deserialization () 时, __wakeup () 方法将首先被调用。 |
| __toString() | 当使用 echo 方法直接输出显示对象时,__toString () 方法首先被调用。 |
| __invoke() | 使用调用函数(function)访问一个对象时, __invoke () 方法将首先被调用 |
| __set_state($an_array) | 当调用 var_export () 方法时,__set_state () 方法将被调用。 |
| __clone() | 当对象被复制赋值时,__clone () 方法将被调用。 |
| __autoload($className) | 试图载入一个未定义的类时调用。 |
| __debugInfo() | 输出 debug 信息。 |
从序列化到反序列化这几个函数的执行过程是: __construct() →__sleep() → __wakeup() → __toString() → __destruct() |
2.1 方法简介
2.1.1 __construst()
构建函数,在实例化一个对象的时候,会首先自动执行该方法
<?php
class User{
public $username;
public function __construct($username) {
$this->username=$username;
echo “触发了构造函数1次”
}
}
$test=new User("benben");
$ser=serizalize($test);
unserialize($ser)
?>2.1.2 析构函数
触发时机:在程序结束或显示销毁对象时触发destruct()
显示销毁:通过显式地将对象引用设置为 null 来间接触发对象的销毁
<?php
class test{
public function __destruct(){
echo '析构函数执行';
}
}
$test0 =new test();
$test1 = new test();
$test0 = null;
echo "\n程序继续\n";
# 运行结果:
# 析构函数执行
# 程序继续
# 析构函数执行2.1.3 __toString()方法
触发时机:尝试echo打印对象时触发,返回一个字符串
<?php
class test{
public function __toString(){
return 'toString方法触发';
}
}
$test = new test();
echo $test;
# 运行结果:
# toString方法触发2.1.4 __invoke()方法
__invoke(): 把test对象当成函数test()来调用,此时触发invoke()
<?php
class test{
public function __invoke(){
echo 'invoke方法触发';
}
}
$test = new test();
$test();
# 运行结果:
# invoke方法触发2.1.5 __call方法
触发时机:调用类不存在的方法
<?php
class test{
public function __call($name,$arguments){
echo "$name 方法不存在 带着你的参数有夺远爬多远";
echo "\n";
print_r($arguments);
}
}
$test = new test();
$test -> hello('world');
# 运行结果:
# hello 方法不存在 带着你的参数有夺远爬多远
# Array
# (
# [0] => world
# )2.1.6 __sleep()方法
触发时机:在调用 serialize() 函数序列化对象之前被调用
<?php
class test{
public function __sleep(){
echo "sleeeep触发\n";
}
}
$test = new test();
echo "序列化结果".serialize($test);
# 运行结果
# sleeeep触发
# 序列化结果 N;2.1.7 __wakeup()方法
CVE-2016-7124,当成员属性大于实际存在的属性个数时,会绕过wakeup方法
版本:5-5.6.25 7-7.0.10
例如:payload: O:4:“test”:2:{s:3:“var”;s:1:“2”;}
已知存在多种对于其他版本的wakeup绕过姿势,自行查阅
触发时机:调用unserialize()反序列化函数时反序列化之后调用
示例中,先反序列化把hello给$evil,再调用wakeup,打印hello
<?php
class test{
public $evil;
public function __wakeup(){
echo $this->evil;
}
}
$test = new test();
$test->evil = 'hello';
unserialize(serialize($test));
# 运行结果:
# hello2.1.8 3.1.8 __get($argu)方法
触发时机:对象调用不存在的成员属性
<?php
class test{
public function __get($argu){
echo "$argu 不存在,整点别的?";
}
}
$test =new test();
echo $test-> hello;
# 运行结果:
# hello 不存在,整点别的?2.1.9 __set($param,$value)方法
触发时机:在给不存在或没有访问权限的属性赋值时触发
搭配get方法,使用一个数组储存键值对
<?php
class test{
public $data=[];
public function __set($param,$value){
$this->data[$param] = $value;
}
public function __get($param){
return $this->data[$param];
}
}
$test =new test();
$test->hello = 'world';
echo $test->hello;
# 运行结果:
# world2.1.10 __isset()方法
触发时机:对属性使用 isset() 或 empty() 时,__isset() 会被调用。用于检测属性是否被设置。
<?php
class test{
public $var1=11;
}
$test =new test();
var_dump(isset($test->var1));
var_dump(isset($test->var2));
# 运行结果:
# bool(true)
# bool(false)2.2 利用手法
序列化和反序列化本身没有问题,但是如果反序列化的内容是用户可以控制的,且后台不正当的使用了PHP中的魔法函数,就会导致安全问题。当传给unserialize()的参数可控时,可以通过传入一个精心构造的序列化字符串,从而控制对象内部的变量甚至是函数。
存在漏洞的思路:一个类用于临时将日志储存进某个文件,当__destruct被调用时,日志文件将会被删除:
//logdata.php
<?php
class logfile
{
//log文件名
public $filename = 'error.log';
//一些用于储存日志的代码
public function logdata($text)
{
echo 'log data:'.$text.'<br />';
file_put_contents($this->filename,$text,FILE_APPEND);
}
//destrcuctor 删除日志文件
public function __destruct()
{
echo '__destruct deletes '.$this->filename.'file.<br />';
unlink(dirname(__FILE__).'/'.$this->filename);
}
}
?>
调用这个类:
<?php
include 'logdata.php'
class User
{
//类数据
public $age = 0;
public $name = '';
//输出数据
public function printdata()
{
echo 'User '.$this->name.' is'.$this->age.' years old.<br />';
}
}
//重建数据
$usr = unserialize($_GET['usr_serialized']);
?>代码$usr = unserialize($_GET['usr_serialized']);中的$_GET[‘usr_serialized’]是可控的,那么可以构造输入,删除任意文件。
如构造输入删除目录下的index.php文件:
<?php
include 'logdata.php';
$object = new logfile();
$object->filename = 'index.php';
echo serialize($object).'<br />';
?>unserialize漏洞依赖条件:
- unserialize函数的参数可控
- 脚本中存在一个构造函数(
__construct())、析构函数(__destruct())、__wakeup()函数中有向PHP文件中写数据的操作类 - 所写的内容需要有对象中的成员变量的值
3 POP链构造
3.1 成员属性赋值对象
使用反推查看攻击方法,反序列化的字符串由我们自己决定,由此构造pop链
魔术方法触发前提:魔术方法所在类或对象被调用
4 session反序列化利用
5 PHAR反序列化
Phar是PHP的压缩文档,是PHP中类似于JAR的一种打包文件。它可以把多个文件存放至同一个文件中,无需解压,PHP就可以进行访问并执行内部语句。
5.1 文件结构
1、Stub //Phar文件头
2、manifest //压缩文件信息
3、contents //压缩文件内容
4、signature //签名
5.1.1 Stub
Stub是Phar的文件标识,也可以理解为它就是Phar的文件头 。这个Stub其实就是一个简单的PHP文件,它的格式具有一定的要求,具体如下:
xxx<?php xxx; __HALT_COMPILER();?>
前面的内容是不限制的,但在该PHP语句中,必须有__HALT_COMPILER(),没有这个,PHP就无法识别出它是Phar文件。
5.1.2 manifest
用于存放文件的属性、权限等信息。
这里也是反序列化的攻击点,因为这里以序列化的形式存储了用户自定义的Meta-data
5.1.3 contents
用于存放Phar文件的内容
5.1.4 signature
签名(可选参数),位于文件末尾
签证尾部的01代表md5加密,02代表sha1加密,04代表sha256加密,08代表sha512加密
当我们修改文件的内容时,签名就会变得无效,这个时候需要更换一个新的签名,更换签名的脚本
from hashlib import sha1
with open('test.phar', 'rb') as file:
f = file.read()
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型和GBMB标识
newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
with open('newtest.phar', 'wb') as file:
file.write(newf) # 写入新文件
5.2 使用方式
Phar之所以能反序列化,是因为Phar文件会以序列化的形式存储用户自定义的meta-data,PHP使用phar_parse_metadata在解析meta数据时,会调用php_var_unserialize进行反序列化操作。
利用条件
phar文件能够上传至服务器- 有可利用的魔术方法
- 文件操作函数可控。且
/、phar、:等等数字符没有被过滤 php.ihi中的phar.readonly,需要为off
Phar属于伪协议,伪协议使用较多的是一些文件操作函数,如fopen()、copy()、file_exists()等,具体如下图,也就是下面的函数如果参数可控可以造成Phar反序列化

<?php
class test{
public $name="qwq";
function __destruct()
{
echo $this->name;
}
}
$a = new test();
$a->name="phpinfo();";
$phartest=new phar('phartest.phar',0);//后缀名必须为phar
$phartest->startBuffering();//开始缓冲 Phar 写操作
$phartest->setMetadata($a);//自定义的meta-data存入manifest
$phartest->setStub("<?php __HALT_COMPILER();?>");//设置stub,stub是一个简单的php文件。PHP通过stub识别一个文件为PHAR文件,可以利用这点绕过文件上传检测
$phartest->addFromString("test.txt","test");//添加要压缩的文件
$phartest->stopBuffering();//停止缓冲对 Phar 归档的写入请求,并将更改保存到磁盘
?>