
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을 가리킬 수 있습니다.