vscode 调 PHP-FPM+Nginx漏洞 (CVE-2019-11043)

环境配置

php-fpm

将最大子进程数置为1

1
2
3
4
5
[www]
...
pm = static
...
pm.max_children = 1

nginx

1
2
3
4
5
6
7
8
9
location ~ [^/]\.php(/|$) {
root /PATH/TO/WEBROOT;
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
fastcgi_pass 127.0.0.1:9000;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}

vscode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "(lldb) Attach",
"type": "cppdbg",
"request": "attach",
"program": "/PATH/TO/php-fpm",
"processId": "${command:pickProcess}",
"MIMode": "lldb"
}
]
}

漏洞解析

先来说说nginx的配置
fastcgi_split_path_info ^(.+?\.php)(/.*)$;

  • 第一个捕获的值会重新赋值给$fastcgi_script_name变量。
  • 第二个捕获到的值会重新赋值给$fastcgi_path_info变量。

如果访问/index.php/123

这里的env_xxx其实是FCGI_GETENV(request, "XXX")获得的,而XXX则是nginx给的参数,这会在后面解释

但是如果访问/index.php/%0a123

会出现上图的情况
php-fpm对这种env_script_filename无法解析的情况会对其重新进行解析,分离出env_path_info
我们看到这重新解析的代码(也就是存在漏洞的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}

继续以/index.php/%0a123为例
结合上下代码可以知道

  • ptlen/usr/local/Cellar/nginx/1.15.11/html/index.php的长度也就是46
  • len/usr/local/Cellar/nginx/1.15.11/html/index.php/\n123的长度也就是51
  • pilen 从上文中看到为0

这样的话path_info = env_path_info - 5
往前走得到前面字符串PATH_INFOINFO

继续往后看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}
...

可以看到path_info[0]被赋0
所以这个漏洞的作用可以概括为将一个任意字节改成\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()入手

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#define FCGI_PUTENV(request, name, value) \
fcgi_quick_putenv(request, name, sizeof(name)-1,FCGI_HASH_FUNC(name, sizeof(name)-1), value)
...
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;
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); // value
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)) {
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;
}

列一下几个相关的结构体

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
34
35
36
37
38
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_data_seg {
char *pos;
char *end;
struct _fcgi_data_seg *next;
char data[1];
} fcgi_data_seg;

从代码中可以看到fcgi_data_seg.data[1]不代表只有一个元素,代表的是长度可变
fcgi_data_seg.pos始终指向fcgi_data_seg.data未使用空间空间的起始位置
如果我们进入第二个fcgi_hash_strndup函数(也就是给value赋值
value的值是可控的
并且将h->data->pos利用上述漏洞置0
则可以覆盖全局变量的值

开始实验

首先需要知道request->env.data->data里的内容
这可以从request->env.buckets->data的var和val看到

但是如上图所示,现在是在data[19]位置,但如果在非调试位置下,我们无法确定真实的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)) {
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;
}

h->data->pos + str_len + 1 >= h->data->end
会malloc一个新的空间
这时候可能可以将env_path_info放到第一个data位置上

1
2
3
4
5
6
7
8
char *pos 
------------- +8
char *end
------------- +8
char *next
------------- +8
\x00 <---- env_path_info
-------------

这样env_path_infopos的地址相差24
那么如何使他malloc
根据exp,第一步先发送?QQQQ..字符串会出现在两个地方


之后将pos的第五个字节置0,由于地址非法导致崩溃返回502


这一步exp里是爆破出来的,由于我能看到地址,直接调试出来了
然后在加4位
修改前

修改后

修改前

修改后

可以修改全局变量了,首先想到的是修改PHP_VALUE,这里先看下FCGI_GETENV的源码

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
#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)
...
#define FCGI_GETENV(request, name) \
fcgi_quick_getenv(request, name, sizeof(name)-1, FCGI_HASH_FUNC(name, sizeof(name)-1))
...
char* fcgi_quick_getenv(fcgi_request *req, const char* var, int var_len, unsigned int hash_value)
{
unsigned int val_len;

return fcgi_hash_get(&req->env, hash_value, (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;
}

根据FCGI_HASH_FUNC取索引


这里我们不能直接控制PHP_VALUE这个变量,但恰巧有那么一个ebut请求头,它在进入php-fpm时会变成HTTP_EBUT,这个和PHP_VALUE的长度索引都一样
全局搜索PHP_VALUE并下断点
利用D-Pisos头来调节位置最后使其到如图所示的位置

最后可以从返回中看到

漏洞复现成功
之后就是利用php_value拿shell,这里就不展开了

总结

这次复现学到了很多,漏洞的利用实在是太巧了

参考

https://github.com/neex/phuip-fpizdam
https://bithack.io/forum/639
https://www.lorexxar.cn/2019/10/25/php-fpm-rce/