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。

14931267923354.jpg

其中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;
  1. key、value均小于128字节,用FCGI_NameValuePair11
  2. key大于128字节,value小于128字节,用FCGI_NameValuePair41
  3. key小于128字节,value大于128字节,用FCGI_NameValuePair14
  4. key、value均大于128字节,用FCGI_NameValuePair44

PHP-FPM

FPM是Fastcgi协议的解析器.中间件以fastcgi协议把用户传来的数据封装传给FPM。下面这个图就是fastcgi协议的模样

QQ截图20210219142802

键值对.即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的数据是类似这样的

QQ截图20210219193924

按理说应该报错404吧,但是fix_pathinfo会判断这个SCRIPT_FILENAME是否存在,若不存在就会去掉最后一个/后面的内容再次判断,知道文件存在为止,再把该文件当作PHP文件执行

RCE

上面那个解析漏洞只是个题外话。 我们来讲讲RCE。服务器默认PHP-FPM端口是9000,如果这个端口暴露在公网,我们就可以自己构造fastcgi协议与fpm通信.

此时我们就能想出rce的雏形,控制SCRIPT_FILENAME去执行我们的shell,反弹个shell什么的,但是前提是我们必须得上传一个shell上去,太笨比,于是我们继续思考.

上面提到我们可以通过fastcgi协议临时更改PHP的一些配置项(环境参数).我们不如把 auto_prepend_fileauto_append_file(自动包含某文件) 设置为php\://input(需allow_url_include=on),然后SCRIPT_FILENAME设置为任意一个服务器上存在的PHP文件(PHP文件不仅仅在服务器目录才会有,PHP程序目录下也会有PHP文件),即可通过控制POST的包体来实现RCE。就像这样

QQ截图20210219142817

exp: https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75

QQ截图20210219142836