46. Deserialization 취약점 — 객체 역직렬화로 RCE 터뜨리기
직렬화는 객체의 상태를 저장·전달 가능한 표현으로 바꾸는 일이고, 역직렬화는 그 표현을 다시 객체로 만드는 일이다. 위험은 포맷 자체보다 입력 데이터가 객체 생성과 메서드 호출을 통제하는 구조에서 생긴다. 다음 예시는 승인된 CTF와 격리된 로컬 실습에서 취약성을 이해하기 위한 것이며, 운영 대상에 페이로드를 보내는 방법이 아니다.
실행이 생기는 이유
Java ObjectInputStream.readObject()는 클래스패스의 타입과 역직렬화 훅을 조합할 수 있다. Python pickle.loads()도 데이터 포맷처럼 보이지만 복원 과정에서 호출 가능한 객체를 지정할 수 있으므로 신뢰할 수 없는 바이트를 받아서는 안 된다. PHP unserialize()는 __wakeup, __destruct 같은 매직 메서드와 애플리케이션 클래스의 상태가 연결될 때 객체 인젝션으로 이어질 수 있다. “JSON으로 바꾸면 자동으로 안전하다”도 틀린 말이다. JSON을 파싱한 뒤 위험한 타입·경로·쿼리로 넘기는 코드가 취약할 수 있다.
개념을 확인할 때는 실제 명령 실행 대신 부작용 없는 표시를 사용한다.
import pickle
class Marker:
def __reduce__(self):
return (str, ("LAB-ONLY",))
payload = pickle.dumps(Marker())
# 이 바이트는 자기 소유의 테스트 프로그램에서만 사용한다.
assert pickle.loads(payload) == "LAB-ONLY"
이 예제는 __reduce__가 복원 시 호출된다는 사실만 보여준다. 운영 코드에서 pickle.loads를 허용하는 근거가 되지 않는다.
분석의 단위는 gadget chain이다
소스가 있으면 입력 지점에서 시작해 타입 허용 목록, 생성자와 매직 메서드, 파일·프로세스·네트워크 싱크로 이어지는 호출 그래프를 그린다. 소스가 없는 CTF에서는 오류 메시지와 클래스패스 단서를 관찰하되, 자동 생성 도구의 체인이 현재 라이브러리 버전과 실제 런타임에서 동작한다고 가정하지 않는다. 체인 이름 하나보다 버전, 패치 수준, 필터 설정을 먼저 확인해야 한다.
방어와 검증
가능하면 역직렬화 대신 명시적 스키마의 데이터 전송을 사용한다. Java는 ObjectInputFilter로 허용 타입과 깊이·배열 길이를 제한하고, PHP·Python은 신뢰 경계 밖 입력에 네이티브 객체 복원을 적용하지 않는다. 서명은 변조 방지에는 도움이 되지만 키가 노출되거나 서명 전에 위험한 파싱을 하면 해결책이 아니다.
테스트에서는 허용 타입 외 객체, 깊은 중첩, 큰 배열, 잘못된 길이를 넣어 정상적으로 거부되는지 확인하고, 프로세스·파일·네트워크 부작용이 없는 격리 환경에서 실행한다. 성공 기준은 RCE 시연이 아니라 입력이 최소 권한의 데이터 구조로 제한되고 거부 이벤트가 감사되는지다.