【翻译】看我如何利用PHP的0day黑掉Pornhub并获得2W美刀奖励

漏洞发现

在分析了Pornhub使用的平台之后,我们在其网站上检测到了unserialize函数的使用,其中的很多功能点(例如上传图片的地方等等)都受到了影响,例如下面两个URL:

  • http://www.pornhub.com/album_upload/create
  • http://www.pornhub.com/uploading/photo

在所有情况下,都通过POST请求中名为cookie的参数传递反序列化数据,然再通过HTTP响应包的Set-Cookie头反映出来。请求示例:

这可以通过发送一个包含数组的特制反序列化对象来进一步验证:

HTTP响应:

乍一看,这可能只是一个信息泄露漏洞,但众所周知,在反序列化时使用用户可控的输入是会产生安全问题的:

  • ROP in PHP applications(https://www.owasp.org/images/9/9e/Utilizing-Code-Reuse-Or-Return-Oriented-Programming-In-PHP-Application-Exploits.pdf)
  • Shocking News in PHP Exploitation(https://www.nds.ruhr-uni-bochum.de/media/hfs/attachments/files/2010/03/hackpra09_fu_esser_php_exploits1.pdf)

常规的利用技术要使用所谓的Property-Oriented-Programming (POP)技术,这种技术利用已定义类的魔术方法来触发恶意代码执行。不幸的是,我们很难收集有关Pornhub使用的框架和PHP对象的任何信息。我们利用了多个常用框架中包含的类进行了测试,但都没有成功。

问题描述

单独的核心反序列化器相对复杂,因为它涉及PHP 5.6中的1200多行代码。此外,许多PHP内部类都有其自己的反序列化方法。因为PHP支持诸如对象,数组,整数,字符串甚至引用之类的结构,所以其中包含很多逻辑错误和内存破坏漏洞就不足为奇了。遗憾的是,由反序列化产生的漏洞在过去已经引起了很多关注(例如phpcodz),所以诸如PHP 5.6或PHP 7之类的更新版本的PHP中没有这种类型漏洞的报告。因此,审计它好比挤压已经压榨过的柠檬,经过如此多的关注和众多的安全修复,潜在漏洞不应该已经全部被修复掉了吗?

模糊测试unserialize函数

为了找到答案,Dario实现了一个模糊测试器,专门用于产生传递给unserialize函数的序列化字符串。在PHP 7下运行模糊测试器会立即导致意外行为。不过,在针对Pornhub的服务器进行测试时,这种行为无法复现。因此,我们假设Pornhub的服务器使用的是PHP 5版本。 在对较新版本的PHP 5运行模糊测试器之后会生成了超过1 TB的日志,但并没有从中发现崩溃或者异常行为。最终,在经过越来越多的努力之后,我们又偶然发现了意外行为。此时我们必须搞清楚几个问题:这些问题与安全性有关吗?我们只能在本地利用还是可以远程利用这些漏洞?为了覆盖这些更加复杂的情况,模糊测试器生成了超过200 KB的不可打印数据块。

分析意外行为

分析潜在问题需要大量时间。最终,我们在这些产生的意外行为中发现了一个use-after-free(UAF)漏洞!经过进一步的调查,我们发现根本原因可以在PHP的垃圾回收算法中找到,这是一个与PHP反序列化完全无关的组件。但是,这两个组件的交互仅在反序列化完成其工作之后才发生。因此,它不太适合远程利用。经过进一步分析,对问题的根本原因有了更深入的了解,并进行了许多艰苦的工作,发现了类似的UAF漏洞,这对于远程利用来说似乎很有希望:

  • PHP Bug – ID 72433 CVE-2016-5771 https://bugs.php.net/bug.php?id=72433 https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-5771
  • PHP Bug – ID 72434 CVE-2016-5773 https://bugs.php.net/bug.php?id=72434 https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-5773

发现的PHP漏洞及其发现方法的高度复杂性使得有必要撰写单独的文章。您可以在Dario的模糊反序列化文章中阅读更多详细信息: fuzzing unserialize write-up(https://www.evonide.com/fuzzing-unserialize) 此外,我们还写了一篇有关破坏PHP的垃圾回收和反序列化的文章: Breaking PHP’s Garbage Collection and Unserialize(https://www.evonide.com/breaking-phps-garbage-collection-and-unserialize/)

漏洞利用

即使这个有希望的UAF漏洞也很难利用。特别是,它涉及多个开发阶段。 由于我们的主要目标是执行任意代码,因此我们需要以某种方式控制x86_64上称为RIP的CPU指令指针。这通常涉及以下障碍:

  1. 栈和堆以及任何其他可写段都被标记为不可执行 https://en.wikipedia.org/wiki/Executable_space_protection
  2. 即使能够控制指令指针(RIP),但也需要知道要执行的内容,即需要获得可执行内存段的有效地址。在PHP上下文中,通常使用zend_eval_string就足够了,这是一个在PHP内核中实现的C函数,它使我们能够执行任意PHP代码,而不必过渡到其他相关的库中。

第一个问题可以通过使用Return-oriented programming

(https://en.wikipedia.org/wiki/Return-oriented_programming)来解决,可以在其中利用二进制文件本身或它导入的库中已经存在的内存片段。但是,解决第二个问题需要找到zend_eval_string函数的正确起始地址。通常,执行动态链接程序时,加载器会将进程映射到0x400000,这是x86_64上的标准加载地址。如果可以通过某种方式获得了Pornhub服务器中所使用的的PHP可执行文件(例如,通过找到目标所提供的确切软件包),则可以在本地查找所需功能的偏移量。我们发现Pornhub使用的是php5-cgi的自定义编译版本,因此很难确定确切的PHP版本,也很难获得有关PHP进程内存布局的任何信息。

获取PHP binary和必需的指针

在PHP中利用UAF通常遵循相同的规则。一旦能够写入已经释放过的内存,以后再将其作为PHP内部变量(即zval)重用,就可以生成攻击向量,以允许从任意内存中读取数据并触发代码执行。

准备进行内存数据泄露

如前所述,我们需要获得有关Pornhub服务器上的PHP二进制文件的更多信息。因此,第一步是利用UAF来注入一个代表PHP字符串的zval结构体。PHP 5.6中的zval结构体的定义如下所示:

zvalue_value字段被定义为联合,因此使类型混淆变得容易。

PHP中的字符串变量是用type字段为6的zval结构体表示的。因此,它将zvalue_value这个联合视为包含char类型的指针和length字段的结构体,如下图所示:

因此,制作具有任意起点和任意长度的字符串类型的zval(即type字段为6)会产生强大的信息泄漏,当Pornhub的setcookie函数在响应头中输出注入的zval时,就会触发该信息泄漏。

获取PHP的image base

通常,可以从泄漏二进制文件的相关信息开始,如前所述,二进制文件的起始地址一般从0x400000开始。不幸的是,Pornhub的服务器使用了PIE和ASLR等保护机制,这些机制随机化了可执行文件及其导入的共享库的加载基址。随着越来越多的发行版软件包支持位置无关代码,这也已成为默认设置。 接下来的挑战是:找到二进制文件的正确加载地址。 第一个困难是要以某种方式获得一个我们可以从其泄漏的有效地址。在此有助于了解有关PHP内存管理的一些详细信息。尤其是,一旦释放了zval,PHP将使用先前释放的块的地址覆盖其前八个字节。因此,获得第一个有效地址的技巧是创建一个整数zval,释放该整数zval,最后使用指向该zval的悬空指针获取其当前值。

由于php-cgi的实现中所有的worker都是由主进程使用fork系统调用产生的,因此只要不断发送相同大小的数据,内存布局就不会在不同请求之间发生改变。这样的话我们就可以不断的发送请求,并每次修改zval字符串指向的地址来泄漏不同内存地址的数据。但是,单靠获取已释放块的堆地址不足以获取有关可执行位置的任何线索。这是由于该chunk周围缺少有用的信息。 为了获得有用的地址,有一种相对复杂的技术,在反序列化过程中需要多次释放和分配PHP结构(参见ROP in PHP applications 第67页:https://www.owasp.org/images/9/9e/Utilizing-Code-Reuse-Or-Return-Oriented-Programming-In-PHP-Application-Exploits.pdf)。由于我们这个漏洞的特殊性质,并且为了使复杂度尽可能低,我们使用了自己的技巧。

通过使用序列化的字符串(例如“ i:0; a:0:{} i:0; a:0:{} […] i:0; a:0:{}”)作为我们payload的一部分,我们可以利用反序列化以创建许多空数组,并在终止时释放它们。当初始化数组时,PHP会为其zval和哈希表连续分配内存。空数组的一个默认哈希表条目是uninitialized_bucket符号。总而言之,我们能够获得类似于以下内容的内存片段:

地址0xeae040是PHP的uninitialized_bucket的符号地址,直接指向PHP的BSS段。您可以看到它在最后释放的块附近多次发生。如前所述,释放了许多空数组。因此,通过利用某些哈希表条目在堆中保持不变的情况,我们能够泄漏这个特定符号。

最后,我们可以从uninitialized_bucket符号地址开始应用逐页向后扫描,以找到ELF标头:

获取PHP binary segments

此时的情况更加复杂,因为每个请求只能泄漏1 KB数据(这是由于Pornhub的服务器限制标头的大小)。一个PHP二进制文件最多可能占用30 MB的大小。假设每秒发出一个请求,则整个泄露过程将花费大约8小时20分钟才能完成。由于我们担心这个漏洞利用过程随时可能被中断,因此必须尽可能快且隐秘地进行。所以我们要先使用一些启发式算法来预先猜测一些可能有用的部分。不过,我们还可以解析ELF的字符串表和符号表中的结构。还有其他一些技术(例如ret2dlresolve)可以省略整个泄漏过程,但由于需要制作更多的数据结构并且需要知道更多内存位置的信息,因此并非完全适用于此。

要获取zend_eval_string的地址,首先必须找到偏移量为32的ELF程序头,然后向前扫描,直到找到类型2(PT_DYNAMIC)的程序标头条目,以获取ELF的动态部分,这其中包含对字符串和符号表(类型5和6)的引用,可以通过使用它们的size字段获取任何函数的内存地址。另外,您也可以使用哈希表(DT_HASH)更快地找到函数,但是在这种情况下,这无关紧要,因为我们可以在本地快速遍历表。除了zend_eval_string之外,我们还对其他符号和POST变量的位置感兴趣(因为稍后它们应被用作ROP堆栈)。

获取post数据的地址

要获取提供的post数据的地址,您可以通过读取以下内容来泄漏更多的指针:

遍历该链看起来很复杂,其实只需要解引用一些具有正确偏移量的指针,即可快速找到指向堆中POST数据的stdin流。

准备ROP payload

第二部分涉及控制PHP流程并获得任意代码执行。为此,我们首先需要讨论如何修改指令指针(RIP)。

控制指令指针寄存器

我们将有效负载调整为包含伪造的对象(而不是先前使用的字符串zval),并带有指向特制zend_object_handlers表的指针。本质上,该表是一个函数指针数组,其结构定义可以在以下位置找到:

当创建这样一个伪造的zend_object_handlers表时,我们可以简单地设置add_ref。这个指针指向的函数通常用于增加对象的引用计数。一旦我们创建的伪造的对象作为参数传递给setcookie函数,就会发生以下情况:

在这里,根据“ s | sl […]”,可以看到setcookie函数将字符串作为其第一和第二个参数(|表示可选参数的开始)。因此,它将尝试将第二个参数传递的对象转换为字符串。最后,_zval_copy_ctor将执行:

特别是,这将使用我们对象的地址作为参数来调用提供的add_ref函数(参见PHP Internals Book –复制zval以查看说明)。相应的程序集如下所示:

RDI是_zval_copy_ctor_func函数的第一个参数,这也是我们伪造的对象zval(以上源代码中的zvalue)的地址。如先前在_zvalue_value typedef的定义中所见,对象包含zend_object_value类型的obj元素,其定义如下:

因此,0x8(%rdi)将指向_zend_object_value中的第二个条目,它对应于第一个zend_object_handlers条目的地址。如前所述,该条目是我们自定义的add_ref函数,并说明了为什么我们也可以直接控制RAX。 为了绕过前面讨论的不可执行内存的问题,我们必须获得更多的信息。特别是,由于对堆栈的控制还不够,我们需要收集有用的gadgets并为构造ROP链准备stack pivoting。

获取ROP gadgets

现在我们可以分别设置add_ref指针或RAX来接管指令指针(instruction pointer)。尽管这提供了一个起点,但并不能确保所有ROP gadgets都已执行,因为一旦从第一个gadget返回,CPU就会从当前堆栈中弹出下一条指令的地址。我们对此堆栈没有任何控制权,因此,有必要将堆栈转移到我们的ROP链中。这就是为什么下一步是将RAX复制到RSP并继续从那里进行ROP的原因。使用本地编译的PHP版本,我们搜索了可用于stack pivoting gadget的代码片段,并发现php_stream_bucket_split包含以下代码:

这可以被用来修改RSP以指向我们的POST数据提供的ROP链,并有效地链接了所有提供的gadget calls。 根据x86_64调用约定,函数的前两个参数是RDI和RSI,因此我们也必须找到pop %rdi和pop %rsi对应的gadgets。这些是很常见的,因此很容易找到。但是,我们仍然不知道这些gadgets是否确实存在于Pornhub的PHP版本中。因此,我们必须手动验证它们的存在。

验证所需ROP gadgets在目标服务器是否存在

infoleak向量使我们能够快速转储反汇编的php_stream_bucket_split函数并检查我们的stack pivoting gadget在远程版本上是否可用。幸运的是,只需要对gadgets的偏移量进行少量校正即可。最后,我们实施了一些检查以确认所有地址都是正确的:

制作ROP stack

最终的ROP payload可以执行如下PHP代码: zend_eval_string(code); exit(0); 这个payload看起来像下面的代码片段:

因为stack pivot包含pop %r13和pop %r14,所以在其余ROP链中需要使用0xdeadbeef填充来继续设置RDI。作为zend_eval_string函数的第一个参数,需要RDI指向要执行的代码的内存地址。该代码位于ROP链之后。还需要在每个请求之间保持发送完全相同的数据量,以使所有计算出的偏移量保持正确。这是通过在需要的地方设置不同的填充来实现的。 下一步是通过返回PHP解释器最终触发代码执行。实际上,诸如return2libc之类的其他技术也同样适用,但是会产生一些其他问题,这些问题在PHP的上下文中更容易解决。

Returning into PHP

能够执行任意PHP代码是重要的一步,但是能够查看其输出同样重要,除非有人想利用侧信道接收响应。因此,剩下的棘手部分是以某种方式在Pornhub的网站上显示输出结果。

Clean termination of PHP

通常,php-cgi将生成的内容转发回Web服务器,以便将其显示在网站上,但是由于坏的控制流使得PHP异常终止,因此其结果将永远不会到达HTTP服务器。为了解决这个问题,我们只是简单地配置PHP使用通常用于HTTP流传输的直接无缓冲响应:

最终,这使我们可以直接获取生成的PHP payload的每个输出,而不必担心CGI进程将数据发送到Web服务器时通常涉及的清理例程。这通过最小化潜在的错误和崩溃的数量,进一步增加了攻击过程的隐蔽性。 总而言之,我们的payload包含一个伪造的对象,其add_ref函数指针指向我们的第一个ROP gadget。下图将这个概念形象化:

连同通过POST数据提供的ROP stack,我们的payload执行了以下操作:

  1. 创建了我们的伪造对象,该伪造对象随后作为参数传递给setcookie函数。
  2. 这导致了对我们提供的add_ref函数的调用,即它使我们获得了程序计数器(program counter)的控制权。
  3. 然后,我们的ROP链准备了所有已讨论的寄存器/参数。
  4. 接下来,我们可以通过调用zend_eval_string函数来执行任意PHP代码。
  5. 最后,整个攻击过程使得程序可以正常的终止,同时还从响应主体中获取了输出。

一旦运行了上面的代码,我们就可以看到Pornhub的“/etc/passwd”文件。不仅如此,我们还能够执行其他命令,或者直接脱离PHP执行任意系统调用。但是,此时利用PHP是更方便的。最后,我们转储了有关底层系统的一些信息,并立即编写了报告并通过Hackerone向Pornhub提交。

本文为翻译文章(有删改),点击阅读原文,跳转至原文链接。

https://www.evonide.com/how-we-broke-php-hacked-pornhub-and-earned-20000-dollar/

本文分享自微信公众号 - ChaMd5安全团队(chamd5sec)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-10-30

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏nginx遇上redis

Libmicrohttpd嵌入式服务

GNU Libmicrohttpd是一个用来在项目中内嵌http服务器的C语言库,它具有以下几个非常鲜明的特点:

9440
来自专栏APP自动化测试

NSObject头文件解析 / 消息机制 / Runtime解读 (一)

上面是NSObject对象的头文件类部分, 可以看到还有一个NSObject protocol 我们也仔细看看都有什么协议方法@protocol NSObjec

8420
来自专栏用户5521492的专栏

聊聊 Python 字符串连接的七种方式

我是狗哥,一名程序猿。做过 Android、撸过 Java、目前在自学 Python 。注册 「一个优秀的废人」这个公号已有些日子,真正有心将它运营起来是这两天...

10830
来自专栏SeanCheney的专栏

《机器学习实战:基于Scikit-Learn、Keras和TensorFlow》第10章 使用Keras搭建人工神经网络

下载本书代码和电子书:https://www.jianshu.com/p/4a94798f7dcc

20430
来自专栏玩转JavaEE

猜一猜, for (;;) 与 while (true) 哪个更快?

其次,for (;;) 在Java中的来源。个人看法是喜欢用这种写法的人,追根溯源是受到C语言里的写法的影响。这些人不一定是自己以前写C习惯了这样写,而可能是间...

11710
来自专栏C语言及其他语言

C++语言的特点 【上】

C++语言是在C语言的基础上发展而来,同时它又支持面向对象的程序设计,它主要具有以下特点:

6810
来自专栏CDA数据分析师

Python入门你要懂哪些?这篇文章总算讲清楚了

从今天开始学习Python,今后会不定期更新Python的相关文章。好了,言归正传,今天我们来看看对于Python初学者,你要知道了解Python的哪些基础知识...

15920
来自专栏EffectiveCoding

Go panic & recover

之前针对于go 的错误和异常做了简单的介绍,对于panic介绍的不算多,本篇从原理和源码的角度来看一下panic 和 recover是怎么运作的。 panic...

8420
来自专栏Linux内核及编程语言底层相关技术研究

c语言内嵌汇编代码相关文章列表

最近为了了解一些操作系统的知识,学了下如何在c中写汇编代码,参考的gcc官方文档如下:

11120
来自专栏JAVAandPython君

【Basic algorithm学习笔记】 排序

原理: 其实原理很简单 大白话来讲 就是 先定义很多 容器 即这里形象的表达 为桶,之后每次输入的数字则为选择哪个桶,将对应的 桶中扔一个标志物表示有这个 桶...

7120

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励