当前位置: 首页 > news >正文

SSRF 攻击及其防御策略

1. 什么是 SSRF 攻击⚔

SSRF 全称 Server-side Request Forgery,即服务器端请求伪造

其原理就是利用服务器端的某些漏洞,在客户端访问到服务器内部或者服务器所在内网的信息;而在正常情况下,这些信息是无法直接访问到的。

2. 一个简单的例子🌰

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests

from flask import Flask, request
app = Flask(__name__)


@app.route("/")
def index():
    url = request.args.get("url")
    data = requests.get(url).text
    return data


if __name__ == "__main__":
    app.run()

这个例子中,我们实现了一个简易的 HTTP 服务器,这个服务器的功能只有一个,那就是获取 url 这个唯一的 URL 参数,在服务器端请求这个 url 参数所对应的地址,得到响应之后将响应的数据返回给客户端。

假设客户端通过某种途径知道了服务器所在的内网的某些地址,比如说 http://10.0.0.1/api/get。在正常情况下,我们是无法直接访问到这个地址的。

如果这台服务器对外公布的地址为 http://test.test:5000,通过刚才的服务器程序,我们只需请求 http://test.test:5000/?url=http://10.0.0.1/api/get,即可借助服务器的 SSRF 漏洞,访问到 http://10.0.0.1/api/get 的内容。

这看起来是十分危险的,所以在服务器端必须采取某些措施来修复这一可能带来重大安全隐患的漏洞。

3. 基本的应对措施🛡

  1. 首先要限制住可以请求的协议,通常只允许 http/https 协议。

  2. 其次,要限制住可以请求的端口号,通常情况下,http 协议只允许 80 端口访问;https 协议只允许 443 端口访问。

  3. SSRF 攻击的一个重中之重是可以访问到内网的数据,因此我们必须解析出要请求的地址所对应的 IP,如果这个 IP 是内网 IP 的话,就必须拒绝这次请求。

    下面总结内网 IP 的类型

    IP 段解释
    10/8第一类 IP 地址中的内网 IP
    172.16/12第二类 IP 地址中的内网 IP
    192.168/16第三类 IP 地址中的内网 IP
    127/8本地环回测试地址
    0/8指向本网络,例如通过 0.0.0.0:4000 可以访问到 127.0.0.1:4000

    因为同一个 IP 段的前若干位(/ 后面的数字表示这一前缀的二进制位数)的二进制表示为一个定值,我们只需将解析出的 IP 通过二进制前缀比较的方式逐一判断是否为内网 IP 即可。

  4. 某些网站可能并没有解析为内网地址,但是它们通过 301/302 重定向的方式,还是可以请求到内网的地址。因此,非必要情况下,我们需要禁止所有的重定向请求/在服务器端以无跳转模式请求内容。如果一定要重定向,则可使用 requests 包中的 hook,具体可参考:https://www.leavesongs.com/PYTHON/defend-ssrf-vulnerable-in-python.html#0x04-requestshooksssrf。

  5. 很多时候,我们只需要请求某一种特定类型的内容,比如图片。因此,在得到响应数据之后,我们可以对数据做类型检验,对于类型不符的内容,拒绝返回响应数据。

4. 上述应对措施的简单 Python 实现📟

4.1 准备工作 1:IP 地址的转换

我们知道,IP 地址通常使用点分十进制的形式表示,但在本例中,为方便处理,我们统一将点分十进制形式的 IP 地址转化为 32 位二进制整数。

 

1
2
3
4
# 解析点分十进制 IP 字符串为 32 位二进制整数
def parse_ip_str(ip_str):
    ip_part_str = ip_str.split(".")
    return (int(ip_part_str[0]) << 24) + (int(ip_part_str[1]) << 16) + (int(ip_part_str[2]) << 8) + int(ip_part_str[3])

其原理就是将 8 位一组的 IP 子串取出,转化为 int 类型之后进行移位操作,将其归到正确的位置即可。

4.2 准备工作 2:内网 IP 检测

根据上文总结的内网 IP 类型,再结合刚才处理好的 32 位二进制形式的 IP 地址,我们只需比较 IP 地址的固定前缀是否相同即可判断这个 IP 地址是否为内网地址。取到前缀最简便的方法是通过二进制右移来得到指定长度的前缀。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 检查 IP 地址是否为内网地址或者本地环回测试地址
def check_if_intranet_ip(ip_str):
    binary_ip = parse_ip_str(ip_str)
    if (binary_ip >> 24) == (parse_ip_str("10.0.0.1") >> 24):
        return True
    elif (binary_ip >> 20) == (parse_ip_str("172.16.0.1") >> 20):
        return True
    elif (binary_ip >> 16) == (parse_ip_str("192.168.0.1") >> 16):
        return True
    elif (binary_ip >> 24) == (parse_ip_str("127.0.0.1") >> 24):
        return True
    elif (binary_ip >> 24) == (parse_ip_str("0.0.0.1") >> 24):
        return True
    return False

