這片文章所花的時間比我想象中的要長, 不過這時間花得值! 我想通過視頻的方式來講解如何利用這漏洞, 所以這片文章沒有之前兩篇描述得詳細.
可能會讓一些人失望的是, 這篇文章只是介紹了如何寫一個 POC (并沒有實際放出 POC). 文章末尾的視頻會給你們展示我的自動化遠程利用工具, 以及在現實環境中為了能讓這 POC 成功執行所使用的 tips & trick.
為了能更簡單的解釋如何利用這個漏洞, 我使用下面的代碼作為演示, 具體情況具體分析.
#!php
<?php
echo serialize(unserialize(base64_decode($_GET['data'])));
?>
我們的目的是能夠執行任意 PHP 代碼. 當然啦, 我們可以嘗試去注入 shellcode 來達到目的, 但是這種方法既沒有創造性, 也不優雅(高版本的 PHP 可能不會成功). 如果你還記得 Part1, 為了能夠執行任意 PHP 代碼, 我們需要調用 php_execute_script
和 zend_eval_string
. 然而, 我們希望能夠進行遠程攻擊, 所以我們必須找到 executor_globals
以及 JMP_BUF
. 至于為什么要這樣做, 后面會詳細介紹.
簡而言之, 我們需要找到 (沒有特別的順序):
executor_globals
zend_eval_string
JMP_BUF
比較幸運的是, 上面所列舉的要求, 有些還是比較好找的, 因為它們就在 binary 中. 我們直接 dump PHP binay 的 strtab.
Great! 我們直接從里面找到 zend_eval_string
的地址, 然后在 GDB 中驗證這個地址是否正確.
查找 zend_eval_string
的地址
在 gdb 中查看對應的地址
查找 executor_global
的地址
在 gdb 中查看對應的地址
Awesome! 現在我們要怎么找到 JMP_BUF
呢? 通過閱讀代碼, 我們找到了 _zend_executor_globals
對象, 并在其中發現了一個 JMP_BUF
指針, 名為 bailout
. 讓我們掛上 GDB 看看地址是否正確.
查看 zend_executor_globals
對象
打印 zend_executor_globals->bailout
好, 我們拿到了這個地址, 但是這個地址指向的是什么? 有什么用? 好吧, 在 PHP 中 JMP_BUF
被用來實現 PHP 的 "try{} - catch{}" 機制. 后面會詳細介紹這個.
我們現在還只差一樣東西: 將任意數據寫入 stack. 利用方法 2 會詳細討論 Stefan 在 2010 Syscan 公布的方法. 既然我們可以釋放任意內存, 那我們下一步該干什么呢? 我們該怎么寫數據到 stack 中? 怎么確保寫入的數據以后不會被覆蓋? (Google is your friend :))
RFC, 更確切的說是: RFC 1867
這個 RFC 指明允許帶有 multipart/form-data 的 POST 請求的數據寫入到 stack 中, 而且完全不會被 php 覆蓋(由于各種各樣的原因). 讓我們上傳一個"普通"的文件吧.
Awesome! 我們可以寫入任意數據到 stack 中. 但是我們要寫什么到 stack 中?
Hint: 我們之前所尋找的東西 : )
既然之前我們花了那么多時間去找那么多地址, 該時候使用它們了! 所以, 我們應該如何使用之前的地址呢? 經過一些研究, 我們需要這樣布局:
從最簡單的開始: 一開始我們就用 readelf 獲取到了 zend_eval_string
的地址. Ret Pointer
和 Zend_Bailout
我們都不需要理會(會導致 PHP crash). 我們在 stack 中填寫兩次指向 eval_string
的指針, 下面就是我們現有的數據:
Sweet! 我們快填寫完這些東西了, excellent! 但是, 看起來我們還需要一些 ROP gadgets. 我個人比較喜歡用 ROPGadget, 不過其它工具也是可以的. 我們需要查找 XCHG EAX, ESP; RET (0x94 0xc3), 還需要找 POP EBP; RET (0x5d 0x3c). 一旦找到了這些 gadgets, 我們就可以繼續下一步了.
我們拿到了這兩個地址了(為啥這些地址相差那么大, 因為它們是相對地址), 我們可以繼續完成 stack 中的數據:
好了, 該是時候測試了.
Hmmm, 這不是我想要的結果, 現在怎么辦? 看起來好像我們的代碼嘗試跳到我們的 gadget (c394). 不幸的是, 你還需要知道一些事情. SPLObjectStorage 要求這些 gadget 在 php 是可以訪問的, 所以我們還需要修改一下. 經過修改之后:
方法 1 就到此為止了, 方法 1 只能影響老版本的 PHP. 我們繼續研究新版本的 PHP 利用方法.
比較走運的是, 之前找到 php_execute_script
和 jmp_buf
地址, 在新 exploit 中都會被用到.
jmp_buf
在 setjmp & longjmp 中被用來保存 "環境" 以預防 "不可恢復" 的錯誤. 在 32 位系統中, jmp_buf
是一個存儲 6 個 int 的數組, 在 64 位系統中, jmp_buf
存儲的是 8 個 int 的數組. 不幸的是, 需要自己查看代碼來判斷 jmp_buf
保存的寄存器的順序. 這里有個 jmp_buf
樣例布局. 讓我們看一下 PHP 中的內容...
在我的機器上, 寄存器的順序是: ebx, esp, ebp, esi, edi, eip. 值得完成的事情一般都不怎么容易完成, 在這里也一樣, 我們的 edi & eip 看起來貌似被 Glibc 混淆了, Glibc 有個宏叫 PTR_MANGLE
, 在視頻中, 我們會講解如何破解 JMPBUF.
一旦破解出了 edi & eip, 我們就可以繼續重寫和釋放內存了. 幸運的是, 我們可以繼續利用 SPLObjectStorage
遠程釋放內存. 剩下的事情就是將如何寫到 stack 中. 和 Part 2, 我們可以任意操縱 PHP 內存. 我們先釋放一些內存, 然后再寫 7 byte 數據填充, 當 php 重寫我們的數據時, 再重復之前的操作. 第二次重寫能夠讓我們寫入任意長度的數據到 stack 中 (我測試的時候, 這個長度大概可以達到 2048 byte). 我們寫入的數據和之前使用 ROP 的那個例子差不多. 我們還要繼續 "加密" 我們寫入 stack 中的數據. 這是攻擊效果:
video, 自備梯子
就和作者說的一樣, 這篇文章沒有之前兩篇寫得詳細.
原文下有評論, 作者說他通過 memory leak 獲取了整個 php binary 文件.
正常情況下, 一般的套路就是:
\x7fELF
找到起始地址strtab
, symtab
找到 zend_eval_string
, php_execute_script
, executor_globals
地址.jmpbuf 是 setjmp, longjmp 所使用的數據結構, 以實現 try--catch 機制的東西, 和 goto 語法效果差不多, setjmp 相當于在某個位置的 label, longjmp 相當于 goto, 但是 goto 語法并不能跨函數跳轉. jmpbuf 主要保存著 caller 的寄存器信息以方便 longjmp 恢復. 另外 glibc 會混淆一些寄存器的值(除了有漏洞的glibc).
先查看 setjmp 代碼:
#!bash
(gdb) disassemble setjmp
Dump of assembler code for function setjmp:
0xb7c94410 <+0>: mov eax,DWORD PTR [esp+0x4]
0xb7c94414 <+4>: mov DWORD PTR [eax],ebx # 1. 保存 ebx
0xb7c94416 <+6>: mov DWORD PTR [eax+0x4],esi # 2. 保存 esi
0xb7c94419 <+9>: mov DWORD PTR [eax+0x8],edi # 3. 保存 edi
0xb7c9441c <+12>: lea ecx,[esp+0x4]
0xb7c94420 <+16>: xor ecx,DWORD PTR gs:0x18
0xb7c94427 <+23>: rol ecx,0x9
0xb7c9442a <+26>: mov DWORD PTR [eax+0x10],ecx # 4. 保存 esp
0xb7c9442d <+29>: mov ecx,DWORD PTR [esp]
0xb7c94430 <+32>: xor ecx,DWORD PTR gs:0x18
0xb7c94437 <+39>: rol ecx,0x9
0xb7c9443a <+42>: mov DWORD PTR [eax+0x14],ecx # 5. 保存 eip
0xb7c9443d <+45>: mov DWORD PTR [eax+0xc],ebp # 6. 保存 ebp
0xb7c94440 <+48>: push 0x1
0xb7c94442 <+50>: push DWORD PTR [esp+0x8]
0xb7c94446 <+54>: call 0xb7c943c0 <__sigjmp_save>
0xb7c9444b <+59>: pop ecx
0xb7c9444c <+60>: pop edx
0xb7c9444d <+61>: ret
上面的寄存器保存的都是 caller 的寄存器狀態, 其中 esp, eip 都被混淆過了(作者自己的圖也是 esp 和 eip 被混淆), 就是使用 PTR_MANGLE
進行混淆.
PTR_MANGLE
和 PTR_DEMANGLE
宏定義如下:
#!cpp
# define PTR_MANGLE(reg) xorl %gs:POINTER_GUARD, reg; \
roll $9, reg
# define PTR_DEMANGLE(reg) rorl $9, reg; \
xorl %gs:POINTER_GUARD, reg
其中 gs:0x18 就是上面的 POINTER_GUARD
setjmp() 使用 PTR_MANGLE
進行混淆寄存器, longjmp() 使用 PTR_DEMANGLE
解出正常的寄存器. 為了后續能過正常覆蓋 jmpbuf, 所以我們需要獲得 POINTER_GUARD
的值, 由于 jmpbuf 數據結構可以越界讀, caller 的 eip 也可以拿到, 所以通過 PTR_DEMANGLE
就可以獲得 POINTER_GUARD
的值.
通過閱讀代碼, 我們可以知道 php_execute_script
調用了 setjmp, 并將 jmpbuf 保存到 EG(bailout) 中, 通過泄漏 php_execute_script
地址 即可知道調用 setjmp 時到 eip.
jmpbuf 地址向前搜索數值 XX 00 00 00 (XX>0x0c and XX<0x8f), 搜索到一個這樣的值之后, 可以把這個值當作一個 memory block.
先看看 ZMM 的幾個結構體:
#!cpp
/* mm block type */
typedef struct _zend_mm_block_info {
size_t _size;
size_t _prev;
} zend_mm_block_info;
.
#!cpp
typedef struct _zend_mm_free_block {
zend_mm_block_info info;
struct _zend_mm_free_block *prev_free_block;
struct _zend_mm_free_block *next_free_block;
struct _zend_mm_free_block **parent;
struct _zend_mm_free_block *child[2];
} zend_mm_free_block;
.
#!cpp
struct _zend_mm_heap {
int use_zend_alloc;
void *(*_malloc)(size_t);
void (*_free)(void*);
void *(*_realloc)(void*, size_t);
size_t free_bitmap;
size_t large_free_bitmap;
size_t block_size;
size_t compact_size;
zend_mm_segment *segments_list;
zend_mm_storage *storage;
size_t real_size;
size_t real_peak;
size_t limit;
size_t size;
size_t peak;
size_t reserve_size;
void *reserve;
int overflow;
int internal;
#if ZEND_MM_CACHE
unsigned int cached;
zend_mm_free_block *cache[ZEND_MM_NUM_BUCKETS];
#endif
zend_mm_free_block *free_buckets[ZEND_MM_NUM_BUCKETS*2];
zend_mm_free_block *large_free_buckets[ZEND_MM_NUM_BUCKETS];
zend_mm_free_block *rest_buckets[2];
int rest_count;
};
我們需要關注的是 _zend_mm_heap
中的 cached. ZMM 會將 0x10 大小的內存塊放進 cached 中, 所以當我們找到一個可以當做 memory block 之后, 最后幾個字節(7 byte 數據)偽造一個 memory header (_zend_mm_block_info), 然后再用 string 重用這個偽造后的 memory block, 如果寫入的長度不足以覆蓋 jmpbuf, 繼續偽造 memory header 相關的操作, 直到能夠覆蓋 jmpbuf 為止.
將 eip 設置為 zend_eval_string
, 將 esp 設置為一個直接可控的 stack(比如說 jmpbuf 之后), 填充好 jmpbuf, 該混淆的寄存器繼續混淆. 然后在這個可控的 stack 上設置好 zend_eval_string
的參數, zend_eval_string
的定義如下:
#!c
ZEND_API int zend_eval_string(char *str, zval *retval_ptr, char *string_name TSRMLS_DC)
最后觸發一個 exception, 即可執行我們想要的代碼.
php7 的 zval 格式有很大的變化, 通過字符串數據覆蓋 zval 結構沒法再做到讀取任意地址數據了, 只能向后讀取數據(drops 這篇文章的作者 libnex 說他有辦法, 期待新文章).
#!c
struct _zval_struct {
zend_value value; /* value */
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar type, /* active type */
zend_uchar type_flags,
zend_uchar const_flags,
zend_uchar reserved) /* call info for EX(This) */
} v;
uint32_t type_info;
} u1;
union {
uint32_t var_flags;
uint32_t next; /* hash collision chain */
uint32_t cache_slot; /* literal cache slot */
uint32_t lineno; /* line number (for ast nodes) */
uint32_t num_args; /* arguments number for EX(This) */
uint32_t fe_pos; /* foreach position */
uint32_t fe_iter_idx; /* foreach iterator index */
} u2;
};
.
#!c
struct _zend_string {
zend_refcounted_h gc;
zend_ulong h; /* hash value */
size_t len;
char val[1];
};
如果通過數據去覆蓋 zval_struct
, 只能通過修改 len 來實現向后讀取.
std_object_handlers
信息, 隨便找一個數值較小的地址\x7FELF
.zend_eval_string
, php_execute_script
, executor_globals
(作者圖省事, 文章直接本地 readelf)excutor_globals
可以拿到 bailout 地址 (也就是 jmpbuf 地址)php_execute_script
獲取到調用 setjmp 時的 eipPTR_DEMANGLE
即可獲得 POINTER_GUARD
的值.zend_eval_string
的地址與之前的 POINTER_GUARD
進行 PTR_MANGLE
寫入到 jmpbuf 的 eip 中.PTR_MANGLE
之后寫入到 jmpbuf 的 esp 中.如果還有疑惑的地方, 可以去看看作者的視頻以及樹人的 paper. 如果我補充的有不正確的地方, 請不吝賜教.