top of page
  • Snir Levi

Lateral Movement between AWS Accounts - Abusing Misconfigured Trust Relationships in AWS

I want to share an interesting risk for a potential misconfiguration I found and exploited during my last purple team engagement on a client AWS environment. The reason I call this a risk is because this is not an actual vulnerability, but if you are unaware of how things work in AWS, you can easily create a misconfiguration which may allow an attacker to move laterally between your AWS accounts.


Before we jump into the attack, let’s first explain a few acronyms in AWS:


Roles:

In AWS, roles are IAM identities with specific permissions. This is similar to a user, but the main difference between a role and a user is that a role can be assumed by anyone who needs it. So multiple services/users/applications can use the same role. The other difference is that roles use temporary credentials, and not long-term credentials like users do. When a user or an application assumes a role, they are provided temporary credentials they can use.


Trust Relationships

When you create a role, you can create a trust policy for that role. This is basically a policy that defines who can assume that role and under what conditions. A role as we said, can be assumed by users or applications/services. Assuming a role can be done by different services on the same AWS account, by services from different AWS account, and by third-party systems like Google and Facebook, Amazon Cognito or other identity providers.

We will focus on trust relationships between AWS accounts.


Trust Relationships between AWS Accounts

Why we need this?

Think about an application that is separated and based on multiple AWS accounts. This can be a monitoring application that monitors different AWS accounts from a central account. In that case, you will want to create a role that is assumable by this application, from the monitored account, and allow this application to assume that role.


The Risk

Now that we know what roles are, what trust relationships are, and how we use them, let’s look at the risk I found on AWS. To present the risk we will demo a simple application which uses 2 AWS accounts. The application is using a simple Lambda function on “Account A” and that function needs to read and write to S3 buckets from “Account B”. Here is our topology:


  • We will begin with Account A and create a simple role called “Application-Role”:


  • Now let’s focus on Account B and create the role which trusts Account A’s “Application-Role”. When you create a role, you have the option to choose the trusted entity to assume that role. We said that we can use trust between entities on different AWS accounts, So we will pick this option:

  • We can see that we have an option to enter the Account ID of the trusted account, so we will enter the Account ID. We will not use MFA because an application is going to assume our role and not a human user.

  • Our trusted role should access our S3 buckets, so we will set the “AmazonS3FullAccess” permissions for the role:

  • That’s it, we can set the “StorageRole” name and create the role:


Now here is the catch, have you noticed that we haven’t entered the “Application-Role” ARN or name from the other AWS account? Yes, the only setting we supplied in the trusted entity is the AWS Account number. We didn’t have any other option than the Account ID. Let’s examine how the created trust policy looks like:




Pay attention to line number 7, this means that EVERY USER from Account A can assume that role.

By using these above default steps we created a trusted entity for our "StorageRole", and allowed every user from Account A to assume the "StorageRole".

But remember? We wanted the “Application-Role” to be the only role that is able to assume the "StorageRole". However by adding the “Root Account” ARN, not only does our “Application-Role” receive the ability to assume the “Storage-Role” in Account B but also all users in Account A as well.


Now let’s demo our full exploitation of this misconfigurations, exactly like I did on my Purple Team.


Scenario: Assume Breach - AWS user from Account A

Our scenario is “Assume Breach”. In Assume Breach scenario as the name suggests, we assume that an attacker has access to a network/environment/user/account, and this is the starting point of the engagement. In our scenario, we obtained access to an AWS user from Account A.


  • We begin with standard enumeration of Account A and identify the Lambda function which the application uses. Seems like we have read permissions for that lambda function.

  • In the environment variables of the function, we can see some interesting information:

We can see that there is an environment variable which is set to a role name. probably a role that this function assumes.

  • Ok, interesting. Let’s look at the function logs, can we read them? Yes we can:

