PHP-fpm 远程代码执行漏洞(CVE-2019-11043)源码分析
一、漏洞复现
1、搭建docker环境(yum install docker-re)
2、拉取镜像
配置docker-compose.yml文件,并拉取镜像
docker-compose up -d
version: '2' services: nginx: image: nginx:1 volumes: - ./www:/usr/share/nginx/html - ./default.conf:/etc/nginx/conf.d/default.conf depends_on: - php ports: - "8080:80" php: image: php:7.1.32-fpm volumes: - ./www:/var/www/html
default.conf
server { listen 80 default_server; listen [::]:80 default_server; root /usr/share/nginx/html; index index.html index.php; server_name _; location / { try_files $uri $uri/ =404; } location ~ [^/]\.php(/|$) { fastcgi_split_path_info ^(.+?\.php)(/.*)$; include fastcgi_params; fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_index index.php; fastcgi_param REDIRECT_STATUS 200; fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT /var/www/html; fastcgi_pass php:9000; } }
二、源码分析
static void init_request_info(void)
{
fcgi_request *request = (fcgi_request*) SG(server_context);
//文件绝对路径
char *env_script_filename = FCGI_GETENV(request, "SCRIPT_FILENAME");
//env_path_translated值和env_script_filename值一样
char *env_path_translated = FCGI_GETENV(request, "PATH_TRANSLATED");
char *script_path_translated = env_script_filename;
char *ini;
int apache_was_here = 0;
/* some broken servers do not have script_filename or argv0
* an example, IIS configured in some ways. then they do more
* broken stuff and set path_translated to the cgi script location */
if (!script_path_translated && env_path_translated) {
script_path_translated = env_path_translated;
}
/* initialize the defaults */
SG(request_info).path_translated = NULL;
SG(request_info).request_method = NULL;
SG(request_info).proto_num = 1000;
SG(request_info).query_string = NULL;
SG(request_info).request_uri = NULL;
SG(request_info).content_type = NULL;
SG(request_info).content_length = 0;
SG(sapi_headers).http_response_code = 200;
if (script_path_translated) {
const char *auth;
//获取request请求中的参数
char *content_length = FCGI_GETENV(request, "CONTENT_LENGTH");
char *content_type = FCGI_GETENV(request, "CONTENT_TYPE");
char *env_path_info = FCGI_GETENV(request, "PATH_INFO");
char *env_script_name = FCGI_GETENV(request, "SCRIPT_NAME");
...
if (CGIG(fix_pathinfo)) {
struct stat st;
char *real_path = NULL;
char *env_redirect_url = FCGI_GETENV(request, "REDIRECT_URL");
char *env_document_root = FCGI_GETENV(request, "DOCUMENT_ROOT");
char *orig_path_translated = env_path_translated;
char *orig_path_info = env_path_info;
char *orig_script_name = env_script_name;
char *orig_script_filename = env_script_filename;
int script_path_translated_len;
...
if (script_path_translated &&
//script_path_translated_len是请求uri_path中第一个斜杠前的内容:如http://127.0.0.1/index.php/test,则变量的值为/var/www/html/index.php的长度
(script_path_translated_len = strlen(script_path_translated)) > 0 &&
(script_path_translated[script_path_translated_len-1] == '/' ||
#ifdef PHP_WIN32
script_path_translated[script_path_translated_len-1] == '\\' ||
#endif
(real_path = tsrm_realpath(script_path_translated, NULL)) == NULL)
) {
//字符串复制
char *pt = estrndup(script_path_translated, script_path_translated_len);
//url的长度取决于nginx的配置当请求url,http://127.0.0.1/index.php/123%0atest.php。script_path_translated来自于nginx的配置,为/var/www/html/index.php/123\ntest.php
int len = script_path_translated_len;
...
int ptlen = strlen(pt);
int slen = len - ptlen;
//request中path_info的长度,此参数值可控
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 {
//在c语言中,char *变量,加一个int数字,是一个使指针指向的地址偏移
path_info = env_path_info ? env_path_info + pilen - slen : NULL;
tflag = (orig_path_info != path_info);
}
下面的代码进行举例分析:
path_info = env_path_info ? env_path_info + pilen - slen : NULL;
替换成类似代码:由此可以看出,下面的代码在c语言中可以起到偏移char *首地址的#include <stdio.h>
int main() {
char *a = "aaaaaaaa";
char *b = a-2;
printf("%s",b);
//path_info[0]此地址对应的是path_info的首地址。根据fpm代码,则是可操作堆上任意数据置为0,那我们就可以把_fcgi_data_seg结构体的char* pos置零
FCGI_PUTENV(request, "ORIG_PATH_INFO", orig_path_info); old = path_info[0]; path_info[0] = 0; //orig_script_name变量不为空 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);//进入此函数
}
php源码中FCGI_PUTENV函数
#define FCGI_PUTENV(request, name, value) \ fcgi_quick_putenv(request, name, sizeof(name)-1, FCGI_HASH_FUNC(name, sizeof(name)-1), value)
查看fcgi_quick_putenv函数
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));
}
}
查看fcgi_hash_set函数,其中fcgi_request结构体为
//此为fcgi_request的机构体
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;//此处是request->env存放的位置,存储的是nginx配置的ENV全局变量,在fcgi_hash_set中的变量名为h
};
现在请联系之前,path_info[0]=0这段代码,这段代码表示我们可以随意在栈上的任意位置置为0。现在也就是说,传入fcgi_hash_set函数中的fcgi_hash *h我们可以修改这个变量中的任意位置为0,
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)) {
//php全局变量名称hash_value相等,p中的var值以传入的固定的全局变量名称开头就可以,以及全局变量名称长度相同,则可覆盖到php其他全局变量的值,hash函数如下
/***
*memcmp是比较内存区域buf1和buf2的前count个字节。该函数是按字节进行比较的
*memcmp(p->var, var, var_len)这段函数是比较p->var中的值是否是以var的值开头
*#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)
*/
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;
//进入fcgi_hash_strndup此函数
p->var = fcgi_hash_strndup(h, var, var_len);
p->val_len = val_len;
p->val = fcgi_hash_strndup(h, val, val_len);
return p->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)) {
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;
}
到现在,全局变量的值已经可控的了。
如何使用修改全局变量导致远程代码执行,可参考poc
浙公网安备 33010602011771号