云服务器SSRF漏洞利用详解

发布于 / 运维 / 0 条评论

引言

让我们先假设有这样的场景:

  • 一个应用允许用户上传网络图片,即:用户输入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,只允许访问白名单列表内的主机
  • 不要把响应数据直接返回给用户,而是要做一层解析、过滤、处理
  • 限制应用的网络权限
转载原创文章请注明,转载自: 斐斐のBlog » 云服务器SSRF漏洞利用详解
目前还没有评论,快来抢沙发吧~