Cognito User Pools

Learn AWS hacking from zero to hero with htARTE (HackTricks AWS Red Team Expert)!

Other ways to support HackTricks:

Basic Information

A user pool is a user directory in Amazon Cognito. With a user pool, your users can sign in to your web or mobile app through Amazon Cognito, or federate through a third-party identity provider (IdP). Whether your users sign in directly or through a third party, all members of the user pool have a directory profile that you can access through an SDK.

User pools provide:

  • Sign-up and sign-in services.

  • A built-in, customizable web UI to sign in users.

  • Social sign-in with Facebook, Google, Login with Amazon, and Sign in with Apple, and through SAML and OIDC identity providers from your user pool.

  • User directory management and user profiles.

  • Security features such as multi-factor authentication (MFA), checks for compromised credentials, account takeover protection, and phone and email verification.

  • Customized workflows and user migration through AWS Lambda triggers.

Source code of applications will usually also contain the user pool ID and the client application ID, (and some times the application secret?) which are needed for a user to login to a Cognito User Pool.

Potential attacks

  • Registration: By default a user can register himself, so he could create a user for himself.

  • User enumeration: The registration functionality can be used to find usernames that already exists. This information can be useful for the brute-force attack.

  • Login brute-force: In the Authentication section you have all the methods that a user have to login, you could try to brute-force them find valid credentials.

Tools for pentesting

  • Pacu, the AWS exploitation framework, now includes the "cognito__enum" and "cognito__attack" modules that automate enumeration of all Cognito assets in an account and flag weak configurations, user attributes used for access control, etc., and also automate user creation (including MFA support) and privilege escalation based on modifiable custom attributes, usable identity pool credentials, assumable roles in id tokens, etc.

For a description of the modules' functions see part 2 of the blog post. For installation instructions see the main Pacu page.

Usage

Sample cognito__attack usage to attempt user creation and all privesc vectors against a given identity pool and user pool client:

Pacu (new:test) > run cognito__attack --username randomuser --email XX+sdfs2@gmail.com --identity_pools 
us-east-2:a06XXXXX-c9XX-4aXX-9a33-9ceXXXXXXXXX --user_pool_clients 
59f6tuhfXXXXXXXXXXXXXXXXXX@us-east-2_0aXXXXXXX

Sample cognito__enum usage to gather all user pools, user pool clients, identity pools, users, etc. visible in the current AWS account:

Pacu (new:test) > run cognito__enum
  • Cognito Scanner is a CLI tool in python that implements different attacks on Cognito including unwanted account creation and account oracle.

Installation

$ pip install cognito-scanner

Usage

$ cognito-scanner --help

For more information check https://github.com/padok-team/cognito-scanner

Registration

User Pools allows by default to register new users.

aws cognito-idp sign-up --client-id <client-id> \
    --username <username> --password <password> \
    --region <region> --no-sign-request

If anyone can register

You might find an error indicating you that you need to provide more details of abut the user:

An error occurred (InvalidParameterException) when calling the SignUp operation: Attributes did not conform to the schema: address: The attribute is required

You can provide the needed details with a JSON such as:

--user-attributes '[{"Name": "email", "Value": "carlospolop@gmail.com"}, {"Name":"gender", "Value": "M"}, {"Name": "address", "Value": "street"}, {"Name": "custom:custom_name", "Value":"supername&\"*$"}]'

You could use this functionality also to enumerate existing users. This is the error message when a user already exists with that name:

An error occurred (UsernameExistsException) when calling the SignUp operation: User already exists

