CGI,Fastcgi,PHP-FPM三者的联系
CGI协议
CGI协议是为了保证web server传递过来的数据是标准格式
web server(比如说nginx)如果请求
/index.html
,那么web server会去文件系统中找到这个文件,发送给浏览器,这里分发的是静态数据。如果现在请求的是
/index.php
,根据配置文件,nginx知道这个不是静态文件,需要去找PHP解析器来处理,那么他会把这个请求简单处理后交给PHP解析器。CGI就是处理这个请求的协议。
当web server收到
/index.php
这个请求后,会启动对应的CGI程序,这里就是PHP的解析器。接下来PHP解析器会解析php.ini文件,初始化执行环境,然后处理请求,再以规定CGI规定的格式返回处理后的结果,退出进程。web server再把结果返回给浏览器。
fastcgi协议
Fastcgi是用来提高CGI程序性能的。
“PHP解析器会解析php.ini文件,初始化执行环境”,标准的CGI对每个请求都会执行这些步骤,所以处理每个时间的时间会比较长。那么Fastcgi是怎么做的呢?首先,Fastcgi会先启一个master,解析配置文件,初始化执行环境,然后再启动多个worker。当请求过来时,master会传递给一个worker,然后立即可以接受下一个请求。这样就避免了重复的劳动,提高了效率。而且当worker不够用时,master可以根据配置预先启动几个worker等着;当然空闲worker太多时,也会停掉一些,这样就提高了性能,也节约了资源。这就是fastcgi协议对进程的管理。
PHP-FPM
PHP是一个实现了Fastcgi的程序
PHP的解释器是php-cgi。但php-cgi只是个CGI程序,他自己本身只能解析请求,返回结果,不会进程管理,所以就有了PHP-FPM来管理进程,FPM其实是一个fastcgi协议解析器,Nginx等服务器中间件将用户请求按照fastcgi的规则打包好通过TCP传给谁?其实就是传给FPM。
FPM按照fastcgi的协议将TCP流解析成真正的数据。
细说fastcgi协议
Fastcgi是用来沟通程序(如PHP, Python, Java)和Web服务器(Apache2, Nginx), 理论上任何语言编写的程序都可以通过Fastcgi来提供Web服务。
Fastcgi的特点是会在一个进程中依次完成多个请求,以达到提高效率的目的,大多数Fastcgi实现都会维护一个进程池。
Fastcgi协议由多个record组成,record也有header和body一说,服务器中间件将这二者按照fastcgi的规则封装好发送给语言后端,语言后端解码以后拿到具体数据,进行指定操作,并将结果再按照该协议封装好后返回给服务器中间件。
record的头固定8个字节,body是由头中的contentLength指定,其结构如下:
1 | typedef struct { |
头由8个uchar类型的变量组成,每个变量1字节。
Fastcgi Type
第二个字节type是record的类型,而record包含信息的内容,也就是我们可以利用的内容,下面详细介绍一下。
type
就是指定该record的作用。因为fastcgi一个record的大小是有限的,作用也是单一的,所以我们需要在一个TCP流里传输多个record。通过type
来标志每个record的作用,用requestId
作为同一次请求的id。
当后端语言接收到一个type
为4的record后,就会把这个record的body按照对应的结构解析成key-value对,这就是环境变量。环境变量的结构如下:
1 | typedef struct { |
这其实是4个结构,至于用哪个结构,有如下规则:
- key、value均小于128字节,用
FCGI_NameValuePair11
- key大于128字节,value小于128字节,用
FCGI_NameValuePair41
- key小于128字节,value大于128字节,用
FCGI_NameValuePair14
- key、value均大于128字节,用
FCGI_NameValuePair44
FPM解析数据
FPM按照fastcgi的协议将TCP流解析成真正的数据。
举个例子,用户访问http://127.0.0.1/index.php?a=1&b=2
,如果web目录是/var/www/html
,那么Nginx会将这个请求变成如下key-value对:
1 | { |
这个数组其实就是PHP中$_SERVER
数组的一部分,也就是PHP里的环境变量。但环境变量的作用不仅是填充$_SERVER
数组,也是告诉fpm:“我要执行哪个PHP文件”。
PHP-FPM拿到fastcgi的数据包后,进行解析,得到上述这些环境变量。然后,执行SCRIPT_FILENAME
的值指向的PHP文件,也就是/var/www/html/index.php
。
Nginx(IIS7)解析漏洞
该漏洞的现象是
1.txt/.php是一个不存在的文件,是PHP设置中的一个选项fix_pathinfo
导致了这个漏洞。PHP为了支持Path Info模式而创造了fix_pathinfo
,在这个选项被打开的情况下,fpm会判断SCRIPT_FILENAME
是否存在,如果不存在则去掉最后一个/
及以后的所有内容,再次判断文件是否存在,往次循环,直到文件存在。于是被作为PHP文件执行,导致解析漏洞。
正确的解决方法有两种,一是在Nginx端使用fastcgi_split_path_info
将path info信息去除后,用tryfiles判断文件是否存在;二是借助PHP-FPM的security.limit_extensions
配置项,避免其他后缀文件被解析。
fpm未授权访问
PHP-FPM默认监听9000端口,如果这个端口暴露在公网,则我们可以自己构造fastcgi协议,和fpm进行通信。
此时,SCRIPT_FILENAME
的值就格外重要了。因为fpm是根据这个值来执行php文件的,如果想利用PHP-FPM的未授权访问漏洞,首先就得找到一个已存在的PHP文件。
万幸的是,通常使用源安装php的时候,服务器上都会附带一些php后缀的文件。
任意代码执行
为什么我们控制fastcgi协议通信的内容,就能执行任意PHP代码呢?
理论上当然是不可以的,即使我们能控制SCRIPT_FILENAME
,让fpm执行任意文件,也只是执行目标服务器上的文件,并不能执行我们需要其执行的文件。
但PHP是一门强大的语言,PHP.INI中有两个有趣的配置项,auto_prepend_file
和auto_append_file
。
auto_prepend_file
是告诉PHP,在执行目标文件之前,先包含auto_prepend_file
中指定的文件;auto_append_file
是告诉PHP,在执行完成目标文件后,包含auto_append_file
指向的文件。
那么就有趣了,假设我们设置auto_prepend_file
为php://input
,那么就等于在执行任何php文件前都要包含一遍POST的内容。所以,我们只需要把待执行的代码放在Body中,他们就能被执行了。(当然,还需要开启远程文件包含选项allow_url_include
)
那么,我们怎么设置auto_prepend_file
的值?
这又涉及到PHP-FPM的两个环境变量,PHP_VALUE
和PHP_ADMIN_VALUE
。
所以,我们最后传入如下环境变量:
1 | { |
设置auto_prepend_file = php://input
且allow_url_include = On
,然后将我们需要执行的代码放在Body中,即可执行任意代码。
EXP
兼容Python2和Python3,代码如下:https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75 。
1 | if __name__ == '__main__': |
实战
php-fpm-attack
PHP-FPM默认监听9000端口,如果这个端口暴露在公网,则我们可以自己构造fastcgi协议,和fpm进行通信,但这道题如果用默认的9000端口监听不能成功返回,但其实这道题在内网上还是有打开9000端口的,与外网端口号是一个映射的关系,所以这道题要加上外网的端口号。
EZ-CURL
如果fpm不在外网开放,我们可以通过ssrf进行攻击(如果存在ssrf漏洞的地方)
SSRF(Server-Side Request Forgery:服务器端请求伪造) 是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。一般情况下,SSRF攻击的目标是从外网无法访问的内部系统。(正是因为它是由服务端发起的,所以它能够请求到与它相连而与外网隔离的内部系统)
1 |
|
a
参数是我们可以控制的,a可利用的协议有gopher
、dict
、http
、https
、file
等file
协议可以用于查看文件,dict
协议可以用于刺探端口gopher
协议支持GET&POST
请求,常用于攻击内网ftp
、redis
、telnet
、smtp
等服务
这道题我们利用的是gopher://协议
1 | URL:gopher://<host>:<port>/<gopher-path>_后接TCP数据流 |
说明gopher协议可以直接发送tcp协议流,那么我们就可以把数据流urlencode编码构造ssrf攻击代码
我们发送tcp流给web server,会直接在页面显示出来返回的内容。
利用Gopherus来构造pyload。
需要传入一个参数 –exploit,我们这里是fastcgi
接着输入我们知道的已存在的php文件,和执行的命令。
得出来的payload url解码如下(过滤不可见字符)
生成的payload是经过一次url编码,但需要再urlencode编码一次,因为这里nginx解码一次,php-fpm解码一次。
参考文章
https://blog.csdn.net/a3320315/article/details/102880329
https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html