Rhino Security Labs

Unauthenticated AWS Role Enumeration (IAM Revisited)

Intro: Cross-Account ("Unauthenticated") Reconnaissance

When attacking an AWS cloud environment, its important to use leverage unauthenticated enumeration whenever possible. This kind of IAM recon can help you gain a better understanding of the environment itself, the users and applications that are using the AWS environment, and other information.  IAM Roles and other ‘insider knowledge’ is key for any cloud penetration test.

When it comes to Amazon Web Services, most people think of S3 bucket enumeration as the only option for unauthenticated recon against a target.  However as we’ve shown in past blogs, it’s possible to enumerate IAM users and roles without any keys (or other inside knowledge) to a target account. This allows for information gathering that can potentially expose who is using the environment, what 3rd party services are being utilized,  AWS services utilized, and more. This all happens without any logs (CloudTrail or otherwise) being created in the victim’s account.

If you are unfamiliar with this concept of cross-account IAM resource enumeration in AWS, I suggest you check out our past AWS security posts:

If you have used our open source AWS exploitation framework Pacu recently, you may have noticed that the “iam__enum_assume_role” module was not working correctly. This is because AWS made a change to the API to prevent this cross-account attack.  This post will review what happened to that module, how the previously-discovered bug was fixed, and how Pacu’s new update provides the same functionality.

The Old AWS Role Enumeration Method

The old attack is outlined in the “Assuming the Worst” blog post that we released in the past. Essentially, the old method to enumerate the existence of roles cross-account relied on the Simple Token Server (STS) AssumeRole API. This API allowed for enumeration of IAM roles because it would return an error message if the role existed that was different than the error message if the role did not exist.

When attempting to assume a role that exists, but you didn’t have permissions to assume, the API would return a message like this:

An error occurred (AccessDenied) when calling the 'AssumeRole' operation: User: arn:aws:iam::012345678901:user/MyUser is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::111111111111:role/aws-service-role/rds.amazonaws.com/AWSServiceRoleForRDS

This AWS error message is basically saying that the user “MyUser” in the account “012345678901” is not allowed to assume the role “AWSServiceRoleForRDS” in the account “111111111111”. This message indicated that the role existed.

If the role did not exist, then the following error message would be returned instead:

An error occurred (AccessDenied) when calling the AssumeRole operation: Not authorized to perform sts:AssumeRole

This vulnerability allowed for a quick, scriptable method for cross-account role enumeration. Then, if you were lucky, the role in the target account could be misconfigured and would allow you to successfully assume it and gain access to the target account.

Since this research was released, AWS security made a change to the API so that the STS AssumeRole API will return the same error message, regardless of whether the role exists of not. Now, you will see this error message instead:

An error occurred (AccessDenied) when calling the AssumeRole operation: Access denied

For this reason, it was now necessary to scrap the “iam__enum_assume_role” Pacu module. It has been moved to the “modules_archive” folder in the Pacu repository for historical purposes.

The good news is that a new method was found for the same attack, so we can still enumerate roles in other AWS accounts, which means a new Pacu module was written to do just this.

The New AWS Role Enumeration Method

How It Works

For this new method, the “iam__enum_roles” Pacu module was written. It behaves the same as the “iam__enum_assume_role” module on the outside but uses an alternate method on the inside. Given an AWS account ID, the module will attempt to guess roles in the target account. When that phase is complete, it will cycle through any roles that were discovered and it will try to assume them. If any of those roles are misconfigured, you might be able to gain credentials to the target account by assuming them.

The method behind this cross-account role enumeration is exactly the same method as the cross-account user enumeration that we released previously. This time, cross-account role enumeration is abusing a feature of AWS, rather than the difference between two error messages, so it is highly unlikely that this method will be fixed any time soon.

This method involves IAM role trust policies and abuses a feature that you may not have realized existed. When setting up an IAM role trust policy, you are specifying what AWS resources/services can assume that role and gain temporary credentials. In the background, there is something going on that you might not realize. Let’s consider the following IAM role trust policy, which allows the “Test” role from the account ID “216825089941” to assume it.

