查询 25 次,耗时 0.2260 秒

从“任意 URL 抓取”到内网访问:本地靶场复盘 SSRF 原理与分层防御

monika

New member

monika Rep
0
0
0
Rep
0
monika Vouches
Vouches
0
2 MONTHS
2 2 MONTHS OF SERVICE
monika 获得
0
0
0
获得
0
monika 凭证
0
0
0
凭证
0
主题
4
点赞
0
HakB
2 月
在社区成长 2 月
LEVEL 1 1 XP
 
一、背景

许多业务都需要由服务端主动访问用户提交的 URL,例如网页预览、Webhook 校验、图片代理、文件转存和监控探针。如果应用未经约束便直接请求该 URL,攻击者可能借用服务器的网络位置访问本不应暴露的地址,这类问题通常称为服务端请求伪造(SSRF)。

SSRF 的危险性不只在于读取一个内网页面。它还可能成为资产探测、云环境元数据访问、内部管理接口调用以及跨安全域移动的入口。本文只在本机授权靶场中复现最小风险场景,重点分析请求链路和防御边界,不涉及任何真实目标。

二、核心原理

典型脆弱链路如下:

1. 客户端提交一个 URL。
2. Web 应用调用 HTTP 客户端库访问该 URL。
3. 出站请求从服务器所在网络发出,而不是从客户端发出。
4. 如果服务器能访问环回地址、内网网段或受信任服务,外部用户就可能间接获得相同的网络可达性。

仅检查字符串中是否存在“127.0.0.1”并不可靠,因为同一地址可能有不同表示方式,域名解析结果也可能变化。重定向还会产生第二个 URL:初始地址看似安全,但响应可以将客户端跳转到内部地址。因此,防御必须覆盖 URL 解析、DNS 解析、每次重定向、实际连接目标和网络出口。

三、前置条件与实验边界

实验环境:

1. Linux、macOS 或隔离的测试虚拟机。
2. Python 3.10 以上版本。
3. Flask 与 requests:

python3 -m venv venv
. venv/bin/activate
pip install flask requests

所有请求仅访问 127.0.0.1。不要把示例服务暴露到公网,也不要将测试 URL 替换为未经授权的地址。

四、搭建本地内部服务

创建 internal.py:

from flask import Flask, jsonify

app = Flask(__name__)

@app.get('/internal/status')
def status():
return jsonify({
'service': 'local-admin',
'environment': 'lab',
'message': 'This endpoint should not be reachable through the fetcher'
})

app.run(host='127.0.0.1', port=9000)

启动服务:

python internal.py

另开终端验证:

curl http://127.0.0.1:9000/internal/status

这里的 9000 端口模拟只能由服务器本机访问的管理接口。它没有保存真实凭据或敏感数据。

五、构造存在缺陷的 URL 抓取器

创建 vulnerable_fetcher.py:

from flask import Flask, request, jsonify
import requests

app = Flask(__name__)

@app.get('/fetch')
def fetch():
target = request.args.get('url', '')
if not target:
return jsonify({'error': 'missing url'}), 400

try:
response = requests.get(target, timeout=3)
return jsonify({
'status': response.status_code,
'body': response.text[:1000]
})
except requests.RequestException as exc:
return jsonify({'error': str(exc)}), 502

app.run(host='127.0.0.1', port=5000)

启动:

python vulnerable_fetcher.py

通过抓取器访问内部服务:

curl -G 'http://127.0.0.1:5000/fetch' \
--data-urlencode 'url=http://127.0.0.1:9000/internal/status'

如果响应中出现 local-admin,说明抓取器把调用者提供的 URL 转换成了服务端出站请求。关键问题不是内部服务本身,而是抓取器把“用户可控目标”和“服务器网络权限”直接绑定在了一起。

六、常见但不充分的修复方式

以下方式不能单独作为可靠防线:

1. 只禁止包含 localhost 的字符串。攻击者未必使用这个主机名。
2. 只禁止 127.0.0.1。还要考虑 IPv6 环回、私有地址、链路本地地址及其他特殊用途网段。
3. 只在第一次请求前检查。HTTP 重定向会改变后续目标。
4. 先解析域名,检查后再让 HTTP 库重新解析。两次解析之间可能出现结果变化,形成检查与使用不一致。
5. 仅依赖超时。超时只能限制资源占用,不能阻止访问内部地址。
6. 用黑名单枚举敏感域名。资产变化后,黑名单很容易失效。

七、实现应用层基础校验

如果业务必须接受外部 URL,可以先增加协议、凭据、端口和解析地址校验。创建 safe_fetcher.py:

from flask import Flask, request, jsonify
from urllib.parse import urlsplit
import ipaddress
import socket
import requests

app = Flask(__name__)

ALLOWED_PORTS = {80, 443}

def validate_target(raw_url):
parsed = urlsplit(raw_url)

if parsed.scheme not in {'http', 'https'}:
raise ValueError('only http and https are allowed')
if not parsed.hostname:
raise ValueError('hostname is required')
if parsed.username or parsed.password:
raise ValueError('userinfo is not allowed')

try:
port = parsed.port
except ValueError:
raise ValueError('invalid port')

effective_port = port or (443 if parsed.scheme == 'https' else 80)
if effective_port not in ALLOWED_PORTS:
raise ValueError('port is not allowed')

try:
records = socket.getaddrinfo(
parsed.hostname,
effective_port,
type=socket.SOCK_STREAM
)
except socket.gaierror:
raise ValueError('hostname resolution failed')

