一、序列化与反序列化

1、序列化serialize()

序列化是什么?序列化就是把程序中的对象、数组、结构体等复杂数据,转换成可以存储或传输的格式。

简单说:

把“内存里的对象”变成“字符串/字节流”。

例如 PHP 中有一个对象:

$user = [
    "name" => "admin",
    "age" => 18
];

序列化后可能变成:

a:2:{s:4:"name";s:5:"admin";s:3:"age";i:18;}

其中各个字段代表的含义:

常见序列化格式包括:

JSON
XML
YAML
PHP serialize
Java Serializable
Python pickle

2、反序列化unserialize()

序列化是什么?反序列化就是把序列化后的字符串/字节流,还原成程序中的对象或数据结构。

简单说:

把“字符串/字节流”重新变回“内存里的对象”。

例如:

$data = 'a:2:{s:4:"name";s:5:"admin";s:3:"age";i:18;}';
$user = unserialize($data);

反序列化后,$user 又变成了数组:

[
    "name" => "admin",
    "age" => 18
]

所以序列化与反序列化 的操作可以直观地看作为:

3\

3、为什么反序列化会有漏洞?

问题核心在于:

如果程序反序列化了用户可控的数据,就可能被攻击者构造恶意对象。

危险点不是“反序列化”本身,而是:

反序列化的数据来自用户输入
+
程序中存在危险的魔术方法/特殊方法
+
这些方法里有文件操作、命令执行、SQL操作等危险逻辑

就可能形成反序列化漏洞。

4. PHP 反序列化漏洞例子

例如 PHP 中有一个类:

<?php
class Test {
    public $cmd;

    function __destruct() {
        system($this->cmd);
    }
}
?>

这里的 __destruct() 是魔术方法,对象销毁时会自动执行。

如果程序这样写:

$data = $_GET['data'];
unserialize($data);

那么攻击者可以构造一个恶意序列化字符串:

O:4:"Test":1:{s:3:"cmd";s:2:"id";}

反序列化后,程序会创建 Test 对象,并且 $cmd = "id"

脚本结束时触发:

__destruct()

于是执行:

system("id");

这就可能造成 RCE,远程命令执行

二、常见的魔术方法

__construct(): //当对象new的时候会自动调用

__destruct()://当对象被销毁时会被自动调用

__sleep(): //serialize()执行时被自动调用

__wakeup(): //unserialize()时会被自动调用

__invoke(): //当尝试以调用函数的方法调用一个对象时会被自动调用

__toString(): //把类当作字符串使用时触发

__call(): //调用某个方法,若方法存在,则调用;若不存在,则会去调用__call函数。

__callStatic(): //在静态上下文中调用不可访问的方法时触发

__get(): //读取对象属性时,若存在,则返回属性值;若不存在,则会调用__get函数

__set(): //设置对象的属性时,若属性存在,则赋值;若不存在,则调用__set函数。

__isset(): //在不可访问的属性上调用isset()或empty()触发

__unset(): //在不可访问的属性上使用unset()时触发

__set_state(),调用var_export()导出类时,此静态方法会被调用

__clone(),当对象复制完成时调用

__autoload(),尝试加载未定义的类

__debugInfo(),打印所需调试信息

三、反序列化漏洞利用

1、POP链

POP 链就是 PHP 反序列化漏洞里的“调用链”。全称:Property-Oriented Programming Chain(面向属性编程链)。

它的核心是:

攻击者通过控制反序列化对象的属性,让程序自动触发魔术方法,然后一步步调用已有代码,最后到达危险函数。

POP常用于上层语言构造特定调用链的方法,序列化攻击都在PHP魔术方法中出现可利用的漏洞,因自动调用触发漏洞,但如关键代码没在魔术方法中,而是在一个类的普通方法中。这时候就可以通过构造POP链寻找相同的函数名将类的属性和敏感函数的属性联系起来。

2. POP 链的基本结构

一条 POP 链一般有三部分:

入口点 Trigger
    ↓
中间跳板 Gadget
    ↓
危险终点 Sink

入口点 Trigger

一般是魔术方法,例如:

__destruct()
__wakeup()
__toString()

中间跳板 Gadget

就是普通业务代码中的方法,比如:

$this->obj->run();
$this->handler->save();
$this->logger->write();

这些方法本来可能不是漏洞,但可以被攻击者利用来继续调用下一个对象。

危险终点 Sink

最终危险操作,例如:

system()
eval()
assert()
include()
require()
file_put_contents()
unlink()

3. 一个简单例子

看这三个类:

class A {
    public $b;

    public function __destruct() {
        $this->b->run();
    }
}

class B {
    public $c;

    public function run() {
        $this->c->write();
    }
}

class C {
    public $filename;
    public $content;

