Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 64 additions & 15 deletions LambdaLooter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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):
"""
Expand Down
20 changes: 16 additions & 4 deletions claws.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import re
import json
import argparse
import subprocess
Expand Down Expand Up @@ -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


Expand All @@ -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))
Expand Down Expand Up @@ -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)
5 changes: 3 additions & 2 deletions parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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


Expand Down
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
boto3
msal
python-dateutil
requests
12 changes: 8 additions & 4 deletions secretvalidation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]}
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down