Note in the previous command how the custom attributes start with "custom:". Also know that when registering you cannot create for the user new custom attributes. You can only give value to default attributes (even if they aren't required) and custom attributes specified.

Or just to test if a client id exists. This is the error if the client-id doesn't exist:

An error occurred (ResourceNotFoundException) when calling the SignUp operation: User pool client 3ig612gjm56p1ljls1prq2miut does not exist.

If only admin can register users

You will find this error and you own't be able to register or enumerate users:

An error occurred (NotAuthorizedException) when calling the SignUp operation: SignUp is not permitted for this user pool

Verifying Registration

Cognito allows to verify a new user by verifying his email or phone number. Therefore, when creating a user usually you will be required at least the username and password and the email and/or telephone number. Just set one you control so you will receive the code to verify your newly created user account like this:

aws cognito-idp confirm-sign-up --client-id <cliet_id> \
    --username aasdasd2 --confirmation-code <conf_code> \
    --no-sign-request --region us-east-1

Even if looks like you can use the same email and phone number, when you need to verify the created user Cognito will complain about using the same info and won't let you verify the account.

Privilege Escalation / Updating Attributes

By default a user can modify the value of his attributes with something like:

aws cognito-idp update-user-attributes \
    --region us-east-1 --no-sign-request \
    --user-attributes Name=address,Value=street \
    --access-token <access token>

Custom attribute privesc

You might find custom attributes being used (such as isAdmin), as by default you can change the values of your own attributes you might be able to escalate privileges changing the value yourself!

Email/username modification privesc

You can use this to modify the email and phone number of a user, but then, even if the account remains as verified, those attributes are set in unverified status (you need to verify them again).

You won't be able to login with email or phone number until you verify them, but you will be able to login with the username. Note that even if the email was modified and not verified it will appear in the ID Token inside the email field and the filed email_verified will be false, but if the app isn't checking that you might impersonate other users.

Moreover, note that you can put anything inside the name field just modifying the name attribute. If an app is checking that field for some reason instead of the email (or any other attribute) you might be able to impersonate other users.

Anyway, if for some reason you changed your email for example to a new one you can access you can confirm the email with the code you received in that email address:

aws cognito-idp verify-user-attribute \
    --access-token <access_token> \
    --attribute-name email --code <code> \
    --region <region> --no-sign-request

Use phone_number instead of email to change/verify a new phone number.

The admin could also enable the option to login with a user preferred username. Note that you won't be able to change this value to any username or preferred_username already being used to impersonate a different user.

Recover/Change Password

It's possible to recover a password just knowing the username (or email or phone is accepted) and having access to it as a code will be sent there:

aws cognito-idp forgot-password \
    --client-id <client_id> \
    --username <username/email/phone> --region <region>

The response of the server is always going to be positive, like if the username existed. You cannot use this method to enumerate users

With the code you can change the password with:

aws cognito-idp confirm-forgot-password \
    --client-id <client_id> \
    --username <username> \
    --confirmation-code <conf_code> \
    --password <pwd> --region <region>

To change the password you need to know the previous password:

aws cognito-idp change-password \
    --previous-password <value> \
    --proposed-password <value> \
    --access-token <value>

Authentication

A user pool supports different ways to authenticate to it. If you have a username and password there are also different methods supported to login. Moreover, when a user is authenticated in the Pool 3 types of tokens are given: The ID Token, the Access token and the Refresh token.

  • ID Token: It contains claims about the identity of the authenticated user, such as name, email, and phone_number. The ID token can also be used to authenticate users to your resource servers or server applications. You must verify the signature of the ID token before you can trust any claims inside the ID token if you use it in external applications.

    • The ID Token is the token that contains the attributes values of the user, even the custom ones.

  • Access Token: It contains claims about the authenticated user, a list of the user's groups, and a list of scopes. The purpose of the access token is to authorize API operations in the context of the user in the user pool. For example, you can use the access token to grant your user access to add, change, or delete user attributes.

  • Refresh Token: With refresh tokens you can get new ID Tokens and Access Tokens for the user until the refresh token is invalid. By default, the refresh token expires 30 days after your application user signs into your user pool. When you create an application for your user pool, you can set the application's refresh token expiration to any value between 60 minutes and 10 years.

ADMIN_NO_SRP_AUTH & ADMIN_USER_PASSWORD_AUTH

This is the server side authentication flow:

  • The server-side app calls the AdminInitiateAuth API operation (instead of InitiateAuth). This operation requires AWS credentials with permissions that include cognito-idp:AdminInitiateAuth and cognito-idp:AdminRespondToAuthChallenge. The operation returns the required authentication parameters.

  • After the server-side app has the authentication parameters, it calls the AdminRespondToAuthChallenge API operation. The AdminRespondToAuthChallenge API operation only succeeds when you provide AWS credentials.

This method is NOT enabled by default.

To login you need to know:

  • user pool id

  • client id

  • username

  • password

  • client secret (only if the app is configured to use a secret)

In order to be able to login with this method that application must allow to login with ALLOW_ADMIN_USER_PASSWORD_AUTH. Moreover, to perform this action you need credentials with the permissions**cognito-idp:AdminInitiateAuth**and cognito-idp:AdminRespondToAuthChallenge

aws cognito-idp admin-initiate-auth \
    --client-id <client-id> \
    --auth-flow ADMIN_USER_PASSWORD_AUTH \
    --region <region> \
    --auth-parameters 'USERNAME=<username>,PASSWORD=<password>,SECRET_HASH=<hash_if_needed>'
    --user-pool-id "<pool-id>"

# Check the python code to learn how to generate the hsecret_hash
Code to Login
import boto3
import botocore
import hmac
import hashlib
import base64


client_id = "<client-id>"
user_pool_id = "<user-pool-id>"
client_secret = "<client-secret>"
username = "<username>"
password = "<pwd>"

boto_client = boto3.client('cognito-idp', region_name='us-east-1')

def get_secret_hash(username, client_id, client_secret):
    key = bytes(client_secret, 'utf-8')
    message = bytes(f'{username}{client_id}', 'utf-8')
    return base64.b64encode(hmac.new(key, message, digestmod=hashlib.sha256).digest()).decode()

# If the Client App isn't configured to use a secret
## just delete the line setting the SECRET_HASH
def login_user(username_or_alias, password, client_id, client_secret, user_pool_id):
    try:
        return boto_client.admin_initiate_auth(
            UserPoolId=user_pool_id,
            ClientId=client_id,
            AuthFlow='ADMIN_USER_PASSWORD_AUTH',
            AuthParameters={
                'USERNAME': username_or_alias,
                'PASSWORD': password,
                'SECRET_HASH': get_secret_hash(username_or_alias, client_id, client_secret)
            }
        )
    except botocore.exceptions.ClientError as e:
        return e.response

print(login_user(username, password, client_id, client_secret, user_pool_id))

USER_PASSWORD_AUTH

This method is another simple and traditional user & password authentication flow. It's recommended to migrate a traditional authentication method to Cognito and recommended to then disable it and use then ALLOW_USER_SRP_AUTH method instead (as that one never sends the password over the network). This method is NOT enabled by default.

The main difference with the previous auth method inside the code is that you don't need to know the user pool ID and that you don't need extra permissions in the Cognito User Pool.

To login you need to know:

  • client id

  • username

  • password

  • client secret (only if the app is configured to use a secret)

In order to be able to login with this method that application must allow to login with ALLOW_USER_PASSWORD_AUTH.

aws cognito-idp initiate-auth  --client-id <client-id> \
    --auth-flow USER_PASSWORD_AUTH --region <region> \
    --auth-parameters 'USERNAME=<username>,PASSWORD=<password>,SECRET_HASH=<hash_if_needed>'
    
# Check the python code to learn how to generate the secret_hash
Python code to Login
import boto3
import botocore
import hmac
import hashlib
import base64


client_id = "<client-id>"
user_pool_id = "<user-pool-id>"
client_secret = "<client-secret>"
username = "<username>"
password = "<pwd>"

boto_client = boto3.client('cognito-idp', region_name='us-east-1')

def get_secret_hash(username, client_id, client_secret):
    key = bytes(client_secret, 'utf-8')
    message = bytes(f'{username}{client_id}', 'utf-8')
    return base64.b64encode(hmac.new(key, message, digestmod=hashlib.sha256).digest()).decode()

# If the Client App isn't configured to use a secret
## just delete the line setting the SECRET_HASH
def login_user(username_or_alias, password, client_id, client_secret, user_pool_id):
    try:
        return boto_client.initiate_auth(
            ClientId=client_id,
            AuthFlow='ADMIN_USER_PASSWORD_AUTH',
            AuthParameters={
                'USERNAME': username_or_alias,
                'PASSWORD': password,
                'SECRET_HASH': get_secret_hash(username_or_alias, client_id, client_secret)
            }
        )
    except botocore.exceptions.ClientError as e:
        return e.response

print(login_user(username, password, client_id, client_secret, user_pool_id))

USER_SRP_AUTH

This is scenario is similar to the previous one but instead of of sending the password through the network to login a challenge authentication is performed (so no password navigating even encrypted through he net). This method is enabled by default.

To login you need to know:

  • user pool id

  • client id

  • username

  • password

  • client secret (only if the app is configured to use a secret)

Code to login
from warrant.aws_srp import AWSSRP
import os

USERNAME='xxx'
PASSWORD='yyy'
POOL_ID='us-east-1_zzzzz'
CLIENT_ID = '12xxxxxxxxxxxxxxxxxxxxxxx'
CLIENT_SECRET = 'secreeeeet'
os.environ["AWS_DEFAULT_REGION"] = "<region>"

aws = AWSSRP(username=USERNAME, password=PASSWORD, pool_id=POOL_ID, 
    client_id=CLIENT_ID, client_secret=CLIENT_SECRET)
tokens = aws.authenticate_user()
id_token = tokens['AuthenticationResult']['IdToken']
refresh_token = tokens['AuthenticationResult']['RefreshToken']
access_token = tokens['AuthenticationResult']['AccessToken']
token_type = tokens['AuthenticationResult']['TokenType']

REFRESH_TOKEN_AUTH & REFRESH_TOKEN

This method is always going to be valid (it cannot be disabled) but you need to have a valid refresh token.

aws cognito-idp initiate-auth \
    --client-id 3ig6h5gjm56p1ljls1prq2miut \
    --auth-flow REFRESH_TOKEN_AUTH \
    --region us-east-1 \
    --auth-parameters 'REFRESH_TOKEN=<token>'
Code to refresh
import boto3
import botocore
import hmac
import hashlib
import base64

client_id = "<client-id>"
token = '<token>'

boto_client = boto3.client('cognito-idp', region_name='<region>')

def refresh(client_id, refresh_token):
    try:
        return boto_client.initiate_auth(
            ClientId=client_id,
            AuthFlow='REFRESH_TOKEN_AUTH',
            AuthParameters={
                'REFRESH_TOKEN': refresh_token
            }
        )
    except botocore.exceptions.ClientError as e:
        return e.response


print(refresh(client_id, token))

CUSTOM_AUTH

In this case the authentication is going to be performed through the execution of a lambda function.

Extra Security

Advanced Security

By default it's disabled, but if enabled, Cognito could be able to find account takeovers. To minimise the probability you should login from a network inside the same city, using the same user agent (and IP is thats possible).

MFA Remember device

If the user logins from the same device, the MFA might be bypassed, therefore try to login from the same browser with the same metadata (IP?) to try to bypass the MFA protection.

User Pool Groups IAM Roles

It's possible to add users to User Pool groups that are related to one IAM roles. Moreover, users can be assigned to more than 1 group with different IAM roles attached.

Note that even if a group is inside a group with an IAM role attached, in order to be able to access IAM credentials of that group it's needed that the User Pool is trusted by an Identity Pool (and know the details of that Identity Pool).

Another requisite to get the IAM role indicated in the IdToken when a user is authenticated in the User Pool (aws cognito-idp initiate-auth...) is that the Identity Provider Authentication provider needs indicate that the role must be selected from the token.

The roles a user have access to are inside the IdToken, and a user can select which role he would like credentials for with the --custom-role-arn from aws cognito-identity get-credentials-for-identity. However, if the default option is the one configured (use default role), and you try to access a role from the IdToken, you will get error (that's why the previous configuration is needed):

An error occurred (InvalidParameterException) when calling the GetCredentialsForIdentity operation: Only SAML providers and providers with RoleMappings support custom role ARN.

Note that the role assigned to a User Pool Group needs to be accesible by the Identity Provider that trust the User Pool (as the IAM role session credentials are going to be obtained from it).

{
    "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:2361092e-9db6-a876-1027-10387c9de439"
                },
                "ForAnyValue:StringLike": {
                    "cognito-identity.amazonaws.com:amr": "authenticated"
                }
            }
        }
    ]
}js
Learn AWS hacking from zero to hero with htARTE (HackTricks AWS Red Team Expert)!

Other ways to support HackTricks:

Last updated