引言
让我们先假设有这样的场景:
- 一个应用允许用户上传网络图片,即:用户输入URL,我的服务器会下载这个URL对应的图片,并返回给用户
- 应用间通讯,允许用户修改回调地址,应用在通讯时访问用户提供的回调地址完成通讯
- ...
这些都是真实存在的案例,它们的共同特征在于,应用提供了某种跳板功能,而且完全信任了用户输入的URL参数,并真正会请求这个URL。
让我们写个python脚本来描述上述功能:
from flask import Flask, request, jsonify
import requests
app = Flask(__name__)
@app.route("/fetch", methods=["GET"])
def fetch():
url = request.args.get("url")
if not url:
return jsonify({"error": "missing url parameter"}), 400
try:
resp = requests.get(url, timeout=10)
return resp.text, resp.status_code
except Exception as e:
return jsonify({"error": str(e)}), 500
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5800)
聪明的你可能一下子就发现了问题,这个脚本存在严重的漏洞,因为它无条件信任了用户的输入,导致用户可以访问一些内网服务(下图只做演示)

所以你修改了脚本,禁止其访问内网网段:
BLOCK_PATTERNS = [
r"^127\.", # 127.*
r"^10\.", # 10.*
r"^192\.168\.", # 192.168.*
r"^localhost$", # localhost
]
def is_blocked(host):
for pattern in BLOCK_PATTERNS:
if re.match(pattern, host):
return True
return False
@app.route("/fetch", methods=["GET"])
def fetch():
url = request.args.get("url")
if not url:
return jsonify({"error": "missing url parameter"}), 400
try:
parsed = urlparse(url)
host = parsed.hostname
# 是否访问了内网网段、本机端口
if not host:
return jsonify({"error": "invalid url"}), 400
if is_blocked(host):
return jsonify({"error": "blocked: local or private network address"}), 403
resp = requests.get(url, timeout=10)
return resp.text, resp.status_code
except Exception as e:
return jsonify({"error": str(e)}), 500

看似安全了很多,但是真的安全了吗?
云服务器SSRF攻击
攻击示例
我们运行上述python服务端的实验环境是华为云ECS实例,并在服务器内部部署了数据库等服务。我们通过屏蔽127.0.0.0/8、10.0.0.0/8、192.168.0.0/16、localhost等以禁止用户访问服务器内部资产和内网网段的其它资产。但是这仍然存在SSRF攻击漏洞:

可以看到,我们通过让服务器请求http://169.254.169.254下的API,可以轻松获取服务器的主机名、ECS规格、ECS ID、内网/公网IP,甚至可用区等敏感信息。
而某些厂商还能通过该接口获取到临时IAM密钥,相当于有了云服务的部分底层操控权限。
何为169.254.169.254
169.254.169.254大多数云平台都使用的内部保留地址(阿里云的是100.100.100.200),也叫Metadata Service,其用途是给云虚拟机提供自身配置、身份、网络与安全组、启动脚本、临时权限凭证等信息,让虚拟机知道“自己是谁”、“该做什么”,这就给了攻击者可乘之机。
以AWS为例,在EC2内部可以通过如下API请求临时密钥:
http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE_NAME
其返回样例为:
{
"AccessKeyId": "...",
"SecretAccessKey": "...",
"Token": "...",
"Expiration": "...",
}
该凭证相当于可以直接登录AWS的后台,允许创建新实例、下载S3等高危操作。
接下来让我们来修复前面写的python服务漏洞,添加黑名单,禁止访问169.254.169.254:
BLOCK_PATTERNS = [
r"^127\.", # 127.*
r"^10\.", # 10.*
r"^192\.168\.", # 192.168.*
r"^localhost$", # localhost
r"^169\.254\.169\.254"
]

可以看到,成功阻止了外部访问Metadata Service。
但是...这样真的就安全了吗
变种云上SSRF攻击
假设我是一个黑客,我购买了一个hack-domain.com域名,并添加了A记录,将meta.hack-domain.com指向169.254.169.254,继续进行实验

可以看到,我们之前写的黑名单完全失效了。这叫做DNS重绑定攻击。
即使服务器防御了DNS重绑定攻击,还有一种方式也能执行攻击:
黑客开启一个webserver,内部有一个html,类似:
<img src="http://169.254.169.254/..." />
并将url参数只想该网页。如果后端服务使用了Chrome无头模式访问和解析该网页,同样可以触发SSRF攻击。
解决方法
- 严格校验用户输入的URL,只允许访问白名单列表内的主机
- 不要把响应数据直接返回给用户,而是要做一层解析、过滤、处理
- 限制应用的网络权限