
CVE-2026-43512 (Apache Tomcat 다이제스트 인증 우회)에 대한 익스플로잇 가능성 PoC
익스플로잇 가능성 분석 결과: 근본 원인이 확인되었습니다. 표준
UserDatabaseRealm배포에 대한 종단 간 익스플로잇은 재현되지 않습니다. 자세한 내용은 분석을 참조하세요.
CVE-2026-43512는 Apache Tomcat의 HTTP DIGEST 인증 메커니즘의 취약점입니다. RealmBase.getDigest() 메서드는 A1 해시 입력을 구성하기 전에 getPassword(username)의 반환값을 검증하지 않습니다. 구성된 영역(Realm)에 사용자 이름이 존재하지 않으면 getPassword()는 null을 반환하며, Java의 문자열 연결 연산자는 이를 자동으로 4글자 리터럴 "null"로 변환합니다.
따라서 서버는 다음을 계산합니다:
A1 = MD5("<username>:<realm>:null")
비밀번호로 리터럴 문자열 "null"을 사용하여 계산된 DIGEST 응답을 제출하는 클라이언트는 동일한 해시를 생성합니다. 권고에 따르면 이는 인증 우회에 해당합니다.
이 저장소는 최소한의 재현 가능 환경과 Go 기반 개념 증명(PoC)을 포함하여 실제 Tomcat 인스턴스에서 해당 주장을 검증합니다.
| 영향받는 범위 | 수정된 버전 |
|---|---|
| 7.0.0 – 7.0.109 | 7.0.110 |
| 8.5.0 – 8.5.100 | 8.5.101 |
| 9.0.0.M1 – 9.0.117 | 9.0.118 |
| 10.1.0.M1 – 10.1.54 |
RealmBase.java의 취약한 코드 경로(영향받는 모든 분기):
// RealmBase.java — vulnerable
protected String getDigest(String username, String realmName, String algorithm) {
if (hasMessageDigest(algorithm)) {
return getPassword(username); // returns null for unknown users
}
// null is concatenated as the literal "null" by Java
String a1 = username + ":" + realmName + ":" + getPassword(username);
return HexUtils.toHexString(
ConcurrentMessageDigest.digest(algorithm, a1.getBytes(...))
);
}
수정 사항(커밋 6565a6c)은 명시적인 null 검사를 추가합니다:
// RealmBase.java — patched
protected String getDigest(String username, String realmName, String algorithm) {
String password = getPassword(username);
if (password == null) {
return null;
}
...
}
FINE 수준 로깅을 활성화한 Tomcat 11.0.0-M1에 대해 PoC를 실행하면 다음이 드러납니다:
Digest: 2388e2c78407def640f37f092a8d3a84 ← client
Server digest: 2388e2c78407def640f37f092a8d3a84 ← server
Failed to authenticate user [ghost]
다이제스트 해시가 일치합니다. getDigest()의 버그는 실제이며 확인되었습니다. 그러나 RealmBase.authenticate()에 두 번째 독립적인 검사가 있기 때문에 인증은 여전히 실패합니다:
// RealmBase.authenticate()
if (serverDigest.equals(clientDigest)) {
return getPrincipal(username); // returns null for non-existent users
}
return null;
표준 tomcat-users.xml로 백업된 UserDatabaseRealm에서 getPrincipal()은 메모리 내 사용자 데이터베이스를 조회합니다. 해당 데이터베이스에 존재하지 않는 사용자 이름에 대해 null을 반환합니다. 호출자는 null Principal을 인증 실패로 처리하고 401을 발급합니다.
cve-2026-43512-poc/
├── Dockerfile # Tomcat 11.0.0-M1 (영향받는 버전)
├── tomcat-users.xml # 최소 Realm 구성 — "ghost" 사용자 없음
├── web.xml
├── exploit/
│ ├── exploit.go # PoC — Go, stdlib만 사용
│ └── go.mod
└── README.md
| 도구 | 버전 | 비고 |
|---|---|---|
| Podman | ≥ 4.0 | Docker도 작동 |
| Go | ≥ 1.22 | 로컬에서 익스플로잇 실행 전용 |
podman build -t tomcat-cve-2026-43512 .
podman run -d --name tomcat-vuln -p 8080:8080 tomcat-cve-2026-43512
Tomcat이 시작을 완료할 때까지 몇 초 기다린 후, 작동 중인지 확인합니다:
curl -si http://localhost:8080/protected/secret.html | head -1
# Expected: HTTP/1.1 401
cd exploit
go run exploit.go \
-target http://localhost:8080 \
-path /protected/secret.html \
-username ghost
사용 가능한 플래그:
내부 인증 상태를 관찰하려면 logging.properties 파일을 추가하고 마운트합니다:
org.apache.catalina.authenticator.level = FINE
org.apache.catalina.realm.level = FINE
podman run -d --name tomcat-vuln -p 8080:8080 \
-v ./logging.properties:/usr/local/tomcat/conf/logging.properties:ro \
tomcat-cve-2026-43512
로그는 다이제스트 비교 결과를 직접 표시하여 해시가 일치하는지 확인합니다.
podman stop tomcat-vuln && podman rm tomcat-vuln
============================================================
CVE-2026-43512 — Tomcat DIGEST Auth Bypass PoC
============================================================
Target : http://localhost:8080/protected/secret.html
Username : "ghost" (must NOT exist in tomcat-users.xml)
Password : "null" (literal string)
------------------------------------------------------------
[1] Sending unauthenticated request to obtain DIGEST challenge...
[+] HTTP 401 received — DIGEST challenge:
Digest realm="UserDatabase", qop="auth", nonce="...", opaque="..."
[*] realm="UserDatabase" nonce="..." qop="auth" algorithm="MD5"
[2] Computing DIGEST response with password="null"...
Digest username="ghost", realm="UserDatabase", ...
[3] Sending request with crafted DIGEST credentials...
------------------------------------------------------------
[✗] HTTP 401 — exploit failed.
The UserDatabaseRealm provides a second line of defence:
getPrincipal("ghost") returned null after the digest matched.
============================================================
이 저장소는 교육 목적 및 로컬 익스플로잇 가능성 분석만을 위해 제공됩니다. 모든 테스트는 자체 호스팅 컨테이너 환경에서 수행되었습니다. 소유하지 않거나 테스트할 명시적인 서면 승인이 없는 시스템에 대해 이 PoC를 실행하지 마십시오.
| 10.1.55 |
| 11.0.0.M1 – 11.0.21 | 11.0.22 |
| 플래그 | 기본값 | 설명 |
|---|
-target | http://localhost:8080 | Tomcat 기본 URL |
-path | /protected/ | 보호된 리소스의 경로 |
-username | ghost | 사용할 사용자 이름 — tomcat-users.xml에 없어야 함 |
| 자원 | 링크 |
|---|
| Apache Tomcat 보안 권고 | https://tomcat.apache.org/security-9.html |
| 수정 커밋 | https://github.com/apache/tomcat/commit/6565a6cb6499e56fe2f34457cec99f9d1c4f39e9 |
RealmBase.java (main) | https://github.com/apache/tomcat/blob/main/java/org/apache/catalina/realm/RealmBase.java |
| RFC 2617 — HTTP 다이제스트 인증 | https://datatracker.ietf.org/doc/html/rfc2617 |
| 전체 분석 — 블로그 게시물 | https://return-zero.dev/posts/cve-2026-43512 |