├── requirements.txt ├── screenshot └── tool.gif ├── looters ├── helpers │ ├── Color.py │ └── Entropy.py ├── CodeBuildLooter.py ├── EC2Looter.py └── LambdaLooter.py ├── README.md └── awsloot.py /requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | requests 3 | prompt_toolkit==1.0.14 4 | PyInquirer 5 | -------------------------------------------------------------------------------- /screenshot/tool.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastian-mora/AWS-Loot/HEAD/screenshot/tool.gif -------------------------------------------------------------------------------- /looters/helpers/Color.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Color: 4 | GREEN = '\033[92m' 5 | BLUE = '\033[94m' 6 | RED = '\033[91m' 7 | # noinspection SpellCheckingInspection 8 | ENDC = '\033[0m' 9 | 10 | @staticmethod 11 | def print(color, text): 12 | print(f'{color}{text}{Color.ENDC}') 13 | -------------------------------------------------------------------------------- /looters/helpers/Entropy.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | 4 | def shannon_entropy(data): 5 | if not data: 6 | return 0 7 | 8 | entropy = 0 9 | for character_i in range(256): 10 | px = data.count(chr(character_i)) / len(data) 11 | if px > 0: 12 | entropy += - px * math.log(px, 2) 13 | return entropy 14 | 15 | 16 | # This is slightly arbitrary. It's the value used in TruffleHog. Could be improved 17 | def contains_secret(data, THRESHOLD=3.5): 18 | return shannon_entropy(data) > THRESHOLD 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS_Loot 2 | 3 | Searches an AWS environment looking for secrets, by enumerating environment variables and source code. This tool allows quick enumeration over large sets of AWS instances and services. 4 | 5 | ![](screenshot/tool.gif) 6 | 7 | ## Install 8 | 9 | ``` 10 | pip install -r requirements.txt 11 | ``` 12 | An AWS credential file (.aws/credentials) is required for authentication to the target environment 13 | - Access Key 14 | - Access Key Secret 15 | 16 | ## How it works 17 | 18 | Awsloot works by going through EC2, Lambda, CodeBuilder instances and searching for high entropy strings. The EC2 Looter works by querying all available instance ID's in all regions and requesting instance's USERDATA where often developers leave secrets. 19 | The Lambda looter operates across regions as well. Lambada looter can search all available versions of a found function. 20 | It starts by searching the functions environment variables then downloads the source code and scans the source for secrets. 21 | The Codebuilder Looter works by searching for build instances and searching those builds for environment variables that might contain secrets. 22 | 23 | ## Usage 24 | ``` 25 | Python3 awsloot.py 26 | ``` 27 | 28 | ## Next Features 29 | - Allow users to specify an ARN to scan 30 | - Looter for additional services 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /looters/CodeBuildLooter.py: -------------------------------------------------------------------------------- 1 | from botocore.exceptions import ClientError, EndpointConnectionError 2 | from looters.helpers.Entropy import contains_secret 3 | from looters.helpers.Color import Color 4 | 5 | 6 | class CodeBuilder: 7 | 8 | def __init__(self, session, threshold, regions): 9 | self.session = session 10 | self.threshold = threshold 11 | self.regions = regions 12 | 13 | def run(self): 14 | 15 | Color.print(Color.BLUE, "\nSearching Regions for Codebuilds") 16 | for region in self.regions: 17 | try: 18 | ids = self.get_build_id(region) 19 | 20 | if ids: 21 | env_vars = self.get_environment_vars(ids, region) 22 | print(f"\tSearching {len(env_vars)} Builds in {region} \n") 23 | for build in env_vars: 24 | loot = [] 25 | [loot.append(var) for var in env_vars[build] if contains_secret(var['value'], THRESHOLD=self.threshold)] 26 | 27 | if loot: 28 | print(f"\t{'-' * 10} {build} {'-' * 10}") 29 | [Color.print(Color.GREEN, f'\t\t[+] {item}') for item in loot] 30 | 31 | except ClientError as e: 32 | if e.response['Error']['Code'] == 'AccessDeniedException': 33 | Color.print(Color.RED, f'\n\t[-] {e}') 34 | except EndpointConnectionError as e: 35 | #Color.print(Color.RED, f'\n\t[-] {e}') 36 | pass 37 | 38 | def get_build_id(self, region): 39 | try: 40 | code_sess = self.session.client('codebuild', region_name=region) 41 | ids = code_sess.list_builds() 42 | return ids['ids'] 43 | except ClientError as e: 44 | return [] 45 | 46 | def get_environment_vars(self, id, region): 47 | code_sess = self.session.client('codebuild', region_name=region) 48 | raw = code_sess.batch_get_builds(ids=id) 49 | return {build['arn']: build['environment']['environmentVariables'] for build in raw['builds']} 50 | -------------------------------------------------------------------------------- /looters/EC2Looter.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from looters.helpers.Entropy import contains_secret 3 | from looters.helpers.Color import Color as Color 4 | from botocore.exceptions import ClientError 5 | 6 | 7 | class Ec2Looter: 8 | 9 | def __init__(self, session, threshold, regions): 10 | 11 | self.session = session 12 | self.threshold = threshold 13 | self.regions = regions 14 | 15 | def run(self): 16 | try: 17 | Color.print(Color.BLUE, "Searching Regions for EC2 Instances") 18 | 19 | # Go through all the regions that have EC2 20 | for region in self.get_regions(): 21 | 22 | # Get all Ids in region 23 | ec2_in_region = self.get_instance_ids(region) 24 | 25 | # If there is an EC2 in that Region 26 | if ec2_in_region: 27 | 28 | print(f'\tChecking {len(ec2_in_region)} EC2 in {region}') 29 | 30 | # Go through all EC2 for that region and get USERDATA 31 | for instance_id in ec2_in_region: 32 | # TODO Make output consistent 33 | loot = [] 34 | # Pull USERDATA for Instanced ID 35 | user_data = self.get_instance_userdata(instance_id, region) 36 | 37 | if user_data: 38 | loot = [item for item in user_data.split() if contains_secret(item, THRESHOLD=self.threshold)] 39 | 40 | # Print secrets if found 41 | if loot: 42 | print(f"\n\t{'-'*10} {instance_id} {'-'*10}") 43 | [Color.print(Color.GREEN, f"\t\t[+] {item}") for item in loot if loot] 44 | 45 | except ClientError as e: 46 | if e.response['Error']['Code'] == 'AccessDeniedException': 47 | Color.print(Color.RED, f'[-] {e}') 48 | 49 | def get_instance_userdata(self, instance_id, region): 50 | ec2 = self.session.client('ec2', region_name=region) 51 | raw_data = ec2.describe_instance_attribute(Attribute='userData', InstanceId=instance_id)[ 52 | 'UserData'] 53 | 54 | if raw_data: 55 | user_data = base64.b64decode(raw_data['Value']).decode("utf-8") 56 | return user_data 57 | else: 58 | return None 59 | 60 | 61 | def get_instance_ids(self, region): 62 | ids = [] 63 | try: 64 | ec2 = self.session.client('ec2', region_name=region) 65 | data = ec2.describe_instances() 66 | 67 | for r in data['Reservations']: 68 | for i in r['Instances']: 69 | ids.append(i['InstanceId']) 70 | except: 71 | print(f"Failed on {region}") 72 | return ids 73 | 74 | def get_regions(self): 75 | regions = [] 76 | ec2 = self.session.client('ec2', region_name='us-west-2') 77 | for reg in ec2.describe_regions()["Regions"]: 78 | if reg['RegionName'] in self.regions: 79 | regions.append(reg['RegionName']) 80 | return regions 81 | -------------------------------------------------------------------------------- /looters/LambdaLooter.py: -------------------------------------------------------------------------------- 1 | from botocore.exceptions import ClientError 2 | 3 | from looters.helpers.Entropy import contains_secret 4 | from looters.helpers.Color import Color as Color 5 | import requests 6 | import zipfile 7 | import os 8 | import re 9 | 10 | ''' 11 | BOTO3 Issue? (https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/lambda.html#Lambda.Client.list_functions) 12 | Documentation lists you can use MASTERREGION='ALL' to search all regions. I think this is bugged bc it only returns 13 | empty lists. This might be a conflicting with the client requirement of a region. I will open an issue on github 14 | ''' 15 | 16 | 17 | class LambdaLooter: 18 | 19 | def __init__(self, session, output, threshold, regions): 20 | self.session = session 21 | self.o_path = output 22 | self.regions = regions 23 | self.lambd_threshold_evn = threshold 24 | self.lambd_threshold_source = self.lambd_threshold_evn + .3 25 | self.pattern = "(#.*|//.*|\\\".*\\\"|'.*'|/\\*.*)" 26 | 27 | 28 | def run(self): 29 | try: 30 | Color.print(Color.BLUE, "\nSearching Regions for Lambda Functions") 31 | print(f'\tSource Code will be saved to "output/{self.o_path}"') 32 | 33 | for region in self.regions: 34 | 35 | function_id = self.get_function_ids(region) 36 | 37 | if function_id: 38 | print(f"\tSearching {len(function_id)} in {region} Lambda Functions") 39 | 40 | for id in function_id: 41 | loot = [] 42 | # Get Lambda data (EVN Vars) 43 | environment_vars = self.get_function_data(id[1], region) 44 | 45 | # Find and Store Sensitive ENV Vars 46 | [loot.append(f"EVN_VAR - {key}: {environment_vars[key]}") 47 | for key in environment_vars if contains_secret(environment_vars[key], THRESHOLD=self.lambd_threshold_evn)] 48 | 49 | # Download and load function source code 50 | source_data = self.get_function_source(id, region) 51 | 52 | 53 | # Search Source code and save secrets 54 | # This part uses Regex to find "target" areas then searches those areas by word 55 | for key in self.get_function_source(id, region): 56 | 57 | for line in re.findall(self.pattern, source_data[key]): 58 | 59 | [loot.append(f"IN_SOURCE - {line}") for word in line.split() 60 | if contains_secret(word, self.lambd_threshold_source)] 61 | 62 | # Print ID and Saved Loot 63 | if loot: 64 | print(f"\n\t{'-' * 10} {id} {'-' * 10}") 65 | 66 | [Color.print(Color.GREEN, f"\t\t[+] {item}") for item in loot if loot] 67 | except ClientError as e: 68 | if e.response['Error']['Code'] == 'AccessDeniedException': 69 | Color.print(Color.RED, f'[-] {e}') 70 | 71 | def get_function_ids(self, region): 72 | """ 73 | Returns list [ (Name, ARN) , (Name, ARN) ] 74 | """ 75 | try: 76 | lambda_client = self.session.client('lambda', region_name=region) 77 | data = lambda_client.list_functions(FunctionVersion='ALL')['Functions'] 78 | return [(function['FunctionName'], function['FunctionArn']) for function in data] 79 | except: 80 | return [] 81 | 82 | def get_function_data(self, id, region): 83 | """ 84 | Returns a dict of environment vars 85 | """ 86 | lambda_client = self.session.client('lambda', region_name=region) 87 | data = lambda_client.get_function_configuration(FunctionName=id) 88 | 89 | try: 90 | return data['Environment']['Variables'] 91 | except KeyError: 92 | return {} 93 | 94 | def get_function_source(self, id, region): 95 | lambda_client = self.session.client('lambda', region_name=region) 96 | source = lambda_client.get_function(FunctionName=id[1]) 97 | try: 98 | # Get Link and setup file name 99 | source_loc = source["Code"]['Location'] 100 | fname = source_loc.split('/') 101 | 102 | # Download File from URL 103 | r = requests.get(source_loc, stream=True) 104 | 105 | # Write Zip to output file 106 | fname = os.path.join(f'output/{self.o_path}', f'{fname[len(fname) - 1][:30]}.zip') 107 | zfile = open(fname, 'wb') 108 | zfile.write(r.content) 109 | zfile.close() 110 | 111 | # Load Zip contents into memory 112 | lambda_zip = zipfile.ZipFile(fname) 113 | 114 | return {id: lambda_zip.read(name).decode("utf-8") for name in lambda_zip.namelist()} 115 | 116 | except KeyError: 117 | print(Color.RED, f'Error getting {id[0]} Source') 118 | -------------------------------------------------------------------------------- /awsloot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import print_function, unicode_literals 3 | from PyInquirer import (prompt, style_from_dict, Token) 4 | 5 | import boto3, botocore.exceptions 6 | 7 | from os import makedirs 8 | 9 | from looters import EC2Looter, CodeBuildLooter, LambdaLooter 10 | from looters.helpers.Color import Color as Color 11 | 12 | custom_style = style_from_dict({ 13 | Token.Separator: '#6C6C6C', 14 | Token.QuestionMark: '#FF9D00 bold', 15 | #Token.Selected: '', # default 16 | Token.Selected: '#5F819D', 17 | Token.Pointer: '#FF9D00 bold', 18 | Token.Instruction: '', # default 19 | Token.Answer: '#5F819D bold', 20 | Token.Question: '', 21 | }) 22 | 23 | 24 | 25 | def banner(): 26 | print(''' 27 | 28 | ___ ______ _ _ 29 | / \ \ / / ___| | | ___ ___ | |_ 30 | / _ \ \ /\ / /\___ \ | | / _ \ / _ \| __| 31 | / ___ \ V V / ___) | | |__| (_) | (_) | |_ 32 | /_/ \_\_/\_/ |____/ |_____\___/ \___/ \__| 33 | 34 | 35 | By Sebastian Mora 36 | ''') 37 | 38 | 39 | def create_session(profile): 40 | # Ask the user for the creds 41 | 42 | # Create the root session# and test to see if the session is valid 43 | 44 | try: 45 | session = boto3.Session(profile_name=profile) 46 | session.client('sts').get_caller_identity() 47 | # Create output file 48 | create_output(profile) 49 | return session, profile 50 | # Session failed. Exit 51 | except botocore.exceptions.ClientError: 52 | Color.print(Color.RED, "[-] AWS was not able to validate the provided access credentials") 53 | exit(1) 54 | except botocore.exceptions.ProfileNotFound: 55 | Color.print(Color.RED, f'[-] The config profile {profile.upper()} could not be found ') 56 | exit(1) 57 | 58 | 59 | def create_output(session_name): 60 | # Creates a folder using the profile name. 61 | try: 62 | makedirs(f'output/{session_name}') 63 | except FileExistsError: 64 | pass 65 | 66 | 67 | def ask_profile(): 68 | questions = [ 69 | { 70 | 'type': 'input', 71 | 'name': 'profile_name', 72 | 'message': 'AWS profile name', 73 | } 74 | ] 75 | 76 | return prompt(questions, style=custom_style)['profile_name'] 77 | 78 | 79 | def ask_entropy(): 80 | value = None 81 | question = [{'type': 'input', 82 | 'name': 'threshold', 83 | 'message': f'Set the entropy threshold for the looters.', 84 | 'default': '3.5'}] 85 | 86 | try: 87 | value = float(prompt(question)['threshold']) 88 | except ValueError: 89 | Color.print(Color.RED, '[-] Invalid entry') 90 | exit(1) 91 | 92 | return value 93 | 94 | 95 | def ask_services(): 96 | questions = [ 97 | { 98 | 'type': 'checkbox', 99 | 'message': 'Select services to L00t', 100 | 'name': 'services', 101 | 'choices': [{"name": "ec2"}, {"name": "lambda"}, {"name": "codebuild"}] 102 | } 103 | ] 104 | return prompt(questions, style=custom_style)['services'] 105 | 106 | 107 | def ask_regions(): 108 | 109 | questions = [ 110 | 111 | { 112 | 'type': 'checkbox', 113 | 'message': f'select regions to l00t', 114 | 'name': 'regions', 115 | 'choices': [{"name": "us-east-1"}, {"name": "us-east-2"}, {"name": "us-west-1"}, {"name": "us-west-2"}, 116 | {"name": "ap-east-1"}, {"name": "ap-south-1"}, 117 | {"name": "ap-northeast-3"}, {"name": "ap-northeast-2"}, {"name": "ap-southeast-1"}, 118 | {"name": "ap-southeast-2"}, {"name": "ap-northeast-1"}, 119 | {"name": "ca-central-1"}, {"name": "cn-north-1"}, {"name": "cn-northwest-1"}, 120 | {"name": "eu-central-1"}, {"name": "eu-west-1"}, 121 | {"name": "eu-west-2"}, {"name": "eu-west-3"}, {"name": "eu-north-1"}, 122 | {"name": "me-south-1"}, {"name": "sa-east-1"}] 123 | } 124 | ] 125 | 126 | return prompt(questions, style=custom_style)['regions'] 127 | 128 | if __name__ == '__main__': 129 | 130 | banner() 131 | 132 | 133 | profile = ask_profile() 134 | 135 | session, output_path = create_session(profile) 136 | 137 | services = ask_services() 138 | 139 | entropy_threshold = ask_entropy() 140 | 141 | regions = ask_regions() 142 | 143 | print('\n') 144 | 145 | for service in services: 146 | 147 | if service is 'ec2': 148 | EC2Looter.Ec2Looter(session, entropy_threshold, regions).run() 149 | if service is 'lambda': 150 | LambdaLooter.LambdaLooter(session, profile, entropy_threshold, regions).run() 151 | if service is 'codebuild': 152 | CodeBuildLooter.CodeBuilder(session, entropy_threshold, regions).run() 153 | else: 154 | pass 155 | --------------------------------------------------------------------------------