Pay attention to the “accountid” field, this is the account which may be the one we can assume the “StorageRole” on.

  • Let’s load up AWS CLI tool, and create a profile with our current user, “john”:


  • Ok, now let’s try to assume the role on Account B which we discovered in the logs:

Great! We got our access key, secret access key and our session token.

  • The only thing left for us to do, is to enumerate the account we got access to. Let’s see if we can list the S3 buckets:

Yes we can, now we can exfiltrate the data from these buckets on Account B.

This is the result in our diagram:



Mitigation and Detection:

Before the mitigation and detection part, I wanted to thank the AWS Security team for being responsive and careful. AWS takes security very seriously. I reported this risk to the AWS security team, and they are planning to apply a fix to this possible risk. What I recommended for AWS to do is to add a field of a specific IAM ARN on the menu of the role creation, so the user will be able to see that he has the option to choose between a all IAM identities on AWS account or a specific IAM ARN. A good example I can compare it to is the Elasticsearch service on AWS. The access policy menu for it looks like this:


You have the option to choose between an IP address restriction or a specific IAM ARN. So it’s much more intuitive for a user to pick the right choice.

But remember, first, this will not mitigate the risk, it will just help to prevent this misconfiguration from happening. However, it can still happen anyway and it’s still the customers responsibility to mitigate that risk because under the Shared Responsibility Model the customers are responsible for thier AWS identities. You can mitigate this risk by a few simple steps:

First, make sure to harden the assumed role policy, to allow only the specific entities needed to assume it. Since in the AWS Console this option is missing on creation, you have a few solutions that you can work:

  • Using AWS Console

    • After creation, navigate to the role menu on the AWS Console, under trust relationship, click “Edit trust relationship”:


  • Edit the Principal to the ARN of the role from Account A:

Once the policy will be modified, another attempt to assume the role by “John” will raise “AccessDenied” error:


  • Using AWS CLI

    • You can create a policy on AWS CLI with the same JSON file as above, where the input will be the specific role to trust.

  • Using Infrastructure as Code

    • If you use Infrastructure as Code tools such as CloudFormation template or Terraform to deploy your resources, you can define the policy to include an input field for the specific role name before you deploy a role with a trust policy.


  • In addition, always remember to use the principle of least privilege. Grant a role the minimum permissions needed for that role to work. If you don’t know the exact permissions needed for your role, don’t worry, I am going to release an open source tool which maps the specific permissions needed for a role or a user in AWS environment (as well as GCP and Azure). The idea is simple, just set administrator permissions for your role in a non-production environment and let the application/service run for a couple of days, then map all the permissions which were used by the role to a list, and based on the list, create your policy. This is a very similar approach for deploying a firewall in a network. First, you open all traffic by setting the rules to “ANY->ANY”, and then you inspect what addresses and ports are used. My tool does that automatically for you, and it will be released soon. I will add the link here once it’s out.

Detection


Remember that we can turn any possible vulnerability or misconfiguration exploitation into strength by detecting adversaries who are trying to exploit it.

  • Create a list of the entities that are authorized to assume the role. In our case, it’s the ARN of “Application-Role”.

  • Create an alert event on your SIEM or detection solution to alert on every “AccessDenied” API call of AssumeRole on that role. This is how our event looks on AWS CloudTrail:


On the same idea, create a whitelist of the permissions used by the role, so every permission which appears on the policy should be on that list.

  • If that role is used by an application, there is no reason that the application will use different API calls to other services. If that’s happen, we can say it’s a malicious activity.

  • Based on the permission whitelist create an alert event on your SIEM or detection solution to trigger any malicious API call which the role is unauthorized to do.

Conclusion:

Trust relationships can help us in a lot of use cases, and roles are a secure way to allow applications and services to perform actions. However, sometimes without paying attention, we can use these to expose other attack surfaces that adversaries may exploit. Always make sure to use principle of least privilege for your identities, and grant the minimal permissions needed for them to work. Some people are referring to Identity as “The New Perimeter”, I couldn’t agree more in the era of the cloud.




bottom of page