在分析了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中没有这种类型漏洞的报告。因此,审计它好比挤压已经压榨过的柠檬,经过如此多的关注和众多的安全修复,潜在漏洞不应该已经全部被修复掉了吗?
为了找到答案,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/)
栈和堆以及任何其他可写段都被标记为不可执行
https://en.wikipedia.org/wiki/Executable_space_protection
即使能够控制指令指针(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进程内存布局的任何信息。
如前所述,我们需要获得有关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时,就会触发该信息泄漏。
通常,可以从泄漏二进制文件的相关信息开始,如前所述,二进制文件的起始地址一般从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)。由于我们这个漏洞的特殊性质,并且为了使复杂度尽可能低,我们使用了自己的技巧。
地址0xeae040是PHP的uninitialized_bucket的符号地址,直接指向PHP的BSS段。您可以看到它在最后释放的块附近多次发生。如前所述,释放了许多空数组。因此,通过利用某些哈希表条目在堆中保持不变的情况,我们能够泄漏这个特定符号。
最后,我们可以从uninitialized_bucket符号地址开始应用逐页向后扫描,以找到ELF标头:
要获取提供的post数据的地址,您可以通过读取以下内容来泄漏更多的指针:
当创建这样一个伪造的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元素,其定义如下:
现在我们可以分别设置add_ref指针或RAX来接管指令指针(instruction pointer)。尽管这提供了一个起点,但并不能确保所有ROP gadgets都已执行,因为一旦从第一个gadget返回,CPU就会从当前堆栈中弹出下一条指令的地址。我们对此堆栈没有任何控制权,因此,有必要将堆栈转移到我们的ROP链中。这就是为什么下一步是将RAX复制到RSP并继续从那里进行ROP的原因。使用本地编译的PHP版本,我们搜索了可用于stack pivoting gadget的代码片段,并发现php_stream_bucket_split包含以下代码:
infoleak向量使我们能够快速转储反汇编的php_stream_bucket_split函数并检查我们的stack pivoting gadget在远程版本上是否可用。幸运的是,只需要对gadgets的偏移量进行少量校正即可。最后,我们实施了一些检查以确认所有地址都是正确的:
最终的ROP payload可以执行如下PHP代码:zend_eval_string(code); exit(0);
这个payload看起来像下面的代码片段:
通常,php-cgi将生成的内容转发回Web服务器,以便将其显示在网站上,但是由于坏的控制流使得PHP异常终止,因此其结果将永远不会到达HTTP服务器。为了解决这个问题,我们只是简单地配置PHP使用通常用于HTTP流传输的直接无缓冲响应:
最终,这使我们可以直接获取生成的PHP payload的每个输出,而不必担心CGI进程将数据发送到Web服务器时通常涉及的清理例程。这通过最小化潜在的错误和崩溃的数量,进一步增加了攻击过程的隐蔽性。
总而言之,我们的payload包含一个伪造的对象,其add_ref函数指针指向我们的第一个ROP gadget。下图将这个概念形象化:
创建了我们的伪造对象,该伪造对象随后作为参数传递给setcookie函数。
这导致了对我们提供的add_ref函数的调用,即它使我们获得了程序计数器(program counter)的控制权。
然后,我们的ROP链准备了所有已讨论的寄存器/参数。
接下来,我们可以通过调用zend_eval_string函数来执行任意PHP代码。
最后,整个攻击过程使得程序可以正常的终止,同时还从响应主体中获取了输出。
一旦运行了上面的代码,我们就可以看到Pornhub的“/etc/passwd”文件。不仅如此,我们还能够执行其他命令,或者直接脱离PHP执行任意系统调用。但是,此时利用PHP是更方便的。最后,我们转储了有关底层系统的一些信息,并立即编写了报告并通过Hackerone向Pornhub提交
本文为翻译文章(有删改),原文链接。https://www.evonide.com/how-we-broke-php-hacked-pornhub-and-earned-20000-dollar/
本文作者:ChaMd5安全团队
本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/119239.html