def process(ctx, directory, arn): """Process cloudtrail files""" log.info('Processing CloudTrail...') if not os.path.exists(directory): log.fatal('Invalid Directory Path') files = [] for cloudtrail_file in os.listdir(directory): files.append(os.path.join(directory, cloudtrail_file)) api_calls_logged = process_cloudtrail(arn, files)
def cloudtrail_calls(ctx, services): """Enumerate all calls for AWS Services to determine what shows up in CloudTrail""" log.info('Starting enumeration for CloudTrail...') session = boto3.Session() if not services: services = session.get_available_services() enumerate_services(ctx.config, services, dry_run=ctx.dry_run) log.info('Enumeration complete')
def attack(ctx): """Simulate an attack by making the calls described in the config""" if not ctx.config.get('attack_chain', None): log.fatal('attack_chain not found in config file') log.info('Starting attack simulation...') attack_commands = ctx.config['attack_chain'] simulate_attack(ctx.config, attack_commands, dry_run=ctx.dry_run) log.info('Attack simulation complete')
def record_cloudtrail(arn, files): api_calls = [] for file in files: f = None log.info('Processing file: {}'.format(file)) if file.endswith('.gz'): f = gzip.open(file, 'r') else: f = open(file, 'r') try: cloudtrail = json.load(f) except Exception as e: log.error('Invalid JSON File: {} - {}'.format(file, e)) continue records = sorted(cloudtrail['Records'], key=lambda x: datetime.strptime( x['eventTime'], '%Y-%m-%dT%H:%M:%SZ'), reverse=False) for record, next_record in pairwise(records): if record.get('userIdentity', {}).get('arn', '').startswith(arn): event_source = record['eventSource'].split('.')[0] event_name = record['eventName'] time_delay = 0 if next_record: time_delta = datetime.strptime( next_record['eventTime'], '%Y-%m-%dT%H:%M:%SZ') - datetime.strptime( record['eventTime'], '%Y-%m-%dT%H:%M:%SZ') time_delay = time_delta.seconds call = '{}.{}'.format(event_source, event_name) log.info('{}.{} - {}'.format( record['eventSource'].split('.')[0], record['eventName'], record['userIdentity']['arn'])) api_calls.append({ 'call': '{}.{}'.format(event_source, event_name), 'time_delay': time_delay }) f.close() return api_calls
def record(ctx, directory, arn, output): """Create attack simulation from CloudTrail""" log.info('Recording CloudTrail...') if not os.path.exists(directory): log.fatal('Invalid Directory Path') files = [] for cloudtrail_file in os.listdir(directory): files.append(os.path.join(directory, cloudtrail_file)) api_calls_recorded = record_cloudtrail(arn, files) if output: with open(output, 'w') as yaml_file: yaml.dump({'attack_chain': api_calls_recorded}, yaml_file, default_flow_style=False)
def simulate_attack(config, commands, dry_run=False): log.debug('Attack chain to be executed:') log.debug(json.dumps(commands, indent=4)) session = boto3.Session() for command in commands: service_event = command['call'].split('.') service = service_event[0] api_call = service_event[1] delay = command.get('time_delay', 0) region = command.get('region', None) log.info('Making call - {}.{}'.format(service, api_call)) if not dry_run: make_call(config, session, service, api_call, region) log.info('Sleeping {} until next call'.format(delay)) if not dry_run: time.sleep(delay)
def process_cloudtrail(arn, files): api_calls = [] log.info('EventSource, EventName, Recorded Name, Match') for file in files: f = None log.debug('Processing file: {}'.format(file)) if file.endswith('.gz'): f = gzip.open(file, 'r') else: f = open(file, 'r') try: cloudtrail = json.load(f) except Exception as e: log.error('Invalid JSON File: {} - {}'.format(file, e)) continue for record in cloudtrail['Records']: if record.get('userIdentity', {}).get('arn', '').startswith(arn): event_source = record['eventSource'].split('.')[0] event_name = record['eventName'] call = '{}.{}'.format(event_source, event_name) if call not in api_calls: session = record['userIdentity']['arn'].split('/')[-1] match = (record['eventName'].lower() == session) log.info('{}, {}, {}, {}'.format( record['eventSource'].split('.')[0], record['eventName'], session, match)) api_calls.append(call) f.close() return api_calls
def enumerate_services(config, services, dry_run=False): # Create a boto3 session to use for enumeration session = boto3.Session() authorized_calls = [] for service in services: if service == 's3control': log.info( 'Skipping {} - End-points do not seem to be working'.format( service)) continue if len(session.get_available_regions(service)) == 0: if service in [ 'budgets', 'ce', 'chime', 'cloudfront', 'iam', 'importexport', 'organizations', 'route53', 'sts', 'waf' ]: region = 'us-east-1' else: log.info( 'Skipping {} - No regions exist for this service'.format( service)) continue else: if 'us-east-1' in session.get_available_regions(service): region = 'us-east-1' else: log.info('Skipping {} - Only available in {}'.format( service, session.get_available_regions(service))) continue # Create a service client log.info('Creating {} client...'.format(service)) # Set the user-agent if specified in the config if config.get('user_agent', None): botocore_config.user_agent = config['user_agent'] # Create a client with parameter validation off client = session.client(service, region_name=region, config=botocore_config) # Get the functions that you can call functions_list = get_boto_functions(client) # Get the service file service_file_json = get_service_json_files(config) # Get a list of params needed to make the serialization pass in botocore service_call_params = get_service_call_params( service_file_json[service]) # Loop through all the functions and call them for function in functions_list: # The service_file_json doesn't have underscores in names so let's remove them function_key = function[0].replace('_', '') # Session Name Can only be 64 characters long if len(function_key) > 64: session_name = function_key[:63] log.info('Session Name {} is for {}'.format( session_name, function_key)) else: session_name = function_key # Set the session to the name of the API call we are making session = get_assume_role_session( account_number=config['account_number'], role=config['account_role'], session_id=session_name) new_client = session.client(service, region_name=region, config=botocore_config) new_functions_list = get_boto_functions(new_client) for new_func in new_functions_list: if new_func[0] == function[0]: # We need to pull out the parameters needed in the requestUri, ex. /{Bucket}/{Key+} -> ['Bucket', 'Key'] params = re.findall( '\{(.*?)\}', service_call_params.get(function_key, '/')) params = [p.strip('+') for p in params] try: func_params = {} for param in params: # Set something because we have to func_params[param] = 'testparameter' log.info('Calling {}.{} with params {} in {}'.format( service, new_func[0], func_params, region)) if not dry_run: make_api_call(service, new_func, region, func_params) except ClientError as e: if "ValidationError" in str(e): log.error(e) else: log.debug(e) except boto3.exceptions.S3UploadFailedError as e: log.debug(e) except TypeError as e: log.debug(e) except KeyError as e: log.debug('Unknown Exception: {}.{} - {}'.format( service, new_func[0], e))
def enumerate_services(config, services, dry_run=False): # read a CSV file for seen functions apis = defaultdict(list) with open('botocore_api_2_event_names.csv', 'rb') as csvfile: reader = csv.reader(csvfile) for row in reader: x = row[1].split('_') if len(x) > 1: # service_name without '-' apis[x[0]].append(row[1]) else: # event_source as a service_name without '-' apis[row[0].replace('-', '')].append(row[1]) # Create a boto3 session to use for enumeration session = boto3.Session() authorized_calls = [] for service in services: if len(session.get_available_regions(service)) == 0: log.debug('Skipping {} - No regions exist for this service'.format( service)) continue # Create a service client log.info('Creating {} client...'.format(service)) # Grab a region to use for the calls. This should be us-west-2 region = session.get_available_regions(service)[-1] # Set the user-agent if specified in the config if config.get('user_agent', None): botocore_config.user_agent = config['user_agent'] # Create a client with parameter validation off client = session.client(service, region_name=region, config=botocore_config) # Get the functions that you can call functions_list = get_boto_functions(client) # Get the service file service_file_json = get_service_json_files(config) # Get a list of params needed to make the serialization pass in botocore service_call_params = get_service_call_params( service_file_json[service]) # Loop through all the functions and call them for function in functions_list: # The service_file_json doesn't have underscores in names so let's remove them function_key = function[0].replace('_', '') ## Session Name Can only be 64 characters long srv_len = len(service) session_name = service.replace('-', '') if srv_len > 20: session_name = service[:19] srv_len = 20 func_key_limit = 64 - srv_len - 1 if len(function_key) > func_key_limit: session_name += '_' + function_key[:func_key_limit - 1] log.info('Session Name {} is for {}:{}'.format( session_name, service, function_key)) else: session_name += "_" + function_key # check session_name in the seen functions if session_name in apis[service.replace('-', '')]: log.info('found {}:{}, skipping'.format(service, session_name)) continue # Set the session to the name of the API call we are making session = get_assume_role_session( account_number=config['account_number'], role=config['account_role'], session_id=session_name) new_client = session.client(service, region_name=region, config=botocore_config) new_functions_list = get_boto_functions(new_client) for new_func in new_functions_list: if new_func[0] == function[0]: # We need to pull out the parameters needed in the requestUri, ex. /{Bucket}/{Key+} -> ['Bucket', 'Key'] params = re.findall( '\{(.*?)\}', service_call_params.get(function_key, '/')) params = [p.strip('+') for p in params] try: func_params = {} for param in params: # Set something because we have to func_params[param] = 'testparameter' log.info('Calling {}:{} with params {} in {}'.format( service, new_func[0], func_params, region)) # fill values for required members operation_name = new_client._PY_TO_OP_NAME[new_func[0]] operation_model = new_client._service_model.operation_model( operation_name) input_shape = operation_model.input_shape if service == 'sts' and operation_name == 'AssumeRole': log.info( 'skipped sts:AssumeRole for required member') elif input_shape.type_name == 'structure': # find the required members extra_params = _fillvalue(input_shape) if extra_params: func_params.update(extra_params) else: log.info('skipped this input_shape: {}'.format( input_shape)) if not dry_run: make_api_call(service, new_func, region, func_params) except ClientError as e: log.error('ClientError: {}:{} - {}'.format( service, new_func[0], e)) except EndpointConnectionError as e: log.error('EndpointConnectionError: {}:{} - {}'.format( service, new_func[0], e)) except boto3.exceptions.S3UploadFailedError as e: log.error('S3UploadFailedError: {}:{} - {}'.format( service, new_func[0], e)) except TypeError as e: log.error('TypeError: {}:{} - {}'.format( service, new_func[0], e)) except KeyError as e: log.error('Unknown Exception: {}:{} - {}'.format( service, new_func[0], e))