干货|CVE-2019-11043: PHP-FPM在Nginx特定配置下任意代码执行漏洞分析
近期,国外安全研究员Andrew Danau,在参加夺旗赛(CTF: Capture the
Flag)期间,偶然发现php-fpm组件处理特定请求时存在缺陷:在特定Nginx配置下,特定构造的请求会造成php-fpm处理异常,进而导致远程执行任意代码。当前,作者已经在github上公布了相关漏洞信息及自动化利用程序。鉴于Nginx+PHP组合在Web应用开发领域拥有极高的市场占有率,该漏洞影响范围较为广泛。
漏洞概述
PHP-FPM在Nginx特定配置下存在任意代码执行漏洞。具体为:
使用Nginx + PHP-FPM搭建的服务器在使用类似如下配置的nginx.conf时:
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的置零操作位置可控,通过构造特定长度和内容的请求,可以覆盖写特定位置数据,插入特定环境变量,进而导致代码执行。
漏洞分析
首先,分析其补丁:在进行request_info结构体初始化的static void init_request_info(void)函数中,增添对pilen 和slen的大小校验,规避了指针的非预期回溯移动。
// 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;
}
...
其中
//以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便是这样一处位置。这里需要说明一下各变量的存储方式。
通过fastcgi协议传入的各环境变量会存储到_fcgi_request->env 这个fcgi_hash结构体中,供后续执行取用,结构具体定义如下:
// 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操作,实现如下:
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还存在以下要求及限制:
指针前移受当前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")干货|CVE-2019-11043: PHP-FPM在Nginx特定配置下任意代码执行漏洞分析的更多相关文章
- 【第六课】Nginx常用配置下详解
目录 Nginx常用配置下详解 1.Nginx虚拟主机 2.部署wordpress开源博客 3.部署discuz开源论坛 4.域名重定向 5.Nginx用户认证 6.Nginx访问日志配置 7.Ngi ...
- CVE-2019-11043 Nginx PHP 远程代码执行漏洞复现
漏洞背景:来自Wallarm的安全研究员Andrew Danau在9月14-16号举办的Real World CTF中,意外的向服务器发送%0a(换行符)时,服务器返回异常信息.由此发现了这个0day ...
- MongoDB干货系列2-MongoDB执行计划分析详解(2)(转载)
写在之前的话 作为近年最为火热的文档型数据库,MongoDB受到了越来越多人的关注,但是由于国内的MongoDB相关技术分享屈指可数,不少朋友向我抱怨无从下手. <MongoDB干货系列> ...
- FPM制作Nginx的RPM软件包
FPM制作Nginx的rpm软件包 FPM相关参数-s:指定源类型-t:指定目标类型,即想要制作为什么包-n:指定包的名字-v:指定包的版本号-C:指定打包的相对路径-d:指定依赖于哪些包-f:第二次 ...
- nginx服务配置---php服务接入
前言: 最近要搭建一个内部的wiki系统, 网上搜了一圈, 也从知乎上搜集了一些大神的评价和推荐. 重点找了几个开源的wiki系统, 不过发现他们都是采用php来实现的. 于是乎需要配置php环境, ...
- Nginx安装配置PHP(FastCGI)环境的教程
这篇是Nginx安装配置PHP(FastCGI)环境的教程.Nginx不支持对外部程序的直接调用或者解析,所有的外部程序(包括PHP)必须通过FastCGI接口来调用. 一.什么是 FastCGI F ...
- nginx上配置phpmyadmin
Nginx配置phpmyadmin流程如下: 一.准备软件和环境(这里我以ubuntu16.04为例) 1.安装php7.1 sudo LC_ALL=C.UTF- add-apt-repository ...
- mac 安装nginx,并配置nginx的运行环境
1. 安装nginx // 查询有没有nginx brew search nginx //开始安装nignx brew install nginx 2. 检查nignx是否安装成功 nginx -V ...
- nginx解析漏洞,配置不当,目录遍历漏洞环境搭建、漏洞复现
nginx解析漏洞,配置不当,目录遍历漏洞复现 1.Ubuntu14.04安装nginx-php5-fpm 安装了nginx,需要安装以下依赖 sudo apt-get install libpcre ...
随机推荐
- 使用WinDbg分析蓝屏dump原因
大多数人或许都经历过系统蓝屏问题,然而大多数人不清楚该怎么处理蓝屏问题,这里主要对系统蓝屏做一些解释,同时介绍下蓝屏问题分析工具WinDbg分析蓝屏问题的一般步骤. 微软官方对蓝屏的定义是,当系统遇到 ...
- 吴裕雄--天生自然java开发常用类库学习笔记:正则表达式
public class RegexDemo01{ public static void main(String args[]){ String str = "1234567890" ...
- asp.net mvc3用file上传文件大小限制问题
在Windows2008下,如果上传比较大的文件,可能会出现404错误,(请求筛选模块被配置为拒绝超过请求内容长度的请求). 可通过如下方法解决: 打开URTracker根目录下的web.config ...
- 【pwnable.kr】 codemap
pwnable新的一题. download: http://pwnable.kr/bin/codemap.exe ssh codemap@pwnable.kr -p2222 (pw:guest) 这道 ...
- expect 自动输入密码
测试环境centos 6.5 7.4 1.远程登陆执行命令 scp #!/bin/bash expect -c ' set timeout 10000 spawn ssh root@192.168. ...
- nginx常用配置解析
1.常用公共参数(一般放在http下面,虽然很多参数都支持server和location) keepalive_timeout 60; #单位为s keepalive_request 2; #设 ...
- 119-PHP调用private成员的方法
<?php class ren{ //定义人类 private $birthday='1990-12-20'; //定义private修饰的成员属性 public function say_bi ...
- Spark调优(一)
一.对多次使用的RDD进行持久化 如何选择一种最合适的持久化策略? 默认情况下,性能最高的当然是MEMORY_ONLY,但前提是你的内存必须足够足够大, 可以绰绰有余地存放下整个RDD的所有数据.因为 ...
- JavaWeb学习记录
服务器端跳转(请求重定向): 1.jsp内跳转 : <jsp:forward page="page_scope_03.jsp"/> 客户端跳转(请求转发): 1.通过超 ...
- c++程序—输入
#include<iostream> using namespace std; #include<string> int main() { string str = " ...
- 【第六课】Nginx常用配置下详解