近期,国外安全研究员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还存在以下要求及限制:

  1. 指针前移受当前fcgi_hash_seg空间结构影响,过短无法将char*

    pos置零,过长会分配到新fcgi_hash_seg空间。(如传递"形如"http://127.0.0.1/Somefile_exits/AAAAA.php/"也可造成指针后移,)

  2. path_info[0] = 0 仅能将单字节置零,最好为最低位,否则会造成指针位置偏离过多。

  3. 鉴于条件 2 被覆盖写的地址最低位应为0,且其后为符合条件的可覆盖的环境变量。

  4. 被覆盖位置环境变量的key必须与预期写入的key满足:var、hash_value和var_len均相同,才可能被读取。

  5. 执行FCGI_PUTENV(request, "ORIG_PATH_INFO",

    orig_path_info);时,分别写入ORIG_SCRIPT_NAME、orig_script_name("ORIG_SCRIPT_NAME/index.php/PHP_VALUE\nAAAAAA")。

相应地,我们可以:

  1. 通过控制query_string的长度,使path_info恰好处于新fcgi_hash_seg的data首位,这时我们仅需移动8+8+8+len("PATH_INFO\0")+N= 34 + N即可完成对char* pos的篡改。满足条件1,2的要求。
  2. 通过自定义http header,操纵request header的长度将预期覆盖的环境变量放置到特定的位置(0x____00+len("ORIG_SCRIPT_NAME")+len("/index.php/"))。满足条件3,5要求。(在NGINX中,HTTP中的请求头会以"HTTP_XXX"的形式传入PHP-FPM,随后写入到request-env中)
  3. Exp作者提供了EBUT这个自定义头,其env变量名HTTP_EBUT与PHP_VALUE在长度和hash_value方面相等,且PHP_VALUE会在后续处理中被尝试读取(ini =

    FCGI_GETENV(request, "PHP_VALUE")

    干货|CVE-2019-11043: PHP-FPM在Nginx特定配置下任意代码执行漏洞分析的更多相关文章

    1. 【第六课】Nginx常用配置下详解

      目录 Nginx常用配置下详解 1.Nginx虚拟主机 2.部署wordpress开源博客 3.部署discuz开源论坛 4.域名重定向 5.Nginx用户认证 6.Nginx访问日志配置 7.Ngi ...

    2. CVE-2019-11043 Nginx PHP 远程代码执行漏洞复现

      漏洞背景:来自Wallarm的安全研究员Andrew Danau在9月14-16号举办的Real World CTF中,意外的向服务器发送%0a(换行符)时,服务器返回异常信息.由此发现了这个0day ...

    3. MongoDB干货系列2-MongoDB执行计划分析详解(2)(转载)

      写在之前的话 作为近年最为火热的文档型数据库,MongoDB受到了越来越多人的关注,但是由于国内的MongoDB相关技术分享屈指可数,不少朋友向我抱怨无从下手. <MongoDB干货系列> ...

    4. FPM制作Nginx的RPM软件包

      FPM制作Nginx的rpm软件包 FPM相关参数-s:指定源类型-t:指定目标类型,即想要制作为什么包-n:指定包的名字-v:指定包的版本号-C:指定打包的相对路径-d:指定依赖于哪些包-f:第二次 ...

    5. nginx服务配置---php服务接入

      前言: 最近要搭建一个内部的wiki系统, 网上搜了一圈, 也从知乎上搜集了一些大神的评价和推荐. 重点找了几个开源的wiki系统, 不过发现他们都是采用php来实现的. 于是乎需要配置php环境, ...

    6. Nginx安装配置PHP(FastCGI)环境的教程

      这篇是Nginx安装配置PHP(FastCGI)环境的教程.Nginx不支持对外部程序的直接调用或者解析,所有的外部程序(包括PHP)必须通过FastCGI接口来调用. 一.什么是 FastCGI F ...

    7. nginx上配置phpmyadmin

      Nginx配置phpmyadmin流程如下: 一.准备软件和环境(这里我以ubuntu16.04为例) 1.安装php7.1 sudo LC_ALL=C.UTF- add-apt-repository ...

    8. mac 安装nginx,并配置nginx的运行环境

      1. 安装nginx // 查询有没有nginx brew search nginx //开始安装nignx brew install nginx 2. 检查nignx是否安装成功 nginx -V ...

    9. nginx解析漏洞,配置不当,目录遍历漏洞环境搭建、漏洞复现

      nginx解析漏洞,配置不当,目录遍历漏洞复现 1.Ubuntu14.04安装nginx-php5-fpm 安装了nginx,需要安装以下依赖 sudo apt-get install libpcre ...

    随机推荐

    1. 019.CI4框架CodeIgniter辅助函数类之:Array数组查询

      01. 数组辅助函数,可以方便的查看数组内部的成员,用法如下图所示: <?php namespace App\Controllers; class Hello extends BaseContr ...

    2. eshop7-mysql

      1. Mysql 安装 执行 yum -y install mysql-server 注意:(1)是否使用sudo 权限执行请根据您具体环境来决定 (2)检查是否已经安装mysql-server rp ...

    3. ②初识spring

      一:基础搭建 需要:eclipse.spring插件(确认版本号并下载对应插件详见:https://blog.csdn.net/a1150499208/article/details/87988392 ...

    4. DStream-04 Window函数的原理和源码

      DStream 中 window 函数有两种,一种是普通 WindowedDStream,另外一种是针对 window聚合 优化的 ReducedWindowedDStream. Demo objec ...

    5. 10nm Ice Lake处理器值得等待!

      处理器.显卡等产品往往习惯先在 Linux 平台测试,所以 Linux 的内核源码往往成为曝光新品的宝藏之地. 经查,在 Linux v5.2 内核最新源码的 x86 分支中,出现了多款 Ice La ...

    6. 微信小程序添加背景图片的坑

      给微信小程序页面加载背景图片解决方案 直接附上原文地址: 给微信小程序页面加载背景图片解决方案 - YUSIR 完美CODING世界 - CSDN博客  https://blog.csdn.net/y ...

    7. Centos7.4 Storm2.0.0 + Zookeeper3.5.5 高可用集群搭建

      想了下还是把kafka集群和storm集群分开比较好 集群规划: Nimbus Supervisor storm01 √ √ storm02 √(备份) √ storm03 √ 准备工作 老样子复制三 ...

    8. NumPy 矩阵库函数

      章节 Numpy 介绍 Numpy 安装 NumPy ndarray NumPy 数据类型 NumPy 数组创建 NumPy 基于已有数据创建数组 NumPy 基于数值区间创建数组 NumPy 数组切 ...

    9. 读书笔记 - js高级程序设计 - 第八章 BOM

        BOM的核心对象是window 它表示浏览器的一个实例,在浏览器中,window对象有双重角色,它既是通过js访问浏览器窗口的一个接口,又是ECMAScript规定的Global对象,这意味着在网 ...

    10. PHP上传图片,路径保存在数据库中,根据图片路径显示图片

      1.创建数据表   CREATE TABLE image( id int(4) unsigned NOT NULL AUTO_INCREMENT, name varchar(100) default ...