4.3 步骤 1:检查协议和端口

这里利用到了 urllib.parse() 类。

这个类可以将一个 URL 字符串进行解析,格式如下:

 

1
<scheme>://<netloc>/<path>?<query>#<fragment>

其中,urllib.parse.urlparse(url).scheme 返回 URL 所使用的协议,urllib.parse.urlparse(url).port 返回 URL 所显式请求的端口(只有显式请求的端口才能被获取到),如果没有显式请求的话,则返回 None

 

1
2
3
4
5
6
7
8
9
10
url_info = urllib.parse.urlparse(url)

# Step 1: 只允许 http/https 协议,并只允许 80/443 端口
protocol = str(url_info.scheme)
port = url_info.port
if protocol != "http" and protocol != "https":
    return None
if port is not None:
    if not ((protocol == "http" and port == 80) or (protocol == "https" and port == 443)):
        return None

4.4 步骤 2:解析域名,获得 IP,并进行内网 IP 检测

我们通过 urllib.parse.urlparse(url).hostname 获取到所请求的域名。

之后,我们通过 socket.getaddrinfo() 对域名进行解析,这个函数的运行结果如下所示:

 

1
2
3
4
5
6
7
8
Python 3.7.6 (default, Dec 30 2019, 19:38:28)
[Clang 11.0.0 (clang-1100.0.33.16)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import socket
>>> socket.getaddrinfo("codecho.xyz", "443")
[(<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_DGRAM: 2>, 17, '', ('39.105.149.86', 443)), (<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('39.105.149.86', 443))]
>>> socket.getaddrinfo("codecho.xyz", "80")
[(<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_DGRAM: 2>, 17, '', ('39.105.149.86', 80)), (<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('39.105.149.86', 80))]

有两点需要注意:

  1. 这个函数需要传入两个参数,分别是域名和端口号。在本例的情况下,对于 http 协议的请求,传入 80 即可;对于 https 协议的请求,传入 443 即可。
  2. 这个函数返回一个五元组列表,其中我们要用的是每个五元组的第 4 个元素中的第 0 个元素。同时我们发现每个域名可以解析出多个 IP,对于每个 IP 都要进行检测。

因此有了下面的代码:

 

1
2
3
4
5
6
7
8
9
# Step 2: 判断是否解析为了内网 IP,如果是就直接返回 None
domain = url_info.hostname
if protocol == "http":
    ip_list = [addr_info[4][0] for addr_info in socket.getaddrinfo(domain, "80")]
else:
    ip_list = [addr_info[4][0] for addr_info in socket.getaddrinfo(domain, "443")]
    for ip in ip_list:
        if check_if_intranet_ip(ip):
            return None

4.5 禁用跳转

这步很简单,对于基于 requests 包的请求,只需加上 allow_redirects=False 参数即可。

 

1
2
3
# Step 3: 以无跳转模式请求内容
data = requests.get(url, allow_redirects=False, **kwargs).content
return data

4.6 完整代码(基于 Python 3 实现)

 

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
import requests
import urllib.parse
import socket

# 解析点分十进制 IP 字符串为 32 位二进制整数
def parse_ip_str(ip_str):
    ip_part_str = ip_str.split(".")
    return (int(ip_part_str[0]) << 24) + (int(ip_part_str[1]) << 16) + (int(ip_part_str[2]) << 8) + int(ip_part_str[3])


# 检查 IP 地址是否为内网地址或者本地环回测试地址
def check_if_intranet_ip(ip_str):
    binary_ip = parse_ip_str(ip_str)
    if (binary_ip >> 24) == (parse_ip_str("10.0.0.1") >> 24):
        return True
    elif (binary_ip >> 20) == (parse_ip_str("172.16.0.1") >> 20):
        return True
    elif (binary_ip >> 16) == (parse_ip_str("192.168.0.1") >> 16):
        return True
    elif (binary_ip >> 24) == (parse_ip_str("127.0.0.1") >> 24):
        return True
    elif (binary_ip >> 24) == (parse_ip_str("0.0.0.1") >> 24):
        return True
    return False


def secure_get(url, **kwargs):
    url_info = urllib.parse.urlparse(url)

    # Step 1: 只允许 http/https 协议,并只允许 80/443 端口
    protocol = str(url_info.scheme)
    port = url_info.port
    if protocol != "http" and protocol != "https":
        return None
    if port is not None:
        if not ((protocol == "http" and port == 80) or (protocol == "https" and port == 443)):
            return None

    # Step 2: 判断是否解析为了内网 IP,如果是就直接返回 None
    domain = url_info.hostname
    if protocol == "http":
        ip_list = [addr_info[4][0] for addr_info in socket.getaddrinfo(domain, "80")]
    else:
        ip_list = [addr_info[4][0] for addr_info in socket.getaddrinfo(domain, "443")]
    for ip in ip_list:
        if check_if_intranet_ip(ip):
            return None

    # Step 3: 以无跳转模式请求内容
    data = requests.get(url, allow_redirects=False, **kwargs).content
    return data

5. 参考资料

https://www.leavesongs.com/PYTHON/defend-ssrf-vulnerable-in-python.html#0x04-requestshooksssrf

https://www.cnblogs.com/hnrainll/archive/2011/10/13/2210101.html

https://xz.aliyun.com/t/2115

相关文章:

  • 安徽大学正方教务系统 用JS 一键完成 教师评价
  • Mac切换窗口
  • Dreamweaver开发人员工作区 标准工作区的区别
  • UltraISO写入U盘镜像 无法选择镜像文件
  • 永久关闭 搜狗输入法的 头条新闻
  • Windows下使用coding.net搭建Hexo博客的记录
  • Next主题 关闭加载动画效果
  • Coding+Hexo 配置自定义域名 并强制https访问
  • Hexo博客使用 Next主题 后的一些相关配置 记录
  • Hexo博客Next主题 字数统计和阅读时长失效
  • Win10 BIOS改AHCI蓝屏无法启动的 两个解决方法
  • Hexo使用添加本地图片不用图床 的完美解决方案
  • 微PE安装系统 不显示U盘中镜像文件 的解决方法
  • 不加 “...快捷方式“后缀 和 移除快捷方式箭头的方法
  • Windows 谷歌浏览器插件无法从该网站添加应用
  • 《Java8实战》-第四章读书笔记(引入流Stream)
  • 《网管员必读——网络组建》(第2版)电子课件下载
  • 2017前端实习生面试总结
  • java2019面试题北京
  • js中forEach回调同异步问题
  • Traffic-Sign Detection and Classification in the Wild 论文笔记
  • ⭐ Unity 开发bug —— 打包后shader失效或者bug (我这里用Shader做两张图片的合并发现了问题)
  • v-if和v-for连用出现的问题
  • Webpack 4x 之路 ( 四 )
  • 闭包--闭包之tab栏切换(四)
  • 读懂package.json -- 依赖管理
  • 海量大数据大屏分析展示一步到位:DataWorks数据服务+MaxCompute Lightning对接DataV最佳实践...
  • 码农张的Bug人生 - 见面之礼
  • 微服务核心架构梳理
  • 优化 Vue 项目编译文件大小
  • C# - 为值类型重定义相等性
  • 仓管云——企业云erp功能有哪些?
  • $.ajax()参数及用法
  • (cos^2 X)的定积分,求积分 ∫sin^2(x) dx
  • (备忘)Java Map 遍历
  • (附源码)springboot美食分享系统 毕业设计 612231
  • (排序详解之 堆排序)
  • (转)C#调用WebService 基础
  • (转)es进行聚合操作时提示Fielddata is disabled on text fields by default
  • (转)JAVA中的堆栈
  • **Java有哪些悲观锁的实现_乐观锁、悲观锁、Redis分布式锁和Zookeeper分布式锁的实现以及流程原理...
  • .Net CF下精确的计时器
  • .net6解除文件上传限制。Multipart body length limit 16384 exceeded
  • .net企业级架构实战之7——Spring.net整合Asp.net mvc
  • .NET中统一的存储过程调用方法(收藏)
  • /etc/fstab和/etc/mtab的区别
  • @Valid和@NotNull字段校验使用
  • [Android View] 可绘制形状 (Shape Xml)
  • [android学习笔记]学习jni编程
  • [C++]高精度 bign (重载运算符版本)
  • [C++随笔录] 红黑树
  • [CentOs7]图形界面
  • [Deep Learning] 神经网络基础
  • [Design Pattern] 工厂方法模式
  • [Docker]十一.Docker Swarm集群raft算法,Docker Swarm Web管理工具