Merge branch 'main' into presence

This commit is contained in:
Alexander Neff
2025-05-25 16:25:42 -04:00
71 changed files with 739 additions and 351 deletions
+6 -11
View File
@@ -1,24 +1,20 @@
## Description
Please include a summary of the change and which issue is fixed, or what the enhancement does.
Please also include relevant motivation and context.
List any dependencies that are required for this change.
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
- [ ] This requires a third party update (such as Impacket, Dploot, lsassy, etc)
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes (e2e, single commands, etc)
Please also list any relevant details for your test configuration, such as your locally running machine Python version & OS, as well as the target(s) you tested against, including software versions
If you are using poetry, you can easily run tests via:
`poetry run python tests/e2e_tests.py -t $TARGET -u $USER -p $PASSWORD`
There are additional options like `--errors` to display ALL errors (some may not be failures), `--poetry` (output will include the poetry run prepended), `--line-num $START-$END $SINGLE` for only running a subset
## Setup guide for the review
Please provide guidance on what setup is needed to test the introduced changes, such as your locally running machine Python version & OS, as well as the target(s) you tested against, including software versions.
In particular:
- Bug Fix: Please provide a short description on how to trigger the bug, to make the bug reproducable for the reviewer.
- Added Feature/Enhancement: Please specify what setup is needed in order to test the changes. E.g. is additional software needed? GPO changes required? Specific registry settings that need to be changed?
## Screenshots (if appropriate):
Screenshots are always nice to have and can give a visual representation of the change.
@@ -29,8 +25,7 @@ If appropriate include before and after screenshot(s) to show which results are
- [ ] I have ran Ruff against my changes (via poetry: `poetry run python -m ruff check . --preview`, use `--fix` to automatically fix what it can)
- [ ] I have added or updated the tests/e2e_commands.txt file if necessary
- [ ] New and existing e2e tests pass locally with my changes
- [ ] My code follows the style guidelines of this project (should be covered by Ruff above)
- [ ] If reliant on third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
- [ ] If reliant on changes of third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation (PR here: https://github.com/Pennyw0rth/NetExec-Wiki)
+5
View File
@@ -18,6 +18,11 @@ if "nxc" not in nxc_config.sections():
# Check if there are any missing options in the config file
for section in nxc_default_config.sections():
if not nxc_config.has_section(section):
nxc_logger.display(f"Adding missing section '{section}' to nxc.conf")
nxc_config.add_section(section)
with open(path_join(NXC_PATH, "nxc.conf"), "w") as config_file:
nxc_config.write(config_file)
for option in nxc_default_config.options(section):
if not nxc_config.has_option(section, option):
nxc_logger.display(f"Adding missing option '{option}' in config section '{section}' to nxc.conf")
+4 -6
View File
@@ -275,7 +275,7 @@ class connection:
extra={
"module_name": module.name.upper(),
"host": self.host,
"port": self.args.port,
"port": self.port,
"hostname": self.hostname,
},
)
@@ -293,8 +293,7 @@ class connection:
module.on_admin_login(context, self)
def inc_failed_login(self, username):
global global_failed_logins
global user_failed_logins
global global_failed_logins, user_failed_logins
if username not in user_failed_logins:
user_failed_logins[username] = 0
@@ -304,8 +303,7 @@ class connection:
self.failed_logins += 1
def over_fail_limit(self, username):
global global_failed_logins
global user_failed_logins
global global_failed_logins, user_failed_logins
if global_failed_logins == self.args.gfail_limit:
return True
@@ -313,7 +311,7 @@ class connection:
if self.failed_logins == self.args.fail_limit:
return True
if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]:
if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]: # noqa: SIM103
return True
return False
+3
View File
@@ -15,6 +15,9 @@ bh_port = 7687
bh_user = neo4j
bh_pass = bloodhoundcommunityedition
[BloodHound-CE]
bhce_enabled = True
[Empire]
api_host = 127.0.0.1
api_port = 1337
+2
View File
@@ -111,6 +111,7 @@ def initialize_db():
# Even if the default workspace exists, we still need to check if every protocol has a database (in case of a new protocol)
init_protocol_dbs("default")
def format_host_query(q, filter_term, HostsTable):
"""One annoying thing is that if you search for an ip such as '10.10.10.5',
it will return 10.10.10.5 and 10.10.10.52, so we have to check if its an ip address first
@@ -141,6 +142,7 @@ def format_host_query(q, filter_term, HostsTable):
return q
class BaseDB:
def __init__(self, db_engine):
self.db_engine = db_engine
+1
View File
@@ -1,6 +1,7 @@
from argparse import ArgumentDefaultsHelpFormatter, SUPPRESS, OPTIONAL, ZERO_OR_MORE
from argparse import Action
class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter):
def _get_help_string(self, action):
help_string = action.help
+9
View File
@@ -5,6 +5,7 @@ import uuid
from datetime import datetime
class Substitution:
def __init__(self, buf, offset):
(sub_token, sub_id, sub_type) = struct.unpack_from("<BHB", buf, offset)
@@ -46,6 +47,7 @@ class Substitution:
else:
print("Unknown value type", hex(value.type))
class Value:
def __init__(self, buf, offset):
token, string_type, length = struct.unpack_from("<BBH", buf, offset)
@@ -56,6 +58,7 @@ class Value:
def xml(self, template=None):
return self._val
class Attribute:
def __init__(self, buf, offset):
struct.unpack_from("<B", buf, offset)
@@ -75,6 +78,7 @@ class Attribute:
val = self._value.xml(template)
return None if val is None else f'{self._name.val}="{val}"'
class Name:
def __init__(self, buf, offset):
hashs, length = struct.unpack_from("<HH", buf, offset)
@@ -82,6 +86,7 @@ class Name:
self.val = buf[offset + 4:offset + 4 + length * 2].decode("utf16")
self.length = 4 + (length + 1) * 2
class Element:
def __init__(self, buf, offset):
token, dependency_id, length = struct.unpack_from("<BHI", buf, offset)
@@ -151,6 +156,7 @@ class Element:
children = (x.xml(template) for x in self._children)
return "<{}{}>{}</{}>".format(self._name.val, attrs, "".join(children), self._name.val)
class ValueSpec:
def __init__(self, buf, offset, value_offset):
self.length, self.type, value_eof = struct.unpack_from("<HBB", buf, offset)
@@ -159,6 +165,7 @@ class ValueSpec:
if self.type == 0x21:
self.template = BinXML(buf, value_offset)
class TemplateInstance:
def __init__(self, buf, offset):
token, unknown0, guid, length, next_token = struct.unpack_from("<BB16sIB", buf, offset)
@@ -179,6 +186,7 @@ class TemplateInstance:
def xml(self, template=None):
return self._xml.xml(self)
class BinXML:
def __init__(self, buf, offset):
header_token, major_version, minor_version, flags, next_token = struct.unpack_from("<BBBBB", buf, offset)
@@ -195,6 +203,7 @@ class BinXML:
def xml(self, template=None):
return self._element.xml(template)
class ResultSet:
def __init__(self, buf):
total_size, header_size, event_offset, bookmark_offset, binxml_size = struct.unpack_from("<IIIII", buf)
+71 -2
View File
@@ -4,6 +4,8 @@ import re
import inspect
import os
from ipaddress import ip_address
def identify_target_file(target_file):
with open(target_file) as target_file_handle:
@@ -22,7 +24,7 @@ def gen_random_string(length=10):
def validate_ntlm(data):
allowed = re.compile("^[0-9a-f]{32}", re.IGNORECASE)
allowed = re.compile(r"^[0-9a-f]{32}", re.IGNORECASE)
return bool(allowed.match(data))
@@ -40,7 +42,7 @@ def called_from_cmd_args():
# Stolen from https://github.com/pydanny/whichcraft/
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
"""Find the path which conforms to the given mode on the PATH for a command.
Given a command, mode, and a PATH string, return the path which conforms to the given mode on the PATH, or None if there is no such file.
`mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result of os.environ.get("PATH"), or can be overridden with a custom search path.
Note: This function was backported from the Python 3 source code.
@@ -77,3 +79,70 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
name = os.path.join(p, thefile)
if _access_check(name, mode):
return name
def get_bloodhound_info():
"""
Detect which BloodHound package is installed (regular or CE) and its version.
Returns
-------
tuple: (package_name, version, is_ce)
- package_name: Name of the installed package ('bloodhound', 'bloodhound-ce', or None)
- version: Version string of the installed package (or None if not installed)
- is_ce: Boolean indicating if it's the Community Edition
"""
import importlib.metadata
import importlib.util
# First check if any BloodHound package is available to import
if importlib.util.find_spec("bloodhound") is None:
return None, None, False
# Try to get version info from both possible packages
version = None
package_name = None
is_ce = False
# Check for bloodhound-ce first
try:
version = importlib.metadata.version("bloodhound-ce")
package_name = "bloodhound-ce"
is_ce = True
except importlib.metadata.PackageNotFoundError:
# Check for regular bloodhound
try:
version = importlib.metadata.version("bloodhound")
package_name = "bloodhound"
# Even when installed as 'bloodhound', check if it's actually the CE version
if version and ("ce" in version.lower() or "community" in version.lower()):
is_ce = True
except importlib.metadata.PackageNotFoundError:
# No bloodhound package found via metadata
pass
# In case we can import it but metadata is not working, check the module itself
if not version:
try:
import bloodhound
version = getattr(bloodhound, "__version__", "unknown")
package_name = "bloodhound"
# Check if it's CE based on version string
if "ce" in version.lower() or "community" in version.lower():
is_ce = True
package_name = "bloodhound-ce"
except ImportError:
pass
return package_name, version, is_ce
def detect_if_ip(target):
try:
ip_address(target)
return True
except Exception:
return False
+15 -30
View File
@@ -30,7 +30,6 @@ import os
import secrets
import hashlib
import datetime
import logging
import random
import base64
@@ -47,8 +46,7 @@ from asn1crypto import keys
from minikerberos.pkinit import PKINIT, DirtyDH
from minikerberos.protocol.constants import NAME_TYPE, PaDataType
from minikerberos.protocol.encryption import Enctype, _enctype_table, Key
from minikerberos.protocol.asn1_structs import KDC_REQ_BODY, PrincipalName, KDCOptions, EncASRepPart, AS_REQ, PADATA_TYPE, \
PA_PAC_REQUEST
from minikerberos.protocol.asn1_structs import KDC_REQ_BODY, PrincipalName, KDCOptions, EncASRepPart, AS_REQ, PADATA_TYPE, PA_PAC_REQUEST
from minikerberos.protocol.rfc4556 import PKAuthenticator, AuthPack, PA_PK_AS_REP, KDCDHKeyInfo, PA_PK_AS_REQ
from pyasn1.codec.der import decoder, encoder
@@ -70,6 +68,7 @@ from minikerberos.common.ccache import CCACHE
from impacket.krb5.ccache import CCache as impacket_CCache
from nxc.paths import NXC_PATH
from nxc.logger import nxc_logger
class myPKINIT(PKINIT):
@@ -304,8 +303,8 @@ class myPKINIT(PKINIT):
key = Key(cipher.enctype, t_key)
enc_data = as_rep["enc-part"]["cipher"]
logging.info("AS-REP encryption key (you might need this later):")
logging.info(hexlify(t_key).decode("utf-8"))
nxc_logger.info("AS-REP encryption key (you might need this later):")
nxc_logger.info(hexlify(t_key).decode("utf-8"))
dec_data = cipher.decrypt(key, 3, enc_data)
encasrep = EncASRepPart.load(dec_data).native
cipher = _enctype_table[int(encasrep["key"]["keytype"])]
@@ -327,34 +326,27 @@ class GETPAC:
for _bufferN in range(pacType["cBuffers"]):
infoBuffer = PAC_INFO_BUFFER(buff)
data = pacType["Buffers"][infoBuffer["Offset"] - 8:][:infoBuffer["cbBufferSize"]]
if logging.getLogger().level == logging.DEBUG:
print("TYPE 0x%x" % infoBuffer["ulType"])
nxc_logger.debug(f"TYPE 0x{infoBuffer['ulType']}")
if infoBuffer["ulType"] == 2:
found = True
credinfo = PAC_CREDENTIAL_INFO(data)
if logging.getLogger().level == logging.DEBUG:
credinfo.dump()
newCipher = _enctype_table[credinfo["EncryptionType"]]
out = newCipher.decrypt(key, 16, credinfo["SerializedData"])
type1 = TypeSerialization1(out)
# I'm skipping here 4 bytes with its the ReferentID for the pointer
newdata = out[len(type1) + 4:]
pcc = PAC_CREDENTIAL_DATA(newdata)
if logging.getLogger().level == logging.DEBUG:
pcc.dump()
for cred in pcc["Credentials"]:
credstruct = NTLM_SUPPLEMENTAL_CREDENTIAL(b"".join(cred["Credentials"]))
if logging.getLogger().level == logging.DEBUG:
credstruct.dump()
logging.info("Recovered NT Hash")
logging.info(hexlify(credstruct["NtPassword"]).decode("utf-8"))
nxc_logger.info("Recovered NT Hash")
nxc_logger.info(hexlify(credstruct["NtPassword"]).decode("utf-8"))
nthash = hexlify(credstruct["NtPassword"]).decode("utf-8")
buff = buff[len(infoBuffer):]
if not found:
logging.info("Did not find the PAC_CREDENTIAL_INFO in the PAC. Are you sure your TGT originated from a PKINIT operation?")
nxc_logger.info("Did not find the PAC_CREDENTIAL_INFO in the PAC. Are you sure your TGT originated from a PKINIT operation?")
return nthash
def __init__(self, username, domain, kdcHost, key, tgt):
@@ -399,10 +391,8 @@ class GETPAC:
authenticator["cusec"] = now.microsecond
authenticator["ctime"] = KerberosTime.to_asn1(now)
if logging.getLogger().level == logging.DEBUG:
logging.debug("AUTHENTICATOR")
print(authenticator.prettyPrint())
print("\n")
nxc_logger.debug("AUTHENTICATOR")
nxc_logger.debug(authenticator.prettyPrint() + "\n")
encodedAuthenticator = encoder.encode(authenticator)
@@ -452,23 +442,18 @@ class GETPAC:
myTicket = ticket.to_asn1(TicketAsn1())
seq_set_iter(reqBody, "additional-tickets", (myTicket,))
if logging.getLogger().level == logging.DEBUG:
logging.debug("Final TGS")
print(tgsReq.prettyPrint())
if logging.getLogger().level == logging.DEBUG:
logging.debug("Final TGS")
print(tgsReq.prettyPrint())
nxc_logger.debug("Final TGS")
nxc_logger.debug(tgsReq.prettyPrint())
message = encoder.encode(tgsReq)
logging.info("Requesting ticket to self with PAC")
nxc_logger.info("Requesting ticket to self with PAC")
r = sendReceive(message, self.__domain, self.__kdcHost)
tgs = decoder.decode(r, asn1Spec=TGS_REP())[0]
if logging.getLogger().level == logging.DEBUG:
logging.debug("TGS_REP")
print(tgs.prettyPrint())
nxc_logger.debug("TGS_REP")
nxc_logger.debug(tgs.prettyPrint())
cipherText = tgs["ticket"]["enc-part"]["cipher"]
+9 -22
View File
@@ -12,6 +12,7 @@ import random
obfuscate_ps_scripts = False
def replace_singles(s):
"""Replaces single quotes with a double quote
We do this because quoting is very important in PowerShell, and we are doing multiple layers:
@@ -27,6 +28,7 @@ def replace_singles(s):
"""
return s.replace("'", r"\"")
def get_ps_script(path):
"""Generates a full path to a PowerShell script given a relative path.
@@ -56,19 +58,6 @@ def encode_ps_command(command):
return b64encode(command.encode("UTF-16LE")).decode()
def is_powershell_installed():
"""
Check if PowerShell is installed.
Returns
-------
bool: True if PowerShell is installed, False otherwise.
"""
if which("powershell"):
return True
return False
def obfs_ps_script(path_to_script):
"""
Obfuscates a PowerShell script.
@@ -90,7 +79,7 @@ def obfs_ps_script(path_to_script):
obfs_script_dir = os.path.join(NXC_PATH, "obfuscated_scripts")
obfs_ps_script = os.path.join(obfs_script_dir, ps_script)
if is_powershell_installed() and obfuscate_ps_scripts:
if bool(which("powershell")) and obfuscate_ps_scripts:
if os.path.exists(obfs_ps_script):
nxc_logger.display("Using cached obfuscated Powershell script")
with open(obfs_ps_script) as script:
@@ -116,12 +105,11 @@ def obfs_ps_script(path_to_script):
and debug statements from a PowerShell source file.
"""
# strip block comments
stripped_code = re.sub(re.compile("<#.*?#>", re.DOTALL), "", script.read())
stripped_code = re.sub(re.compile(r"<#.*?#>", re.DOTALL), "", script.read())
# strip blank lines, lines starting with #, and verbose/debug statements
return "\n".join([line for line in stripped_code.split("\n") if ((line.strip() != "") and (not line.strip().startswith("#")) and (not line.strip().lower().startswith("write-verbose ")) and (not line.strip().lower().startswith("write-debug ")))])
def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None, encode=True):
"""
Generates a PowerShell command based on the provided `ps_command` parameter.
@@ -139,7 +127,7 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None
str: The generated PowerShell command.
"""
nxc_logger.debug(f"Creating PS command parameters: {ps_command=}, {force_ps32=}, {obfs=}, {custom_amsi=}, {encode=}")
if custom_amsi:
nxc_logger.debug(f"Using custom AMSI bypass script: {custom_amsi}")
with open(custom_amsi) as file_in:
@@ -154,7 +142,7 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None
command = amsi_bypass + f"$functions = {{function Command-ToExecute{{{amsi_bypass + ps_command}}}}}; if ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64'){{$job = Start-Job -InitializationScript $functions -ScriptBlock {{Command-ToExecute}} -RunAs32; $job | Wait-Job | Receive-Job }} else {{IEX '$functions'; Command-ToExecute}}"
else:
command = f"{amsi_bypass} {ps_command}"
nxc_logger.debug(f"Generated PS command:\n {command}\n")
if obfs:
@@ -163,7 +151,7 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None
while True:
nxc_logger.debug(f"Obfuscation attempt: {obfs_attempts + 1}")
obfs_command = invoke_obfuscation(command)
command = f'powershell.exe -exec bypass -noni -nop -w 1 -C "{replace_singles(obfs_command)}"'
if len(command) <= 8191:
break
@@ -176,11 +164,11 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None
# if we arent encoding or obfuscating anything, we quote the entire powershell in double quotes, otherwise the final powershell command will syntax error
command = f"-enc {encode_ps_command(command)}" if encode else f'"{command}"'
command = f"powershell.exe -noni -nop -w 1 {command}"
if len(command) > 8191:
nxc_logger.error(f"Command exceeds maximum length of 8191 chars (was {len(command)}). exiting.")
exit(1)
nxc_logger.debug(f"Final command: {command}")
return command
@@ -429,4 +417,3 @@ def invoke_obfuscation(script_string):
obfuscated_script = choice(invoke_options)
nxc_logger.debug(f"Script after obfuscation: {obfuscated_script}")
return obfuscated_script
+1 -3
View File
@@ -46,9 +46,7 @@ class ModuleLoader:
self.logger.fail(f"{module_path} missing the on_login/on_admin_login function(s)")
module_error = True
if module_error:
return False
return True
return not module_error
def load_module(self, module_path):
"""Load a module, initializing it and checking that it has the proper attributes"""
+1 -1
View File
@@ -103,7 +103,7 @@ class NXCAdapter(logging.LoggerAdapter):
logging.getLogger("dploot").disabled = True
logging.getLogger("neo4j").setLevel(logging.ERROR)
def format(self, msg, *args, **kwargs): # noqa: A003
def format(self, msg, *args, **kwargs):
"""Format msg for output
This is used instead of process() since process() applies to _all_ messages, including debug calls
+5 -2
View File
@@ -27,7 +27,7 @@ class NXCModule:
SERVER PKI Enrollment Server to enumerate templates for. Default is None, use CN name
BASE_DN The base domain name for the LDAP query
"""
self.regex = re.compile("(https?://.+)")
self.regex = re.compile(r"(https?://.+)")
self.server = None
self.base_dn = None
@@ -70,7 +70,10 @@ class NXCModule:
searchBase="CN=Configuration," + base_dn_root,
)
except LDAPSearchError as e:
context.log.fail(f"Obtained unexpected exception: {e}")
if "noSuchObject" in str(e):
context.log.fail("No ADCS infrastructure found.")
else:
context.log.fail(f"Obtained unexpected exception: {e}")
def process_servers(self, item):
"""Function that is called to process the items obtain by the LDAP search when listing PKI Enrollment Servers."""
+1 -1
View File
@@ -4,6 +4,7 @@ import sys
from impacket.dcerpc.v5 import samr, epm, transport
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
class NXCModule:
"""
Module by CyberCelt: @Cyb3rC3lt
@@ -88,7 +89,6 @@ class NXCModule:
if not self.noLDAPRequired:
self.do_ldaps_add(connection, context)
def do_samr_add(self, context):
"""
Connects to a target server and performs various operations related to adding or deleting machine accounts.
+1
View File
@@ -9,6 +9,7 @@ from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
from nxc.paths import NXC_PATH
class NXCModule:
name = "backup_operator"
description = "Exploit user in backup operator group to dump NTDS @mpgn_x64"
+223
View File
@@ -0,0 +1,223 @@
from impacket.ldap import ldaptypes
from nxc.parsers.ldap_results import parse_result_attributes
from ldap3.protocol.microsoft import security_descriptor_control
RELEVANT_OBJECT_TYPES = {
"00000000-0000-0000-0000-000000000000": "All Objects",
"0feb936f-47b3-49f2-9386-1dedc2c23765": "msDS-DelegatedManagedServiceAccount",
}
EXCLUDED_SIDS_SUFFIXES = ["-512", "-519"] # Domain Admins, Enterprise Admins
EXCLUDED_SIDS = ["S-1-5-32-544", "S-1-5-18"] # Builtin Administrators, Local SYSTEM
# Define all access rights
ACCESS_RIGHTS = {
# Generic Rights
"GenericRead": 0x80000000, # ADS_RIGHT_GENERIC_READ
"GenericWrite": 0x40000000, # ADS_RIGHT_GENERIC_WRITE
"GenericExecute": 0x20000000, # ADS_RIGHT_GENERIC_EXECUTE
"GenericAll": 0x10000000, # ADS_RIGHT_GENERIC_ALL
# Maximum Allowed access type
"MaximumAllowed": 0x02000000,
# Access System Acl access type
"AccessSystemSecurity": 0x01000000, # ADS_RIGHT_ACCESS_SYSTEM_SECURITY
# Standard access types
"Synchronize": 0x00100000, # ADS_RIGHT_SYNCHRONIZE
"WriteOwner": 0x00080000, # ADS_RIGHT_WRITE_OWNER
"WriteDACL": 0x00040000, # ADS_RIGHT_WRITE_DAC
"ReadControl": 0x00020000, # ADS_RIGHT_READ_CONTROL
"Delete": 0x00010000, # ADS_RIGHT_DELETE
# Specific rights
"AllExtendedRights": 0x00000100, # ADS_RIGHT_DS_CONTROL_ACCESS
"ListObject": 0x00000080, # ADS_RIGHT_DS_LIST_OBJECT
"DeleteTree": 0x00000040, # ADS_RIGHT_DS_DELETE_TREE
"WriteProperties": 0x00000020, # ADS_RIGHT_DS_WRITE_PROP
"ReadProperties": 0x00000010, # ADS_RIGHT_DS_READ_PROP
"Self": 0x00000008, # ADS_RIGHT_DS_SELF
"ListChildObjects": 0x00000004, # ADS_RIGHT_ACTRL_DS_LIST
"DeleteChild": 0x00000002, # ADS_RIGHT_DS_DELETE_CHILD
"CreateChild": 0x00000001, # ADS_RIGHT_DS_CREATE_CHILD
}
# Define which rights are considered relevant for potential abuse
RELEVANT_RIGHTS = {
"GenericAll": ACCESS_RIGHTS["GenericAll"],
"GenericWrite": ACCESS_RIGHTS["GenericWrite"],
"WriteOwner": ACCESS_RIGHTS["WriteOwner"],
"WriteDACL": ACCESS_RIGHTS["WriteDACL"],
"CreateChild": ACCESS_RIGHTS["CreateChild"],
"WriteProperties": ACCESS_RIGHTS["WriteProperties"],
"AllExtendedRights": ACCESS_RIGHTS["AllExtendedRights"]
}
FUNCTIONAL_LEVELS = {
"Windows 2000": 0,
"Windows Server 2003": 1,
"Windows Server 2003 R2": 2,
"Windows Server 2008": 3,
"Windows Server 2008 R2": 4,
"Windows Server 2012": 5,
"Windows Server 2012 R2": 6,
"Windows Server 2016": 7,
"Windows Server 2019": 8,
"Windows Server 2022": 9,
"Windows Server 2025": 10,
}
class NXCModule:
"""
-------
Module by @mpgn based on https://www.akamai.com/blog/security-research/abusing-dmsa-for-privilege-escalation-in-active-directory#credentials
and https://raw.githubusercontent.com/akamai/BadSuccessor/refs/heads/main/Get-BadSuccessorOUPermissions.ps1
"""
name = "badsuccessor"
description = "Check if vulnerable to bad successor attack (DMSA)"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = True
def __init__(self):
self.context = None
self.module_options = None
def options(self, context, module_options):
"""No options available"""
def is_excluded_sid(self, sid, domain_sid):
if sid in EXCLUDED_SIDS:
return True
return any(sid.startswith(domain_sid) and sid.endswith(suffix) for suffix in EXCLUDED_SIDS_SUFFIXES)
def get_domain_sid(self, ldap_session, base_dn):
"""Retrieve the domain SID from the domain object in LDAP"""
r = ldap_session.search(
searchBase=base_dn,
searchFilter="(objectClass=domain)",
attributes=["objectSid"]
)
parsed = parse_result_attributes(r)
if parsed and "objectSid" in parsed[0]:
return parsed[0]["objectSid"]
def find_bad_successor_ous(self, ldap_session, entries, base_dn):
domain_sid = self.get_domain_sid(ldap_session, base_dn)
results = {}
parsed = parse_result_attributes(entries)
for entry in parsed:
dn = entry["distinguishedName"]
sd_data = entry["nTSecurityDescriptor"]
sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=sd_data)
for ace in sd["Dacl"]["Data"]:
if ace["AceType"] != ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE:
continue
has_relevant_right = False
mask = int(ace["Ace"]["Mask"]["Mask"])
for right_value in RELEVANT_RIGHTS.values():
if mask & right_value:
has_relevant_right = True
break
if not has_relevant_right:
continue # Skip this ACE if it doesn't have any relevant rights
object_type = getattr(ace, "ObjectType", None)
if object_type:
object_guid = ldaptypes.bin_to_string(object_type).lower()
if object_guid not in RELEVANT_OBJECT_TYPES:
continue
sid = ace["Ace"]["Sid"].formatCanonical()
if self.is_excluded_sid(sid, domain_sid):
continue
results.setdefault(sid, []).append(dn)
if hasattr(sd, "OwnerSid"):
owner_sid = str(sd["OwnerSid"])
if not self.is_excluded_sid(owner_sid, domain_sid):
results.setdefault(owner_sid, []).append(dn)
return results
def resolve_sid_to_name(self, ldap_session, sid, base_dn):
"""
Resolves a SID to a samAccountName using LDAP
Args:
----
ldap_session: The LDAP connection
sid: The SID to resolve
base_dn: The base DN for the LDAP search
Returns:
-------
str: The samAccountName if found, otherwise the original SID
"""
try:
search_filter = f"(objectSid={sid})"
response = ldap_session.search(
searchBase=base_dn,
searchFilter=search_filter,
attributes=["sAMAccountName"]
)
parsed = parse_result_attributes(response)
if parsed and "sAMAccountName" in parsed[0]:
return parsed[0]["sAMAccountName"]
return sid
except Exception:
return sid
def on_login(self, context, connection):
# Check for a domain controller with Windows Server 2025
resp = connection.ldap_connection.search(
searchBase=connection.ldap_connection._baseDN,
searchFilter="(&(objectCategory=computer)(primaryGroupId=516))",
attributes=["operatingSystem", "dNSHostName"]
)
parsed_resp = parse_result_attributes(resp)
for dc in parsed_resp:
if "2025" in dc["operatingSystem"]:
out = connection.resolver(dc["dNSHostName"])
dc_ip = out[0] if out else "Unknown IP"
context.log.success(f"Found domain controller with operating system Windows Server 2025: {dc_ip} ({dc['dNSHostName']})")
else:
context.log.fail("No domain controller with operating system Windows Server 2025 found, attack not possible. Enumerate dMSA objects anyway.")
# Enumerate dMSA objects
controls = security_descriptor_control(sdflags=0x07) # OWNER_SECURITY_INFORMATION
resp = connection.ldap_connection.search(
searchBase=connection.ldap_connection._baseDN,
searchFilter="(objectClass=organizationalUnit)",
attributes=["distinguishedName", "nTSecurityDescriptor"],
searchControls=controls) # Fixed parameter name
context.log.debug(f"Found {len(resp)} entries")
results = self.find_bad_successor_ous(connection.ldap_connection, resp, connection.ldap_connection._baseDN)
if results:
context.log.success(f"Found {len(results)} results")
else:
context.log.highlight("No account found")
for sid, ous in results.items():
samaccountname = self.resolve_sid_to_name(
connection.ldap_connection,
sid,
connection.ldap_connection._baseDN
)
for ou in ous:
if sid == samaccountname:
context.log.highlight(f"{sid}, {ou}")
else:
context.log.highlight(f"{samaccountname} ({sid}), {ou}")
+1
View File
@@ -4,6 +4,7 @@ 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
class NXCModule:
name = "bitlocker"
description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled."
+16 -22
View File
@@ -221,7 +221,7 @@ class ShadowCoerceTrigger:
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
binding_params = {
"Fssagentrpc": {
"stringBinding": r"ncacn_np:%s[\PIPE\Fssagentrpc]" % target,
"stringBinding": rf"ncacn_np:{target}[\PIPE\Fssagentrpc]",
"MSRPC_UUID_FSRVP": ("a8e0653c-2744-4389-a61d-7373df8b2292", "3.0"),
},
}
@@ -338,7 +338,7 @@ class DFSCoerceTrigger:
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
binding_params = {
"netdfs": {
"stringBinding": r"ncacn_np:%s[\PIPE\netdfs]" % target,
"stringBinding": rf"ncacn_np:{target}[\PIPE\netdfs]",
"MSRPC_UUID_DFSNM": ("4fc742e0-4a10-11cf-8273-00aa004ae673", "3.0"),
},
}
@@ -509,23 +509,23 @@ class PetitPotamtTrigger:
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
binding_params = {
"lsarpc": {
"stringBinding": r"ncacn_np:%s[\PIPE\lsarpc]" % target,
"stringBinding": rf"ncacn_np:{target}[\PIPE\lsarpc]",
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
},
"efsrpc": {
"stringBinding": r"ncacn_np:%s[\PIPE\efsrpc]" % target,
"stringBinding": rf"ncacn_np:{target}[\PIPE\efsrpc]",
"MSRPC_UUID_EFSR": ("df1941c5-fe89-4e79-bf10-463657acf44d", "1.0"),
},
"samr": {
"stringBinding": r"ncacn_np:%s[\PIPE\samr]" % target,
"stringBinding": rf"ncacn_np:{target}[\PIPE\samr]",
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
},
"lsass": {
"stringBinding": r"ncacn_np:%s[\PIPE\lsass]" % target,
"stringBinding": rf"ncacn_np:{target}[\PIPE\lsass]",
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
},
"netlogon": {
"stringBinding": r"ncacn_np:%s[\PIPE\netlogon]" % target,
"stringBinding": rf"ncacn_np:{target}[\PIPE\netlogon]",
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
},
}
@@ -758,17 +758,15 @@ class PrinterBugTrigger:
self.context = context
def get_dynamic_endpoint(self, interface: bytes, target: str, timeout: int = 5) -> str:
string_binding = r"ncacn_ip_tcp:%s[135]" % target
string_binding = rf"ncacn_ip_tcp:{target}[135]"
rpctransport = transport.DCERPCTransportFactory(string_binding)
rpctransport.set_connect_timeout(timeout)
dce = rpctransport.get_dce_rpc()
self.context.log.debug(
"Trying to resolve dynamic endpoint %s" % repr(uuid.bin_to_string(interface))
)
self.context.log.debug(f"Trying to resolve dynamic endpoint {uuid.bin_to_string(interface)!r}")
try:
dce.connect()
except Exception as e:
self.context.log.warning("Failed to connect to endpoint mapper: %s" % e)
self.context.log.warning(f"Failed to connect to endpoint mapper: {e}")
raise e
try:
endpoint = epm.hept_map(target, interface, protocol="ncacn_ip_tcp", dce=dce)
@@ -777,17 +775,13 @@ class PrinterBugTrigger:
)
return endpoint
except Exception as e:
self.context.log.debug(
"Failed to resolve dynamic endpoint %s"
% repr(uuid.bin_to_string(interface))
)
self.context.log.debug(f"Failed to resolve dynamic endpoint {uuid.bin_to_string(interface)!r}")
raise e
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
binding_params = {
"spoolss": {
"stringBinding": r"ncacn_np:%s[\PIPE\spoolss]" % target,
"stringBinding": rf"ncacn_np:{target}[\PIPE\spoolss]",
"MSRPC_UUID_RPRN": ("12345678-1234-abcd-ef00-0123456789ab", "1.0"),
"port": 445
},
@@ -835,7 +829,7 @@ class PrinterBugTrigger:
def exploit(self, dce, listener, target, always_continue, pipe):
try:
resp = rprn.hRpcOpenPrinter(dce, "\\\\%s\x00" % target)
resp = rprn.hRpcOpenPrinter(dce, f"\\\\{target}\x00")
except Exception as e:
if str(e).find("Broken pipe") >= 0:
# The connection timed-out. Let's try to bring it back next round
@@ -853,7 +847,7 @@ class PrinterBugTrigger:
request = rprn.RpcRemoteFindFirstPrinterChangeNotificationEx()
request["hPrinter"] = resp["pHandle"]
request["fdwFlags"] = rprn.PRINTER_CHANGE_ADD_JOB
request["pszLocalMachine"] = "\\\\%s\x00" % listener
request["pszLocalMachine"] = f"\\\\{listener}\x00"
request["fdwOptions"] = 0x00000000
request["dwPrinterLocal"] = 0
dce.request(request)
@@ -885,7 +879,7 @@ class PrinterBugTrigger:
request = RpcRemoteFindFirstPrinterChangeNotification()
request["hPrinter"] = resp["pHandle"]
request["fdwFlags"] = rprn.PRINTER_CHANGE_ADD_JOB
request["pszLocalMachine"] = "\\\\%s\x00" % listener
request["pszLocalMachine"] = f"\\\\{listener}\x00"
request["fdwOptions"] = 0x00000000
request["dwPrinterLocal"] = 0
request["cbBuffer"] = NULL
@@ -908,7 +902,7 @@ class MSEvenTrigger:
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
binding_params = {
"eventlog": {
"stringBinding": r"ncacn_np:%s[\PIPE\eventlog]" % target,
"stringBinding": rf"ncacn_np:{target}[\PIPE\eventlog]",
"MSRPC_UUID_EVEN": ("82273fdc-e32a-18c3-3f78-827929dc23ea", "0.0"),
},
}
+3 -7
View File
@@ -429,11 +429,9 @@ class NXCModule:
def parse_dacl(self, context, dacl):
parsed_dacl = []
context.log.debug("Parsing DACL")
i = 0
for ace in dacl["Data"]:
parsed_ace = self.parse_ace(context, ace)
parsed_dacl.append(parsed_ace)
i += 1
return parsed_dacl
# Parses an access mask to extract the different values from a simple permission
@@ -509,11 +507,10 @@ class NXCModule:
parsed_dacl : a parsed DACL from parse_dacl()
"""
context.log.debug("Printing parsed DACL")
i = 0
# If a specific right or a specific GUID has been specified, only the ACE with this right will be printed
# If an ACE type has been specified, only the ACE with this type will be specified
# If a principal has been specified, only the ACE where he is the trustee will be printed
for parsed_ace in parsed_dacl:
for i, parsed_ace in enumerate(parsed_dacl):
print_ace = True
context.log.debug(f"{parsed_ace=}, {self.rights=}, {self.rights_guid=}, {self.ace_type=}, {self.principal_sid=}")
@@ -561,16 +558,15 @@ class NXCModule:
except Exception as e:
context.log.debug(f"Error filtering with {parsed_ace=} and {self.principal_sid=}, probably because of ACE type unsupported for parsing yet ({e})")
if print_ace:
self.context.log.highlight("%-28s" % "ACE[%d] info" % i)
self.context.log.highlight(f"ACE[{i}] info")
self.print_parsed_ace(parsed_ace)
i += 1
# Prints properly a parsed ACE
# - parsed_ace : a parsed ACE from parse_ace()
def print_parsed_ace(self, parsed_ace):
elements_name = list(parsed_ace.keys())
for attribute in elements_name:
self.context.log.highlight(" %-26s: %s" % (attribute, parsed_ace[attribute]))
self.context.log.highlight(f"\t{attribute:<26}: {parsed_ace[attribute]}")
# Retrieves the GUIDs for the specified rights
def build_guids_for_rights(self):
+1
View File
@@ -5,6 +5,7 @@ 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"
+1 -1
View File
@@ -33,7 +33,7 @@ class NXCModule:
obfuscate = "OBFUSCATE" in module_options
# we can use commands instead of backslashes - this is because Linux and OSX treat them differently
default_obfuscation = "Token,All,1"
obfuscate_cmd = module_options["OBFUSCATE_CMD"] if "OBFUSCATE_CMD" in module_options else default_obfuscation
obfuscate_cmd = module_options.get("OBFUSCATE_CMD", default_obfuscation)
context.log.debug(f"Obfuscate: {obfuscate} - Obfuscate_cmd: {obfuscate_cmd}")
# Pull the host and port from the config file
+2 -2
View File
@@ -61,7 +61,7 @@ class NXCModule:
rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
rpctransport.setRemoteHost(connection.host)
rpctransport.set_dport(self.__port)
elif self.__port in [443]:
elif self.__port == 443:
# Setting credentials only for RPC Proxy, but not for the MSRPC level
rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
rpctransport.set_auth_type(AUTH_NTLM)
@@ -86,7 +86,7 @@ class NXCModule:
if uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmpUUID))[:18] in epm.KNOWN_UUIDS:
exename = epm.KNOWN_UUIDS[uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmpUUID))[:18]]
context.log.debug("EXEs %s" % exename)
context.log.debug(f"EXEs {exename}")
if exename == "certsrv.exe":
context.log.highlight("Active Directory Certificate Services Found.")
url = f"http://{connection.host}/certsrv/certfnsh.asp"
+1 -1
View File
@@ -50,7 +50,7 @@ class NXCModule:
rname = text.split(" ")[0]
rtype = text.split(" ")[2]
rvalue = " ".join(text.split(" ")[3:])
if domain_data.get(rtype, False):
if domain_data.get(rtype):
domain_data[rtype].append(f"{rname}: {rvalue}")
else:
domain_data[rtype] = [f"{rname}: {rvalue}"]
+1
View File
@@ -42,5 +42,6 @@ class NXCModule:
"""
res = self.mssql_conn.sql_query(query)
return [user["name"] for user in res] if res else []
def options(self, context, module_options):
pass
+1
View File
@@ -36,5 +36,6 @@ class NXCModule:
query = "SELECT name FROM sys.server_principals WHERE type_desc = 'SQL_LOGIN';"
res = self.mssql_conn.sql_query(query)
return [login["name"] for login in res] if res else []
def options(self, context, module_options):
pass
+23 -17
View File
@@ -20,13 +20,13 @@ class NXCModule:
self.context = None
self.module_options = None
self.method = "execute"
self.limit = 1000
self.limit = None
def options(self, context, module_options):
"""
METHOD EventLog method (Execute or RPCCALL)
METHOD EventLog method (Execute or RPCCALL), default: execute
M Alias for METHOD
LIMIT Limit of the number of records to be fetched
LIMIT Limit of the number of records to be fetched, default: unlimited
L Alias for LIMIT
"""
if "METHOD" in module_options:
@@ -41,8 +41,6 @@ class NXCModule:
def find_credentials(self, content, context):
# remove unnecessary words
content = content.replace("\r\n", "\n")
content = content.replace("/add", "")
content = content.replace("/active:yes", "")
# sort and unique lines
content = "\n".join(sorted(set(content.split("\n"))))
@@ -66,9 +64,16 @@ class NXCModule:
# Extracting credentials
for line in content.split("\n"):
for reg in regexps:
# verbose context.log.debug("Line: " + line)
# verbose context.log.debug("Reg: " + reg)
match = re.search(reg, line, re.IGNORECASE)
# Remove unnecessary words
line_stripped = line.replace("/add", "") \
.replace("/active:yes", "") \
.replace("/delete", "") \
.replace("/domain", "") \
# Remove command lines that were executed with nxc
line_stripped = re.sub(r"1> \\Windows\\Temp\\[\w]{6} 2>&1", "", line_stripped)
# Use regex to find credentials
match = re.search(reg, line_stripped, re.IGNORECASE)
if match:
# eleminate false positives
# C:\Windows\system32\svchost.exe -k DcomLaunch -p -s PlugPlay
@@ -92,11 +97,12 @@ class NXCModule:
def on_admin_login(self, context, connection):
content = ""
if self.method[:1].lower() == "e":
if self.method.lower().startswith("e"):
limit_str = f"/c:{self.limit}" if self.limit is not None else ""
# https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/auditing/event-4688
commands = [
f'wevtutil qe Security /c:{self.limit} /f:text /rd:true /q:"*[System[(EventID=4688)]]" |findstr "Command Line"',
f'wevtutil qe Microsoft-Windows-Sysmon/Operational /c:{self.limit} /f:text /rd:true /q:"*[System[(EventID=1)]]" |findstr "ParentCommandLine"'
f'wevtutil qe Microsoft-Windows-Sysmon/Operational {limit_str} /f:text /rd:true /q:"*[System[(EventID=1)]]" | findstr "ParentCommandLine"',
f'wevtutil qe Security {limit_str} /f:text /rd:true /q:"*[System[(EventID=4688)]]" | findstr "Command Line"',
]
for command in commands:
context.log.debug("Execute Command: " + command)
@@ -127,7 +133,6 @@ class NXCModule:
content += "CommandLine: " + match.group("CommandLine") + "\n"
except Exception as e:
context.log.error(f"Error: {e}")
continue
self.find_credentials(content, context)
@@ -182,7 +187,7 @@ class MSEven6Trigger:
class MSEven6Result:
def __init__(self, conn, handle, limit):
def __init__(self, conn, handle, limit=None):
self._conn = conn
self._handle = handle
self._hardlimit = limit
@@ -192,11 +197,12 @@ class MSEven6Result:
return self
def __next__(self):
self._hardlimit -= 1
if self._hardlimit < 0:
raise StopIteration
if self._hardlimit is not None:
self._hardlimit -= 1
if self._hardlimit < 0:
raise StopIteration
if self._resp is not None and self._resp["NumActualRecords"] == 0:
return None
raise StopIteration
if self._resp is None or self._index == self._resp["NumActualRecords"]:
req = even6.EvtRpcQueryNext()
+3 -5
View File
@@ -32,10 +32,8 @@ def get_dns_resolver(server, context):
# Is our host an IP? In that case make sure the server IP is used
# if not assume lookups are working already
try:
if server.startswith("ldap://"):
server = server[7:]
if server.startswith("ldaps://"):
server = server[8:]
server = server.removeprefix("ldap://")
server = server.removeprefix("ldaps://")
socket.inet_aton(server)
dnsresolver.nameservers = [server]
except OSError:
@@ -44,7 +42,7 @@ def get_dns_resolver(server, context):
def ldap2domain(ldap):
return re.sub(",DC=", ".", ldap[ldap.lower().find("dc="):], flags=re.I)[3:]
return re.sub(r",DC=", ".", ldap[ldap.lower().find("dc="):], flags=re.IGNORECASE)[3:]
def new_record(rtype, serial):
+3 -3
View File
@@ -4,7 +4,7 @@ from time import sleep
from csv import reader
from base64 import b64encode
from io import BytesIO, StringIO
from xml.etree import ElementTree
from xml.etree import ElementTree as ET
from nxc.helpers.powershell import get_ps_script
@@ -358,7 +358,7 @@ class NXCModule:
sys.exit(1)
try:
keepass_config_xml_root = ElementTree.fromstring(buffer.getvalue())
keepass_config_xml_root = ET.fromstring(buffer.getvalue())
except Exception as e:
context.log.fail(f"Error while parsing file '{self.keepass_config_path}', exiting: {e}")
sys.exit(1)
@@ -377,7 +377,7 @@ class NXCModule:
def extract_password(self, context):
xml_doc_path = os.path.abspath(self.local_export_path + "/" + self.export_name)
xml_tree = ElementTree.parse(xml_doc_path)
xml_tree = ET.parse(xml_doc_path)
root = xml_tree.getroot()
root_entries = root.find("./Root/Entry")
-1
View File
@@ -112,7 +112,6 @@ class NXCModule:
context.log.fail(f"Exception in run_ldaps_withEPA: {e}")
return None
# Domain Controllers do not have a certificate setup for
# LDAPS on port 636 by default. If this has not been setup,
# the TLS handshake will hang and you will not be able to
+1 -3
View File
@@ -98,6 +98,4 @@ class NXCModule:
result = self.mssql_conn.sql_query(query)
# Assuming the query returns a list of dictionaries with 'config_value' as the key
self.context.log.debug(f"{option} check result: {result}")
if result and result[0]["config_value"] == 1:
return True
return False
return bool(result and result[0]["config_value"] == 1)
+1 -1
View File
@@ -16,6 +16,7 @@ class MRemoteNgEncryptionAttributes:
encryption_engine: str
full_file_encryption: bool
class NXCModule:
"""
Dump mRemoteNG Passwords
@@ -181,7 +182,6 @@ class NXCModule:
content = conn.readFile(self.context.share, new_path)
self.handle_confCons_file(content)
def extract_remoteng_passwords(self, encrypted_password, encryption_attributes: MRemoteNgEncryptionAttributes):
encrypted_password = b64decode(encrypted_password)
if encrypted_password == b"":
+13 -22
View File
@@ -31,19 +31,19 @@ class SmbHeader(Structure):
]
def __init__(self, buffer):
nxc_logger.debug("server_component : %04x" % self.server_component)
nxc_logger.debug("smb_command : %01x" % self.smb_command)
nxc_logger.debug("error_class : %01x" % self.error_class)
nxc_logger.debug("error_code : %02x" % self.error_code)
nxc_logger.debug("flags : %01x" % self.flags)
nxc_logger.debug("flags2 : %02x" % self.flags2)
nxc_logger.debug("process_id_high : %02x" % self.process_id_high)
nxc_logger.debug("signature : %08x" % self.signature)
nxc_logger.debug("reserved2 : %02x" % self.reserved2)
nxc_logger.debug("tree_id : %02x" % self.tree_id)
nxc_logger.debug("process_id : %02x" % self.process_id)
nxc_logger.debug("user_id : %02x" % self.user_id)
nxc_logger.debug("multiplex_id : %02x" % self.multiplex_id)
nxc_logger.debug(f"server_component : {self.server_component:04x}")
nxc_logger.debug(f"smb_command : {self.smb_command:01x}")
nxc_logger.debug(f"error_class : {self.error_class:01x}")
nxc_logger.debug(f"error_code : {self.error_code:02x}")
nxc_logger.debug(f"flags : {self.flags:01x}")
nxc_logger.debug(f"flags2 : {self.flags2:02x}")
nxc_logger.debug(f"process_id_high : {self.process_id_high:02x}")
nxc_logger.debug(f"signature : {self.signature:08x}")
nxc_logger.debug(f"reserved2 : {self.reserved2:02x}")
nxc_logger.debug(f"tree_id : {self.tree_id:02x}")
nxc_logger.debug(f"process_id : {self.process_id:02x}")
nxc_logger.debug(f"user_id : {self.user_id:02x}")
nxc_logger.debug(f"multiplex_id : {self.multiplex_id:02x}")
def __new__(self, buffer=None):
nxc_logger.debug(f"Creating SMB_HEADER object from buffer: {buffer}")
@@ -72,7 +72,6 @@ class NXCModule:
if str(e) == "Buffer size too small (0 instead of at least 32 bytes)":
context.log.debug("Buffer size too small, which means the response was not the expected size")
def generate_smb_proto_payload(self, *protos):
"""
Flattens a nested list and merges all bytes objects into a single bytes object.
@@ -98,7 +97,6 @@ class NXCModule:
self.logger.debug(f"Packed proto data: {hex_data}")
return hex_data
def calculate_doublepulsar_xor_key(self, s):
"""
Calculate Doublepulsar Xor Key.
@@ -115,8 +113,6 @@ class NXCModule:
x = (2 * s ^ (((s & 0xff00 | (s << 16)) << 8) | (((s >> 16) | s & 0xff0000) >> 8)))
return x & 0xffffffff # truncate to 32 bits
def negotiate_proto_request(self):
"""Generate a negotiate_proto_request packet."""
self.logger.debug("generate negotiate proto request")
@@ -160,7 +156,6 @@ class NXCModule:
# Return the generated SMB protocol payload
return self.generate_smb_proto_payload(netbios, smb_header, negotiate_proto_request)
def session_setup_andx_request(self):
"""Generate session setup andx request."""
self.logger.debug("generate session setup andx request"
@@ -210,7 +205,6 @@ class NXCModule:
return self.generate_smb_proto_payload(netbios, smb_header, session_setup_andx_request)
def tree_connect_andx_request(self, ip, userid):
"""Generate tree connect andx request.
@@ -279,7 +273,6 @@ class NXCModule:
# Generate the final SMB protocol payload
return self.generate_smb_proto_payload(netbios, smb_header, tree_connect_andx_request)
def peeknamedpipe_request(self, treeid, processid, userid, multiplex_id):
"""
Generate tran2 request.
@@ -345,7 +338,6 @@ class NXCModule:
return self.generate_smb_proto_payload(netbios, smb_header, tran_request)
def trans2_request(self, treeid, processid, userid, multiplex_id):
"""Generate trans2 request.
@@ -409,7 +401,6 @@ class NXCModule:
return self.generate_smb_proto_payload(netbios, smb_header, trans2_request)
def check(self, ip, port=445):
"""Check if MS17_010 SMB Vulnerability exists.
+1
View File
@@ -1,5 +1,6 @@
import sys
class NXCModule:
"""Execute arbitrary SQL commands on the target MSSQL server"""
+1 -3
View File
@@ -300,9 +300,7 @@ class NXCModule:
WHERE rp.name = 'db_owner' AND mp.name = SYSTEM_USER
"""
res = self.query_and_get_output(exec_as + query)
if res and "database_role" in res[0] and res[0]["database_role"] == "db_owner":
return True
return False
return bool(res and "database_role" in res[0] and res[0]["database_role"] == "db_owner")
def find_dbowner_priv(self, databases, exec_as="") -> list:
"""
+2
View File
@@ -142,6 +142,8 @@ class NXCModule:
add_ntds_hash.ntds_hashes = 0
add_ntds_hash.added_to_db = 0
connection.output_filename = connection.output_file_template.format(output_folder="ntds")
NTDS = NTDSHashes(
f"{self.dir_result}/Active Directory/ntds.dit",
boot_key,
+1 -1
View File
@@ -40,7 +40,7 @@ class NXCModule:
def on_login(self, context, connection):
# Connect and bind to MS-RPRN (https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/848b8334-134a-4d02-aea4-03b673d6c515)
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
stringbinding = r"ncacn_np:%s[\PIPE\spoolss]" % target
stringbinding = rf"ncacn_np:{target}[\PIPE\spoolss]"
context.log.info(f"Binding to {stringbinding!r}")
+1
View File
@@ -1,6 +1,7 @@
from impacket.dcerpc.v5 import rrp
from impacket.examples.secretsdump import RemoteOperations
class NXCModule:
r"""
WinLogon AutoLogon: extract the credential from the following registry hive
+2 -3
View File
@@ -52,10 +52,11 @@ class NXCModule:
else:
context.log.highlight("Potentially vulnerable to CVE-2019-1040, next step: https://dirkjanm.io/exploiting-CVE-2019-1040-relay-vulnerabilities-for-rce-and-domain-admin/")
class Modify_Func:
# Slightly modified version of impackets computeResponseNTLMv2
def mod_computeResponseNTLMv2(flags, serverChallenge, clientChallenge, serverName, domain, user, password, lmhash="", nthash="",
use_ntlmv2=ntlm.USE_NTLMv2, channel_binding_value=b""):
use_ntlmv2=ntlm.USE_NTLMv2, channel_binding_value=b"", service="cifs"):
responseServerVersion = b"\x01"
hiResponseServerVersion = b"\x01"
@@ -162,7 +163,6 @@ class Modify_Func:
if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_ALWAYS_SIGN == ntlm.NTLMSSP_NEGOTIATE_ALWAYS_SIGN:
responseFlags ^= ntlm.NTLMSSP_NEGOTIATE_ALWAYS_SIGN
keyExchangeKey = ntlm.KXKEY(ntlmChallenge["flags"], sessionBaseKey, lmResponse, ntlmChallenge["challenge"], password,
lmhash, nthash, use_ntlmv2)
@@ -170,7 +170,6 @@ class Modify_Func:
if user == "" and password == "" and lmhash == "" and nthash == "":
keyExchangeKey = b"\x00" * 16
if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_KEY_EXCH:
exportedSessionKey = ntlm.b("".join([random.choice(string.digits + string.ascii_letters) for _ in range(16)]))
encryptedRandomSessionKey = ntlm.generateEncryptedSessionKey(keyExchangeKey, exportedSessionKey)
+1
View File
@@ -4,6 +4,7 @@ from impacket.dcerpc.v5.rpcrt import DCERPCException
from json import loads
from traceback import format_exc as traceback_format_exc
class NXCModule:
"""
Module by Adamkadaban: @Adamkadaban
+1
View File
@@ -1,6 +1,7 @@
from impacket.dcerpc.v5 import rrp
from impacket.examples.secretsdump import RemoteOperations
# Module by @Defte_
# Enables or disables shadow RDP
class NXCModule:
+1 -1
View File
@@ -3,6 +3,7 @@ import ntpath
from sys import exit
from nxc.paths import TMP_PATH
class NXCModule:
"""
Original idea and PoC by Justin Angel (@4rch4ngel86)
@@ -61,7 +62,6 @@ class NXCModule:
self.ico_uri = module_options["ICO_URI"]
context.log.debug("Overriding")
self.lnk_name = module_options["NAME"]
self.local_lnk_path = f"{TMP_PATH}/{self.lnk_name}.lnk"
self.remote_file_path = ntpath.join("\\", f"{self.lnk_name}.lnk")
+1
View File
@@ -10,6 +10,7 @@ MAX_ATTEMPTS = 2000 # False negative chance: 0.04%
# SMBGhost Packet
SMBGHOST_PKT = b'\x00\x00\x00\xc0\xfeSMB@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$\x00\x08\x00\x01\x00\x00\x00\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00x\x00\x00\x00\x02\x00\x00\x00\x02\x02\x10\x02"\x02$\x02\x00\x03\x02\x03\x10\x03\x11\x03\x00\x00\x00\x00\x01\x00&\x00\x00\x00\x00\x00\x01\x00 \x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\n\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00'
class NXCModule:
name = "smbghost"
description = "Module to check for the SMB dialect 3.1.1 and compression capability of the host, which is an indicator for the SMBGhost vulnerability (CVE-2020-0796)."
-3
View File
@@ -22,8 +22,6 @@ class NXCModule:
self.context = context
self.users = [user.lower() for user in module_options["USERS"].split(",")] if "USERS" in module_options else None
def on_admin_login(self, context, connection):
self.context = context
self.connection = connection
@@ -111,7 +109,6 @@ class NXCModule:
if total_files_downloaded > 0 and host_output_path:
context.log.success(f"{total_files_downloaded} file(s) downloaded from host {connection.host} to {host_output_path}.")
def find_screenshots_folders(self, user_folder_name):
"""
Dynamically searches for all Screenshots folders in the user's home directory.
+1 -3
View File
@@ -5,11 +5,11 @@ from socket import socket, AF_INET, SOCK_DGRAM
from struct import pack, unpack
def hashcat_format(rid, hashval, salt):
"""Encodes hash in Hashcat-compatible format (with username prefix)."""
return f"{rid}:$sntp-ms${hexlify(hashval).decode()}${hexlify(salt).decode()}"
class NXCModule:
"""
Module by Disgame: @Disgame
@@ -33,7 +33,6 @@ class NXCModule:
# Static NTP query prefix using the MD5 authenticator. Append 4-byte RID and dummy checksum to create a full query.
self.ntp_prefix = unhexlify("db0011e9000000000001000000000000e1b8407debc7e50600000000000000000000000000000000e1b8428bffbfcd0a")
def options(self, context, module_options):
self.rids = range(1, 2**31)
self.rate = 180
@@ -81,7 +80,6 @@ class NXCModule:
except PermissionError:
context.log.exception(f"No permission to listen on port {src_port}. May need to run as root.")
query_interval = 1 / rate
last_ok_time = time()
rids_received = set()
-2
View File
@@ -1,4 +1,3 @@
import logging
from impacket.dcerpc.v5 import rrp
from impacket.examples.secretsdump import RemoteOperations
@@ -14,7 +13,6 @@ class NXCModule:
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
logging.debug("test")
def options(self, context, module_options):
""" """
+11 -11
View File
@@ -128,17 +128,17 @@ class NXCModule:
self.context.log.debug(f"Error while RegQueryValues {registry_keys} from {user_registry_path}: {e}")
continue
else:
fh = tempfile.NamedTemporaryFile()
fh.write(ntuser_dat_bytes)
fh.seek(0)
reg = winregistry.Registry(fh.name, isRemote=False)
parent_key = reg.findKey(registry_path)
if parent_key is None:
continue
cred["user"] = reg.getValue(ntpath.join(registry_path, registry_keys[0]))[1].decode("latin-1")
password = reg.getValue(ntpath.join(registry_path, registry_keys[1]))[1].decode("utf-16le").rstrip("\0").encode()
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")
with tempfile.NamedTemporaryFile() as fh:
fh.write(ntuser_dat_bytes)
fh.seek(0)
reg = winregistry.Registry(fh.name, isRemote=False)
parent_key = reg.findKey(registry_path)
if parent_key is None:
continue
cred["user"] = reg.getValue(ntpath.join(registry_path, registry_keys[0]))[1].decode("latin-1")
password = reg.getValue(ntpath.join(registry_path, registry_keys[1]))[1].decode("utf-16le").rstrip("\0").encode()
cred["password"] = self.recover_vncpassword(unhexlify(password)).decode("latin-1")
cred["server"] = reg.getValue(ntpath.join(registry_path, registry_keys[2]))[1].decode("latin-1")
self.context.log.highlight(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}")
-1
View File
@@ -24,7 +24,6 @@ class NXCModule:
self.pvkbytes = get_domain_backup_key(connection)
target = Target.create(
domain=connection.domain,
username=username,
+2 -2
View File
@@ -306,7 +306,7 @@ class HostChecker:
value = self.reg_query_value(self.dce, self.connection, key, value_name)
if type(value) == DCERPCSessionError:
if isinstance(value, DCERPCSessionError):
if options["KOIfMissing"]:
ok = False
if value.error_code in (ERROR_NO_MORE_ITEMS, ERROR_FILE_NOT_FOUND):
@@ -462,7 +462,7 @@ class HostChecker:
nbtns_enabled = 0
for subkey in subkeys:
value = self.reg_query_value(self.dce, self.connection, key_name + "\\" + subkey, "NetbiosOptions")
if type(value) == DCERPCSessionError:
if isinstance(value, DCERPCSessionError):
if value.error_code == ERROR_OBJECT_NOT_FOUND:
missing += 1
continue
-1
View File
@@ -15,7 +15,6 @@ import re
import configparser
class NXCModule:
"""Module by @NeffIsBack"""
+4 -4
View File
@@ -40,7 +40,7 @@ if platform.system() != "Windows":
resource.setrlimit(resource.RLIMIT_NOFILE, file_limit)
async def start_run(protocol_obj, args, db, targets):
async def start_run(protocol_obj, args, db, targets): # noqa: RUF029
futures = []
nxc_logger.debug("Creating ThreadPoolExecutor")
if args.no_progress or len(targets) == 1:
@@ -58,7 +58,7 @@ async def start_run(protocol_obj, args, db, targets):
nxc_logger.debug(f"Creating thread for {protocol_obj}")
futures = [executor.submit(protocol_obj, args, db, target) for target in targets]
for _ in as_completed(futures):
current += 1
current += 1 # noqa: SIM113
progress.update(tasks, completed=current)
for future in as_completed(futures):
try:
@@ -102,8 +102,8 @@ def main():
start_id, end_id = cred_id.split("-")
try:
for n in range(int(start_id), int(end_id) + 1):
args.cred_id.append(n)
args.cred_id.remove(cred_id)
args.cred_id.append(n) # noqa: B909
args.cred_id.remove(cred_id) # noqa: B909
except Exception as e:
nxc_logger.error(f"Error parsing database credential id: {e}")
exit(1)
+1 -3
View File
@@ -47,8 +47,7 @@ def write_csv(filename, headers, entries):
def write_list(filename, entries):
"""Writes a file with a simple list"""
with open(os.path.expanduser(filename), "w") as export_file:
for line in entries:
export_file.write(line + "\n")
export_file.writelines(line + "\n" for line in entries)
def complete_import(text, line):
@@ -516,7 +515,6 @@ class NXCDBMenu(cmd.Cmd):
def do_EOF(line):
sys.exit()
@staticmethod
def help_exit():
help_string = """
+1
View File
@@ -5,6 +5,7 @@ from nxc.helpers.logger import highlight
from nxc.logger import NXCAdapter
from ftplib import FTP, error_perm
class ftp(connection):
def __init__(self, args, db, host):
self.protocol = "FTP"
+67 -23
View File
@@ -6,7 +6,7 @@ import os
from errno import EHOSTUNREACH, ETIMEDOUT, ENETUNREACH
from binascii import hexlify
from datetime import datetime
from re import sub, I
from re import sub, IGNORECASE
from zipfile import ZipFile
from termcolor import colored
from dns import resolver
@@ -43,6 +43,7 @@ from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB
from nxc.protocols.ldap.kerberos import KerberosAttacks
from nxc.parsers.ldap_results import parse_result_attributes
from nxc.helpers.ntlm_parser import parse_challenge
from nxc.helpers.misc import get_bloodhound_info
ldap_error_status = {
"1": "STATUS_NOT_SUPPORTED",
@@ -151,6 +152,7 @@ class ldap(connection):
self.admin_privs = False
self.no_ntlm = False
self.sid_domain = ""
self.scope = None
connection.__init__(self, args, db, host)
@@ -173,7 +175,7 @@ class ldap(connection):
ldap_url = f"{proto}://{self.host}"
self.logger.info(f"Connecting to {ldap_url} with no baseDN")
try:
self.ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host, signing=False)
self.ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host)
if self.ldap_connection:
self.logger.debug(f"ldap_connection: {self.ldap_connection}")
except SysCallError as e:
@@ -195,10 +197,10 @@ class ldap(connection):
target = resp_parsed["dnsHostName"]
base_dn = resp_parsed["defaultNamingContext"]
target_domain = sub(
",DC=",
r",DC=",
".",
base_dn[base_dn.lower().find("dc="):],
flags=I,
flags=IGNORECASE,
)[3:]
except ConnectionRefusedError as e:
self.logger.debug(f"{e} on host {self.host}")
@@ -249,6 +251,8 @@ class ldap(connection):
if ntlm_challenge:
ntlm_info = parse_challenge(ntlm_challenge)
self.server_os = ntlm_info["os_version"]
else:
self.no_ntlm = True
if self.args.domain:
self.domain = self.args.domain
@@ -322,7 +326,7 @@ class ldap(connection):
proto = "ldaps" if self.port == 636 else "ldap"
ldap_url = f"{proto}://{self.target}"
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [1]")
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, signing=False)
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
self.ldap_connection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
if self.username == "":
self.username = self.get_ldap_username()
@@ -361,7 +365,7 @@ class ldap(connection):
return False
except (KeyError, KerberosException, OSError) as e:
self.logger.fail(
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (process_secret(kerb_pass))} {e!s}",
f"{self.domain}\\{self.username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} {e!s}",
color="red",
)
return False
@@ -372,9 +376,10 @@ class ldap(connection):
# Connect to LDAPS
self.logger.extra["protocol"] = "LDAPS"
self.logger.extra["port"] = "636"
self.port = 636
ldaps_url = f"ldaps://{self.target}"
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [2]")
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host, signing=False)
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.ldap_connection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
if self.username == "":
self.username = self.get_ldap_username()
@@ -399,21 +404,21 @@ class ldap(connection):
except SessionError as e:
error, desc = e.getErrorString()
self.logger.fail(
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (process_secret(kerb_pass))} {error!s}",
f"{self.domain}\\{self.username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} {error!s}",
color="magenta" if error in ldap_error_status else "red",
)
return False
except Exception as e:
error_code = str(e).split()[-2][:-1]
self.logger.fail(
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status.get(error_code, '')}",
color="magenta" if error_code in ldap_error_status else "red",
)
return False
else:
error_code = str(e).split()[-2][:-1]
self.logger.fail(
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (process_secret(kerb_pass))} {error_code!s}",
f"{self.domain}\\{self.username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} {error_code!s}",
color="magenta" if error_code in ldap_error_status else "red",
)
return False
@@ -438,7 +443,7 @@ class ldap(connection):
proto = "ldaps" if self.port == 636 else "ldap"
ldap_url = f"{proto}://{self.target}"
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [3]")
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, signing=False)
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.check_if_admin()
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}")
@@ -459,9 +464,10 @@ class ldap(connection):
# Connect to LDAPS
self.logger.extra["protocol"] = "LDAPS"
self.logger.extra["port"] = "636"
self.port = 636
ldaps_url = f"ldaps://{self.target}"
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [4]")
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host, signing=False)
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.check_if_admin()
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}")
@@ -478,13 +484,13 @@ class ldap(connection):
except Exception as e:
error_code = str(e).split()[-2][:-1]
self.logger.fail(
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status.get(error_code, '')}",
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
)
else:
error_code = str(e).split()[-2][:-1]
self.logger.fail(
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status.get(error_code, '')}",
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
)
return False
@@ -528,7 +534,7 @@ class ldap(connection):
proto = "ldaps" if self.port == 636 else "ldap"
ldaps_url = f"{proto}://{self.target}"
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}")
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host, signing=False)
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.check_if_admin()
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.hash}")
@@ -549,9 +555,10 @@ class ldap(connection):
# We need to try SSL
self.logger.extra["protocol"] = "LDAPS"
self.logger.extra["port"] = "636"
self.port = 636
ldaps_url = f"ldaps://{self.target}"
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}")
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host, signing=False)
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.check_if_admin()
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.hash}")
@@ -569,13 +576,13 @@ class ldap(connection):
except ldap_impacket.LDAPSessionError as e:
error_code = str(e).split()[-2][:-1]
self.logger.fail(
f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status.get(error_code, '')}",
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
)
else:
error_code = str(e).split()[-2][:-1]
self.logger.fail(
f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status.get(error_code, '')}",
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
)
return False
@@ -623,7 +630,7 @@ class ldap(connection):
return t
def search(self, searchFilter, attributes, sizeLimit=0, baseDN=None) -> list:
if baseDN is None and self.args.base_dn:
if baseDN is None and self.args.base_dn is not None:
baseDN = self.args.base_dn
elif baseDN is None:
baseDN = self.baseDN
@@ -633,19 +640,24 @@ class ldap(connection):
self.logger.debug(f"Search Filter={searchFilter}")
# Microsoft Active Directory set an hard limit of 1000 entries returned by any search
paged_search_control = ldapasn1_impacket.SimplePagedResultsControl(criticality=True, size=1000)
paged_search_control = [ldapasn1_impacket.SimplePagedResultsControl(criticality=True, size=1000)] if not self.no_ntlm else ""
return self.ldap_connection.search(
scope=self.scope,
searchBase=baseDN,
searchFilter=searchFilter,
attributes=attributes,
sizeLimit=sizeLimit,
searchControls=[paged_search_control],
searchControls=paged_search_control,
)
except ldap_impacket.LDAPSearchError as e:
if e.getErrorString().find("sizeLimitExceeded") >= 0:
if "sizeLimitExceeded" in str(e):
# We should never reach this code as we use paged search now
self.logger.fail("sizeLimitExceeded exception caught, giving up and processing the data received")
e.getAnswers()
# if empty username and password is possible that we need to change the scope, we try with a baseObject before returning a fail
elif "operationsError" in str(e) and self.scope is None and self.username == "" and self.password == "":
self.scope = ldapasn1_impacket.Scope("baseObject")
return self.search(searchFilter, attributes, sizeLimit, baseDN)
else:
self.logger.fail(e)
return []
@@ -1208,6 +1220,38 @@ class ldap(connection):
self.logger.fail("No string provided :'(")
def bloodhound(self):
# Check which version is desired
use_bhce = self.config.getboolean("BloodHound-CE", "bhce_enabled", fallback=False)
package_name, version, is_ce = get_bloodhound_info()
if use_bhce and not is_ce:
self.logger.fail("⚠️ Configuration Issue Detected ⚠️")
self.logger.fail("Your configuration has BloodHound-CE enabled, but the regular BloodHound package is installed. Modify your ~/.nxc/nxc.conf config file or follow the instructions:")
self.logger.fail("Please run the following commands to fix this:")
self.logger.fail("poetry remove bloodhound-ce # poetry falsely recognizes bloodhound-ce as a the old bloodhound package")
self.logger.fail("poetry add bloodhound-ce")
self.logger.fail("")
# If using pipx
self.logger.fail("Or if you installed with pipx:")
self.logger.fail("pipx runpip netexec uninstall -y bloodhound")
self.logger.fail("pipx inject netexec bloodhound-ce --force")
return False
elif not use_bhce and is_ce:
self.logger.fail("⚠️ Configuration Issue Detected ⚠️")
self.logger.fail("Your configuration has regular BloodHound enabled, but the BloodHound-CE package is installed.")
self.logger.fail("Please run the following commands to fix this:")
self.logger.fail("poetry remove bloodhound-ce")
self.logger.fail("poetry add bloodhound")
self.logger.fail("")
# If using pipx
self.logger.fail("Or if you installed with pipx:")
self.logger.fail("pipx runpip netexec uninstall -y bloodhound-ce")
self.logger.fail("pipx inject netexec bloodhound --force")
return False
auth = ADAuthentication(
username=self.username,
password=self.password,
@@ -1227,7 +1271,7 @@ class ldap(connection):
)
collect = resolve_collection_methods("Default" if not self.args.collection else self.args.collection)
if not collect:
return
return None
self.logger.highlight("Resolved collection methods: " + ", ".join(list(collect)))
self.logger.debug("Using DNS to retrieve domain information")
+1 -1
View File
@@ -107,4 +107,4 @@ class BloodHound:
computer_enum.enumerate_computers(self.ad.computers, num_workers=num_workers, timestamp=timestamp, fileNamePrefix=fileNamePrefix)
end_time = time.time()
minutes, seconds = divmod(int(end_time - start_time), 60)
self.logger.highlight("Done in %02dM %02dS" % (minutes, seconds))
self.logger.highlight(f"Done in {minutes}M {seconds}S")
+4 -4
View File
@@ -64,7 +64,7 @@ class KerberosAttacks:
# last 12 bytes of the encrypted ticket represent the checksum of the decrypted
# ticket
if decoded_tgs["ticket"]["enc-part"]["etype"] == constants.EncryptionTypes.rc4_hmac.value:
entry = "$krb5tgs$%d$*%s$%s$%s*$%s$%s" % (
entry = "$krb5tgs${}$*{}${}${}*${}${}".format(
constants.EncryptionTypes.rc4_hmac.value,
username,
decoded_tgs["ticket"]["realm"],
@@ -73,7 +73,7 @@ class KerberosAttacks:
hexlify(decoded_tgs["ticket"]["enc-part"]["cipher"][16:].asOctets()).decode(),
)
elif decoded_tgs["ticket"]["enc-part"]["etype"] == constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value:
entry = "$krb5tgs$%d$%s$%s$*%s*$%s$%s" % (
entry = "$krb5tgs${}${}${}$*{}*${}${}".format(
constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value,
username,
decoded_tgs["ticket"]["realm"],
@@ -82,7 +82,7 @@ class KerberosAttacks:
hexlify(decoded_tgs["ticket"]["enc-part"]["cipher"][:-12:].asOctets()).decode,
)
elif decoded_tgs["ticket"]["enc-part"]["etype"] == constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value:
entry = "$krb5tgs$%d$%s$%s$*%s*$%s$%s" % (
entry = "$krb5tgs${}${}${}$*{}*${}${}".format(
constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value,
username,
decoded_tgs["ticket"]["realm"],
@@ -91,7 +91,7 @@ class KerberosAttacks:
hexlify(decoded_tgs["ticket"]["enc-part"]["cipher"][:-12:].asOctets()).decode(),
)
elif decoded_tgs["ticket"]["enc-part"]["etype"] == constants.EncryptionTypes.des_cbc_md5.value:
entry = "$krb5tgs$%d$*%s$%s$%s*$%s$%s" % (
entry = "$krb5tgs${}$*{}${}${}*${}${}".format(
constants.EncryptionTypes.des_cbc_md5.value,
username,
decoded_tgs["ticket"]["realm"],
+4 -4
View File
@@ -95,13 +95,13 @@ class LDAPConnect:
except ldap_impacket.LDAPSessionError as e:
error_code = str(e).split()[-2][:-1]
self.logger.fail(
f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status.get(error_code, '')}",
color="magenta" if error_code in ldap_error_status else "red",
)
else:
error_code = str(e).split()[-2][:-1]
self.logger.fail(
f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status.get(error_code, '')}",
color="magenta" if error_code in ldap_error_status else "red",
)
return False
@@ -152,13 +152,13 @@ class LDAPConnect:
except ldap_impacket.LDAPSessionError as e:
error_code = str(e).split()[-2][:-1]
self.logger.fail(
f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status.get(error_code, '')}",
color="magenta" if error_code in ldap_error_status else "red",
)
else:
error_code = str(e).split()[-2][:-1]
self.logger.fail(
f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status.get(error_code, '')}",
color="magenta" if error_code in ldap_error_status else "red",
)
return False
+64
View File
@@ -0,0 +1,64 @@
from re import sub, I
from errno import EHOSTUNREACH, ETIMEDOUT, ENETUNREACH
from OpenSSL.SSL import SysCallError
from impacket.ldap import ldap as ldap_impacket
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from nxc.parsers.ldap_results import parse_result_attributes
from nxc.logger import nxc_logger
class LDAPResolution:
def __init__(self, host):
self.host = host
def get_resolution(self):
target = ""
target_domain = ""
base_dn = ""
try:
ldap_url = f"ldap://{self.host}"
nxc_logger.info(f"Connecting to {ldap_url} with no baseDN")
try:
self.ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host)
if self.ldap_connection:
nxc_logger.debug(f"ldap_connection: {self.ldap_connection}")
except SysCallError as e:
nxc_logger.fail(f"LDAP connection to {ldap_url} failed: {e}")
return False
resp = self.ldap_connection.search(
scope=ldapasn1_impacket.Scope("baseObject"),
attributes=["defaultNamingContext", "dnsHostName"],
sizeLimit=0,
)
resp_parsed = parse_result_attributes(resp)[0]
target = resp_parsed["dnsHostName"]
base_dn = resp_parsed["defaultNamingContext"]
target_domain = sub(
r",DC=",
".",
base_dn[base_dn.lower().find("dc="):],
flags=I,
)[3:]
# Extract machine name from target (hostname part of FQDN)
if target:
machine_name = target.split(".")[0]
nxc_logger.debug(f"Extracted machine name: {machine_name}")
self.ldap_connection.close()
except ConnectionRefusedError as e:
nxc_logger.debug(f"{e} on host {self.host}")
return False
except OSError as e:
if e.errno in (EHOSTUNREACH, ENETUNREACH, ETIMEDOUT):
nxc_logger.info(f"Error connecting to {self.host} - {e}")
return False
else:
nxc_logger.error(f"Error getting ldap info {e}")
nxc_logger.debug(f"Target: {machine_name}.{target_domain}; target_domain: {target_domain}; base_dn: {base_dn}")
return machine_name, target_domain
+1 -3
View File
@@ -65,9 +65,7 @@ class MSSQLEXEC:
result = self.mssql_conn.sql_query(query)
# Assuming the query returns a list of dictionaries with 'config_value' as the key
self.logger.debug(f"{option} check result: {result}")
if result and result[0]["config_value"] == 1:
return True
return False
return bool(result and result[0]["config_value"] == 1)
def put_file(self, data, remote):
try:
+3 -3
View File
@@ -562,8 +562,8 @@ class nfs(connection):
# Format for the file id see: https://elixir.bootlin.com/linux/v6.13.4/source/include/linux/exportfs.h#L25
fh = bytearray(mount_fh)
if filesystem in [FileID.ext, FileID.unknown]:
root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x02\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x02\x00\x00\x00")) # noqa: E226 FURB113
root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x80\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x80\x00\x00\x00")) # noqa: E226
root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x02\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x02\x00\x00\x00")) # noqa: E226
root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x80\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x80\x00\x00\x00")) # noqa: E226
if filesystem in [FileID.btrfs, FileID.unknown]:
# Iterate over btrfs subvolumes, use 16 as default similar to the guys from nfs-security-tooling
for i in range(16):
@@ -728,7 +728,7 @@ def convert_size(size_bytes):
if size_bytes == 0:
return "0B"
size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
i = int(math.floor(math.log(size_bytes, 1024)))
i = math.floor(math.log(size_bytes, 1024))
p = math.pow(1024, i)
s = round(size_bytes / p, 1)
return f"{s}{size_name[i]}"
+47 -31
View File
@@ -55,6 +55,8 @@ 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.powershell import create_ps_command
from nxc.helpers.misc import detect_if_ip
from nxc.protocols.ldap.resolution import LDAPResolution
from dploot.triage.vaults import VaultsTriage
from dploot.triage.browser import BrowserTriage, LoginData, GoogleRefreshToken, Cookie
@@ -65,7 +67,6 @@ from dploot.triage.sccm import SCCMTriage, SCCMCred, SCCMSecret, SCCMCollection
from time import time, ctime
from datetime import datetime
from traceback import format_exc
import logging
from termcolor import colored
import contextlib
@@ -100,6 +101,7 @@ def get_error_string(exception):
else:
return str(exception)
class smb(connection):
def __init__(self, args, db, host):
self.domain = None
@@ -125,6 +127,7 @@ class smb(connection):
self.no_ntlm = False
self.protocol = "SMB"
self.is_guest = None
self.isdc = False
connection.__init__(self, args, db, host)
@@ -185,13 +188,19 @@ class smb(connection):
if not self.targetDomain: # Not sure if that can even happen but now we are safe
self.targetDomain = self.hostname
else:
# If we can't authenticate with NTLM and the target is supplied as a FQDN we must parse it
try:
import socket
socket.inet_aton(self.host)
self.logger.debug("NTLM authentication not available! Authentication will fail without a valid hostname and domain name")
self.hostname = self.host
self.targetDomain = self.host
# If we know the host is a DC we can still get the hostname over LDAP if NTLM is not available
if self.is_host_dc() and detect_if_ip(self.host):
self.hostname, self.domain = LDAPResolution(self.host).get_resolution()
self.targetDomain = self.domain
# If we can't authenticate with NTLM and the target is supplied as a FQDN we must parse it
else:
# Check if the host is a valid IP address, if not we parse the FQDN in the Exception
import socket
socket.inet_aton(self.host)
self.logger.debug("NTLM authentication not available! Authentication will fail without a valid hostname and domain name")
self.hostname = self.host
self.targetDomain = self.host
except OSError:
if self.host.count(".") >= 1:
self.hostname = self.host.split(".")[0]
@@ -199,6 +208,10 @@ class smb(connection):
else:
self.hostname = self.host
self.targetDomain = self.host
except Exception as e:
self.logger.debug(f"Error getting hostname from LDAP: {e}")
self.hostname = self.host
self.targetDomain = self.host
if self.args.domain:
self.domain = self.args.domain
@@ -283,21 +296,12 @@ class smb(connection):
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1}) {ntlm}")
if self.args.generate_hosts_file or self.args.generate_krb5_file:
from impacket.dcerpc.v5 import nrpc, epm
self.logger.debug("Performing authentication attempts...")
isdc = False
try:
epm.hept_map(self.host, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp")
isdc = True
except DCERPCException:
self.logger.debug("Error while connecting to host: DCERPCException, which means this is probably not a DC!")
if self.args.generate_hosts_file:
with open(self.args.generate_hosts_file, "a+") as host_file:
dc_part = f" {self.targetDomain}" if isdc else ""
dc_part = f" {self.targetDomain}" if self.isdc else ""
host_file.write(f"{self.host} {self.hostname}.{self.targetDomain}{dc_part} {self.hostname}\n")
self.logger.debug(f"{self.host} {self.hostname}.{self.targetDomain}{dc_part} {self.hostname}")
elif self.args.generate_krb5_file and isdc:
self.logger.debug(f"Line added to {self.args.generate_hosts_file} {self.host} {self.hostname}.{self.targetDomain}{dc_part} {self.hostname}")
elif self.args.generate_krb5_file and self.isdc:
with open(self.args.generate_krb5_file, "w+") as host_file:
data = f"""
[libdefaults]
@@ -658,6 +662,18 @@ class smb(connection):
except Exception as e:
self.logger.fail(f"Failed to get TGT: {e}")
def is_host_dc(self):
from impacket.dcerpc.v5 import nrpc, epm
self.logger.debug("Performing authentication attempts...")
try:
epm.hept_map(self.host, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp")
self.isdc = True
return True
except DCERPCException:
self.logger.debug("Error while connecting to host: DCERPCException, which means this is probably not a DC!")
self.isdc = False
return False
@requires_admin
def execute(self, payload=None, get_output=False, methods=None) -> str:
"""
@@ -920,19 +936,19 @@ class smb(connection):
return
self.enumerate_sessions_info(sessions)
maxSessionNameLen = max([len(sessions[i]["SessionName"]) + 1 for i in sessions])
maxSessionNameLen = max(len(sessions[i]["SessionName"]) + 1 for i in sessions)
maxSessionNameLen = maxSessionNameLen if len("SESSIONNAME") < maxSessionNameLen else len("SESSIONNAME") + 1
maxUsernameLen = max([len(sessions[i]["Username"] + sessions[i]["Domain"]) + 1 for i in sessions]) + 1
maxUsernameLen = max(len(sessions[i]["Username"] + sessions[i]["Domain"]) + 1 for i in sessions) + 1
maxUsernameLen = maxUsernameLen if len("Username") < maxUsernameLen else len("Username") + 1
maxIdLen = max([len(str(i)) for i in sessions])
maxIdLen = max(len(str(i)) for i in sessions)
maxIdLen = maxIdLen if len("ID") < maxIdLen else len("ID") + 1
maxStateLen = max([len(sessions[i]["state"]) + 1 for i in sessions])
maxStateLen = max(len(sessions[i]["state"]) + 1 for i in sessions)
maxStateLen = maxStateLen if len("STATE") < maxStateLen else len("STATE") + 1
maxRemoteIp = max([len(sessions[i]["RemoteIp"]) + 1 for i in sessions])
maxRemoteIp = max(len(sessions[i]["RemoteIp"]) + 1 for i in sessions)
maxRemoteIp = maxRemoteIp if len("RemoteAddress") < maxRemoteIp else len("RemoteAddress") + 1
maxClientName = max([len(sessions[i]["ClientName"]) + 1 for i in sessions])
maxClientName = max(len(sessions[i]["ClientName"]) + 1 for i in sessions)
maxClientName = maxClientName if len("ClientName") < maxClientName else len("ClientName") + 1
template = ("{SESSIONNAME: <%d} "
template = ("{SESSIONNAME: <%d} " # noqa: UP031
"{USERNAME: <%d} "
"{ID: <%d} "
"{IPv4: <16} "
@@ -1001,9 +1017,9 @@ class smb(connection):
if not res:
return
self.logger.success("Enumerated processes")
maxImageNameLen = max([len(i["ImageName"]) for i in res])
maxSidLen = max([len(i["pSid"]) for i in res])
template = "{: <%d} {: <8} {: <11} {: <%d} {: >12}" % (maxImageNameLen, maxSidLen)
maxImageNameLen = max(len(i["ImageName"]) for i in res)
maxSidLen = max(len(i["pSid"]) for i in res)
template = "{: <%d} {: <8} {: <11} {: <%d} {: >12}" % (maxImageNameLen, maxSidLen) # noqa: UP031
self.logger.highlight(template.format("Image Name", "PID", "Session#", "SID", "Mem Usage"))
self.logger.highlight(template.replace(": ", ":=").format("", "", "", "", ""))
for procInfo in res:
@@ -1136,7 +1152,7 @@ class smb(connection):
self.logger.highlight(f"{name:<15} {','.join(perms):<15} {remark}")
return permissions
def dir(self): # noqa: A003
def dir(self):
search_path = ntpath.join(self.args.dir, "*")
try:
contents = self.conn.listPath(self.args.share, search_path)
@@ -1420,7 +1436,7 @@ class smb(connection):
try:
string_binding = KNOWN_PROTOCOLS[self.port]["bindstr"]
logging.debug(f"StringBinding {string_binding}")
self.logger.debug(f"StringBinding {string_binding}")
rpc_transport = transport.DCERPCTransportFactory(string_binding)
rpc_transport.setRemoteHost(self.host)
+1 -1
View File
@@ -37,7 +37,7 @@ class TSCH_EXEC:
if self.__password is None:
self.__password = ""
stringbinding = r"ncacn_np:%s[\pipe\atsvc]" % self.__target
stringbinding = rf"ncacn_np:{self.__target}[\pipe\atsvc]"
self.__rpctransport = transport.DCERPCTransportFactory(stringbinding)
self.__rpctransport.setRemoteHost(self.__remoteHost)
+2
View File
@@ -45,6 +45,7 @@ def get_domain_backup_key(context):
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 = {}
@@ -85,6 +86,7 @@ def collect_masterkeys_from_target(context, target, dploot_connection, user=True
return masterkeys
def upgrade_to_dploot_connection(target, connection=None):
conn = None
try:
+23 -21
View File
@@ -18,12 +18,13 @@ from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
CKA_ID = unhexlify("f8000000000000000000000000000001")
@dataclass
class FirefoxData:
def __init__(self, winuser: str, url: str, username: str, password: str):
self.winuser = winuser
self.url = url
self.username = username
self.password = password
winuser: str
url: str
username: str
password: str
@dataclass
class FirefoxCookie:
@@ -36,6 +37,7 @@ class FirefoxCookie:
expires_utc: str
last_access_utc: str
class FirefoxTriage:
"""
Firefox by @zblurx
@@ -110,11 +112,11 @@ class FirefoxTriage:
password = self.decrypt(key=key, iv=pwd[1], ciphertext=pwd[2]).decode("utf-8")
if password is not None and decoded_username is not None:
data = FirefoxData(
winuser=user,
url=host,
username=decoded_username,
password=password,
)
winuser=user,
url=host,
username=decoded_username,
password=password,
)
if self.per_secret_callback is not None:
self.per_secret_callback(data)
firefox_data.append(data)
@@ -126,7 +128,7 @@ class FirefoxTriage:
def parse_cookie_data(self, windows_user, cookies_data):
cookies = []
fh = tempfile.NamedTemporaryFile(delete=False)
fh = tempfile.NamedTemporaryFile(delete=False) # noqa: SIM115
fh.write(cookies_data)
fh.seek(0)
db = sqlite3.connect(fh.name)
@@ -134,15 +136,15 @@ class FirefoxTriage:
cursor.execute("SELECT name, value, host, path, expiry, lastAccessed, creationTime FROM moz_cookies;")
for name, value, host, path, expiry, lastAccessed, creationTime in cursor:
cookie = FirefoxCookie(
winuser=windows_user,
host=host,
path=path,
cookie_name=name,
cookie_value=value,
creation_utc=creationTime,
last_access_utc=lastAccessed,
expires_utc=expiry,
)
winuser=windows_user,
host=host,
path=path,
cookie_name=name,
cookie_value=value,
creation_utc=creationTime,
last_access_utc=lastAccessed,
expires_utc=expiry,
)
if self.per_secret_callback is not None:
self.per_secret_callback(cookie)
cookies.append(cookie)
@@ -165,7 +167,7 @@ class FirefoxTriage:
# Instead of disabling "delete" and removing the file manually,
# in the future (py3.12) we could use "delete_on_close=False" as a cleaner solution
# Related issue: #134
fh = tempfile.NamedTemporaryFile(delete=False)
fh = tempfile.NamedTemporaryFile(delete=False) # noqa: SIM115
fh.write(key4_data)
fh.seek(0)
db = sqlite3.connect(fh.name)
+1 -1
View File
@@ -23,7 +23,7 @@ def convert(low, high, lockout=False):
time = ""
tmp = 0
if low == 0 and high == -0x8000_0000 or low == 0 and high == -0x8000_0000_0000_0000:
if (low == 0 and high == -0x8000_0000) or (low == 0 and high == -0x8000_0000_0000_0000):
return "Not Set"
if low == 0 and high == 0:
return "None"
+1
View File
@@ -97,6 +97,7 @@ def proto_args(parser, parents):
return parser
def get_conditional_action(baseAction):
class ConditionalAction(baseAction):
def __init__(self, option_strings, dest, **kwargs):
+3 -5
View File
@@ -2,8 +2,6 @@
# Which in turn stole from Impacket :)
# Code refactored and added to by @mjhallenbeck (Marshall-Hallenbeck on GitHub)
import logging
from impacket.dcerpc.v5 import transport, lsat, lsad, samr
from impacket.dcerpc.v5.dtypes import MAXIMUM_ALLOWED
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
@@ -43,7 +41,7 @@ class SamrFunc:
domains = self.samr_query.get_domains()
members = {}
if "Builtin" not in domains:
logging.error("No Builtin group to query locally on")
self.logger.error("No Builtin group to query locally on")
return None
domain_handle = self.samr_query.get_domain_handle("Builtin")
@@ -128,10 +126,10 @@ class SAMRQuery:
dce.connect()
dce.bind(samr.MSRPC_UUID_SAMR)
except NetBIOSError as e:
logging.error(f"NetBIOSError on Connection: {e}")
self.logger.error(f"NetBIOSError on Connection: {e}")
return None
except SessionError as e:
logging.error(f"SessionError on Connection: {e}")
self.logger.error(f"SessionError on Connection: {e}")
return None
return dce
+1
View File
@@ -23,6 +23,7 @@ def proto_args(parser, parents):
return parser
def get_conditional_action(baseAction):
class ConditionalAction(baseAction):
def __init__(self, option_strings, dest, **kwargs):
Generated
+29 -28
View File
@@ -335,15 +335,15 @@ files = [
]
[[package]]
name = "bloodhound"
name = "bloodhound-ce"
version = "1.8.0"
description = "Python based ingestor for BloodHound"
description = "Python based ingestor for BloodHound Community Edition"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "bloodhound-1.8.0-py3-none-any.whl", hash = "sha256:97dcef77fa38dbab7219909c117eb9fd7263aff107cee0bf6fc7a0d0db9a61ac"},
{file = "bloodhound-1.8.0.tar.gz", hash = "sha256:35ed0f1fdda2b1d79a4e9d891cabe2c55309a32743aeed16d885f3d809f409b3"},
{file = "bloodhound_ce-1.8.0-py3-none-any.whl", hash = "sha256:0d5f39c2ab157448313f6c0ea8afdcf081238682f445c81e065684395ba5484b"},
{file = "bloodhound_ce-1.8.0.tar.gz", hash = "sha256:f663d6181e2a1ab8de9d57948011662e2a47880d8caca9b85369a7efaed13a70"},
]
[package.dependencies]
@@ -865,7 +865,7 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2
[[package]]
name = "impacket"
version = "0.13.0.dev0+20250422.104055.27bebb13"
version = "0.13.0.dev0+20250513.162347.b7288f23"
description = "Network protocols Constructors and Dissectors"
optional = false
python-versions = "*"
@@ -888,9 +888,9 @@ six = "*"
[package.source]
type = "git"
url = "https://github.com/fortra/impacket.git"
reference = "HEAD"
resolved_reference = "27bebb1347569fa810e432326266acf17560f274"
url = "https://github.com/zblurx/impacket.git"
reference = "ldap_signing"
resolved_reference = "b7288f233c154b6f73610797f8e0fa3a1541b3a9"
[[package]]
name = "iniconfig"
@@ -2018,29 +2018,30 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruff"
version = "0.0.292"
description = "An extremely fast Python linter, written in Rust."
version = "0.11.3"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "ruff-0.0.292-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:02f29db018c9d474270c704e6c6b13b18ed0ecac82761e4fcf0faa3728430c96"},
{file = "ruff-0.0.292-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:69654e564342f507edfa09ee6897883ca76e331d4bbc3676d8a8403838e9fade"},
{file = "ruff-0.0.292-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c3c91859a9b845c33778f11902e7b26440d64b9d5110edd4e4fa1726c41e0a4"},
{file = "ruff-0.0.292-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4476f1243af2d8c29da5f235c13dca52177117935e1f9393f9d90f9833f69e4"},
{file = "ruff-0.0.292-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be8eb50eaf8648070b8e58ece8e69c9322d34afe367eec4210fdee9a555e4ca7"},
{file = "ruff-0.0.292-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9889bac18a0c07018aac75ef6c1e6511d8411724d67cb879103b01758e110a81"},
{file = "ruff-0.0.292-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bdfabd4334684a4418b99b3118793f2c13bb67bf1540a769d7816410402a205"},
{file = "ruff-0.0.292-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa7c77c53bfcd75dbcd4d1f42d6cabf2485d2e1ee0678da850f08e1ab13081a8"},
{file = "ruff-0.0.292-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e087b24d0d849c5c81516ec740bf4fd48bf363cfb104545464e0fca749b6af9"},
{file = "ruff-0.0.292-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f160b5ec26be32362d0774964e218f3fcf0a7da299f7e220ef45ae9e3e67101a"},
{file = "ruff-0.0.292-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ac153eee6dd4444501c4bb92bff866491d4bfb01ce26dd2fff7ca472c8df9ad0"},
{file = "ruff-0.0.292-py3-none-musllinux_1_2_i686.whl", hash = "sha256:87616771e72820800b8faea82edd858324b29bb99a920d6aa3d3949dd3f88fb0"},
{file = "ruff-0.0.292-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b76deb3bdbea2ef97db286cf953488745dd6424c122d275f05836c53f62d4016"},
{file = "ruff-0.0.292-py3-none-win32.whl", hash = "sha256:e854b05408f7a8033a027e4b1c7f9889563dd2aca545d13d06711e5c39c3d003"},
{file = "ruff-0.0.292-py3-none-win_amd64.whl", hash = "sha256:f27282bedfd04d4c3492e5c3398360c9d86a295be00eccc63914438b4ac8a83c"},
{file = "ruff-0.0.292-py3-none-win_arm64.whl", hash = "sha256:7f67a69c8f12fbc8daf6ae6d36705037bde315abf8b82b6e1f4c9e74eb750f68"},
{file = "ruff-0.0.292.tar.gz", hash = "sha256:1093449e37dd1e9b813798f6ad70932b57cf614e5c2b5c51005bf67d55db33ac"},
{file = "ruff-0.11.3-py3-none-linux_armv6l.whl", hash = "sha256:cb893a5eedff45071d52565300a20cd4ac088869e156b25e0971cb98c06f5dd7"},
{file = "ruff-0.11.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:58edd48af0e201e2f494789de80f5b2f2b46c9a2991a12ea031254865d5f6aa3"},
{file = "ruff-0.11.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:520f6ade25cea98b2e5cb29eb0906f6a0339c6b8e28a024583b867f48295f1ed"},
{file = "ruff-0.11.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ca4405a93ebbc05e924358f872efceb1498c3d52a989ddf9476712a5480b16"},
{file = "ruff-0.11.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4341d38775a6be605ce7cd50e951b89de65cbd40acb0399f95b8e1524d604c8"},
{file = "ruff-0.11.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72bf5b49e4b546f4bea6c05448ab71919b09cf75363adf5e3bf5276124afd31c"},
{file = "ruff-0.11.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9fa791ee6c3629ba7f9ba2c8f2e76178b03f3eaefb920e426302115259819237"},
{file = "ruff-0.11.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c81d3fe718f4d303aaa4ccdcd0f43e23bb2127da3353635f718394ca9b26721"},
{file = "ruff-0.11.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4c38e9b6c01caaba46b6d8e732791f4c78389a9923319991d55b298017ce02"},
{file = "ruff-0.11.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9686f5d1a2b4c918b5a6e9876bfe7f47498a990076624d41f57d17aadd02a4dd"},
{file = "ruff-0.11.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4800ddc4764d42d8961ce4cb972bcf5cc2730d11cca3f11f240d9f7360460408"},
{file = "ruff-0.11.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e63a2808879361aa9597d88d86380d8fb934953ef91f5ff3dafe18d9cb0b1e14"},
{file = "ruff-0.11.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8f8b1c4ae62638cc220df440140c21469232d8f2cb7f5059f395f7f48dcdb59e"},
{file = "ruff-0.11.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3ea2026be50f6b1fbedd2d1757d004e1e58bd0f414efa2a6fa01235468d4c82a"},
{file = "ruff-0.11.3-py3-none-win32.whl", hash = "sha256:73d8b90d12674a0c6e98cd9e235f2dcad09d1a80e559a585eac994bb536917a3"},
{file = "ruff-0.11.3-py3-none-win_amd64.whl", hash = "sha256:faf1bfb0a51fb3a82aa1112cb03658796acef978e37c7f807d3ecc50b52ecbf6"},
{file = "ruff-0.11.3-py3-none-win_arm64.whl", hash = "sha256:67f8b68d7ab909f08af1fb601696925a89d65083ae2bb3ab286e572b5dc456aa"},
{file = "ruff-0.11.3.tar.gz", hash = "sha256:8d5fcdb3bb359adc12b757ed832ee743993e7474b9de714bb9ea13c4a8458bf9"},
]
[[package]]
@@ -2462,4 +2463,4 @@ files = [
[metadata]
lock-version = "2.1"
python-versions = ">=3.10,<4.0"
content-hash = "1b8bf07cb55b385df03716a6bd4a553579e1b325ccb53e2f77ce28605e6903c5"
content-hash = "e748a99b7137fb81541ad5152dd98603856a2bfea56d7b9ff23913e950ea2f70"
+24 -22
View File
@@ -21,7 +21,7 @@ dependencies = [
"argcomplete>=3.1.4",
"asyauth>=0.0.20",
"beautifulsoup4>=4.11,<5",
"bloodhound>=1.8.0",
"bloodhound-ce>=1.8.0",
"dploot>=3.1.0",
"dsinternals>=1.2.4",
"jwt>=1.3.1",
@@ -44,7 +44,7 @@ dependencies = [
"terminaltables>=3.1.0",
"xmltodict>=0.13.0",
# Git Dependencies
"impacket @ git+https://github.com/fortra/impacket.git",
"impacket @ git+https://github.com/zblurx/impacket.git@ldap_signing",
"oscrypto @ git+https://github.com/wbond/oscrypto",
"pynfsclient @ git+https://github.com/Pennyw0rth/NfsClient",
]
@@ -85,38 +85,40 @@ build-backend = "poetry_dynamic_versioning.backend"
flake8 = "*"
shiv = "*"
pytest = "^7.2.2"
ruff = "=0.0.292"
ruff = "*"
[tool.ruff]
select = [
"E", "F", "D", "UP", "YTT", "ASYNC", "B", "A", "C4", "ISC", "ICN", "PIE", "PT",
"Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "FURB", "LOG", "RUF"
]
ignore = [
"E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106",
"D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D415",
"D417", "D419", "RET503", "RET505", "RET506", "RET507", "RET508",
"PERF203", "RUF012"
]
# Allow autofix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
target-version = "py310"
exclude = [
".bzr", ".direnv", ".eggs", ".git", ".git-rewrite", ".hg", ".mypy_cache",
".nox", ".pants.d", ".pytype", ".ruff_cache", ".svn", ".tox", ".venv",
"__pypackages__", "_build", "buck-out", "build", "dist", "node_modules", "venv"
]
per-file-ignores = {}
line-length = 65000
preview = true
[tool.ruff.lint]
select = [
"E", "F", "D", "UP", "YTT", "ASYNC", "B", "A", "C4", "ISC", "ICN", "PIE", "PT",
"Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "LOG", "RUF"
]
ignore = [
"A004", "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106",
"D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D413", "D415",
"D417", "D419", "FURB", "RET503", "RET505", "RET506", "RET507", "RET508",
"PERF203", "RUF012", "RUF052", "RUF059"
]
# THE SETTINGS BELOW ARE DEFAULTS, left in here to override potential vs-code settings
# Allow autofix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
per-file-ignores = {}
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
target-version = "py310"
[tool.ruff.flake8-quotes]
[tool.ruff.lint.flake8-quotes]
docstring-quotes = "double"
inline-quotes = "double"
multiline-quotes = "double"
+2 -2
View File
@@ -152,8 +152,8 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M webdav -
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wifi
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 change-password -o NEWPASS=Password123
#netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M change-password -o NEWNTHASH=58A478135A93AC3BF058A5EA0E8FDB71
# 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 runasppl -M uac -M webdav -M wifi -M coerce_plus
##### SMB Anonymous Auth
+1 -1
View File
@@ -38,7 +38,7 @@ def db_setup(db_engine):
delete_workspace("test")
@pytest.fixture()
@pytest.fixture
def db(db_setup):
yield db_setup
db_setup.clear_database()