PHP-FPM && PHP-CGI && FASTCGI
CGI
早期的Web服务器,只能响应浏览器发来的HTTP静态资源的请求,并将存储在服务器中的静态资源返回给浏览器。随着Web技术的发展,逐渐出现了动态技术,但是Web服务器并不能够直接运行动态脚本,为了解决Web服务器与外部应用程序(CGI程序)之间数据互通,于是出现了CGI(Common Gateway Interface)通用网关接口。简单理解,可以认为CGI是Web服务器和运行在其上的应用程序进行“交流”的一种约定。
当遇到动态脚本请求时,Web服务器主进程就会Fork创建出一个新的进程来启动CGI程序,运行外部C程序或Perl、PHP脚本等,也就是将动态脚本交给CGI程序来处理。启动CGI程序需要一个过程,如读取配置文件、加载扩展等。当CGI程序启动后会去解析动态脚本,然后将结果返回给Web服务器,最后由Web服务器将结果返回给客户端,之前Fork出来的进程也随之关闭。
但是因为每次请求都会使用system-fork产生一个线程去运行启动cgi程序, 而服务器能执行的进程是有限的, 所以当出现高并发的时候会使服务器崩掉
这时CGI的升级版FAST-GUI就出现了
FASTCGI
简单来说Fastcgi其实是一个通信协议,和http一样是进行数据交换的通道
FastCGI程序和web服务器之间通过可靠的流式传输(Unix Domain Socket或TCP)来通信
Record
fastcgi由多个record组成,record和http一样有head和body, 服务器中间件将这二者按照fastcgi的规则封装好发送给语言后端,语言后端解码以后拿到具体数据,进行指定操作,并将结果再按照该协议封装好后返回给服务器中间件。
和HTTP头不同,record的头固定8个字节,body是由头中的contentLength指定
代码语言:javascript复制typedef struct {
/* Header */
unsigned char version; // 版本
unsigned char type; // 本次record的类型
unsigned char requestIdB1; // 本次record对应的请求id
unsigned char requestIdB0;
unsigned char contentLengthB1; // 用于表示body体的大小
unsigned char contentLengthB0;
unsigned char paddingLength; // 额外块大小,不需要用到的时候设置为0
unsigned char reserved;
/* Body */
unsigned char contentData[contentLength];
unsigned char paddingData[paddingLength];
} FCGI_Record;
头由8个 uchar 类型的变量组成,每个变量一个字节。其中,requestId
占两个字节,一个唯一的标志id,以避免多个请求之间的影响;contentLength
占两个字节,表示 Body 的大小。可见,一个 Fastcgi Record 结构最大支持的 Body 大小是2^16
,也就是 65536 字节。
后端语言解析了 Fastcgi 头以后,拿到 contentLength
,然后再在请求的 TCP 流里读取大小等于 contentLength
的数据,这就是 Body 体。
Body 后面还有一段额外的数据(Padding),其长度由头中的 paddingLength
指定,起保留作用。不需要该Padding的时候,将其长度设置为0即可。
Fastcgi Type
刚才我们介绍了 Fastcgi 协议中Record部分中各个结构的含义,其中第二个字节为 type
type
就是指定该 Record 的作用。因为 Fastcgi 中一个 Record 的大小是有限的,作用也是单一的,所以我们需要在一个TCP流里传输多个 Record,通过 type
来标志每个 Record 的作用,并用 requestId
来标识同一次请求的id。也就是说,每次请求,会有多个 Record,他们的 requestId
是相同的。
下面给出一个表格,列出最主要的几种 type
:
#define FCGI_BEGIN_REQUEST 1 表示一个请求的开始,
#define FCGI_ABORT_REQUEST 2 表示服务器希望终止一个请求
#define FCGI_END_REQUEST 3 表示该请求处理完毕
#define FCGI_PARAMS 4 对应于CGI程序的环境变量,php $_SERVER 数组中的数据绝大多数来自于此
#define FCGI_STDIN 5 对应CGI程序的标准输入,FastCGI程序从此消息获取 http请求的POST数据
#define FCGI_STDOUT 6 对应CGI程序的标准输出,web服务器会把此消息当作html返回给浏览器
#define FCGI_STDERR 7 对应CGI程序的标准错误输出, web服务器会把此消息记录到错误日志中
--------------------------------------我们主要用到的是以上七种-----------------------------------
#define FCGI_DATA 8
#define FCGI_GET_VALUES 9
#define FCGI_GET_VALUES_RESULT 10
#define FCGI_UNKNOWN_TYPE 11 FastCGI程序无法解析该消息类型
#define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE)
服务器中间件和后端语言通信,第一个数据包就是type
为1的record,后续互相交流,发送type
为4、5、6、7的record,结束时发送type
为2、3的record。
type
为4的record后,就会把这个record的body按照对应的结构解析成key-value对,这就是环境变量。
当后端语言接收到一个 type
为4的 Record 后,就会把这个 Record 的 Body 按照对应的结构解析成 key-value 对,这就是环境变量。环境变量的结构如下:
typedef struct {
unsigned char nameLengthB0; /* nameLengthB0 >> 7 == 0 */
unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */
unsigned char nameData[nameLength];
unsigned char valueData[valueLength];
} FCGI_NameValuePair11;
typedef struct {
unsigned char nameLengthB0; /* nameLengthB0 >> 7 == 0 */
unsigned char valueLengthB3; /* valueLengthB3 >> 7 == 1 */
unsigned char valueLengthB2;
unsigned char valueLengthB1;
unsigned char valueLengthB0;
unsigned char nameData[nameLength];
unsigned char valueData[valueLength
((B3 & 0x7f) << 24) (B2 << 16) (B1 << 8) B0];
} FCGI_NameValuePair14;
typedef struct {
unsigned char nameLengthB3; /* nameLengthB3 >> 7 == 1 */
unsigned char nameLengthB2;
unsigned char nameLengthB1;
unsigned char nameLengthB0;
unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */
unsigned char nameData[nameLength
((B3 & 0x7f) << 24) (B2 << 16) (B1 << 8) B0];
unsigned char valueData[valueLength];
} FCGI_NameValuePair41;
typedef struct {
unsigned char nameLengthB3; /* nameLengthB3 >> 7 == 1 */
unsigned char nameLengthB2;
unsigned char nameLengthB1;
unsigned char nameLengthB0;
unsigned char valueLengthB3; /* valueLengthB3 >> 7 == 1 */
unsigned char valueLengthB2;
unsigned char valueLengthB1;
unsigned char valueLengthB0;
unsigned char nameData[nameLength
((B3 & 0x7f) << 24) (B2 << 16) (B1 << 8) B0];
unsigned char valueData[valueLength
((B3 & 0x7f) << 24) (B2 << 16) (B1 << 8) B0];
} FCGI_NameValuePair44;
这其实是 4 个结构,至于用哪个结构,有如下规则:
- key、value均小于128字节,用
FCGI_NameValuePair11
- key大于128字节,value小于128字节,用
FCGI_NameValuePair41
- key小于128字节,value大于128字节,用
FCGI_NameValuePair14
- key、value均大于128字节,用
FCGI_NameValuePair44
为什么我只介绍 type
为4的 Record?因为环境变量在后面 PHP-FPM 里有重要作用,之后写代码也会写到这个结构。type
的其他情况,大家可以自己翻文档理解理解。
PHP-FPM
那么,PHP-FPM又是什么东西?
FPM其实是一个fastcgi协议解析器,Nginx等服务器中间件将用户请求按照fastcgi的规则打包好通过TCP传给谁?其实就是传给FPM。
FPM按照fastcgi的协议将TCP流解析成真正的数据。
举个例子,用户访问http://127.0.0.1/index.php?a=1&b=2
,如果web目录是/var/www/html
,那么Nginx会将这个请求变成如下key-value对:
{
'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中_SERVER数组的一部分,也就是PHP里的环境变量。但环境变量的作用不仅是填充_SERVER数组,也是告诉fpm:“我要执行哪个PHP文件”。
PHP-FPM拿到fastcgi的数据包后,进行解析,得到上述这些环境变量。然后,执行SCRIPT_FILENAME
的值指向的PHP文件,也就是/var/www/html/index.php
。
PHP-CGI
PHP-CGI和上面的PHP-FPM差不多,不过不同在于PHP-FPM解析的是fastcgi协议而PHP-CGI解析的是cgi协议
一些PHP-FPM漏洞
还有一些PHP-FPM的漏洞在WHOAMI大佬的文章浅入深出 Fastcgi 协议分析与 PHP-FPM 攻击方法都有讲到使用方法,以下是文章中收集的内容目录
- PHP-FPM 未授权访问漏洞
- SSRF 中对 FPM/FastCGI 的攻击
- FTP – SSRF 攻击 FPM/FastCGI
- 通过加载恶意 .so 实现 RCE 绕过 Disable_Dunctions
参考文章:
https://whoamianony.top/浅入深出-Fastcgi-协议分析与-PHP-FPM-攻击方法/