    public function write() {
        file_put_contents($this->filename, $this->content);
    }
}

单独看:

A::__destruct()
B::run()
C::write()

都像正常代码。

但是如果程序中有:

unserialize($_COOKIE['user']);

攻击者就可能构造对象关系:

A 对象
 └── b = B 对象
       └── c = C 对象
             ├── filename = 可控文件名
             └── content = 可控内容

执行流程变成:

unserialize()
    ↓
恢复 A 对象
    ↓
脚本结束触发 A::__destruct()
    ↓
调用 $this->b->run()
    ↓
进入 B::run()
    ↓
调用 $this->c->write()
    ↓
进入 C::write()
    ↓
file_put_contents($filename, $content)

这就是一条 POP 链。

三、属性类型

1、对象变量属性:

public(公共的):在本类内部、外部类、子类都可以访问

protect(受保护的):只有本类或子类或父类中可以访问

private(私人的):只有本类内部可以使用

2、序列化数据显示:

public属性序列化的时候格式是正常成员名

private属性序列化的时候格式是%00类名%00成员名

protect属性序列化的时候格式是%00*%00成员名

3、例

<?php
header("Content-type: text/html; charset=utf-8");
//public private protected说明
class test{
    public $name="xiaodi";
    private $age="31";
    protected $sex="man";
}
$a=new test();
$a=serialize($a);
print_r($a);
?>

输出为:

O:4:"test":3:{s:4:"name";s:6:"xiaodi";s:9:" test age";s:2:"31";s:6:" * sex";s:3:"man";}

其中:

age ——> " test age"
sex ——> " * sex"

四、漏洞绕过

1、__wakeup绕过

为什么要绕过 __wakeup()?因为有些反序列化利用链中,__wakeup() 会破坏攻击者控制的属性。

比如:

class Test {
    public $cmd;

    public function __wakeup() {
        $this->cmd = "echo safe";
    }

    public function __destruct() {
        system($this->cmd);
    }
}

攻击者本来想控制:

$cmd = "id";

但是反序列化时会先触发:

__wakeup()

$cmd 改成:

echo safe

这样 POP 链就被破坏了。

所以绕过 __wakeup() 的目的就是:

让对象恢复出来
但是不执行 __wakeup()
保留攻击者控制的属性
等到 __destruct() 或其他方法触发

当属性数量不一致时会绕过__wakeup()

正常序列化对象:

O:4:"Test":1:{s:3:"cmd";s:2:"id";}

这里属性数量是:1。因为只有一个属性:$cmd

如果把属性数量故意改大:

O:4:"Test":2:{s:3:"cmd";s:2:"id";}

注意这里:属性数量从 1 改成了 2。但实际后面只有一个属性。

在部分老版本 PHP 中,这种属性数量不一致会导致:

对象仍然可能被反序列化
但是 __wakeup() 不会被调用

于是就达到了绕过效果。


2、字符逃逸

字符逃逸在 PHP 反序列化里,核心意思是:

原本应该被当成“普通字符串内容”的字符,因为长度变化或过滤处理,逃出了字符串边界,被 unserialize() 当成了新的序列化结构来解析。

它经常出现在:

serialize()
    ↓
字符串过滤 / 替换
    ↓
unserialize()

这个流程里。

参考:php反序列化-字符逃逸看这一篇就够了_php 反序列化-CSDN博客

五、反序列化链项目

1、-NotSoSecure

https://github.com/NotSoSecure/SerializedPayloadGenerator

为了利用反序列化漏洞,需要设置不同的工具,如 YSoSerial(Java)、YSoSerial.NET、PHPGGC 和它的先决条件。DeserializationHelper 是包含对 YSoSerial(Java)、YSoSerial.Net、PHPGGC 和其他工具的支持的Web界面。使用Web界面,您可以为各种框架生成反序列化payload.

Java – YSoSerial

NET – YSoSerial.NET

PHP – PHPGGC

Python - 原生

2、-PHPGGC

https://github.com/ambionics/phpggc

PHPGGC是一个包含unserialize()有效载荷的库以及一个从命令行或以编程方式生成它们的工具。当在您没有代码的网站上遇到反序列化时,或者只是在尝试构建漏洞时,此工具允许您生成有效负载,而无需执行查找小工具并将它们组合的繁琐步骤。 它可以看作是frohoff的ysoserial的等价物,但是对于PHP。目前该工具支持的小工具链包括:CodeIgniter4、Doctrine、Drupal7、Guzzle、Laravel、Magento、Monolog、Phalcon、Podio、ThinkPHP、Slim、SwiftMailer、Symfony、Wordpress、Yii和ZendFramework等。

七、反序列化框架利用-ThinkPHP&Yii&Laravel

通过上述工具,构造payload实现反序列化

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