{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"arn:aws:iam::216825089941:role\/Test"},"Action":"sts:AssumeRole"}]}

If we look at the trust relationships tab in the IAM web console, this is what we see:

Now, if we go and delete the “Test” role, then look at the trust relationships page again, we will see something different:

Then, if we hit “Edit trust relationship”, we will see that same value specified as the principal in the trust policy, but if we click “Update Trust Policy”, we will be shown this error message:

So, what happened? We didn’t change the value, but it was changed to an invalid value automatically. This is because when you save the trust policy document of a role, AWS security will find the resource specified in the principal somewhere in AWS to ensure that it exists. If the resource is found, the trust policy will save successfully, but if it is not found, then an error will be thrown, indicating an invalid principal was supplied. When AWS does this on the back-end, it takes the ARN that you supplied (“arn:aws:iam::216825089941:role/Test” for us) and matches it to a unique identifier that correlates to the resource in AWS. Then, when we deleted the “Test” role, AWS was no longer able to match the ARN we specified to an AWS resource, so by default, it will replace the ARN with the unique ID that was associated with that resource originally.

Why It Works Like This

There are potentially multiple reasons that this is done, but the best example is as follows.

Let’s say that a role allows the IAM user “Mike” in account “111111111111” to assume it. Mike is then fired from the company and has his AWS user deleted. Then, a week later, a new, different “Mike” is hired to the company and has an IAM user “Mike” created for him. Because of how AWS originally associates “Mike” to that unique ID (“AROAJUFJY2PBF22P35J4A” in our example above), the new “Mike” that just got hired would not be able to assume that original role, even though he has the same user name.

To allow the new Mike to assume that old role, the trust policy of the old role would need to be updated to allow access to the same ARN as before, but the update allows AWS to re-associate that ARN with the new “Mike” that exists, rather than the old “Mike” that doesn’t exist.

This is helpful in preventing situations where the old “Mike” was supposed to have more access than the new “Mike”, but because they had the same name, the new “Mike” gained additional privileges on accident. Instead, this problem is solved by associating ARNs to unique IDs for IAM resources.

The New Pacu Module

So ultimately, we can abuse this functionality to enumerate users and roles that exist in another account.

To try out the new module, you can run the following Pacu command from a Pacu session that has your own personal AWS credentials set:

run iam__enum_roles --role-name MyOwnRole --account-id 111111111111

This will use the “MyOwnRole” role in your own account to try and enumerate what roles exist in the account “111111111111”, then it will try to assume any that were discovered and provide you the credentials if successful. The following screenshot shows the module being run, where it finds three roles in the target account, then successfully assumes the “CodeDeploy” role.

Now we have the same functionality the old “iam__enum_assume_role” module had, just with a different method. Like it was previously noted, this is the same method used in the “iam__enum_users” module that is used for cross-account IAM user enumeration.

This attack will create a large amount of “UpdateAssumeRolePolicy” API calls in your own account’s CloudTrail logs, but the account you are targeting will not see anything in their logs, unless you successfully assume a misconfigured role. In that case, they will see that the role was assumed cross-account, but the basic cross-account enumeration is impossible to detect from their side.

In addition to our default list of known users and roles to look for, check out this list that Daniel Grzelak (@dagrz) created, which includes many known 3rd party integrations.

Conclusion

We wrote Pacu so that it was easy to handle AWS attacks like this. When the API changes or AWS vulnerabilities are fixed, its simple to quickly fix/update our modules to work again.

Again, for all your AWS penetration testing needs, check out our exploitation framework Pacu: https://github.com/RhinoSecurityLabs/pacu.  For support and reporting bugs in Github, open an issue in GitHub and we’ll get back to you as soon as possible.

For a vulnerable-by-design AWS environment to test out Pacu and other attack tools, check out CloudGoat on our GitHub as well: https://github.com/RhinoSecurityLabs/cloudgoat

Stay tuned to our blog, as we have a lot more AWS research coming your way soon!