From 68abd128e6f2cd239a50578391d18f6c65bdfcb0 Mon Sep 17 00:00:00 2001 From: TristanInSec Date: Sat, 16 May 2026 16:17:22 -0400 Subject: [PATCH 1/5] Sanitize NTLM hostname to prevent path traversal and DoS Strip non-alphanumeric characters (except hyphens and dots) from server-provided NTLM hostname before use in file paths or content. Prevents: - Path traversal via ../ in hostname (file creation outside ~/.nxc/logs/) - DoS via null byte (ValueError crash in open()) - DoS via { characters (KeyError crash in str.format()) - Newline injection in --generate-hosts-file output - Affects: SMB, RDP, VNC, WinRM, MSSQL credential dump and screenshot paths --- nxc/connection.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nxc/connection.py b/nxc/connection.py index f20d05ddaa..b4926a0472 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -1,5 +1,6 @@ from datetime import datetime import os +import re import random import sys import contextlib @@ -245,6 +246,7 @@ def proto_flow(self): else: self.logger.debug("Created connection object") self.enum_host_info() + self.hostname = re.sub(r'[^\w\-.]', '_', self.hostname) # Construct the output file template using os.path.join for OS compatibility base_log_dir = os.path.join(NXC_PATH, "logs") From 9c293a8eb9c70af105b4f1f280b264419e86827c Mon Sep 17 00:00:00 2001 From: TristanInSec Date: Mon, 18 May 2026 14:42:53 -0400 Subject: [PATCH 2/5] Warn when NTLM hostname is sanitized Log a warning showing the original and sanitized hostname so users are alerted to potential non-compliant implementations or rogue servers. --- nxc/connection.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nxc/connection.py b/nxc/connection.py index b4926a0472..17ff3cfc87 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -246,7 +246,10 @@ def proto_flow(self): else: self.logger.debug("Created connection object") self.enum_host_info() - self.hostname = re.sub(r'[^\w\-.]', '_', self.hostname) + sanitized = re.sub(r'[^\w\-.]', '_', self.hostname) + if sanitized != self.hostname: + self.logger.warning(f"Hostname contains invalid characters (received: {self.hostname!r}), sanitized to: {sanitized!r}") + self.hostname = sanitized # Construct the output file template using os.path.join for OS compatibility base_log_dir = os.path.join(NXC_PATH, "logs") From ccf36b492e7253d4a3ecc9c9edce3b9586a0f0ff Mon Sep 17 00:00:00 2001 From: TristanInSec Date: Sat, 23 May 2026 15:52:44 -0400 Subject: [PATCH 3/5] Use display() for sanitization notice so it shows at default verbosity --- nxc/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/connection.py b/nxc/connection.py index 17ff3cfc87..76a1392ee4 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -248,7 +248,7 @@ def proto_flow(self): self.enum_host_info() sanitized = re.sub(r'[^\w\-.]', '_', self.hostname) if sanitized != self.hostname: - self.logger.warning(f"Hostname contains invalid characters (received: {self.hostname!r}), sanitized to: {sanitized!r}") + self.logger.display(f"Hostname contains invalid characters (received: {self.hostname!r}), sanitized to: {sanitized!r}") self.hostname = sanitized # Construct the output file template using os.path.join for OS compatibility From c20060a64a11aa08f697344bcb58aa63b8acf4cb Mon Sep 17 00:00:00 2001 From: TristanInSec Date: Tue, 2 Jun 2026 16:49:46 -0400 Subject: [PATCH 4/5] Refactor hostname sanitization into helpers/misc.py Move the hostname sanitization regex from an inline check in connection.py into a reusable sanitize_hostname() function in nxc/helpers/misc.py, and apply it at the source in each protocol's enum_host_info where hostnames are received from NTLM/server data. Protocols covered: SMB, WinRM, WMI, MSSQL, RDP. Addresses review feedback on PR #1243. --- nxc/connection.py | 5 ----- nxc/helpers/misc.py | 14 ++++++++++++++ nxc/protocols/mssql.py | 4 ++-- nxc/protocols/rdp.py | 3 ++- nxc/protocols/smb.py | 2 ++ nxc/protocols/winrm.py | 3 ++- nxc/protocols/wmi.py | 3 ++- 7 files changed, 24 insertions(+), 10 deletions(-) diff --git a/nxc/connection.py b/nxc/connection.py index 76a1392ee4..f20d05ddaa 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -1,6 +1,5 @@ from datetime import datetime import os -import re import random import sys import contextlib @@ -246,10 +245,6 @@ def proto_flow(self): else: self.logger.debug("Created connection object") self.enum_host_info() - sanitized = re.sub(r'[^\w\-.]', '_', self.hostname) - if sanitized != self.hostname: - self.logger.display(f"Hostname contains invalid characters (received: {self.hostname!r}), sanitized to: {sanitized!r}") - self.hostname = sanitized # Construct the output file template using os.path.join for OS compatibility base_log_dir = os.path.join(NXC_PATH, "logs") diff --git a/nxc/helpers/misc.py b/nxc/helpers/misc.py index dd9dbe8705..68ca41edf1 100755 --- a/nxc/helpers/misc.py +++ b/nxc/helpers/misc.py @@ -26,6 +26,20 @@ def gen_random_string(length=10): return "".join(random.sample(string.ascii_letters, int(length))) +_HOSTNAME_SANITIZE_RE = re.compile(r"[^\w\-.]") + + +def sanitize_hostname(hostname, logger=None): + """Strip characters from a server-provided hostname that could cause path + traversal, newline injection, or format-string issues when used in file + paths or output content. Logs a warning when the value is modified. + """ + sanitized = _HOSTNAME_SANITIZE_RE.sub("_", hostname) + if sanitized != hostname and logger: + logger.display(f"Hostname contained invalid characters (received: {hostname!r}), sanitized to: {sanitized!r}") + return sanitized + + def validate_ntlm(data): allowed = re.compile(r"^[0-9a-f]{32}", re.IGNORECASE) return bool(allowed.match(data)) diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index b7edc248ec..8127bf3d7b 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -6,7 +6,7 @@ from nxc.config import process_secret, host_info_colors from nxc.connection import connection from nxc.connection import requires_admin -from nxc.helpers.misc import gen_random_string +from nxc.helpers.misc import gen_random_string, sanitize_hostname from nxc.logger import NXCAdapter from nxc.helpers.bloodhound import add_user_bh from nxc.helpers.negotiate_parser import parse_challenge, login7_integrated_auth_error_message @@ -138,7 +138,7 @@ def enum_host_info(self): if challenge.startswith(b"NTLMSSP\x00"): ntlm_info = parse_challenge(challenge) self.targetDomain = self.domain = ntlm_info["domain"] - self.hostname = ntlm_info["hostname"] + self.hostname = sanitize_hostname(ntlm_info["hostname"], self.logger) self.server_os = ntlm_info["os_version"] self.logger.extra["hostname"] = self.hostname else: diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index fdbdd923c4..a84dda304e 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -8,6 +8,7 @@ from impacket.krb5.ccache import CCache from nxc.connection import connection +from nxc.helpers.misc import sanitize_hostname from nxc.helpers.bloodhound import add_user_bh from nxc.logger import NXCAdapter from nxc.config import host_info_colors, process_secret @@ -142,7 +143,7 @@ def create_conn_obj(self): pass else: self.domain = info_domain["dnsdomainname"] - self.hostname = info_domain["computername"] + self.hostname = sanitize_hostname(info_domain["computername"], self.logger) self.server_os = info_domain["os_guess"] + " Build " + str(info_domain["os_build"]) self.logger.extra["hostname"] = self.hostname break diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 6c5618464e..431ad051f8 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -6,6 +6,7 @@ import ipaddress from Cryptodome.Hash import MD4 from textwrap import dedent +from nxc.helpers.misc import sanitize_hostname from impacket.smbconnection import SMBConnection, SessionError from impacket.smb import SMB_DIALECT @@ -200,6 +201,7 @@ def enum_host_info(self): self.hostname = dns_hostname else: self.hostname = self.conn.getServerName() + self.hostname = sanitize_hostname(self.hostname, self.logger) self.targetDomain = self.conn.getServerDNSDomainName() if not self.targetDomain: # Not sure if that can even happen but now we are safe self.targetDomain = self.hostname diff --git a/nxc/protocols/winrm.py b/nxc/protocols/winrm.py index 50bd0010fa..3f758b8c9f 100644 --- a/nxc/protocols/winrm.py +++ b/nxc/protocols/winrm.py @@ -8,6 +8,7 @@ import xml.etree.ElementTree as ET from pypsrp.wsman import NAMESPACES +from nxc.helpers.misc import sanitize_hostname from pypsrp.client import Client from pypsrp.powershell import PSDataStreams from termcolor import colored @@ -68,7 +69,7 @@ def enum_host_info(self): return False self.targetDomain = self.domain = ntlm_info["domain"] - self.hostname = ntlm_info["hostname"] + self.hostname = sanitize_hostname(ntlm_info["hostname"], self.logger) self.server_os = ntlm_info["os_version"] self.logger.extra["hostname"] = self.hostname diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index c5e5af0dea..845adc0702 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -2,6 +2,7 @@ from io import StringIO from nxc.helpers.negotiate_parser import parse_challenge +from nxc.helpers.misc import sanitize_hostname from nxc.config import process_secret from nxc.connection import connection, dcom_FirewallChecker, requires_admin from nxc.logger import NXCAdapter @@ -130,7 +131,7 @@ def enum_host_info(self): bindResp = MSRPCBindAck(response.getData()) ntlm_info = parse_challenge(bindResp["auth_data"]) self.targetDomain = self.domain = ntlm_info["domain"] - self.hostname = ntlm_info["hostname"] + self.hostname = sanitize_hostname(ntlm_info["hostname"], self.logger) self.server_os = ntlm_info["os_version"] self.logger.extra["hostname"] = self.hostname else: From 09f3f823c64333e3c4506f068c47c38e7eb08b35 Mon Sep 17 00:00:00 2001 From: TristanInSec Date: Sat, 6 Jun 2026 16:19:38 -0400 Subject: [PATCH 5/5] Address review: make logger required, use .fail() for sanitization warnings --- nxc/helpers/misc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nxc/helpers/misc.py b/nxc/helpers/misc.py index 68ca41edf1..2aa98d138f 100755 --- a/nxc/helpers/misc.py +++ b/nxc/helpers/misc.py @@ -29,14 +29,14 @@ def gen_random_string(length=10): _HOSTNAME_SANITIZE_RE = re.compile(r"[^\w\-.]") -def sanitize_hostname(hostname, logger=None): +def sanitize_hostname(hostname, logger): """Strip characters from a server-provided hostname that could cause path traversal, newline injection, or format-string issues when used in file paths or output content. Logs a warning when the value is modified. """ sanitized = _HOSTNAME_SANITIZE_RE.sub("_", hostname) - if sanitized != hostname and logger: - logger.display(f"Hostname contained invalid characters (received: {hostname!r}), sanitized to: {sanitized!r}") + if sanitized != hostname: + logger.fail(f"Hostname contained invalid characters (received: {hostname!r}), sanitized to: {sanitized!r}") return sanitized