PHP unserialize로 비신뢰 입력 역직렬화할 때 POP 체인 공격 원리

PHP의 unserialize() 함수에 비신뢰 데이터를 전달하면 객체 복원 과정에서 __wakeup()이나 __unserialize() 같은 마법 메서드가 자동으로 호출됩니다. 이때 공격자가 정교하게 조작한 직렬화 데이터로 여러 클래스의 마법 메서드를 순차적으로 트리거하는 POP(Property Oriented Programming) 체인을 구성하면 임의의 코드 실행까지 이어질 수 있습니다.

unserialize()의 동작 방식과 보안 위험성

PHP의 unserialize() 함수는 직렬화된 문자열을 다시 PHP 값으로 되돌립니다. 이 함수 자체가 임의의 PHP 코드를 직접 실행하지는 않습니다. 다만 역직렬화 대상이 객체일 때는 복원에 성공한 뒤 해당 클래스에 __unserialize() 또는 __wakeup() 메서드가 존재하면 PHP가 이를 자동으로 호출합니다.

이런 자동 호출은 개발자에게 편의성을 주지만, 신뢰할 수 없는 사용자 입력값이 들어오면 심각한 보안 취약점이 되기 마련입니다. PHP 공식 문서도 allowed_classes 옵션 설정 값과 상관없이 비신뢰 사용자 입력을 unserialize()에 전달하지 말라고 명시적으로 경고합니다. 역직렬화 과정에서 일어나는 객체 인스턴스화와 오토로딩(autoloading) 때문에 의도치 않은 코드가 로드되고 실행될 수 있기 때문입니다.

POP 체인(Property Oriented Programming)의 원리와 성립 조건

POP 체인은 가젯 체인(gadget chain)이라고도 부르는데, 애플리케이션 내에 존재하는 여러 클래스를 연결해 공격자가 원하는 동작을 끝까지 수행하게 만드는 기법입니다. 각 클래스의 마법 메서드가 다음 클래스의 메서드를 트리거하며 연쇄 반응을 일으키는 구조입니다.

POP 체인 공격의 성립 조건

OWASP에 따르면 PHP 객체 주입(PHP Object Injection)으로 POP 체인 공격이 성립하려면 다음 두 가지 조건이 필요합니다.

  1. 마법 메서드를 구현한 클래스의 존재: 공격을 시작하거나 수행할 수 있는 마법 메서드(__wakeup, __destruct 등)를 구현한 클래스가 애플리케이션 안에 있어야 합니다.
  2. 클래스의 가용성: 공격에 사용되는 모든 클래스가 unserialize() 호출 시점에 이미 선언되어 있거나, 해당 클래스들에 대한 오토로딩이 지원되어야 합니다.

주요 마법 메서드와 공격 경로

마법 메서드는 프로그래머가 명시적으로 호출하지 않아도 특정 이벤트 발생 시 자동으로 실행되는 메서드입니다. 공격자는 직렬화된 데이터 내의 속성(Property) 값을 조작해 이 메서드들이 가젯으로 동작하게 만듭니다.

마법 메서드 자동 호출 시점 잠재적 악용 경로 (사례)
`__wakeup()` / `__unserialize()` 객체 역직렬화 직후 `eval()` 호출을 통한 코드 인젝션
`__destruct()` 객체 소멸 시 (스크립트 종료 등) 경로 조작으로 임의 파일 삭제
`__toString()` 객체가 문자열로 취급될 때 다른 객체 메서드 호출로 SQL 인젝션 유도

실제 코드 실행 경로 분석 사례

POP 체인이 어떻게 실제 원격 코드 실행(RCE)으로 이어지는지는 가젯 체인의 구성 단계를 따라가면 보입니다. 한 분석 사례에서는 Laravel 9.1.8 환경에서 다음과 같은 3단계 체인이 구성되어 system() 함수가 실행되었습니다.

  1. 진입점: 사용자 입력값이 unserialize()에 전달되어 PendingBroadcast 인스턴스가 생성되고, 이후 객체 소멸 시 __destruct 메서드가 자동 호출됩니다.
  2. 전이: 호출된 __destruct 메서드가 내부적으로 Dispatcher.dispatch를 거쳐 dispatchToQueue 메서드로 이어집니다.
  3. 최종 실행: dispatchToQueue 내의 call_user_func($this->queueResolver, $connection) 구문에서 공격자가 조작한 $this->queueResolver$connection 값이 전달되면서 system()과 같은 함수가 실행됩니다.

신뢰 경계 점검 및 안전한 대안

개발 팀이 가장 먼저 할 일은 애플리케이션 내에서 직렬화 데이터가 처리되는 ‘신뢰 경계’를 정확히 짚어보는 것입니다. Laravel 같은 프레임워크를 쓴다면 더욱 그렇습니다.

Laravel 큐 환경의 특성

Laravel 큐에서는 Job 처리 시 Eloquent 모델과 로드된 관계가 직렬화와 역직렬화를 거칩니다. 이때 Job 생성자에 Eloquent 모델을 전달하면 큐에는 모델의 식별자만 직렬화되어 저장됩니다. 이 동작 자체는 정상적인 프레임워크 메커니즘이지만, 큐 페이로드가 신뢰 경계를 넘어 외부로 노출되거나 공격자가 수정할 수 있는 환경이라면 역직렬화 위험과 함께 점검할 부분입니다.

권장 방어 전략

비신뢰 입력을 unserialize()로 처리하는 일은 매우 위험하며, 아래 대안과 검증 방안으로 대체하는 편이 안전합니다.

  1. 표준 데이터 교환 형식 사용: PHP 공식 문서는 unserialize() 대신 json_decode()json_encode() 같은 안전한 표준 형식인 JSON을 사용할 것을 강력히 권고합니다.
  2. 무결성 검증 적용: 외부 저장소에 직렬화된 데이터를 보관해야 하는 상황이라면 hash_hmac()으로 데이터의 변조 여부를 검증하는 절차가 반드시 필요합니다.
  3. allowed_classes 옵션의 한계 이해: allowed_classes => false를 지정하면 직렬화 객체가 원래 클래스로 인스턴스화되지 않고 __PHP_Incomplete_Class로 표현되어 마법 메서드가 트리거되지 않습니다. 다만 이는 완화책일 뿐이며, PHP 공식 문서는 이 옵션 값과 무관하게 비신뢰 입력을 전달하지 말라고 경고하고 있습니다.

PHP 환경에서 외부 입력값을 처리할 때는 직렬화된 객체를 복원하는 방식보다 구조화된 데이터 포맷을 사용하고, 데이터의 출처와 무결성을 엄격히 검증하는 것이 가장 안전한 접근 방식입니다.

댓글 남기기