addresses = {item[4][0] for item in records}
if not addresses:
raise ValueError('no address resolved')

for text in addresses:
ip = ipaddress.ip_address(text)
if not ip.is_global:
raise ValueError('non-public destination is blocked')

return raw_url

@app.get('/fetch')
def fetch():
raw_url = request.args.get('url', '')
try:
target = validate_target(raw_url)
response = requests.get(
target,
timeout=(2, 5),
allow_redirects=False,
stream=True
)
content = response.raw.read(65536, decode_content=True)
return jsonify({
'status': response.status_code,
'content_type': response.headers.get('Content-Type', ''),
'body_preview': content.decode('utf-8', errors='replace')
})
except ValueError as exc:
return jsonify({'error': str(exc)}), 400
except requests.RequestException:
return jsonify({'error': 'upstream request failed'}), 502

app.run(host='127.0.0.1', port=5001)

启动并测试:

python safe_fetcher.py

curl -G 'http://127.0.0.1:5001/fetch' \
--data-urlencode 'url=http://127.0.0.1:9000/internal/status'

预期返回 non-public destination is blocked。还可以验证协议限制:

curl -G 'http://127.0.0.1:5001/fetch' \
--data-urlencode 'url=file:///etc/hosts'

预期返回 only http and https are allowed。

这段代码适合说明校验思路,但仍不能被视为完整生产方案。原因是校验阶段执行了一次 DNS 解析,而 requests 建立连接时可能再次解析;如果解析结果发生变化,仍可能产生检查与实际连接目标不一致的问题。

八、生产环境的分层防御

1. 优先采用目标白名单

如果业务只需调用少数合作方接口,应允许明确的域名、协议、端口和路径前缀,而不是接受任意 URL。白名单匹配必须基于解析后的 hostname,不能使用简单的字符串包含判断。例如 approved.example.invalid.attacker.test 不能因为包含 approved.example.invalid 就被放行。

2. 关闭自动重定向

默认拒绝重定向最简单。如果业务确实需要跳转,应逐跳解析 Location,把相对地址转换为绝对地址,并对每一跳重新执行协议、端口、域名和 IP 校验,同时限制最大跳转次数。

3. 使用受控出站代理

更稳妥的方案是让抓取服务只能连接专用代理,由代理统一完成域名白名单、DNS 解析、目标地址校验、连接固定和审计。应用容器通过防火墙策略禁止直接访问其他网络,只允许访问代理地址。这样即使应用层校验出现遗漏,网络层仍可阻断环回、私网、链路本地和管理网段。

4. 限制响应资源

设置连接超时、读取超时、最大响应体、最大重定向次数和并发数。不要直接调用 response.text 读取不受限内容,否则大响应或持续数据流可能造成内存与连接池耗尽。示例中只读取 65536 字节,就是为了体现资源上限。

5. 隔离身份与权限

执行 URL 抓取的进程不应携带不必要的云凭据、内部 API 令牌或高权限客户端证书。即使网络控制被绕过,最小权限仍可缩小影响面。抓取器也不应与后台管理服务部署在同一高信任网络区域。

6. 建立审计与告警

建议记录请求发起者、原始 URL、规范化主机名、解析出的 IP、实际连接 IP、响应状态、耗时、响应字节数和拦截原因。日志中不要保存 URL 内可能存在的令牌,也不要完整记录敏感响应体。短时间内大量访问不同端口、连续命中保留地址、异常重定向链都应触发告警。

九、风险与注意事项

本地复现时不要在 internal.py 中放入真实密钥。若使用容器,应确认端口只绑定到 127.0.0.1,而非 0.0.0.0。生产环境不能简单复制示例代码后直接上线,还需处理国际化域名、尾点域名、IPv4 与 IPv6 双栈、代理环境变量、连接池复用及 TLS 主机名验证。

若采用“解析后固定 IP 连接”的方案,HTTPS 仍必须使用原始域名执行证书校验和 SNI,不能为了固定 IP 而关闭 TLS 验证。关闭证书验证会把 SSRF 修复转化为新的中间人攻击风险。

十、常见问题

问题一:禁止所有私网地址是否足够?

不够。还要处理环回、链路本地、保留地址、未指定地址、IPv6 特殊地址、重定向和 DNS 结果变化,并用网络出口策略兜底。

问题二:业务需要抓取用户的内网 Webhook,怎么办?

不要让公共抓取器直接访问任意内网。可部署位于目标网络中的受控连接器,由管理员建立绑定关系,并通过短期凭据、消息队列或双向认证接收任务。

问题三:返回内容不展示给用户,是否仍有风险?

仍有风险。攻击者可能通过状态码、响应时间、错误差异或外带回调判断目标是否可达,也可能调用具有副作用的内部接口。

问题四:WAF 能解决 SSRF 吗?

WAF 可以拦截部分已知特征,但通常无法可靠观察应用最终解析和连接到的 IP。SSRF 的核心控制点应位于 URL 使用逻辑、出站代理、防火墙和身份权限层。

十一、总结

SSRF 的本质是信任边界错误:应用把外部输入转换成了带有服务器网络身份的请求。可靠修复不能停留在字符串黑名单,而应依次落实目标白名单、规范化解析、地址分类、逐跳重定向检查、响应资源限制、受控出站代理、网络隔离和最小权限。应用层负责表达业务允许访问什么,网络层负责保证应用无法越界,两层结合后,单点校验失误才不至于直接演变为内部网络访问能力。
 
顶部