diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6289c4a1..87b04fb3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -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) diff --git a/nxc/config.py b/nxc/config.py index 07f2e55b..69715251 100644 --- a/nxc/config.py +++ b/nxc/config.py @@ -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") diff --git a/nxc/connection.py b/nxc/connection.py index 8557dbd2..89e21b93 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -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 diff --git a/nxc/data/nxc.conf b/nxc/data/nxc.conf index 8554f9e8..c979614d 100755 --- a/nxc/data/nxc.conf +++ b/nxc/data/nxc.conf @@ -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 diff --git a/nxc/database.py b/nxc/database.py index 023288f1..638ccde2 100644 --- a/nxc/database.py +++ b/nxc/database.py @@ -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 diff --git a/nxc/helpers/args.py b/nxc/helpers/args.py index 2336a057..956ae37e 100644 --- a/nxc/helpers/args.py +++ b/nxc/helpers/args.py @@ -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 diff --git a/nxc/helpers/even6_parser.py b/nxc/helpers/even6_parser.py index d6420831..168ee3ed 100644 --- a/nxc/helpers/even6_parser.py +++ b/nxc/helpers/even6_parser.py @@ -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("{}".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("", 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 - diff --git a/nxc/loaders/moduleloader.py b/nxc/loaders/moduleloader.py index d7e45034..ac6e4895 100755 --- a/nxc/loaders/moduleloader.py +++ b/nxc/loaders/moduleloader.py @@ -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""" diff --git a/nxc/logger.py b/nxc/logger.py index 722cc391..ad707e07 100755 --- a/nxc/logger.py +++ b/nxc/logger.py @@ -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 diff --git a/nxc/modules/adcs.py b/nxc/modules/adcs.py index c13c4e56..a2d94062 100644 --- a/nxc/modules/adcs.py +++ b/nxc/modules/adcs.py @@ -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.""" diff --git a/nxc/modules/add-computer.py b/nxc/modules/add-computer.py index d5f7ce9c..a9ad232a 100644 --- a/nxc/modules/add-computer.py +++ b/nxc/modules/add-computer.py @@ -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. diff --git a/nxc/modules/backup_operator.py b/nxc/modules/backup_operator.py index 4b858312..fc29bacd 100644 --- a/nxc/modules/backup_operator.py +++ b/nxc/modules/backup_operator.py @@ -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" diff --git a/nxc/modules/badsuccessor.py b/nxc/modules/badsuccessor.py new file mode 100644 index 00000000..9f509caa --- /dev/null +++ b/nxc/modules/badsuccessor.py @@ -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}") diff --git a/nxc/modules/bitlocker.py b/nxc/modules/bitlocker.py index ef271183..8057cade 100644 --- a/nxc/modules/bitlocker.py +++ b/nxc/modules/bitlocker.py @@ -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." diff --git a/nxc/modules/coerce_plus.py b/nxc/modules/coerce_plus.py index 09017fc5..cabaaa68 100644 --- a/nxc/modules/coerce_plus.py +++ b/nxc/modules/coerce_plus.py @@ -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"), }, } diff --git a/nxc/modules/daclread.py b/nxc/modules/daclread.py index 0bdff145..b1a6d3ba 100644 --- a/nxc/modules/daclread.py +++ b/nxc/modules/daclread.py @@ -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): diff --git a/nxc/modules/dpapi_hash.py b/nxc/modules/dpapi_hash.py index 070121f6..1f02709f 100644 --- a/nxc/modules/dpapi_hash.py +++ b/nxc/modules/dpapi_hash.py @@ -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" diff --git a/nxc/modules/empire_exec.py b/nxc/modules/empire_exec.py index aef27ff6..c2dac1b4 100644 --- a/nxc/modules/empire_exec.py +++ b/nxc/modules/empire_exec.py @@ -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 diff --git a/nxc/modules/enum_ca.py b/nxc/modules/enum_ca.py index 7075b895..613cd3cd 100644 --- a/nxc/modules/enum_ca.py +++ b/nxc/modules/enum_ca.py @@ -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" diff --git a/nxc/modules/enum_dns.py b/nxc/modules/enum_dns.py index fe88cd29..a441ce92 100644 --- a/nxc/modules/enum_dns.py +++ b/nxc/modules/enum_dns.py @@ -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}"] diff --git a/nxc/modules/enum_impersonate.py b/nxc/modules/enum_impersonate.py index 9dc142c5..acfaa216 100644 --- a/nxc/modules/enum_impersonate.py +++ b/nxc/modules/enum_impersonate.py @@ -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 diff --git a/nxc/modules/enum_logins.py b/nxc/modules/enum_logins.py index 42302338..4bdd0233 100644 --- a/nxc/modules/enum_logins.py +++ b/nxc/modules/enum_logins.py @@ -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 diff --git a/nxc/modules/eventlog_creds.py b/nxc/modules/eventlog_creds.py index 5572fcf6..dccb9d6b 100644 --- a/nxc/modules/eventlog_creds.py +++ b/nxc/modules/eventlog_creds.py @@ -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() diff --git a/nxc/modules/get-network.py b/nxc/modules/get-network.py index 732acd2c..cfe5a10b 100644 --- a/nxc/modules/get-network.py +++ b/nxc/modules/get-network.py @@ -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): diff --git a/nxc/modules/keepass_trigger.py b/nxc/modules/keepass_trigger.py index d9d56d4c..df51c0c5 100644 --- a/nxc/modules/keepass_trigger.py +++ b/nxc/modules/keepass_trigger.py @@ -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") diff --git a/nxc/modules/ldap-checker.py b/nxc/modules/ldap-checker.py index 59e9550a..775b5f8f 100644 --- a/nxc/modules/ldap-checker.py +++ b/nxc/modules/ldap-checker.py @@ -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 diff --git a/nxc/modules/link_enable_cmdshell.py b/nxc/modules/link_enable_cmdshell.py index f91f9db3..54d38809 100644 --- a/nxc/modules/link_enable_cmdshell.py +++ b/nxc/modules/link_enable_cmdshell.py @@ -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) diff --git a/nxc/modules/mremoteng.py b/nxc/modules/mremoteng.py index 875fbb73..ecdb186e 100644 --- a/nxc/modules/mremoteng.py +++ b/nxc/modules/mremoteng.py @@ -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"": diff --git a/nxc/modules/ms17-010.py b/nxc/modules/ms17-010.py index be0988f4..3a3a4a66 100644 --- a/nxc/modules/ms17-010.py +++ b/nxc/modules/ms17-010.py @@ -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. diff --git a/nxc/modules/mssql_coerce.py b/nxc/modules/mssql_coerce.py index a4dca25a..d1b597ef 100644 --- a/nxc/modules/mssql_coerce.py +++ b/nxc/modules/mssql_coerce.py @@ -1,5 +1,6 @@ import sys + class NXCModule: """Execute arbitrary SQL commands on the target MSSQL server""" diff --git a/nxc/modules/mssql_priv.py b/nxc/modules/mssql_priv.py index 12f8e265..0abe2f73 100644 --- a/nxc/modules/mssql_priv.py +++ b/nxc/modules/mssql_priv.py @@ -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: """ diff --git a/nxc/modules/ntdsutil.py b/nxc/modules/ntdsutil.py index 81aaf00b..8d101c79 100644 --- a/nxc/modules/ntdsutil.py +++ b/nxc/modules/ntdsutil.py @@ -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, diff --git a/nxc/modules/printnightmare.py b/nxc/modules/printnightmare.py index 9bca9941..36da3f60 100644 --- a/nxc/modules/printnightmare.py +++ b/nxc/modules/printnightmare.py @@ -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}") diff --git a/nxc/modules/reg-winlogon.py b/nxc/modules/reg-winlogon.py index 90dd1103..fc85422c 100644 --- a/nxc/modules/reg-winlogon.py +++ b/nxc/modules/reg-winlogon.py @@ -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 diff --git a/nxc/modules/remove-mic.py b/nxc/modules/remove-mic.py index 40e43c97..29007b53 100644 --- a/nxc/modules/remove-mic.py +++ b/nxc/modules/remove-mic.py @@ -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) diff --git a/nxc/modules/security-questions.py b/nxc/modules/security-questions.py index e729a05a..91630aed 100644 --- a/nxc/modules/security-questions.py +++ b/nxc/modules/security-questions.py @@ -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 diff --git a/nxc/modules/shadowrdp.py b/nxc/modules/shadowrdp.py index 40ad8120..59c148e9 100644 --- a/nxc/modules/shadowrdp.py +++ b/nxc/modules/shadowrdp.py @@ -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: diff --git a/nxc/modules/slinky.py b/nxc/modules/slinky.py index ac4d2420..2b6f97a3 100644 --- a/nxc/modules/slinky.py +++ b/nxc/modules/slinky.py @@ -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") diff --git a/nxc/modules/smbghost.py b/nxc/modules/smbghost.py index 0cc01d64..f8c61341 100644 --- a/nxc/modules/smbghost.py +++ b/nxc/modules/smbghost.py @@ -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)." diff --git a/nxc/modules/snipped.py b/nxc/modules/snipped.py index dd8b9fce..02f3d768 100644 --- a/nxc/modules/snipped.py +++ b/nxc/modules/snipped.py @@ -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. diff --git a/nxc/modules/timeroast.py b/nxc/modules/timeroast.py index bc87f8e0..beeb5ca7 100644 --- a/nxc/modules/timeroast.py +++ b/nxc/modules/timeroast.py @@ -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() diff --git a/nxc/modules/uac.py b/nxc/modules/uac.py index f731b805..d630009f 100644 --- a/nxc/modules/uac.py +++ b/nxc/modules/uac.py @@ -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): """ """ diff --git a/nxc/modules/vnc.py b/nxc/modules/vnc.py index 6aa30f9a..ff26a8a4 100644 --- a/nxc/modules/vnc.py +++ b/nxc/modules/vnc.py @@ -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']}") diff --git a/nxc/modules/wam.py b/nxc/modules/wam.py index ef896b5d..c45e601a 100644 --- a/nxc/modules/wam.py +++ b/nxc/modules/wam.py @@ -24,7 +24,6 @@ class NXCModule: self.pvkbytes = get_domain_backup_key(connection) - target = Target.create( domain=connection.domain, username=username, diff --git a/nxc/modules/wcc.py b/nxc/modules/wcc.py index 2db273f2..fbdd3ece 100644 --- a/nxc/modules/wcc.py +++ b/nxc/modules/wcc.py @@ -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 diff --git a/nxc/modules/winscp.py b/nxc/modules/winscp.py index 85db2790..f84e8750 100644 --- a/nxc/modules/winscp.py +++ b/nxc/modules/winscp.py @@ -15,7 +15,6 @@ import re import configparser - class NXCModule: """Module by @NeffIsBack""" diff --git a/nxc/netexec.py b/nxc/netexec.py index 77c8fab9..9f0e3524 100755 --- a/nxc/netexec.py +++ b/nxc/netexec.py @@ -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) diff --git a/nxc/nxcdb.py b/nxc/nxcdb.py index 4ebe1f8c..7da2c77d 100644 --- a/nxc/nxcdb.py +++ b/nxc/nxcdb.py @@ -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 = """ diff --git a/nxc/protocols/ftp.py b/nxc/protocols/ftp.py index 859f2d03..decac478 100644 --- a/nxc/protocols/ftp.py +++ b/nxc/protocols/ftp.py @@ -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" diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 63cf0e59..f541d805 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -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") diff --git a/nxc/protocols/ldap/bloodhound.py b/nxc/protocols/ldap/bloodhound.py index 4532b7a3..14109de4 100644 --- a/nxc/protocols/ldap/bloodhound.py +++ b/nxc/protocols/ldap/bloodhound.py @@ -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") diff --git a/nxc/protocols/ldap/kerberos.py b/nxc/protocols/ldap/kerberos.py index 2e47fd17..bc989ac4 100644 --- a/nxc/protocols/ldap/kerberos.py +++ b/nxc/protocols/ldap/kerberos.py @@ -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"], diff --git a/nxc/protocols/ldap/laps.py b/nxc/protocols/ldap/laps.py index 3a3c4397..873cd10d 100644 --- a/nxc/protocols/ldap/laps.py +++ b/nxc/protocols/ldap/laps.py @@ -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 diff --git a/nxc/protocols/ldap/resolution.py b/nxc/protocols/ldap/resolution.py new file mode 100644 index 00000000..9a80a0fd --- /dev/null +++ b/nxc/protocols/ldap/resolution.py @@ -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 \ No newline at end of file diff --git a/nxc/protocols/mssql/mssqlexec.py b/nxc/protocols/mssql/mssqlexec.py index 3436a002..a9010359 100755 --- a/nxc/protocols/mssql/mssqlexec.py +++ b/nxc/protocols/mssql/mssqlexec.py @@ -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: diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index e17953d3..e9d60ef0 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -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]}" diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index df6f1a24..64dc1615 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -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) diff --git a/nxc/protocols/smb/atexec.py b/nxc/protocols/smb/atexec.py index 105983d4..3311be3c 100755 --- a/nxc/protocols/smb/atexec.py +++ b/nxc/protocols/smb/atexec.py @@ -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) diff --git a/nxc/protocols/smb/dpapi.py b/nxc/protocols/smb/dpapi.py index 9e041f11..ce43461a 100644 --- a/nxc/protocols/smb/dpapi.py +++ b/nxc/protocols/smb/dpapi.py @@ -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: diff --git a/nxc/protocols/smb/firefox.py b/nxc/protocols/smb/firefox.py index ec63705d..8084fcd9 100644 --- a/nxc/protocols/smb/firefox.py +++ b/nxc/protocols/smb/firefox.py @@ -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) diff --git a/nxc/protocols/smb/passpol.py b/nxc/protocols/smb/passpol.py index dbea1931..f5a93cf1 100644 --- a/nxc/protocols/smb/passpol.py +++ b/nxc/protocols/smb/passpol.py @@ -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" diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index a72e0408..cc914988 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -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): diff --git a/nxc/protocols/smb/samrfunc.py b/nxc/protocols/smb/samrfunc.py index e65fb5c8..cd28ce98 100644 --- a/nxc/protocols/smb/samrfunc.py +++ b/nxc/protocols/smb/samrfunc.py @@ -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 diff --git a/nxc/protocols/ssh/proto_args.py b/nxc/protocols/ssh/proto_args.py index 8f82b787..add5454c 100644 --- a/nxc/protocols/ssh/proto_args.py +++ b/nxc/protocols/ssh/proto_args.py @@ -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): diff --git a/poetry.lock b/poetry.lock index 5f43907e..439f04e9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml index c3c68ae0..2a745815 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index b6cef76e..9c23d258 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -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 diff --git a/tests/test_smb_database.py b/tests/test_smb_database.py index f4da5c09..f2e5479c 100644 --- a/tests/test_smb_database.py +++ b/tests/test_smb_database.py @@ -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()