FastCgi协议\&PHP-FPM未授权导致RCE
title: FastCgi协议\&PHP-FPM未授权导致RCE date: tags: php开发与安全
参考: https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html 离别歌
Fastcgi
要说PHP-FPM,首先就要说一下Fastcgi协议.
Fastcgi其实是一个和HTTP本质一样的通信协议。 HTTP用于浏览器和服务器中间件通信,Fastcgi用于服务器中间件与某个语言后端通信。 Fastcgi协议由多个record组成,record由header和body组成。 服务器中间件将body和header按照fastcgi规则封装好发送给语言后端,后端解码后拿到具体数据进行指定的操作,再按fastcgi协议封装号结果返回给服务器
record Header固定8个字节,每个变量一个字节 Body分为两类:真正的内容数据,和额外数据(非必须) 一个fastcgi record结构最大支持2^16=65536字节的body
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; // 额外块大小
unsigned char reserved;
/* Body */
unsigned char contentData[contentLength];
unsigned char paddingData[paddingLength];
} FCGI_Record;
Fastcgi type
也就是一个record的type变量。type用于表明该record的作用,以下是type主要的一些值
type
就是指定该record的作用。因为fastcgi一个record的大小是有限的,作用也是单一的,所以我们需要在一个TCP流里传输多个record。通过type
来标志每个record的作用,用requestId
作为同一次请求的id。
其中type=4 对我们接下来讲PHP-FPM有重要作用,他有四个不同的结构
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;
- key、value均小于128字节,用
FCGI_NameValuePair11
- key大于128字节,value小于128字节,用
FCGI_NameValuePair41
- key小于128字节,value大于128字节,用
FCGI_NameValuePair14
- key、value均大于128字节,用
FCGI_NameValuePair44
PHP-FPM
FPM是Fastcgi协议的解析器.中间件以fastcgi协议把用户传来的数据封装传给FPM。下面这个图就是fastcgi协议的模样
键值对.即fastcgi的type=4,这就是上面专门说这个的目的. 其中script_filename只向要执行的php文件
Nginx(IIS7)解析漏洞深入
以前记录过这个中间件漏洞,但没有详细的去了解为什么。这里就说说
在php fix_pathinfo开启的情况下,传入 url/1.txt/.php时,1.txt会被当作php文件解析.
究其原因,是因为配置文件中 security.limit_extensions默认限定了.php后缀文件才交给php-fpm处理,传入给fpm的数据是类似这样的
按理说应该报错404吧,但是fix_pathinfo会判断这个SCRIPT_FILENAME是否存在,若不存在就会去掉最后一个/后面的内容再次判断,知道文件存在为止,再把该文件当作PHP文件执行
RCE
上面那个解析漏洞只是个题外话。 我们来讲讲RCE。服务器默认PHP-FPM端口是9000,如果这个端口暴露在公网,我们就可以自己构造fastcgi协议与fpm通信.
此时我们就能想出rce的雏形,控制SCRIPT_FILENAME去执行我们的shell,反弹个shell什么的,但是前提是我们必须得上传一个shell上去,太笨比,于是我们继续思考.
上面提到我们可以通过fastcgi协议临时更改PHP的一些配置项(环境参数).我们不如把 auto_prepend_file
或auto_append_file
(自动包含某文件) 设置为php\://input(需allow_url_include=on),然后SCRIPT_FILENAME设置为任意一个服务器上存在的PHP文件(PHP文件不仅仅在服务器目录才会有,PHP程序目录下也会有PHP文件),即可通过控制POST的包体来实现RCE。就像这样
exp: https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75