ObjectInputStream readObject를 사용한 기본 Java 역직렬화
Tip
AWS Hacking을 배우고 연습하세요:
HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking을 배우고 연습하세요:HackTricks Training GCP Red Team Expert (GRTE)
Az Hacking을 배우고 연습하세요:HackTricks Training Azure Red Team Expert (AzRTE)
평가 트랙 (ARTA/GRTA/AzRTA)과 Linux Hacking Expert (LHE)를 보려면 전체 HackTricks Training 카탈로그를 둘러보세요.
HackTricks 지원하기
- subscription plans를 확인하세요!
- 💬 Discord group, telegram group에 참여하고, X/Twitter에서 @hacktricks_live를 팔로우하거나, LinkedIn page와 YouTube channel을 확인하세요.
- HackTricks 및 HackTricks Cloud github repos에 PR을 제출해 hacking tricks를 공유하세요.
이 글에서는 java.io.Serializable을 사용하는 예제와, 들어오는 스트림이 공격자에 의해 제어될 경우 readObject()를 오버라이드하는 것이 왜 매우 위험할 수 있는지를 설명합니다.
Serializable
Java의 Serializable 인터페이스(java.io.Serializable)는 클래스가 직렬화되고 역직렬화되기 위해 구현해야 하는 마커 인터페이스입니다. Java 객체의 직렬화(쓰기)는 ObjectOutputStream으로, 역직렬화(읽기)는 ObjectInputStream으로 수행됩니다.
상기: 역직렬화 중에 암묵적으로 호출되는 메서드는 무엇인가?
readObject()– 클래스별 읽기 로직(구현되어 있고 private인 경우).readResolve()– 역직렬화된 객체를 다른 객체로 대체할 수 있습니다.validateObject()–ObjectInputValidation콜백을 통해 호출됩니다.readExternal()–Externalizable을 구현한 클래스용입니다.- 생성자는 실행되지 않습니다 – 따라서 gadget chains는 이전 콜백들에만 전적으로 의존합니다.
해당 체인 내의 어떤 메서드라도 공격자가 제어하는 데이터를 호출하게 되면(명령 실행, JNDI lookups, reflection 등), 역직렬화 루틴은 RCE gadget으로 변합니다.
이제 class Person(직렬화 가능)을 예로 살펴보겠습니다. 이 클래스는 readObject 함수를 오버라이드했기 때문에 이 클래스의 어떤 객체든지 역직렬화될 때 이 함수가 실행됩니다.
예제에서, Person 클래스의 readObject 함수는 그의 pet의 eat() 함수를 호출하고, 그 eat() 함수는(어떤 이유인지) calc.exe를 호출합니다. 이제 Person 객체를 직렬화하고 역직렬화하여 이 계산기(calc.exe)를 실행하는 방법을 보겠습니다:
The following example is from https://medium.com/@knownsec404team/java-deserialization-tool-gadgetinspector-first-glimpse-74e99e493649
import java.io.Serializable;
import java.io.*;
public class TestDeserialization {
interface Animal {
public void eat();
}
//Class must implements Serializable to be serializable
public static class Cat implements Animal,Serializable {
@Override
public void eat() {
System.out.println("cat eat fish");
}
}
//Class must implements Serializable to be serializable
public static class Dog implements Animal,Serializable {
@Override
public void eat() {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("dog eat bone");
}
}
//Class must implements Serializable to be serializable
public static class Person implements Serializable {
private Animal pet;
public Person(Animal pet){
this.pet = pet;
}
//readObject implementation, will call the readObject from ObjectInputStream and then call pet.eat()
private void readObject(java.io.ObjectInputStream stream)
throws IOException, ClassNotFoundException {
pet = (Animal) stream.readObject();
pet.eat();
}
}
public static void GeneratePayload(Object instance, String file)
throws Exception {
//Serialize the constructed payload and write it to the file
File f = new File(file);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
out.writeObject(instance);
out.flush();
out.close();
}
public static void payloadTest(String file) throws Exception {
//Read the written payload and deserialize it
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
Object obj = in.readObject();
System.out.println(obj);
in.close();
}
public static void main(String[] args) throws Exception {
// Example to call Person with a Dog
Animal animal = new Dog();
Person person = new Person(animal);
GeneratePayload(person,"test.ser");
payloadTest("test.ser");
// Example to call Person with a Cat
//Animal animal = new Cat();
//Person person = new Person(animal);
//GeneratePayload(person,"test.ser");
//payloadTest("test.ser");
}
}
결론 (클래식 시나리오)
이 매우 기본적인 예에서 볼 수 있듯, 여기서 “취약점”은 readObject() 메서드가 공격자 제어 코드를 호출하기 때문에 발생합니다. 실제의 gadget chains에서는 Commons-Collections, Spring, Groovy, Rome, SnakeYAML 등 외부 라이브러리에 포함된 수천 개의 클래스가 악용될 수 있으며, 공격자는 코드 실행을 얻기 위해 하나의 도달 가능한 gadget만 있으면 됩니다.
2023-2025: 실제 Java deserialization 버그에서 무엇이 바뀌었나?
최근 사례들은 ObjectInputStream 버그가 더 이상 단순히 “레거시 HTTP 엔드포인트에 .ser 파일 업로드”에만 국한되지 않는다는 것을 상기시켜줍니다:
- Broker / queue consumers: Spring-Kafka (
CVE-2023-34040)는 공격자가 제어하는 토픽에서 예외 헤더를 deserialize하는 것만으로도 충분하다는 것을 보여주었는데, 이는 consumer가 특이한checkDeserExWhen*플래그를 활성화한 경우였습니다. - Client-side trust of remote servers: Aerospike Java client (
CVE-2023-36480)는 서버로부터 받은 객체를 deserialize했습니다. 벤더의 대응은 주목할 만합니다: 최신 클라이언트는 약한 필터로 보존하려 하기보다는 Java 런타임 직렬화/역직렬화 지원을 제거했습니다. - “Restricted” streams는 종종 여전히 너무 광범위함:
pac4j-core(CVE-2023-25581)는 RestrictedObjectInputStream으로 역직렬화를 보호하려 했지만, 허용된 클래스 집합이 여전히 충분히 커서 gadget 악용이 가능했습니다.
공격 관점에서의 교훈은 위험한 신뢰 경계는 종종 “사용자가 blob을 업로드함”이 아니라 “개발자가 신뢰한다고 간주한 어떤 컴포넌트가 결국 readObject()에 도달하는 스트림에 바이트를 주입할 수 있다”는 점입니다.
노이즈가 적은 도달성 체크가 필요하고 전체 gadget 연구에 시간을 쓰기 전에 빠르게 확인하고 싶다면, 다음 전용 Java 페이지를 사용하세요:
Java DNS Deserialization, GadgetProbe and Java Deserialization Scanner
readObject()가 여전히 gadget 진입점을 만드는 안티패턴
클래스 자체가 명백한 RCE gadget가 아니더라도, 공격자 제어 객체가 그래프에 삽입될 때 다음 패턴들은 충분히 악용 가능하게 만듭니다:
readObject()에서 오버라이드 가능한 메서드나 인터페이스 메서드를 호출하는 것 (pet.eat()은 위의 PoC에서 고전적인 예).- 역직렬화 도중 조회(lookup), 리플렉션, 클래스 로딩, 표현식 평가, 또는 JNDI 연산을 수행하는 것.
- 공격자 제어 컬렉션이나 맵을 반복(iterate)하는 것 — 이때
hashCode(),equals(), comparator, transformer 등이 부작용으로 트리거될 수 있습니다. - 위험한 후처리를 수행하는
ObjectInputValidation콜백을 등록하는 것. - “private
readObject()가 충분한 보호를 제공한다”라고 가정하는 것. 이 접근은 디스패치(호출) 시맨틱만 제어할 뿐, 역직렬화를 안전하게 만들지는 않습니다.
배포해야 할 현대적 완화책
- JEP 290 / Serialization Filtering (Java 9+) 허용 목록과 명시적 그래프 제한 사용:
-Djdk.serialFilter="com.example.dto.*;java.base/*;maxdepth=5;maxrefs=1000;maxbytes=16384;!*"
- 전역 설정뿐 아니라 모든 신뢰할 수 없는 스트림에 필터 적용:
try (var ois = new ObjectInputStream(input)) {
var filter = ObjectInputFilter.Config.createFilter(
"com.example.dto.*;java.base/*;maxdepth=5;maxrefs=1000;!*"
);
ois.setObjectInputFilter(filter);
return (Message) ois.readObject();
}
- JEP 415 (Java 17+) Context-Specific Filter Factories 같은 JVM에 여러 역직렬화 컨텍스트(RMI, 캐시 복제, 메시지 컨슈머, 관리자 전용 import 등)가 있고 각각 다른 허용 목록이 필요할 때 이는 더 바람직합니다.
readObject()를 단순하게 유지 가능한 한defaultReadObject()/ 명시적 필드 읽기만 호출하고 엄격한 불변성 검사를 수행하세요. I/O, 공격자 제어 객체를 역참조하는 로깅, 동적 조회, 역직렬화된 하위 객체에 대한 메서드 호출을 하지 마세요.- 가능하면 설계에서 Java 네이티브 직렬화를 제거
Aerospike의 수정 방식이 좋은 모델입니다: 기능이 필수적이지 않다면
readObject()/writeObject()사용을 삭제하는 것이 완벽한 필터를 영구히 유지하려고 시도하는 것보다 종종 더 안전합니다.
탐지 및 연구 워크플로우
ysoserial은 gadget 검증과 빠른 RCE/URLDNS 프로브의 기준입니다.marshalsec은 sink가 JNDI/LDAP/RMI 영역으로 전환될 때 여전히 유용합니다.GadgetInspector는 대상 jar들이 있고 애플리케이션 특정 gadget 체인을 찾아야 할 때 유용합니다.- Java 17은
jdk.DeserializationFlight Recorder 이벤트를 추가했으며, 이는 ObjectInputStream이 실제로 어디에서 사용되는지와 필터가 적용되는지를 확인하는 데 유용합니다.
안전한 readObject() 구현을 위한 빠른 체크리스트
- 메서드를
private으로 만들고 직렬화 훅에는@Serial을 주석으로 달아 컴파일러가 잘못 선언된 시그니처를 잡아내도록 하세요. - 특별한 이유가 있어 전체 객체 그래프를 수동으로 읽지 않는 한
defaultReadObject()를 먼저 호출하세요. - 모든 중첩 객체를 검증될 때까지 공격자 제어로 간주하세요.
readObject()내부에서 역직렬화된 협력 객체에게 메서드를 호출하지 마세요.- 코드 리뷰를
ObjectInputFilter리뷰와 쌍으로 수행하세요; “안전해 보이는readObject()코드”만으로는 스트림이 여전히 임의의 클래스를 허용한다면 충분치 않습니다.
References
- OpenJDK JEP 415: Context-Specific Deserialization Filters
- GitHub Security Lab: GHSL-2022-085 / CVE-2023-25581 (
pac4j-coredeserialization leading to RCE)
Tip
AWS Hacking을 배우고 연습하세요:
HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking을 배우고 연습하세요:HackTricks Training GCP Red Team Expert (GRTE)
Az Hacking을 배우고 연습하세요:HackTricks Training Azure Red Team Expert (AzRTE)
평가 트랙 (ARTA/GRTA/AzRTA)과 Linux Hacking Expert (LHE)를 보려면 전체 HackTricks Training 카탈로그를 둘러보세요.
HackTricks 지원하기
- subscription plans를 확인하세요!
- 💬 Discord group, telegram group에 참여하고, X/Twitter에서 @hacktricks_live를 팔로우하거나, LinkedIn page와 YouTube channel을 확인하세요.
- HackTricks 및 HackTricks Cloud github repos에 PR을 제출해 hacking tricks를 공유하세요.


