diff --git a/LambdaLooter.py b/LambdaLooter.py index ef1008f..49e52ff 100644 --- a/LambdaLooter.py +++ b/LambdaLooter.py @@ -9,7 +9,7 @@ import boto3 import requests from pprint import pprint -from time import gmtime, strftime +from time import gmtime, strftime, sleep from datetime import datetime, timedelta @@ -87,29 +87,78 @@ def zipEnvironmentVariableFiles(profile, deldownloads): print(f'Number Environment Variables Zipped: {counter}') +def downloadLayers(profile, func_details, lambda_client): + layers = func_details['Configuration'].get('Layers', []) + for layer in layers: + layer_arn = layer['Arn'] + # ARN format: arn:aws:lambda:region:account:layer:name:version + parts = layer_arn.split(':') + layer_name = parts[6] + layer_version = parts[7] + layer_path = "./loot/" + profile + "/lambda/layer-" + layer_name + "-version-" + layer_version + ".zip" + if os.path.exists(layer_path): + continue + try: + layer_details = lambda_client.get_layer_version_by_arn(Arn=layer_arn) + url = layer_details['Content']['Location'] + r = requests.get(url) + if r.status_code == 200: + with open(layer_path, "wb") as f: + f.write(r.content) + else: + with open('./logs/failures.log', 'a') as log: + log.write(f"Failed to download layer, {profile}, {layer_arn}, HTTP {r.status_code}\n") + except Exception as e: + with open('./logs/failures.log', 'a') as log: + log.write(f"Failed to get layer, {profile}, {layer_arn}, {str(e)}\n") + + def downloadExecution(profile, strFunction, lambda_client): """ execute the download of the lambdas function(s) and Envionrment Varilables - Variables - + Variables - profile: the AWS Profile we are looting - lambda_client: lambda client object for downloading. + lambda_client: lambda client object for downloading. strFunction: arn of the lambda to download profile: the AWS profile lambdas are downloaded from """ func_details = lambda_client.get_function(FunctionName=strFunction) - downloadDir = "./loot/" + profile + "/lambda/lambda-" + func_details['Configuration']['FunctionName'] + "-version-" + func_details['Configuration']['Version'] + ".zip" - url = func_details['Code']['Location'] - - r = requests.get(url) - with open(downloadDir, "wb") as code: - code.write(r.content) - - saveEnvFilePath = os.path.join(os.path.dirname(os.path.realpath(__file__)), "loot/" + profile + "/env/lambda-env_"+ func_details['Configuration']['FunctionName'] + "-" + func_details['Configuration']['Version'] + "-environmentVariables-loot.txt") - env_details = lambda_client.get_function_configuration(FunctionName=strFunction) - details = env_details['Environment']['Variables'] - with open(saveEnvFilePath, 'a') as outputfile: - outputfile.write(details + "\n") + func_name = func_details['Configuration']['FunctionName'] + func_version = func_details['Configuration']['Version'] + repo_type = func_details['Code'].get('RepositoryType', 'S3') + + # Environment variables are available for all Lambda types. + saveEnvFilePath = os.path.join(os.path.dirname(os.path.realpath(__file__)), "loot/" + profile + "/env/lambda-env_" + func_name + "-" + func_version + "-environmentVariables-loot.txt") + env_details = lambda_client.get_function_configuration(FunctionName=strFunction) + variables = env_details.get('Environment', {}).get('Variables', {}) + if variables: + with open(saveEnvFilePath, 'a') as outputfile: + outputfile.write(json.dumps(variables) + "\n") + + if repo_type == 'ECR': + image_uri = func_details['Code'].get('ImageUri', 'unknown') + with open('./logs/ecr_functions.log', 'a') as log: + log.write(f"{profile}, {func_name}, {func_version}, {image_uri}\n") + return + + downloadLayers(profile, func_details, lambda_client) + + downloadDir = "./loot/" + profile + "/lambda/lambda-" + func_name + "-version-" + func_version + ".zip" + for attempt in range(3): + if attempt > 0: + # Re-fetch to get a fresh presigned URL — previous one may have expired + func_details = lambda_client.get_function(FunctionName=strFunction) + sleep(2 ** attempt) + url = func_details['Code']['Location'] + r = requests.get(url) + if r.status_code == 200: + with open(downloadDir, "wb") as code: + code.write(r.content) + break + else: + with open('./logs/failures.log', 'a') as code: + code.write(f"Failed to download after retries, {profile}, {strFunction}, HTTP {r.status_code}\n") def checkVersions(profile, strFunction, lambda_client, getversions): """ diff --git a/claws.py b/claws.py index d5b3e45..fb4942a 100644 --- a/claws.py +++ b/claws.py @@ -1,4 +1,5 @@ import os +import re import json import argparse import subprocess @@ -141,11 +142,19 @@ def lootDirCheck(profile, ec2, lambduh, ssm): os.mkdir("./loot/" + profile + "/env") +def _is_valid_account_id(account_id): + return bool(re.fullmatch(r'\d{12}', account_id)) + # Put in here how to ingest whatever or wherever your list is. def getAccounts(): accounts = [] - for account in open('./accounts.txt').readlines(): - accounts.append(account.strip('\n')) + with open('./accounts.txt') as f: + for line in f: + account = line.strip() + if _is_valid_account_id(account): + accounts.append(account) + elif account: + print(f"Skipping invalid account ID: {account!r}") return accounts @@ -155,7 +164,8 @@ def trackCheck(profileID): trackFile = os.path.exists(f'./track/{profileID}.json') if trackFile: try: - jsonTrack = json.load(open(f'./track/{profileID}.json')) + with open(f'./track/{profileID}.json') as f: + jsonTrack = json.load(f) return jsonTrack except Exception as e: print("Something went wrong and we can't load the track file. (./track.json)" + str(e)) @@ -231,5 +241,7 @@ def awsProfileSetup(profileID, region, threads, deldownloads, getversions, ec2, if __name__ == "__main__": args = parse_args() - + if args.profile is not None and not _is_valid_account_id(args.profile): + print(f"Error: profile must be a 12-digit AWS account ID, got: {args.profile!r}") + raise SystemExit(1) main(args.region, args.threads, args.deldownloads, args.versions, args.hunt, args.ec2, args.lambduh, args.ssm, args.role, profile=args.profile) diff --git a/parsing.py b/parsing.py index d67cd98..3bd1e51 100644 --- a/parsing.py +++ b/parsing.py @@ -63,7 +63,8 @@ def getSigs(): if sigfile.startswith('sig_'): #pull in all sig files from the signature dir sigfilePath = os.path.join(os.path.dirname(os.path.realpath(__file__)), "signatures/" + sigfile) - jsonSigs = json.load(open(sigfilePath)) + with open(sigfilePath) as f: + jsonSigs = json.load(f) for sigType in jsonSigs[0]["sigs"]: sigs.append(sigType) except Exception as e: @@ -90,7 +91,7 @@ def threadSecrets(threads, deldownloads, profile, sigs): def regexChecker(pattern, fileread): #print('regexChecker()') - returnvalue = re.finditer(b"%b" % pattern.encode(), fileread, re.MULTILINE | re.IGNORECASE) + returnvalue = re.finditer(pattern.encode(), fileread, re.MULTILINE | re.IGNORECASE) return returnvalue diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c36367f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +boto3 +msal +python-dateutil +requests diff --git a/secretvalidation.py b/secretvalidation.py index 3e24502..183ab54 100644 --- a/secretvalidation.py +++ b/secretvalidation.py @@ -83,7 +83,8 @@ def logNotValidated(profile, output, context=None): filepath = './findings/notvalidated.json' submittedFile = os.path.isfile(filepath) if submittedFile: - subJSON = json.load(open(filepath)) + with open(filepath) as f: + subJSON = json.load(f) subJSON['unvalidated'].append(context) else: subJSON = {'unvalidated':[context]} @@ -98,7 +99,8 @@ def seenBefore(secret, profile, output): filepath = './findings/notvalidated.json' nonSubmittedFile = os.path.isfile(filepath) if nonSubmittedFile: - subJSON = json.load(open(filepath)) + with open(filepath) as f: + subJSON = json.load(f) for sub in subJSON['unvalidated']: if sub['sha2'] == sha256(secret.encode('utf-8')).hexdigest(): seen = True @@ -109,7 +111,8 @@ def seenBefore(secret, profile, output): filepath = './findings/obvfalsepositive.json' obvfalsepositiveFile = os.path.isfile(filepath) if obvfalsepositiveFile: - subJSON = json.load(open(filepath)) + with open(filepath) as f: + subJSON = json.load(f) for sub in subJSON['obvfalsepositive']: if sub['sha2'] == sha256(secret.encode('utf-8')).hexdigest(): seen = True @@ -120,7 +123,8 @@ def seenBefore(secret, profile, output): filepath = './findings/expiredJWT.json' expiredFile = os.path.isfile(filepath) if expiredFile: - subJSON = json.load(open(filepath)) + with open(filepath) as f: + subJSON = json.load(f) for sub in subJSON['expired']: if sub['sha2'] == sha256(secret.encode('utf-8')).hexdigest(): seen = True