diff --git a/README.md b/README.md index 759c60d..885c926 100644 --- a/README.md +++ b/README.md @@ -31,13 +31,13 @@ Please note: ### Command Line Arguments ``` -usage: python3 hashtopolis.zip [-h] [--de-register] [--version] [--number-only] [--disable-update] [--debug] [--voucher VOUCHER] [--url URL] [--cert CERT] [--files-path FILES_PATH] - [--crackers-path CRACKERS_PATH] [--hashlists-path HASHLISTS_PATH] [--preprocessors-path PREPROCESSORS_PATH] [--zaps-path ZAPS_PATH] [--cpu-only] - +usage: python3 hashtopolis.zip [-h] [--de-register] [--version] [--number-only] [--disable-update] [--debug] [--voucher VOUCHER] [--url URL] [--cert CERT] + [--files-path FILES_PATH] [--crackers-path CRACKERS_PATH] [--hashlists-path HASHLISTS_PATH] [--preprocessors-path PREPROCESSORS_PATH] + [--zaps-path ZAPS_PATH] [--cpu-only] [--use-mtls USE_MTLS] [--mtls-cert MTLS_CERT] [--mtls-key MTLS_KEY] [--mtls-ca-cert MTLS_CA_CERT] Hashtopolis Client v0.7.2 -optional arguments: +options: -h, --help show this help message and exit --de-register client should automatically de-register from server now --version show version information @@ -58,6 +58,12 @@ optional arguments: --zaps-path ZAPS_PATH Use given folder path as zaps location --cpu-only Force client to register as CPU only and also only reading out CPU information + --use-mtls USE_MTLS Use mTLS for client API + --mtls-cert MTLS_CERT + Use given path as client cert location for mTLS + --mtls-key MTLS_KEY Use given path as client key location for mTLS + --mtls-ca-cert MTLS_CA_CERT + Use given path as CA cert location for mTLS verification ``` ### Config @@ -98,6 +104,10 @@ When you run the client for the first time it will ask automatically for all the | preprocessors-path | string | | Use given folder path as preprocessors location | | zaps-path | string | | Use given folder path as zaps location | | cpu-only | boolean | false | Only send CPU information about agent (for CPU only agents) | +| use-mtls | boolean | false | Use mTLS for client API | +| mtls-cert | string | | Use given path as client cert location for mTLS | +| mtls-key | string | | Use given path as client key location for mTLS | +| mtls-ca-cert | string | | Use given path as CA cert location for mTLS verification | ### Debug example diff --git a/__main__.py b/__main__.py index 83bfe3c..5b193fb 100644 --- a/__main__.py +++ b/__main__.py @@ -32,60 +32,66 @@ def run_health_check(): logging.info("Health check requested by server!") logging.info("Retrieving health check settings...") - query = copy_and_set_token(dict_getHealthCheck, CONFIG.get_value('token')) + query = copy_and_set_token(dict_getHealthCheck, CONFIG.get_value("token")) req = JsonRequest(query) ans = req.execute() if ans is None: logging.error("Failed to get health check!") sleep(5) return - elif ans['response'] != 'SUCCESS': + elif ans["response"] != "SUCCESS": logging.error("Error on getting health check: " + str(ans)) sleep(5) return - binaryDownload.check_version(ans['crackerBinaryId']) - check_id = ans['checkId'] + binaryDownload.check_version(ans["crackerBinaryId"]) + check_id = ans["checkId"] logging.info("Starting check ID " + str(check_id)) # write hashes to file - hash_file = open(CONFIG.get_value('hashlists-path') + "/health_check.txt", "w") - hash_file.write("\n".join(ans['hashes'])) + hash_file = open(CONFIG.get_value("hashlists-path") + "/health_check.txt", "w") + hash_file.write("\n".join(ans["hashes"])) hash_file.close() # delete old file if necessary - if os.path.exists(CONFIG.get_value('hashlists-path') + "/health_check.out"): - os.unlink(CONFIG.get_value('hashlists-path') + "/health_check.out") + if os.path.exists(CONFIG.get_value("hashlists-path") + "/health_check.out"): + os.unlink(CONFIG.get_value("hashlists-path") + "/health_check.out") # run task - cracker = HashcatCracker(ans['crackerBinaryId'], binaryDownload) + cracker = HashcatCracker(ans["crackerBinaryId"], binaryDownload) start = int(time.time()) - [states, errors] = cracker.run_health_check(ans['attack'], ans['hashlistAlias']) + [states, errors] = cracker.run_health_check(ans["attack"], ans["hashlistAlias"]) end = int(time.time()) # read results - if os.path.exists(CONFIG.get_value('hashlists-path') + "/health_check.out"): - founds = file_get_contents(CONFIG.get_value('hashlists-path') + "/health_check.out").replace("\r\n", "\n").split("\n") + if os.path.exists(CONFIG.get_value("hashlists-path") + "/health_check.out"): + founds = ( + file_get_contents(CONFIG.get_value("hashlists-path") + "/health_check.out") + .replace("\r\n", "\n") + .split("\n") + ) else: founds = [] if len(states) > 0: num_gpus = len(states[0].get_temps()) else: - errors.append("Faild to retrieve one successful cracker state, most likely due to failing.") + errors.append( + "Faild to retrieve one successful cracker state, most likely due to failing." + ) num_gpus = 0 - query = copy_and_set_token(dict_sendHealthCheck, CONFIG.get_value('token')) - query['checkId'] = check_id - query['start'] = start - query['end'] = end - query['numGpus'] = num_gpus - query['numCracked'] = len(founds) - 1 - query['errors'] = errors + query = copy_and_set_token(dict_sendHealthCheck, CONFIG.get_value("token")) + query["checkId"] = check_id + query["start"] = start + query["end"] = end + query["numGpus"] = num_gpus + query["numCracked"] = len(founds) - 1 + query["errors"] = errors req = JsonRequest(query) ans = req.execute() if ans is None: logging.error("Failed to send health check results!") sleep(5) return - elif ans['response'] != 'OK': + elif ans["response"] != "OK": logging.error("Error on sending health check results: " + str(ans)) sleep(5) return @@ -96,18 +102,18 @@ def run_health_check(): def init_logging(args): global CONFIG - log_format = '[%(asctime)s] [%(levelname)-5s] %(message)s' - print_format = '%(message)s' - date_format = '%Y-%m-%d %H:%M:%S' + log_format = "[%(asctime)s] [%(levelname)-5s] %(message)s" + print_format = "%(message)s" + date_format = "%Y-%m-%d %H:%M:%S" log_level = logging.INFO - logfile = open('client.log', "a", encoding="utf-8") + logfile = open("client.log", "a", encoding="utf-8") logging.getLogger("requests").setLevel(logging.WARNING) CONFIG = Config() if args.debug: - CONFIG.set_value('debug', True) - if CONFIG.get_value('debug'): + CONFIG.set_value("debug", True) + if CONFIG.get_value("debug"): log_level = logging.DEBUG logging.getLogger("requests").setLevel(logging.DEBUG) logging.basicConfig(level=log_level, format=print_format, datefmt=date_format) @@ -119,46 +125,72 @@ def init_logging(args): def init(args): global CONFIG, binaryDownload - if len(CONFIG.get_value('files-path')) == 0: - CONFIG.set_value('files-path', os.path.abspath('files')) - if len(CONFIG.get_value('crackers-path')) == 0: - CONFIG.set_value('crackers-path', os.path.abspath('crackers')) - if len(CONFIG.get_value('hashlists-path')) == 0: - CONFIG.set_value('hashlists-path', os.path.abspath('hashlists')) - if len(CONFIG.get_value('zaps-path')) == 0: - CONFIG.set_value('zaps-path', os.path.abspath('.')) - if len(CONFIG.get_value('preprocessors-path')) == 0: - CONFIG.set_value('preprocessors-path', os.path.abspath('preprocessors')) + if len(CONFIG.get_value("files-path")) == 0: + CONFIG.set_value("files-path", os.path.abspath("files")) + if len(CONFIG.get_value("crackers-path")) == 0: + CONFIG.set_value("crackers-path", os.path.abspath("crackers")) + if len(CONFIG.get_value("hashlists-path")) == 0: + CONFIG.set_value("hashlists-path", os.path.abspath("hashlists")) + if len(CONFIG.get_value("zaps-path")) == 0: + CONFIG.set_value("zaps-path", os.path.abspath(".")) + if len(CONFIG.get_value("preprocessors-path")) == 0: + CONFIG.set_value("preprocessors-path", os.path.abspath("preprocessors")) if args.files_path and len(args.files_path): - CONFIG.set_value('files-path', os.path.abspath(args.files_path)) + CONFIG.set_value("files-path", os.path.abspath(args.files_path)) if args.crackers_path and len(args.crackers_path): - CONFIG.set_value('crackers-path', os.path.abspath(args.crackers_path)) + CONFIG.set_value("crackers-path", os.path.abspath(args.crackers_path)) if args.hashlists_path and len(args.hashlists_path): - CONFIG.set_value('hashlists-path', os.path.abspath(args.hashlists_path)) + CONFIG.set_value("hashlists-path", os.path.abspath(args.hashlists_path)) if args.zaps_path and len(args.zaps_path): - CONFIG.set_value('zaps-path', os.path.abspath(args.zaps_path)) + CONFIG.set_value("zaps-path", os.path.abspath(args.zaps_path)) if args.preprocessors_path and len(args.preprocessors_path): - CONFIG.set_value('preprocessors-path', os.path.abspath(args.preprocessors_path)) + CONFIG.set_value("preprocessors-path", os.path.abspath(args.preprocessors_path)) + if args.use_mtls: + CONFIG.set_value("use_mtls", args.use_mtls) + if args.mtls_cert and len(args.mtls_cert): + CONFIG.set_value("mtls_cert", os.path.abspath(args.mtls_cert)) + if args.mtls_key and len(args.mtls_key): + CONFIG.set_value("mtls_key", os.path.abspath(args.mtls_key)) + if args.mtls_ca_cert and len(args.mtls_ca_cert): + CONFIG.set_value("mtls_ca_cert", os.path.abspath(args.mtls_ca_cert)) logging.info("Starting client '" + Initialize.get_version() + "'...") # check if there are running hashcat.pid files around (as we assume that nothing is running anymore if the client gets newly started) - if os.path.exists(CONFIG.get_value('crackers-path')): - for root, dirs, files in os.walk(CONFIG.get_value('crackers-path')): + if os.path.exists(CONFIG.get_value("crackers-path")): + for root, dirs, files in os.walk(CONFIG.get_value("crackers-path")): for folder in dirs: - if folder.isdigit() and os.path.exists(CONFIG.get_value('crackers-path') + "/" + folder + "/hashtopolis.pid"): - logging.info("Cleaning hashcat PID file from " + CONFIG.get_value('crackers-path') + "/" + folder) - os.unlink(CONFIG.get_value('crackers-path') + "/" + folder + "/hashtopolis.pid") + if folder.isdigit() and os.path.exists( + CONFIG.get_value("crackers-path") + + "/" + + folder + + "/hashtopolis.pid" + ): + logging.info( + "Cleaning hashcat PID file from " + + CONFIG.get_value("crackers-path") + + "/" + + folder + ) + os.unlink( + CONFIG.get_value("crackers-path") + + "/" + + folder + + "/hashtopolis.pid" + ) session = Session(requests.Session()).s - session.headers.update({'User-Agent': Initialize.get_version()}) + session.headers.update({"User-Agent": Initialize.get_version()}) - if CONFIG.get_value('proxies'): - session.proxies = CONFIG.get_value('proxies') + if CONFIG.get_value("proxies"): + session.proxies = CONFIG.get_value("proxies") - if CONFIG.get_value('auth-user') and CONFIG.get_value('auth-password'): - session.auth = (CONFIG.get_value('auth-user'), CONFIG.get_value('auth-password')) + if CONFIG.get_value("auth-user") and CONFIG.get_value("auth-password"): + session.auth = ( + CONFIG.get_value("auth-user"), + CONFIG.get_value("auth-password"), + ) # connection initialization Initialize().run(args) @@ -167,7 +199,7 @@ def init(args): binaryDownload.run() # if multicast is set to run, we need to start the daemon - if CONFIG.get_value('multicast') and Initialize().get_os() == 0: + if CONFIG.get_value("multicast") and Initialize().get_os() == 0: start_uftpd(Initialize().get_os_extension(), CONFIG) @@ -186,7 +218,7 @@ def loop(): CONFIG.update() files.deletion_check() # check if there are deletion orders from the server if task.get_task() is not None: - last_task_id = task.get_task()['taskId'] + last_task_id = task.get_task()["taskId"] task.load_task() if task.get_task_id() == -1: # get task returned to run a health check run_health_check() @@ -196,42 +228,53 @@ def loop(): task_change = True continue else: - if task.get_task()['taskId'] is not last_task_id: + if task.get_task()["taskId"] is not last_task_id: task_change = True # try to download the needed cracker (if not already present) - if not binaryDownload.check_version(task.get_task()['crackerId']): + if not binaryDownload.check_version(task.get_task()["crackerId"]): task_change = True task.reset_task() continue # if prince is used, make sure it's downloaded (deprecated, as preprocessors are integrated generally now) - if 'usePrince' in task.get_task() and task.get_task()['usePrince']: + if "usePrince" in task.get_task() and task.get_task()["usePrince"]: if not binaryDownload.check_prince(): continue # if preprocessor is used, make sure it's downloaded - if 'usePreprocessor' in task.get_task() and task.get_task()['usePreprocessor']: + if "usePreprocessor" in task.get_task() and task.get_task()["usePreprocessor"]: if not binaryDownload.check_preprocessor(task): continue # check if all required files are present - if not files.check_files(task.get_task()['files'], task.get_task()['taskId']): + if not files.check_files(task.get_task()["files"], task.get_task()["taskId"]): task.reset_task() continue # download the hashlist for the task - if task_change and not hashlist.load_hashlist(task.get_task()['hashlistId']): + if task_change and not hashlist.load_hashlist(task.get_task()["hashlistId"]): task.reset_task() continue - if task_change: # check if the client version is up-to-date and load the appropriate cracker + if ( + task_change + ): # check if the client version is up-to-date and load the appropriate cracker binaryDownload.check_client_version() - logging.info("Got cracker binary type " + binaryDownload.get_version()['name']) - if binaryDownload.get_version()['name'].lower() == 'hashcat': - cracker = HashcatCracker(task.get_task()['crackerId'], binaryDownload) + logging.info( + "Got cracker binary type " + binaryDownload.get_version()["name"] + ) + if binaryDownload.get_version()["name"].lower() == "hashcat": + cracker = HashcatCracker(task.get_task()["crackerId"], binaryDownload) else: - cracker = GenericCracker(task.get_task()['crackerId'], binaryDownload) + cracker = GenericCracker(task.get_task()["crackerId"], binaryDownload) # if it's a task using hashcat brain, we need to load the found hashes - if task_change and 'useBrain' in task.get_task() and task.get_task()['useBrain'] and not hashlist.load_found(task.get_task()['hashlistId'], task.get_task()['crackerId']): + if ( + task_change + and "useBrain" in task.get_task() + and task.get_task()["useBrain"] + and not hashlist.load_found( + task.get_task()["hashlistId"], task.get_task()["crackerId"] + ) + ): task.reset_task() continue task_change = False - chunk_resp = chunk.get_chunk(task.get_task()['taskId']) + chunk_resp = chunk.get_chunk(task.get_task()["taskId"]) if chunk_resp == 0: task.reset_task() continue @@ -254,10 +297,10 @@ def loop(): # some error must have occurred on benchmarking continue # send result of benchmark - query = copy_and_set_token(dict_sendBenchmark, CONFIG.get_value('token')) - query['taskId'] = task.get_task()['taskId'] - query['result'] = result - query['type'] = task.get_task()['benchType'] + query = copy_and_set_token(dict_sendBenchmark, CONFIG.get_value("token")) + query["taskId"] = task.get_task()["taskId"] + query["result"] = result + query["type"] = task.get_task()["benchType"] req = JsonRequest(query) ans = req.execute() if ans is None: @@ -265,7 +308,7 @@ def loop(): sleep(5) task.reset_task() continue - elif ans['response'] != 'SUCCESS': + elif ans["response"] != "SUCCESS": logging.error("Error on sending benchmark: " + str(ans)) sleep(5) task.reset_task() @@ -275,7 +318,7 @@ def loop(): continue # check if we have an invalid chunk - if chunk.chunk_data() is not None and chunk.chunk_data()['length'] == 0: + if chunk.chunk_data() is not None and chunk.chunk_data()["length"] == 0: logging.error("Invalid chunk size (0) retrieved! Retrying...") task.reset_task() continue @@ -294,46 +337,132 @@ def de_register(): global CONFIG logging.info("De-registering client..") - query = copy_and_set_token(dict_deregister, CONFIG.get_value('token')) + query = copy_and_set_token(dict_deregister, CONFIG.get_value("token")) req = JsonRequest(query) ans = req.execute() if ans is None: logging.error("De-registration failed!") - elif ans['response'] != 'SUCCESS': + elif ans["response"] != "SUCCESS": logging.error("Error on de-registration: " + str(ans)) else: logging.info("Successfully de-registered!") # cleanup - dirs = [CONFIG.get_value('crackers-path'), CONFIG.get_value('preprocessors-path'), CONFIG.get_value('hashlists-path'), CONFIG.get_value('files-path')] - files = ['config.json', '7zr.exe', '7zr'] + dirs = [ + CONFIG.get_value("crackers-path"), + CONFIG.get_value("preprocessors-path"), + CONFIG.get_value("hashlists-path"), + CONFIG.get_value("files-path"), + ] + files = ["config.json", "7zr.exe", "7zr"] for file in files: if os.path.exists(file): os.unlink(file) for directory in dirs: if os.path.exists(directory): shutil.rmtree(directory) - r = glob.glob(CONFIG.get_value('zaps-path') + '/hashlist_*') + r = glob.glob(CONFIG.get_value("zaps-path") + "/hashlist_*") for i in r: shutil.rmtree(i) logging.info("Cleanup finished!") if __name__ == "__main__": - parser = argparse.ArgumentParser(description='Hashtopolis Client v' + Initialize.get_version_number(), prog='python3 hashtopolis.zip') - parser.add_argument('--de-register', action='store_true', help='client should automatically de-register from server now') - parser.add_argument('--version', action='store_true', help='show version information') - parser.add_argument('--number-only', action='store_true', help='when using --version show only the number') - parser.add_argument('--disable-update', action='store_true', help='disable retrieving auto-updates of the client from the server') - parser.add_argument('--debug', '-d', action='store_true', help='enforce debugging output') - parser.add_argument('--voucher', type=str, required=False, help='voucher to use to automatically register') - parser.add_argument('--url', type=str, required=False, help='URL to Hashtopolis client API') - parser.add_argument('--cert', type=str, required=False, help='Client TLS cert bundle for Hashtopolis client API') - parser.add_argument('--files-path', type=str, required=False, help='Use given folder path as files location') - parser.add_argument('--crackers-path', type=str, required=False, help='Use given folder path as crackers location') - parser.add_argument('--hashlists-path', type=str, required=False, help='Use given folder path as hashlists location') - parser.add_argument('--preprocessors-path', type=str, required=False, help='Use given folder path as preprocessors location') - parser.add_argument('--zaps-path', type=str, required=False, help='Use given folder path as zaps location') - parser.add_argument('--cpu-only', action='store_true', help='Force client to register as CPU only and also only reading out CPU information') + parser = argparse.ArgumentParser( + description="Hashtopolis Client v" + Initialize.get_version_number(), + prog="python3 hashtopolis.zip", + ) + parser.add_argument( + "--de-register", + action="store_true", + help="client should automatically de-register from server now", + ) + parser.add_argument( + "--version", action="store_true", help="show version information" + ) + parser.add_argument( + "--number-only", + action="store_true", + help="when using --version show only the number", + ) + parser.add_argument( + "--disable-update", + action="store_true", + help="disable retrieving auto-updates of the client from the server", + ) + parser.add_argument( + "--debug", "-d", action="store_true", help="enforce debugging output" + ) + parser.add_argument( + "--voucher", + type=str, + required=False, + help="voucher to use to automatically register", + ) + parser.add_argument( + "--url", type=str, required=False, help="URL to Hashtopolis client API" + ) + parser.add_argument( + "--cert", + type=str, + required=False, + help="Client TLS cert bundle for Hashtopolis client API", + ) + parser.add_argument( + "--files-path", + type=str, + required=False, + help="Use given folder path as files location", + ) + parser.add_argument( + "--crackers-path", + type=str, + required=False, + help="Use given folder path as crackers location", + ) + parser.add_argument( + "--hashlists-path", + type=str, + required=False, + help="Use given folder path as hashlists location", + ) + parser.add_argument( + "--preprocessors-path", + type=str, + required=False, + help="Use given folder path as preprocessors location", + ) + parser.add_argument( + "--zaps-path", + type=str, + required=False, + help="Use given folder path as zaps location", + ) + parser.add_argument( + "--cpu-only", + action="store_true", + help="Force client to register as CPU only and also only reading out CPU information", + ) + parser.add_argument( + "--use-mtls", type=bool, required=False, help="Use mTLS for client API" + ) + parser.add_argument( + "--mtls-cert", + type=str, + required=False, + help="Use given path as client cert location for mTLS", + ) + parser.add_argument( + "--mtls-key", + type=str, + required=False, + help="Use given path as client key location for mTLS", + ) + parser.add_argument( + "--mtls-ca-cert", + type=str, + required=False, + help="Use given path as CA cert location for mTLS verification", + ) args = parser.parse_args() if args.version: @@ -346,7 +475,7 @@ def de_register(): if args.de_register: init_logging(args) session = Session(requests.Session()).s - session.headers.update({'User-Agent': Initialize.get_version()}) + session.headers.update({"User-Agent": Initialize.get_version()}) de_register() sys.exit(0) @@ -356,21 +485,32 @@ def de_register(): # check if there is a lock file and check if this pid is still running hashtopolis if os.path.exists("lock.pid") and os.path.isfile("lock.pid"): pid = file_get_contents("lock.pid") - logging.info("Found existing lock.pid, checking if python process is running...") + logging.info( + "Found existing lock.pid, checking if python process is running..." + ) if psutil.pid_exists(int(pid)): try: - command = psutil.Process(int(pid)).cmdline()[0].replace('\\', '/').split('/') + command = ( + psutil.Process(int(pid)) + .cmdline()[0] + .replace("\\", "/") + .split("/") + ) print(command) if str.startswith(command[-1], "python"): - logging.fatal("There is already a hashtopolis agent running in this directory!") + logging.fatal( + "There is already a hashtopolis agent running in this directory!" + ) sys.exit(-1) except Exception: # if we fail to determine the cmd line we assume that it's either not running anymore or another process (non-hashtopolis) pass - logging.info("Ignoring lock.pid file because PID is not existent anymore or not running python!") + logging.info( + "Ignoring lock.pid file because PID is not existent anymore or not running python!" + ) # create lock file - with open("lock.pid", 'w') as f: + with open("lock.pid", "w") as f: f.write(str(os.getpid())) f.close() diff --git a/htpclient/download.py b/htpclient/download.py index 36bbfd3..93b1508 100644 --- a/htpclient/download.py +++ b/htpclient/download.py @@ -14,17 +14,28 @@ class Download: def download(url, output, no_header=False): try: session = Session().s - + # If agent is using mtls, we need to set the session params to the default + # so that sites that are internet accesible can still be accessed for downloading + # even if mtls is not enabled, the session_params created is the default config + session_params = {} + if Session().using_mtls: + mtls_exempt_sites = ["hashcat.net"] + for site in mtls_exempt_sites: + if site in url: + session_params = { + "cert": None, + "verify": "/etc/ssl/certs/ca-certificates.crt" + } # Check header if not no_header: - head = session.head(url) + head = session.head(url, **session_params) # not sure if we only should allow 200/301/302, but then it's present for sure if head.status_code not in [200, 301, 302]: logging.error("File download header reported wrong status code: " + str(head.status_code)) return False - + with open(output, "wb") as file: - response = session.get(url, stream=True) + response = session.get(url, stream=True, **session_params) total_length = response.headers.get('Content-Length') if total_length is None: # no content length header diff --git a/htpclient/initialize.py b/htpclient/initialize.py index 8e1431c..cd8f757 100644 --- a/htpclient/initialize.py +++ b/htpclient/initialize.py @@ -20,6 +20,7 @@ def get_version_number(): def run(self, args): self.__check_cert(args) + self.__check_mtls(args) self.__check_url(args) self.__check_token(args) self.__update_information() @@ -41,46 +42,52 @@ def get_os_extension(): return dict_ext[operating_system] def __login(self): - query = copy_and_set_token(dict_login, self.config.get_value('token')) - query['clientSignature'] = self.get_version() + query = copy_and_set_token(dict_login, self.config.get_value("token")) + query["clientSignature"] = self.get_version() req = JsonRequest(query) ans = req.execute() if ans is None: logging.error("Login failed!") sleep(5) self.__login() - elif ans['response'] != 'SUCCESS': + elif ans["response"] != "SUCCESS": logging.error("Error from server: " + str(ans)) - self.config.set_value('token', '') + self.config.set_value("token", "") self.__login() else: logging.info("Login successful!") - if 'server-version' in ans: - logging.info("Hashtopolis Server version: " + ans['server-version']) - if 'multicastEnabled' in ans and ans['multicastEnabled'] and self.get_os() == 0: # currently only allow linux + if "server-version" in ans: + logging.info("Hashtopolis Server version: " + ans["server-version"]) + if ( + "multicastEnabled" in ans + and ans["multicastEnabled"] + and self.get_os() == 0 + ): # currently only allow linux logging.info("Multicast enabled!") - self.config.set_value('multicast', True) + self.config.set_value("multicast", True) if not os.path.isdir("multicast"): os.mkdir("multicast") def __update_information(self): - if not self.config.get_value('uuid'): - self.config.set_value('uuid', str(uuid.uuid4())) + if not self.config.get_value("uuid"): + self.config.set_value("uuid", str(uuid.uuid4())) # collect devices logging.info("Collecting agent data...") devices = [] if Initialize.get_os() == 0: # linux output = subprocess.check_output("cat /proc/cpuinfo", shell=True) - output = output.decode(encoding='utf-8').replace("\r\n", "\n").split("\n") + output = output.decode(encoding="utf-8").replace("\r\n", "\n").split("\n") tmp = [] for line in output: line = line.strip() - if not line.startswith('model name') and not line.startswith('physical id'): + if not line.startswith("model name") and not line.startswith( + "physical id" + ): continue - value = line.split(':', 1)[1].strip() - while ' ' in value: - value = value.replace(' ', ' ') + value = line.split(":", 1)[1].strip() + while " " in value: + value = value.replace(" ", " ") tmp.append(value) pairs = [] @@ -88,31 +95,38 @@ def __update_information(self): pairs.append("%s:%s" % (tmp[i + 1], tmp[i])) for line in sorted(set(pairs)): - devices.append(line.split(':', 1)[1].replace('\t', ' ')) + devices.append(line.split(":", 1)[1].replace("\t", " ")) - if not self.config.get_value('cpu-only'): + if not self.config.get_value("cpu-only"): try: - output = subprocess.check_output("lspci | grep -E 'VGA compatible controller|3D controller'", shell=True) + output = subprocess.check_output( + "lspci | grep -E 'VGA compatible controller|3D controller'", + shell=True, + ) except subprocess.CalledProcessError: # we silently ignore this case on machines where lspci is not present or architecture has no pci bus output = b"" - output = output.decode(encoding='utf-8').replace("\r\n", "\n").split("\n") + output = ( + output.decode(encoding="utf-8").replace("\r\n", "\n").split("\n") + ) for line in output: if not line: continue - line = ' '.join(line.split(' ')[1:]).split(':') + line = " ".join(line.split(" ")[1:]).split(":") devices.append(line[1].strip()) elif Initialize.get_os() == 1: # windows output = subprocess.check_output("wmic cpu get name", shell=True) - output = output.decode(encoding='utf-8').replace("\r\n", "\n").split("\n") + output = output.decode(encoding="utf-8").replace("\r\n", "\n").split("\n") for line in output: line = line.rstrip("\r\n ") if line == "Name" or not line: continue devices.append(line) - output = subprocess.check_output("wmic path win32_VideoController get name", shell=True) - output = output.decode(encoding='utf-8').replace("\r\n", "\n").split("\n") + output = subprocess.check_output( + "wmic path win32_VideoController get name", shell=True + ) + output = output.decode(encoding="utf-8").replace("\r\n", "\n").split("\n") for line in output: line = line.rstrip("\r\n ") if line == "Name" or not line: @@ -120,8 +134,10 @@ def __update_information(self): devices.append(line) else: # OS X - output = subprocess.check_output("system_profiler SPDisplaysDataType -detaillevel mini", shell=True) - output = output.decode(encoding='utf-8').replace("\r\n", "\n").split("\n") + output = subprocess.check_output( + "system_profiler SPDisplaysDataType -detaillevel mini", shell=True + ) + output = output.decode(encoding="utf-8").replace("\r\n", "\n").split("\n") for line in output: line = line.rstrip("\r\n ") if "Chipset Model" not in line: @@ -129,72 +145,100 @@ def __update_information(self): line = line.split(":") devices.append(line[1].strip()) - query = copy_and_set_token(dict_updateInformation, self.config.get_value('token')) - query['uid'] = self.config.get_value('uuid') - query['os'] = self.get_os() - query['devices'] = devices + query = copy_and_set_token( + dict_updateInformation, self.config.get_value("token") + ) + query["uid"] = self.config.get_value("uuid") + query["os"] = self.get_os() + query["devices"] = devices req = JsonRequest(query) ans = req.execute() if ans is None: logging.error("Information update failed!") sleep(5) self.__update_information() - elif ans['response'] != 'SUCCESS': + elif ans["response"] != "SUCCESS": logging.error("Error from server: " + str(ans)) sleep(5) self.__update_information() def __check_token(self, args): - if not self.config.get_value('token'): - if self.config.get_value('voucher'): + if not self.config.get_value("token"): + if self.config.get_value("voucher"): # voucher is set in config and can be used to autoregister - voucher = self.config.get_value('voucher') + voucher = self.config.get_value("voucher") elif args.voucher: voucher = args.voucher else: - voucher = input("No token found! Please enter a voucher to register your agent:\n").strip() + voucher = input( + "No token found! Please enter a voucher to register your agent:\n" + ).strip() name = platform.node() query = dict_register.copy() - query['voucher'] = voucher - query['name'] = name - if self.config.get_value('cpu-only'): - query['cpu-only'] = True + query["voucher"] = voucher + query["name"] = name + if self.config.get_value("cpu-only"): + query["cpu-only"] = True req = JsonRequest(query) ans = req.execute() if ans is None: logging.error("Request failed!") self.__check_token(args) - elif ans['response'] != 'SUCCESS' or not ans['token']: + elif ans["response"] != "SUCCESS" or not ans["token"]: logging.error("Registering failed: " + str(ans)) self.__check_token(args) else: - token = ans['token'] - self.config.set_value('voucher', '') - self.config.set_value('token', token) + token = ans["token"] + self.config.set_value("voucher", "") + self.config.set_value("token", token) logging.info("Successfully registered!") def __check_cert(self, args): - cert = self.config.get_value('cert') + cert = self.config.get_value("cert") if cert is None: if args.cert is not None: cert = os.path.abspath(args.cert) logging.debug("Setting cert to: " + cert) - self.config.set_value('cert', cert) - + self.config.set_value("cert", cert) if cert is not None: Session().s.cert = cert logging.debug("Configuration session cert to: " + cert) + def __check_mtls(self, args): + if self.config.get_value("use_mtls"): + logging.debug("Using mTLS..") + client_cert = self.config.get_value("mtls_cert") + client_key = self.config.get_value("mtls_key") + ca_cert = self.config.get_value("mtls_ca_cert") + if args.mtls_cert is not None: + client_cert = os.path.abspath(args.mtls_cert) + logging.debug("Setting mtls_cert to: " + client_cert) + self.config.set_value("mtls_cert", client_cert) + if args.mtls_key is not None: + client_key = os.path.abspath(args.mtls_key) + logging.debug("Setting mtls_key to: " + client_key) + self.config.set_value("mtls_key", client_key) + if args.mtls_ca_cert is not None: + ca_cert = os.path.abspath(args.mtls_ca_cert) + logging.debug("Setting mtls_ca_cert to: " + ca_cert) + self.config.set_value("mtls_ca_cert", ca_cert) + if client_cert is not None and client_key is not None and ca_cert is not None: + Session().s.cert = (client_cert, client_key) + Session().s.verify = ca_cert + Session().using_mtls = True + def __check_url(self, args): - if not self.config.get_value('url'): + if not self.config.get_value("url"): # ask for url if args.url is None: - url = input("Please enter the url to the API of your Hashtopolis installation:\n").strip() + url = input( + "Please enter the url to the API of your Hashtopolis installation:\n" + ).strip() else: url = args.url logging.debug("Setting url to: " + url) - self.config.set_value('url', url) + self.config.set_value("url", url) else: return query = dict_testConnection.copy() @@ -202,25 +246,25 @@ def __check_url(self, args): ans = req.execute() if ans is None: logging.error("Connection test failed!") - self.config.set_value('url', '') + self.config.set_value("url", "") self.__check_url(args) - elif ans['response'] != 'SUCCESS': + elif ans["response"] != "SUCCESS": logging.error("Connection test failed: " + str(ans)) - self.config.set_value('url', '') + self.config.set_value("url", "") self.__check_url(args) else: logging.debug("Connection test successful!") - + if args.cpu_only is not None and args.cpu_only: logging.debug("Setting agent to be CPU only..") - self.config.set_value('cpu-only', True) + self.config.set_value("cpu-only", True) def __build_directories(self): - if not os.path.isdir(self.config.get_value('crackers-path')): - os.makedirs(self.config.get_value('crackers-path')) - if not os.path.isdir(self.config.get_value('files-path')): - os.makedirs(self.config.get_value('files-path')) - if not os.path.isdir(self.config.get_value('hashlists-path')): - os.makedirs(self.config.get_value('hashlists-path')) - if not os.path.isdir(self.config.get_value('preprocessors-path')): - os.makedirs(self.config.get_value('preprocessors-path')) + if not os.path.isdir(self.config.get_value("crackers-path")): + os.makedirs(self.config.get_value("crackers-path")) + if not os.path.isdir(self.config.get_value("files-path")): + os.makedirs(self.config.get_value("files-path")) + if not os.path.isdir(self.config.get_value("hashlists-path")): + os.makedirs(self.config.get_value("hashlists-path")) + if not os.path.isdir(self.config.get_value("preprocessors-path")): + os.makedirs(self.config.get_value("preprocessors-path")) diff --git a/htpclient/jsonRequest.py b/htpclient/jsonRequest.py index 1d68e45..35b2c32 100644 --- a/htpclient/jsonRequest.py +++ b/htpclient/jsonRequest.py @@ -13,7 +13,9 @@ def __init__(self, data): def execute(self): try: logging.debug(self.data) - r = self.session.post(self.config.get_value('url'), json=self.data, timeout=30) + r = self.session.post( + self.config.get_value("url"), json=self.data, timeout=30 + ) if r.status_code != 200: logging.error("Status code from server: " + str(r.status_code)) return None diff --git a/htpclient/session.py b/htpclient/session.py index 5552125..bbf5094 100644 --- a/htpclient/session.py +++ b/htpclient/session.py @@ -4,8 +4,9 @@ class Session: __instance = None - def __new__(cls, s=None): + def __new__(cls, s=None, using_mtls=False): if Session.__instance is None: Session.__instance = object.__new__(cls) Session.__instance.s = s + Session.__instance.using_mtls = using_mtls return Session.__instance