mirror of
https://github.com/Pennyw0rth/NetExec
synced 2026-06-06 16:34:30 +00:00
Merge branch 'main' into patch-8
This commit is contained in:
+1
-1
@@ -384,7 +384,7 @@ class connection:
|
||||
if isfile(user):
|
||||
with open(user) as user_file:
|
||||
for line in user_file:
|
||||
if "\\" in line:
|
||||
if "\\" in line and len(line.split("\\")) == 2:
|
||||
domain_single, username_single = line.split("\\")
|
||||
else:
|
||||
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain else self.domain
|
||||
|
||||
+3
-3
@@ -49,10 +49,10 @@ class NXCModule:
|
||||
|
||||
try:
|
||||
sc = ldap.SimplePagedResultsControl()
|
||||
base_dn_root = connection.ldapConnection._baseDN if self.base_dn is None else self.base_dn
|
||||
base_dn_root = connection.ldap_connection._baseDN if self.base_dn is None else self.base_dn
|
||||
|
||||
if self.server is None:
|
||||
connection.ldapConnection.search(
|
||||
connection.ldap_connection.search(
|
||||
searchFilter=search_filter,
|
||||
attributes=[],
|
||||
sizeLimit=0,
|
||||
@@ -61,7 +61,7 @@ class NXCModule:
|
||||
searchBase="CN=Configuration," + base_dn_root,
|
||||
)
|
||||
else:
|
||||
connection.ldapConnection.search(
|
||||
connection.ldap_connection.search(
|
||||
searchFilter=search_filter + base_dn_root + ")",
|
||||
attributes=["certificateTemplates"],
|
||||
sizeLimit=0,
|
||||
|
||||
@@ -274,8 +274,8 @@ class NXCModule:
|
||||
self.context = context
|
||||
"""On a successful LDAP login we perform a search for the targets' SID, their Security Descriptors and the principal's SID if there is one specified"""
|
||||
context.log.highlight("Be careful, this module cannot read the DACLS recursively.")
|
||||
self.baseDN = connection.ldapConnection._baseDN
|
||||
self.ldap_session = connection.ldapConnection
|
||||
self.baseDN = connection.ldap_connection._baseDN
|
||||
self.ldap_session = connection.ldap_connection
|
||||
|
||||
# Searching for the principal SID
|
||||
if self.principal_sAMAccountName is not None:
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
class NXCModule:
|
||||
"""
|
||||
Enumerate SQL Server users with impersonation rights
|
||||
Module by deathflamingo
|
||||
"""
|
||||
|
||||
name = "enum_impersonate"
|
||||
description = "Enumerate users with impersonation privileges"
|
||||
supported_protocols = ["mssql"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
self.mssql_conn = None
|
||||
self.context = None
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
impersonate_users = self.get_impersonate_users()
|
||||
if impersonate_users:
|
||||
self.context.log.success("Users with impersonation rights:")
|
||||
for user in impersonate_users:
|
||||
self.context.log.display(f" - {user}")
|
||||
else:
|
||||
self.context.log.fail("No users with impersonation rights found.")
|
||||
|
||||
def get_impersonate_users(self) -> list:
|
||||
"""
|
||||
Fetches a list of users with impersonation rights.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list: List of user names.
|
||||
"""
|
||||
query = """
|
||||
SELECT DISTINCT b.name
|
||||
FROM sys.server_permissions a
|
||||
INNER JOIN sys.server_principals b
|
||||
ON a.grantor_principal_id = b.principal_id
|
||||
WHERE a.permission_name LIKE 'IMPERSONATE%'
|
||||
"""
|
||||
res = self.mssql_conn.sql_query(query)
|
||||
return [user["name"] for user in res] if res else []
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
@@ -0,0 +1,53 @@
|
||||
class NXCModule:
|
||||
"""
|
||||
Enumerate SQL Server linked servers
|
||||
Module by deathflamingo, NeffIsBack
|
||||
"""
|
||||
|
||||
name = "enum_links"
|
||||
description = "Enumerate linked SQL Servers and their login configurations."
|
||||
supported_protocols = ["mssql"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
self.mssql_conn = None
|
||||
self.context = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
linked_servers = self.get_linked_servers()
|
||||
if linked_servers:
|
||||
self.context.log.success("Linked servers found:")
|
||||
for server in linked_servers:
|
||||
self.context.log.display(f" - {server}")
|
||||
else:
|
||||
self.context.log.fail("No linked servers found.")
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
res = self.mssql_conn.sql_query("EXEC sp_helplinkedsrvlogin")
|
||||
srvs = [srv for srv in res if srv["Local Login"] != "NULL"]
|
||||
if not srvs:
|
||||
self.context.log.fail("No linked servers found.")
|
||||
return
|
||||
self.context.log.success("Linked servers found:")
|
||||
for srv in srvs:
|
||||
self.context.log.display(f"Linked server: {srv['Linked Server']}")
|
||||
self.context.log.display(f" - Local login: {srv['Local Login']}")
|
||||
self.context.log.display(f" - Remote login: {srv['Remote Login']}")
|
||||
|
||||
def get_linked_servers(self) -> list:
|
||||
"""
|
||||
Fetches a list of linked servers.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list: List of linked server names.
|
||||
"""
|
||||
query = "EXEC sp_linkedservers;"
|
||||
res = self.mssql_conn.sql_query(query)
|
||||
return [server["SRV_NAME"] for server in res] if res else []
|
||||
@@ -0,0 +1,40 @@
|
||||
class NXCModule:
|
||||
"""
|
||||
Enumerate SQL Server logins
|
||||
Module by deathflamingo
|
||||
"""
|
||||
|
||||
name = "enum_logins"
|
||||
description = "Enumerate SQL Server logins"
|
||||
supported_protocols = ["mssql"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
self.mssql_conn = None
|
||||
self.context = None
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
logins = self.get_logins()
|
||||
if logins:
|
||||
self.context.log.success("Logins found:")
|
||||
for login in logins:
|
||||
self.context.log.display(f" - {login}")
|
||||
else:
|
||||
self.context.log.fail("No logins found.")
|
||||
|
||||
def get_logins(self) -> list:
|
||||
"""
|
||||
Fetches a list of SQL Server logins.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list: List of login names.
|
||||
"""
|
||||
query = "SELECT name FROM sys.server_principals WHERE type_desc = 'SQL_LOGIN';"
|
||||
res = self.mssql_conn.sql_query(query)
|
||||
return [login["name"] for login in res] if res else []
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
@@ -21,7 +21,7 @@ class NXCModule:
|
||||
attributes = ["flatName", "trustPartner", "trustDirection", "trustAttributes"]
|
||||
|
||||
context.log.debug(f"Search Filter={search_filter}")
|
||||
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
|
||||
trusts = []
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
class NXCModule:
|
||||
"""
|
||||
Execute commands on linked servers
|
||||
Module by deathflamingo
|
||||
"""
|
||||
|
||||
name = "exec_on_link"
|
||||
description = "Execute commands on a SQL Server linked server"
|
||||
supported_protocols = ["mssql"]
|
||||
opsec_safe = False
|
||||
multiple_hosts = False
|
||||
|
||||
def __init__(self):
|
||||
self.mssql_conn = None
|
||||
self.context = None
|
||||
self.linked_server = None
|
||||
self.command = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
LINKED_SERVER: The name of the linked server to execute the command on.
|
||||
COMMAND: The command to execute on the linked server.
|
||||
"""
|
||||
if "LINKED_SERVER" in module_options:
|
||||
self.linked_server = module_options["LINKED_SERVER"]
|
||||
if "COMMAND" in module_options:
|
||||
self.command = module_options["COMMAND"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
if not self.linked_server or not self.command:
|
||||
self.context.log.fail("Please specify both LINKED_SERVER and COMMAND options.")
|
||||
return
|
||||
|
||||
self.execute_on_link()
|
||||
|
||||
def execute_on_link(self):
|
||||
"""Executes the specified command on the linked server."""
|
||||
query = f"EXEC ('{self.command}') AT [{self.linked_server}];"
|
||||
result = self.mssql_conn.sql_query(query)
|
||||
self.context.log.display(f"Command output: {result}")
|
||||
@@ -39,7 +39,7 @@ class NXCModule:
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={search_filter}")
|
||||
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=["dNSHostName", "operatingSystem"], sizeLimit=0)
|
||||
resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=["dNSHostName", "operatingSystem"], sizeLimit=0)
|
||||
except LDAPSearchError as e:
|
||||
if e.getErrorString().find("sizeLimitExceeded") >= 0:
|
||||
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
|
||||
|
||||
@@ -40,7 +40,7 @@ class NXCModule:
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
resp = connection.ldap_connection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["sAMAccountName", "description"],
|
||||
sizeLimit=0,
|
||||
|
||||
@@ -121,7 +121,7 @@ class NXCModule:
|
||||
sfilter = "(DC=*)"
|
||||
|
||||
try:
|
||||
list_sites = connection.ldapConnection.search(
|
||||
list_sites = connection.ldap_connection.search(
|
||||
searchBase=search_target,
|
||||
searchFilter=sfilter,
|
||||
attributes=["dnsRecord", "dNSTombstoned", "name"],
|
||||
|
||||
@@ -24,7 +24,7 @@ class NXCModule:
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
resp = connection.ldap_connection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["sAMAccountName", "unixUserPassword"],
|
||||
sizeLimit=0,
|
||||
|
||||
@@ -24,7 +24,7 @@ class NXCModule:
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
resp = connection.ldap_connection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["sAMAccountName", "userPassword"],
|
||||
sizeLimit=0,
|
||||
|
||||
@@ -68,7 +68,7 @@ class NXCModule:
|
||||
def do_search(self, context, connection, searchFilter, attributeName):
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(searchFilter=searchFilter, attributes=[attributeName], sizeLimit=0)
|
||||
resp = connection.ldap_connection.search(searchFilter=searchFilter, attributes=[attributeName], sizeLimit=0)
|
||||
context.log.debug(f"Total number of records returned {len(resp)}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
|
||||
@@ -37,7 +37,7 @@ class NXCModule:
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
resp = connection.ldap_connection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["memberOf", "primaryGroupID"],
|
||||
sizeLimit=0,
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
class NXCModule:
|
||||
"""
|
||||
Enable or disable xp_cmdshell on a linked SQL server
|
||||
Module by deathflamingo
|
||||
"""
|
||||
|
||||
name = "link_enable_xp"
|
||||
description = "Enable or disable xp_cmdshell on a linked SQL server"
|
||||
supported_protocols = ["mssql"]
|
||||
opsec_safe = False
|
||||
multiple_hosts = False
|
||||
|
||||
def __init__(self):
|
||||
self.action = None
|
||||
self.linked_server = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
Defines the options for enabling or disabling xp_cmdshell on the linked server.
|
||||
ACTION Specifies whether to enable or disable:
|
||||
- enable (default)
|
||||
- disable
|
||||
LINKED_SERVER The name of the linked SQL server to target.
|
||||
"""
|
||||
self.action = module_options.get("ACTION", "enable")
|
||||
self.linked_server = module_options.get("LINKED_SERVER")
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
if not self.linked_server:
|
||||
self.context.log.fail("Please provide a linked server name using the LINKED_SERVER option.")
|
||||
return
|
||||
|
||||
# Enable or disable xp_cmdshell based on action
|
||||
if self.action == "enable":
|
||||
self.enable_xp_cmdshell()
|
||||
elif self.action == "disable":
|
||||
self.disable_xp_cmdshell()
|
||||
else:
|
||||
self.context.log.fail(f"Unknown action: {self.action}")
|
||||
|
||||
def enable_xp_cmdshell(self):
|
||||
"""Enable xp_cmdshell on the linked server."""
|
||||
query = f"EXEC ('sp_configure ''show advanced options'', 1; RECONFIGURE;') AT [{self.linked_server}]"
|
||||
self.context.log.display(f"Enabling advanced options on {self.linked_server}...")
|
||||
out = self.query_and_get_output(query)
|
||||
query = f"EXEC ('sp_configure ''xp_cmdshell'', 1; RECONFIGURE;') AT [{self.linked_server}]"
|
||||
self.context.log.display(f"Enabling xp_cmdshell on {self.linked_server}...")
|
||||
out = self.query_and_get_output(query)
|
||||
self.context.log.display(out)
|
||||
self.context.log.success(f"xp_cmdshell enabled on {self.linked_server}")
|
||||
|
||||
def disable_xp_cmdshell(self):
|
||||
"""Disable xp_cmdshell on the linked server."""
|
||||
query = f"EXEC ('sp_configure ''xp_cmdshell'', 0; RECONFIGURE; sp_configure ''show advanced options'', 0; RECONFIGURE;') AT [{self.linked_server}]"
|
||||
self.context.log.display(f"Disabling xp_cmdshell on {self.linked_server}...")
|
||||
self.query_and_get_output(query)
|
||||
self.context.log.success(f"xp_cmdshell disabled on {self.linked_server}")
|
||||
|
||||
def query_and_get_output(self, query):
|
||||
"""Executes a query and returns the output."""
|
||||
return self.mssql_conn.sql_query(query)
|
||||
@@ -0,0 +1,44 @@
|
||||
class NXCModule:
|
||||
"""
|
||||
Run xp_cmdshell commands on a linked SQL server
|
||||
Module by deathflamingo
|
||||
"""
|
||||
|
||||
name = "link_xpcmd"
|
||||
description = "Run xp_cmdshell commands on a linked SQL server"
|
||||
supported_protocols = ["mssql"]
|
||||
opsec_safe = False
|
||||
multiple_hosts = False
|
||||
|
||||
def __init__(self):
|
||||
self.linked_server = None
|
||||
self.command = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
Defines the options for running xp_cmdshell commands on a linked server.
|
||||
LINKED_SERVER The name of the linked SQL server to target.
|
||||
CMD The command to run via xp_cmdshell.
|
||||
"""
|
||||
self.linked_server = module_options.get("LINKED_SERVER")
|
||||
self.command = module_options.get("CMD")
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
if not self.linked_server or not self.command:
|
||||
self.context.log.fail("Please provide both LINKED_SERVER and CMD options.")
|
||||
return
|
||||
|
||||
self.run_xp_cmdshell(self.command)
|
||||
|
||||
def run_xp_cmdshell(self, cmd):
|
||||
"""Run the specified command via xp_cmdshell on the linked server."""
|
||||
query = f"EXEC ('xp_cmdshell ''{cmd}''') AT [{self.linked_server}]"
|
||||
self.context.log.display(f"Running command on {self.linked_server}: {cmd}")
|
||||
result = self.query_and_get_output(query)
|
||||
self.context.log.success(f"Command output:\n{result}")
|
||||
|
||||
def query_and_get_output(self, query):
|
||||
"""Executes a query and returns the output."""
|
||||
return self.mssql_conn.sql_query(query)
|
||||
@@ -75,5 +75,5 @@ class NXCModule:
|
||||
result = self.mssql_conn.sql_query(command)
|
||||
self.context.log.debug(f"Executing command: {command}, Command result: {result}")
|
||||
except Exception as e:
|
||||
self.context.log.error(f"Failed to execute command: {command}, Error: {e}")
|
||||
self.context.log.fail(f"Failed to execute command: {command}, Error: {e}")
|
||||
self.context.log.display("Commands executed successfully, check the listener for results")
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
from io import BytesIO
|
||||
from os import makedirs
|
||||
from os.path import join, abspath
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
# Finds notepad++ unsaved backup files
|
||||
# Module by @Defte_
|
||||
|
||||
name = "notepad++"
|
||||
description = "Extracts notepad++ unsaved files."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
false_positive = [".", "..", "desktop.ini", "Public", "Default", "Default User", "All Users", ".NET v4.5", ".NET v4.5 Classic"]
|
||||
|
||||
def options(self, context, module_options):
|
||||
""""""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
found = 0
|
||||
for directory in connection.conn.listPath("C$", "Users\\*"):
|
||||
if directory.get_longname() not in self.false_positive and directory.is_directory():
|
||||
try:
|
||||
notepad_backup_dir = f"Users\\{directory.get_longname()}\\AppData\\Roaming\\Notepad++\\backup\\"
|
||||
for file in connection.conn.listPath("C$", f"{notepad_backup_dir}\\*"):
|
||||
file_path = f"{notepad_backup_dir}{file.get_longname()}"
|
||||
if file.get_longname() not in self.false_positive:
|
||||
found += 1
|
||||
file_path = f"{notepad_backup_dir}{file.get_longname()}"
|
||||
buf = BytesIO()
|
||||
connection.conn.getFile("C$", file_path, buf.write)
|
||||
buf.seek(0)
|
||||
file_content = buf.read().decode("utf-8", errors="ignore").lower()
|
||||
context.log.highlight(f"C:\\{file_path}")
|
||||
for line in file_content.splitlines():
|
||||
context.log.highlight(f"\t{line}")
|
||||
filename = f"{connection.host}_{directory.get_longname()}_notepad_backup_{found}.txt"
|
||||
export_path = join(NXC_PATH, "modules", "notepad++")
|
||||
path = abspath(join(export_path, filename))
|
||||
makedirs(export_path, exist_ok=True)
|
||||
try:
|
||||
with open(path, "w+") as file:
|
||||
file.write(file_content)
|
||||
context.log.highlight(f"Notepad++ backup written to: {path}")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Failed to write Notepad++ backup to {filename}: {e}")
|
||||
except Exception:
|
||||
pass
|
||||
@@ -40,7 +40,7 @@ class NXCModule:
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={search_filter}")
|
||||
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
except Exception:
|
||||
context.log.error("LDAP search error:", exc_info=True)
|
||||
return False
|
||||
|
||||
@@ -1,73 +1,60 @@
|
||||
import traceback
|
||||
from os import makedirs
|
||||
from os.path import join, abspath
|
||||
from nxc.paths import NXC_PATH
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""Module by @357384n"""
|
||||
# Module by @357384n
|
||||
# Modified by @Defte_ 12/10/2024 to remove unecessary powershell execute command
|
||||
|
||||
name = "powershell_history"
|
||||
description = "Extracts PowerShell history for all users and looks for sensitive commands."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
false_positive = [".", "..", "desktop.ini", "Public", "Default", "Default User", "All Users", ".NET v4.5", ".NET v4.5 Classic"]
|
||||
sensitive_keywords = [
|
||||
"password", "passw", "secret", "credential", "key",
|
||||
"get-credential", "convertto-securestring", "set-localuser",
|
||||
"new-localuser", "set-adaccountpassword", "new-object system.net.webclient",
|
||||
"invoke-webrequest", "invoke-restmethod"
|
||||
]
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""To export all the history you can add the following option: -o export=True"""
|
||||
context.log.info(f"Received module options: {module_options}")
|
||||
def options(self, _, module_options):
|
||||
self.export = bool(module_options.get("EXPORT", False))
|
||||
context.log.info(f"Option export set to: {self.export}")
|
||||
|
||||
def analyze_history(self, history):
|
||||
"""Analyze PowerShell history for sensitive information."""
|
||||
sensitive_keywords = [
|
||||
"password", "passwd", "passw", "secret", "credential", "key",
|
||||
"get-credential", "convertto-securestring", "set-localuser",
|
||||
"new-localuser", "set-adaccountpassword", "new-object system.net.webclient",
|
||||
"invoke-webrequest", "invoke-restmethod"
|
||||
]
|
||||
sensitive_commands = []
|
||||
for command in history:
|
||||
command_lower = command.lower()
|
||||
if any(keyword.lower() in command_lower for keyword in sensitive_keywords):
|
||||
sensitive_commands.append(command.strip())
|
||||
return sensitive_commands
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
"""Main function to retrieve and analyze PowerShell history."""
|
||||
try:
|
||||
context.log.info("Retrieving PowerShell history...")
|
||||
command = 'powershell.exe "type C:\\Users\\*\\AppData\\Roaming\\Microsoft\\Windows\\PowerShell\\PSReadLine\\ConsoleHost_history.txt"'
|
||||
history = connection.execute(command, True).split("\n")
|
||||
if history:
|
||||
sensitive_commands = self.analyze_history(history)
|
||||
if sensitive_commands:
|
||||
context.log.highlight("Sensitive commands found in PowerShell history:")
|
||||
for command in sensitive_commands:
|
||||
context.log.highlight(f" {command}")
|
||||
else:
|
||||
context.log.info("No sensitive commands found in PowerShell history.")
|
||||
else:
|
||||
context.log.info("No PowerShell history found.")
|
||||
|
||||
# Check if export is enabled
|
||||
context.log.info(f"Export option is set to: {self.export}")
|
||||
if self.export and history:
|
||||
host = connection.host # Assuming 'host' contains the target IP or hostname
|
||||
filename = f"{host}_powershell_history.txt"
|
||||
export_path = join(NXC_PATH, "modules", "powershell_history")
|
||||
path = abspath(join(export_path, filename))
|
||||
makedirs(export_path, exist_ok=True)
|
||||
|
||||
context.log.info(f"Export enabled, writing history to {path}")
|
||||
for directory in connection.conn.listPath("C$", "Users\\*"):
|
||||
if directory.get_longname() not in self.false_positive and directory.is_directory():
|
||||
try:
|
||||
with open(path, "w") as file:
|
||||
for cmd in history:
|
||||
file.write(cmd + "\n")
|
||||
context.log.highlight(f"PowerShell history written to: {path}")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Failed to write history to {filename}: {e}")
|
||||
except Exception as e:
|
||||
context.log.fail(f"UNEXPECTED ERROR: {e}")
|
||||
context.log.debug(traceback.format_exc())
|
||||
powershell_history_dir = f"Users\\{directory.get_longname()}\\AppData\\Roaming\\Microsoft\\Windows\\PowerShell\\PSReadLine\\"
|
||||
for file in connection.conn.listPath("C$", f"{powershell_history_dir}\\*"):
|
||||
if file.get_longname() not in self.false_positive:
|
||||
file_path = f"{powershell_history_dir}{file.get_longname()}"
|
||||
|
||||
buf = BytesIO()
|
||||
connection.conn.getFile("C$", file_path, buf.write)
|
||||
buf.seek(0)
|
||||
file_content = buf.read().decode("utf-8", errors="ignore").lower()
|
||||
keywords = [keyword.upper() for keyword in self.sensitive_keywords if keyword in file_content]
|
||||
if len(keywords):
|
||||
context.log.highlight(f"C:\\{file_path} [ {' '.join(keywords)} ]")
|
||||
else:
|
||||
context.log.highlight(f"C:\\{file_path}")
|
||||
|
||||
for line in file_content.splitlines():
|
||||
context.log.highlight(f"\t{line}")
|
||||
if self.export:
|
||||
filename = f"{connection.host}_{directory.get_longname()}_powershell_history.txt"
|
||||
export_path = join(NXC_PATH, "modules", "powershell_history")
|
||||
path = abspath(join(export_path, filename))
|
||||
makedirs(export_path, exist_ok=True)
|
||||
try:
|
||||
with open(path, "w+") as file:
|
||||
file.write(file_content)
|
||||
context.log.highlight(f"PowerShell history written to: {path}")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Failed to write history to {filename}: {e}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -24,7 +24,7 @@ class NXCModule:
|
||||
|
||||
def on_login(self, context, connection):
|
||||
try:
|
||||
ldap_connection = connection.ldapConnection
|
||||
ldap_connection = connection.ldap_connection
|
||||
|
||||
# Define the search filter for pre-created computer accounts
|
||||
search_filter = "(&(objectClass=computer)(userAccountControl=4128))"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import sys
|
||||
from impacket import system_errors
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE, rpc_status_codes
|
||||
from impacket.structure import Structure
|
||||
from impacket.dcerpc.v5 import transport, rprn
|
||||
from impacket.dcerpc.v5.ndr import NDRCALL, NDRPOINTER, NDRSTRUCT, NDRUNION, NULL
|
||||
@@ -39,7 +39,8 @@ class NXCModule:
|
||||
|
||||
def on_login(self, context, connection):
|
||||
# Connect and bind to MS-RPRN (https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/848b8334-134a-4d02-aea4-03b673d6c515)
|
||||
stringbinding = r"ncacn_np:%s[\PIPE\spoolss]" % connection.host
|
||||
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
|
||||
stringbinding = r"ncacn_np:%s[\PIPE\spoolss]" % target
|
||||
|
||||
context.log.info(f"Binding to {stringbinding!r}")
|
||||
|
||||
@@ -55,7 +56,7 @@ class NXCModule:
|
||||
)
|
||||
|
||||
rpctransport.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
rpctransport.setRemoteHost(target)
|
||||
rpctransport.set_dport(self.port)
|
||||
|
||||
try:
|
||||
@@ -101,7 +102,12 @@ class NXCModule:
|
||||
if e.error_code == system_errors.ERROR_INVALID_PARAMETER:
|
||||
context.log.highlight("Vulnerable, next step https://github.com/ly4k/PrintNightmare")
|
||||
return True
|
||||
raise e
|
||||
context.log.fail(f"Unexpected error: {e}")
|
||||
except DCERPCException as e:
|
||||
if rpc_status_codes[e.error_code] == "rpc_s_access_denied":
|
||||
context.log.info("Not vulnerable :'(")
|
||||
return False
|
||||
context.log.fail(f"Unexpected error: {e}")
|
||||
context.log.highlight("Vulnerable, next step https://github.com/ly4k/PrintNightmare")
|
||||
return True
|
||||
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ class NXCModule:
|
||||
def on_login(self, context, connection):
|
||||
# Are there even any FGPPs?
|
||||
context.log.success("Attempting to enumerate policies...")
|
||||
resp = connection.ldapConnection.search(searchBase=f"CN=Password Settings Container,CN=System,{''.join([f'DC={dc},' for dc in connection.domain.split('.')]).rstrip(',')}", searchFilter="(objectclass=*)")
|
||||
resp = connection.ldap_connection.search(searchBase=f"CN=Password Settings Container,CN=System,{''.join([f'DC={dc},' for dc in connection.domain.split('.')]).rstrip(',')}", searchFilter="(objectclass=*)")
|
||||
if len(resp) > 1:
|
||||
context.log.highlight(f"{len(resp) - 1} PSO Objects found!")
|
||||
context.log.highlight("")
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
# Original Author:
|
||||
# Dirk-jan Mollema (@_dirkjan)
|
||||
# dlive (@D1iv3)
|
||||
#
|
||||
# Refernece:
|
||||
# - https://dirkjanm.io/exploiting-CVE-2019-1040-relay-vulnerabilities-for-rce-and-domain-admin/
|
||||
# - https://github.com/fox-it/cve-2019-1040-scanner
|
||||
# - https://github.com/Dliv3/cve-2019-1040-scanner
|
||||
#
|
||||
# Modify by:
|
||||
# XiaoliChan (@Memory_before)
|
||||
|
||||
import calendar
|
||||
import struct
|
||||
import time
|
||||
import random
|
||||
import string
|
||||
|
||||
from impacket import ntlm
|
||||
from impacket import nt_errors
|
||||
from impacket.smbconnection import SessionError
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "remove-mic"
|
||||
description = "Check if host vulnerable to CVE-2019-1040"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
self.action = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""PORT Port to check (defaults to 445)"""
|
||||
self.port = 445
|
||||
if "PORT" in module_options:
|
||||
self.port = int(module_options["PORT"])
|
||||
|
||||
def on_login(self, context, connection):
|
||||
ntlm.computeResponseNTLMv2 = Modify_Func.mod_computeResponseNTLMv2
|
||||
ntlm.getNTLMSSPType3 = Modify_Func.mod_getNTLMSSPType3
|
||||
try:
|
||||
connection.conn.reconnect()
|
||||
except SessionError as e:
|
||||
if e.getErrorCode() == nt_errors.STATUS_INVALID_PARAMETER:
|
||||
context.log.info("Target is not vulnerable to CVE-2019-1040 (authentication was rejected)")
|
||||
else:
|
||||
context.log.info("Unexpected Exception while authentication")
|
||||
else:
|
||||
context.log.highlight("Potentially vulnerable to CVE-2019-1040, next step: https://dirkjanm.io/exploiting-CVE-2019-1040-relay-vulnerabilities-for-rce-and-domain-admin/")
|
||||
|
||||
class Modify_Func:
|
||||
# Slightly modified version of impackets computeResponseNTLMv2
|
||||
def mod_computeResponseNTLMv2(flags, serverChallenge, clientChallenge, serverName, domain, user, password, lmhash="", nthash="",
|
||||
use_ntlmv2=ntlm.USE_NTLMv2, channel_binding_value=b""):
|
||||
|
||||
responseServerVersion = b"\x01"
|
||||
hiResponseServerVersion = b"\x01"
|
||||
responseKeyNT = ntlm.NTOWFv2(user, password, domain, nthash)
|
||||
|
||||
av_pairs = ntlm.AV_PAIRS(serverName)
|
||||
# In order to support SPN target name validation, we have to add this to the serverName av_pairs. Otherwise we will
|
||||
# get access denied
|
||||
# This is set at Local Security Policy -> Local Policies -> Security Options -> Server SPN target name validation
|
||||
# level
|
||||
av_pairs[ntlm.NTLMSSP_AV_TARGET_NAME] = "cifs/".encode("utf-16le") + av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1]
|
||||
if av_pairs[ntlm.NTLMSSP_AV_TIME] is not None:
|
||||
aTime = av_pairs[ntlm.NTLMSSP_AV_TIME][1]
|
||||
else:
|
||||
aTime = struct.pack("<q", (116444736000000000 + calendar.timegm(time.gmtime()) * 10000000))
|
||||
av_pairs[ntlm.NTLMSSP_AV_TIME] = aTime
|
||||
av_pairs[ntlm.NTLMSSP_AV_FLAGS] = b"\x02" + b"\x00" * 3
|
||||
serverName = av_pairs.getData()
|
||||
|
||||
if len(channel_binding_value) > 0:
|
||||
av_pairs[ntlm.NTLMSSP_AV_CHANNEL_BINDINGS] = channel_binding_value
|
||||
|
||||
# Format according to:
|
||||
# https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/aee311d6-21a7-4470-92a5-c4ecb022a87b
|
||||
temp = responseServerVersion # RespType 1 byte
|
||||
temp += hiResponseServerVersion # HiRespType 1 byte
|
||||
temp += b"\x00" * 2 # Reserved1 2 bytes
|
||||
temp += b"\x00" * 4 # Reserved2 4 bytes
|
||||
temp += aTime # TimeStamp 8 bytes
|
||||
temp += clientChallenge # ChallengeFromClient 8 bytes
|
||||
temp += b"\x00" * 4 # Reserved 4 bytes
|
||||
temp += av_pairs.getData() # AvPairs variable
|
||||
|
||||
ntProofStr = ntlm.hmac_md5(responseKeyNT, serverChallenge + temp)
|
||||
|
||||
ntChallengeResponse = ntProofStr + temp
|
||||
lmChallengeResponse = ntlm.hmac_md5(responseKeyNT, serverChallenge + clientChallenge) + clientChallenge
|
||||
sessionBaseKey = ntlm.hmac_md5(responseKeyNT, ntProofStr)
|
||||
|
||||
if user == "" and password == "":
|
||||
# Special case for anonymous authentication
|
||||
ntChallengeResponse = ""
|
||||
lmChallengeResponse = ""
|
||||
|
||||
return ntChallengeResponse, lmChallengeResponse, sessionBaseKey
|
||||
|
||||
def mod_getNTLMSSPType3(type1, type2, user, password, domain, lmhash="", nthash="", use_ntlmv2=ntlm.USE_NTLMv2, channel_binding_value=b""):
|
||||
# Safety check in case somebody sent password = None.. That's not allowed. Setting it to '' and hope for the best.
|
||||
if password is None:
|
||||
password = ""
|
||||
|
||||
# Let's do some encoding checks before moving on. Kind of dirty, but found effective when dealing with
|
||||
# international characters.
|
||||
import sys
|
||||
encoding = sys.getfilesystemencoding()
|
||||
if encoding is not None:
|
||||
try:
|
||||
user.encode("utf-16le")
|
||||
except Exception:
|
||||
user = user.decode(encoding)
|
||||
try:
|
||||
password.encode("utf-16le")
|
||||
except Exception:
|
||||
password = password.decode(encoding)
|
||||
try:
|
||||
domain.encode("utf-16le")
|
||||
except Exception:
|
||||
domain = user.decode(encoding)
|
||||
|
||||
ntlmChallenge = ntlm.NTLMAuthChallenge(type2)
|
||||
|
||||
# Let's start with the original flags sent in the type1 message
|
||||
responseFlags = type1["flags"]
|
||||
|
||||
# Token received and parsed. Depending on the authentication
|
||||
# method we will create a valid ChallengeResponse
|
||||
ntlmChallengeResponse = ntlm.NTLMAuthChallengeResponse(user, password, ntlmChallenge["challenge"])
|
||||
|
||||
clientChallenge = ntlm.b("".join([random.choice(string.digits + string.ascii_letters) for _ in range(8)]))
|
||||
|
||||
serverName = ntlmChallenge["TargetInfoFields"]
|
||||
|
||||
ntResponse, lmResponse, sessionBaseKey = ntlm.computeResponse(ntlmChallenge["flags"], ntlmChallenge["challenge"],
|
||||
clientChallenge, serverName, domain, user, password,
|
||||
lmhash, nthash, use_ntlmv2, channel_binding_value=channel_binding_value)
|
||||
|
||||
# Let's check the return flags
|
||||
if (ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY) == 0:
|
||||
# No extended session security, taking it out
|
||||
responseFlags &= 0xffffffff ^ ntlm.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY
|
||||
if (ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_128) == 0:
|
||||
# No support for 128 key len, taking it out
|
||||
responseFlags &= 0xffffffff ^ ntlm.NTLMSSP_NEGOTIATE_128
|
||||
if (ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_KEY_EXCH) == 0:
|
||||
# No key exchange supported, taking it out
|
||||
responseFlags &= 0xffffffff ^ ntlm.NTLMSSP_NEGOTIATE_KEY_EXCH
|
||||
|
||||
# drop the mic need to unset these flags
|
||||
# https://github.com/fortra/impacket/blob/master/impacket/examples/ntlmrelayx/clients/ldaprelayclient.py#L72
|
||||
if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_SEAL == ntlm.NTLMSSP_NEGOTIATE_SEAL:
|
||||
responseFlags ^= ntlm.NTLMSSP_NEGOTIATE_SEAL
|
||||
if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_SIGN == ntlm.NTLMSSP_NEGOTIATE_SIGN:
|
||||
responseFlags ^= ntlm.NTLMSSP_NEGOTIATE_SIGN
|
||||
if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_ALWAYS_SIGN == ntlm.NTLMSSP_NEGOTIATE_ALWAYS_SIGN:
|
||||
responseFlags ^= ntlm.NTLMSSP_NEGOTIATE_ALWAYS_SIGN
|
||||
|
||||
|
||||
keyExchangeKey = ntlm.KXKEY(ntlmChallenge["flags"], sessionBaseKey, lmResponse, ntlmChallenge["challenge"], password,
|
||||
lmhash, nthash, use_ntlmv2)
|
||||
|
||||
# Special case for anonymous login
|
||||
if user == "" and password == "" and lmhash == "" and nthash == "":
|
||||
keyExchangeKey = b"\x00" * 16
|
||||
|
||||
|
||||
if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_KEY_EXCH:
|
||||
exportedSessionKey = ntlm.b("".join([random.choice(string.digits + string.ascii_letters) for _ in range(16)]))
|
||||
encryptedRandomSessionKey = ntlm.generateEncryptedSessionKey(keyExchangeKey, exportedSessionKey)
|
||||
else:
|
||||
encryptedRandomSessionKey = None
|
||||
exportedSessionKey = keyExchangeKey
|
||||
|
||||
ntlmChallengeResponse["flags"] = responseFlags
|
||||
ntlmChallengeResponse["domain_name"] = domain.encode("utf-16le")
|
||||
ntlmChallengeResponse["host_name"] = type1.getWorkstation().encode("utf-16le")
|
||||
if lmResponse == "":
|
||||
ntlmChallengeResponse["lanman"] = b"\x00"
|
||||
else:
|
||||
ntlmChallengeResponse["lanman"] = lmResponse
|
||||
ntlmChallengeResponse["ntlm"] = ntResponse
|
||||
if encryptedRandomSessionKey is not None:
|
||||
ntlmChallengeResponse["session_key"] = encryptedRandomSessionKey
|
||||
|
||||
return ntlmChallengeResponse, exportedSessionKey
|
||||
+37
-7
@@ -1,5 +1,10 @@
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
from impacket.dcerpc.v5.rrp import DCERPCSessionError
|
||||
|
||||
|
||||
class NXCModule:
|
||||
# Reworked by @Defte_ 13/10/2024 to remove unecessary execute operation
|
||||
name = "runasppl"
|
||||
description = "Check if the registry value RunAsPPL is set or not"
|
||||
supported_protocols = ["smb"]
|
||||
@@ -14,10 +19,35 @@ class NXCModule:
|
||||
""""""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
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 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)
|
||||
try:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
if remote_ops._RemoteOperations__rrp:
|
||||
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
reg_handle,
|
||||
"SYSTEM\\CurrentControlSet\\Control\\Lsa"
|
||||
)
|
||||
key_handle = ans["phkResult"]
|
||||
_ = data = None
|
||||
try:
|
||||
_, data = rrp.hBaseRegQueryValue(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
key_handle,
|
||||
"RunAsPPL\x00",
|
||||
)
|
||||
except rrp.DCERPCSessionError as e:
|
||||
context.log.debug(f"RunAsPPL error {e} on host {connection.host}")
|
||||
|
||||
if data is None or data not in [1, 2]:
|
||||
context.log.highlight("RunAsPPL disabled")
|
||||
else:
|
||||
context.log.highlight("RunAsPPL enabled")
|
||||
|
||||
except DCERPCSessionError as e:
|
||||
context.log.debug(f"Error connecting to RemoteRegistry {e} on host {connection.host}")
|
||||
finally:
|
||||
remote_ops.finish()
|
||||
|
||||
+8
-8
@@ -49,7 +49,7 @@ class NXCModule:
|
||||
"""On a successful LDAP login we perform a search for all PKI Enrollment Server or Certificate Templates Names."""
|
||||
self.context = context
|
||||
self.connection = connection
|
||||
self.base_dn = connection.ldapConnection._baseDN if not self.base_dn else self.base_dn
|
||||
self.base_dn = connection.ldap_connection._baseDN if not self.base_dn else self.base_dn
|
||||
self.sc = ldap.SimplePagedResultsControl()
|
||||
|
||||
# Basic SCCM enumeration
|
||||
@@ -58,7 +58,7 @@ class NXCModule:
|
||||
search_filter = f"(distinguishedName=CN=System Management,CN=System,{self.base_dn})"
|
||||
controls = security_descriptor_control(sdflags=0x04)
|
||||
context.log.display(f"Looking for the SCCM container with filter: '{search_filter}'")
|
||||
result = connection.ldapConnection.search(
|
||||
result = connection.ldap_connection.search(
|
||||
searchFilter=search_filter,
|
||||
attributes=["nTSecurityDescriptor"],
|
||||
sizeLimit=0,
|
||||
@@ -129,7 +129,7 @@ class NXCModule:
|
||||
try:
|
||||
yoinkers = "(|(samaccountname=*sccm*)(samaccountname=*mecm*)(description=*sccm*)(description=*mecm*)(name=*sccm*)(name=*mecm*))"
|
||||
context.log.display("Searching for SCCM related objects")
|
||||
result = connection.ldapConnection.search(
|
||||
result = connection.ldap_connection.search(
|
||||
searchFilter=yoinkers,
|
||||
searchBase=self.base_dn,
|
||||
attributes=["sAMAccountName", "distinguishedName", "sAMAccountType"],
|
||||
@@ -157,7 +157,7 @@ class NXCModule:
|
||||
try:
|
||||
self.context.log.debug(f"Resolving group members recursively for {dn}")
|
||||
# Somehow BaseDN is not working together with the LDAP_MATCHING_RULE_IN_CHAIN
|
||||
result = self.connection.ldapConnection.search(
|
||||
result = self.connection.ldap_connection.search(
|
||||
searchFilter=f"(memberOf:{LDAP_MATCHING_RULE_IN_CHAIN}:={dn})",
|
||||
attributes=["sAMAccountName", "distinguishedName", "sAMAccountType"],
|
||||
)
|
||||
@@ -176,7 +176,7 @@ class NXCModule:
|
||||
def get_management_points(self):
|
||||
"""Searches for all SCCM management points in the Active Directory and maps them to their SCCM site via the site code."""
|
||||
try:
|
||||
response = self.connection.ldapConnection.search(
|
||||
response = self.connection.ldap_connection.search(
|
||||
searchBase=self.base_dn,
|
||||
searchFilter="(objectClass=mSSMSManagementPoint)",
|
||||
attributes=["cn", "dNSHostName", "mSSMSDefaultMP", "mSSMSSiteCode"],
|
||||
@@ -199,7 +199,7 @@ class NXCModule:
|
||||
def get_sites(self):
|
||||
"""Searches for all SCCM sites in the Active Directory, sorted by site code."""
|
||||
try:
|
||||
response = self.connection.ldapConnection.search(
|
||||
response = self.connection.ldap_connection.search(
|
||||
searchBase=self.base_dn,
|
||||
searchFilter="(objectClass=mSSMSSite)",
|
||||
attributes=["cn", "mSSMSSiteCode", "mSSMSAssignmentSiteCode"],
|
||||
@@ -244,7 +244,7 @@ class NXCModule:
|
||||
"""Tries to resolve a SID and add the dNSHostName to the sccm site list."""
|
||||
try:
|
||||
self.context.log.debug(f"Resolving SID: {sid}")
|
||||
result = self.connection.ldapConnection.search(
|
||||
result = self.connection.ldap_connection.search(
|
||||
searchBase=self.base_dn,
|
||||
searchFilter=f"(objectSid={sid})",
|
||||
attributes=["sAMAccountName", "sAMAccountType", "member", "dNSHostName"],
|
||||
@@ -277,7 +277,7 @@ class NXCModule:
|
||||
|
||||
def dn_to_sid(self, dn) -> str:
|
||||
"""Tries to resolve a DN to a SID."""
|
||||
result = self.connection.ldapConnection.search(
|
||||
result = self.connection.ldap_connection.search(
|
||||
searchBase=self.base_dn,
|
||||
searchFilter=f"(distinguishedName={dn})",
|
||||
attributes=["sAMAccountName", "objectSid"],
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
|
||||
# Module by @Defte_
|
||||
# Enables or disables shadow RDP
|
||||
class NXCModule:
|
||||
name = "shadowrdp"
|
||||
description = "Enables or disables shadow RDP"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
self.action = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
|
||||
if "ACTION" not in module_options:
|
||||
context.log.fail("ACTION option not specified!")
|
||||
exit(1)
|
||||
|
||||
if module_options["ACTION"].lower() not in ["enable", "disable"]:
|
||||
context.log.fail("ACTION must be either enable, disable or query")
|
||||
exit(1)
|
||||
self.action = module_options["ACTION"].lower()
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
try:
|
||||
remoteOps = RemoteOperations(connection.conn, False)
|
||||
remoteOps.enableRegistry()
|
||||
if remoteOps._RemoteOperations__rrp:
|
||||
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans["phKey"]
|
||||
|
||||
keyHandle = rrp.hBaseRegOpenKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
regHandle,
|
||||
"Software\\Policies\\Microsoft\\Windows NT\\Terminal Services\\"
|
||||
)["phkResult"]
|
||||
|
||||
# Checks if the key already exists or not
|
||||
try:
|
||||
rrp.hBaseRegQueryValue(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
keyHandle,
|
||||
"Shadow\x00"
|
||||
)
|
||||
except Exception as e:
|
||||
if "ERROR_FILE_NOT_FOUND" in str(e):
|
||||
context.log.debug("here")
|
||||
ans = rrp.hBaseRegCreateKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
keyHandle,
|
||||
"Shadow\x00")
|
||||
|
||||
# Disable remote UAC
|
||||
if self.action == "disable":
|
||||
rrp.hBaseRegSetValue(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
keyHandle,
|
||||
"Shadow\x00",
|
||||
rrp.REG_DWORD,
|
||||
0
|
||||
)
|
||||
context.log.highlight("Shadow RDP disabled")
|
||||
|
||||
# Enable remote UAC
|
||||
if self.action == "enable":
|
||||
rrp.hBaseRegSetValue(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
keyHandle,
|
||||
"Shadow\x00",
|
||||
rrp.REG_DWORD,
|
||||
2
|
||||
)
|
||||
context.log.highlight("Shadow RDP with full access enabled")
|
||||
|
||||
except Exception as e:
|
||||
context.log.debug(f"Error {e}")
|
||||
finally:
|
||||
remoteOps.finish()
|
||||
@@ -0,0 +1,134 @@
|
||||
import ntpath
|
||||
import os
|
||||
from os.path import join, getsize, exists
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
|
||||
name = "snipped"
|
||||
description = "Downloads screenshots taken by the (new) Snipping Tool."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = None
|
||||
self.excluded_files = ["desktop.ini"]
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""USERS: Download only specified user(s); format: -o USERS=user1,user2,user3"""
|
||||
self.context = context
|
||||
self.users = [user.lower() for user in module_options["USERS"].split(",")] if "USERS" in module_options else None
|
||||
|
||||
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
self.context = context
|
||||
self.connection = connection
|
||||
self.share = "C$"
|
||||
|
||||
output_base_dir = join(NXC_PATH, "modules", "snipped", "screenshots")
|
||||
os.makedirs(output_base_dir, exist_ok=True)
|
||||
|
||||
context.log.info("Getting all user folders")
|
||||
try:
|
||||
user_folders = connection.conn.listPath(self.share, "\\Users\\*")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Failed to list user folders: {e}")
|
||||
return
|
||||
|
||||
context.log.info(f"User folders: {[folder.get_longname() for folder in user_folders]}")
|
||||
if not user_folders:
|
||||
context.log.fail("No User folders found!")
|
||||
return
|
||||
else:
|
||||
context.log.info("Attempting to download screenshots if they exist.")
|
||||
|
||||
total_files_downloaded = 0
|
||||
host_output_path = None
|
||||
|
||||
for user_folder in user_folders:
|
||||
folder_name = user_folder.get_longname()
|
||||
if folder_name.lower() not in [".", "..", "all users", "default", "default user", "public"]:
|
||||
normalized_name = folder_name.lower()
|
||||
if self.users and normalized_name not in self.users:
|
||||
continue
|
||||
|
||||
context.log.info(f"Searching for Screenshots folder in {folder_name}'s home directory")
|
||||
screenshots_folders = self.find_screenshots_folders(folder_name)
|
||||
if not screenshots_folders:
|
||||
context.log.debug(f"No Screenshots folder found for user {folder_name}. Skipping.")
|
||||
continue
|
||||
|
||||
for screenshot_path in screenshots_folders:
|
||||
try:
|
||||
screenshot_files = connection.conn.listPath(self.share, screenshot_path + "\\*")
|
||||
except Exception as e:
|
||||
context.log.debug(f"Screenshot folder {screenshot_path} not found for user {folder_name}: {e}")
|
||||
continue
|
||||
|
||||
if not screenshot_files:
|
||||
context.log.debug(f"No screenshots found in {screenshot_path} for user {folder_name}")
|
||||
continue
|
||||
|
||||
user_output_dir = join(output_base_dir, connection.host)
|
||||
os.makedirs(user_output_dir, exist_ok=True)
|
||||
host_output_path = user_output_dir
|
||||
|
||||
for file in screenshot_files:
|
||||
if not file.is_directory():
|
||||
remote_file_name = file.get_longname()
|
||||
|
||||
if remote_file_name.lower() in self.excluded_files:
|
||||
context.log.debug(f"Excluding file {remote_file_name}.")
|
||||
continue
|
||||
|
||||
remote_file_path = ntpath.join(screenshot_path, remote_file_name)
|
||||
sanitized_path = screenshot_path.replace("\\", "_").replace("/", "_")
|
||||
local_file_name = f"{folder_name}_{sanitized_path}_{remote_file_name}"
|
||||
local_file_path = join(user_output_dir, local_file_name)
|
||||
|
||||
try:
|
||||
with open(local_file_path, "wb") as local_file:
|
||||
context.log.debug(f"Downloading {remote_file_path} to {local_file_path}")
|
||||
connection.conn.getFile(self.share, remote_file_path, local_file.write)
|
||||
|
||||
if not exists(local_file_path):
|
||||
context.log.fail(f"Downloaded file '{local_file_path}' does not exist.")
|
||||
continue
|
||||
|
||||
file_size = getsize(local_file_path)
|
||||
if file_size == 0:
|
||||
context.log.fail(f"Downloaded file '{local_file_path}' is 0 bytes. Skipping.")
|
||||
os.remove(local_file_path)
|
||||
else:
|
||||
total_files_downloaded += 1
|
||||
except Exception as e:
|
||||
context.log.debug(f"Failed to download '{remote_file_path}' for user {folder_name}: {e}")
|
||||
|
||||
if total_files_downloaded > 0 and host_output_path:
|
||||
context.log.success(f"{total_files_downloaded} file(s) downloaded from host {connection.host} to {host_output_path}.")
|
||||
|
||||
|
||||
def find_screenshots_folders(self, user_folder_name):
|
||||
"""
|
||||
Dynamically searches for all Screenshots folders in the user's home directory.
|
||||
Returns a list of paths.
|
||||
"""
|
||||
base_path = ntpath.normpath(join(r"Users", user_folder_name))
|
||||
screenshots_folders = []
|
||||
try:
|
||||
subfolders = self.connection.conn.listPath(self.share, base_path + "\\*")
|
||||
for subfolder in subfolders:
|
||||
if subfolder.is_directory() and subfolder.get_longname() not in [".", ".."]:
|
||||
potential_path = ntpath.join(base_path, subfolder.get_longname(), "Screenshots")
|
||||
try:
|
||||
if self.connection.conn.listPath(self.share, potential_path + "\\*"):
|
||||
screenshots_folders.append(potential_path)
|
||||
except Exception:
|
||||
continue
|
||||
except Exception as e:
|
||||
self.context.log.debug(f"Failed to list subfolders for {base_path}: {e}")
|
||||
return screenshots_folders
|
||||
@@ -42,12 +42,12 @@ class NXCModule:
|
||||
multiple_hosts = False
|
||||
|
||||
def on_login(self, context, connection):
|
||||
dn = connection.ldapConnection._baseDN if self.base_dn is None else self.base_dn
|
||||
dn = connection.ldap_connection._baseDN if self.base_dn is None else self.base_dn
|
||||
|
||||
context.log.display("Getting the Sites and Subnets from domain")
|
||||
|
||||
try:
|
||||
list_sites = connection.ldapConnection.search(
|
||||
list_sites = connection.ldap_connection.search(
|
||||
searchBase=f"CN=Configuration,{dn}",
|
||||
searchFilter="(objectClass=site)",
|
||||
attributes=["distinguishedName", "name", "description"],
|
||||
@@ -68,7 +68,7 @@ class NXCModule:
|
||||
site_description = site["description"]
|
||||
|
||||
# Getting subnets of this site
|
||||
list_subnets = connection.ldapConnection.search(
|
||||
list_subnets = connection.ldap_connection.search(
|
||||
searchBase=f"CN=Sites,CN=Configuration,{dn}",
|
||||
searchFilter=f"(siteObject={site_dn})",
|
||||
attributes=["distinguishedName", "name"],
|
||||
@@ -86,7 +86,7 @@ class NXCModule:
|
||||
|
||||
if self.showservers:
|
||||
# Getting machines in these subnets
|
||||
list_servers = connection.ldapConnection.search(
|
||||
list_servers = connection.ldap_connection.search(
|
||||
searchBase=site_dn,
|
||||
searchFilter="(objectClass=server)",
|
||||
attributes=["cn"],
|
||||
|
||||
@@ -71,12 +71,12 @@ class NXCModule:
|
||||
Users can specify additional LDAP filters that are applied to the query.
|
||||
"""
|
||||
self.context = context
|
||||
self.create_log_file(connection.conn.getRemoteHost(), datetime.now().strftime("%Y%m%d_%H%M%S"))
|
||||
self.create_log_file(connection.target, datetime.now().strftime("%Y%m%d_%H%M%S"))
|
||||
context.log.info(f"Starting LDAP search with search filter '{self.search_filter}'")
|
||||
|
||||
try:
|
||||
sc = ldap.SimplePagedResultsControl()
|
||||
connection.ldapConnection.search(
|
||||
connection.ldap_connection.search(
|
||||
searchFilter=self.search_filter,
|
||||
attributes=["sAMAccountName", "description"],
|
||||
sizeLimit=0,
|
||||
|
||||
@@ -17,13 +17,13 @@ class NXCModule:
|
||||
self.username = module_options["USER"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
searchBase = connection.ldapConnection._baseDN
|
||||
searchBase = connection.ldap_connection._baseDN
|
||||
searchFilter = f"(sAMAccountName={connection.username})" if self.username is None else f"(sAMAccountName={format(self.username)})"
|
||||
|
||||
context.log.debug(f"Using naming context: {searchBase} and {searchFilter} as search filter")
|
||||
|
||||
# Get attributes of provided user
|
||||
r = connection.ldapConnection.search(
|
||||
r = connection.ldap_connection.search(
|
||||
searchBase=searchBase,
|
||||
searchFilter=searchFilter,
|
||||
attributes=[
|
||||
|
||||
+55
-142
@@ -13,8 +13,6 @@ from Cryptodome.Hash import MD4
|
||||
from OpenSSL.SSL import SysCallError
|
||||
from bloodhound.ad.authentication import ADAuthentication
|
||||
from bloodhound.ad.domain import AD
|
||||
from impacket.dcerpc.v5.epm import MSRPC_UUID_PORTMAP
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from impacket.dcerpc.v5.samr import (
|
||||
UF_ACCOUNTDISABLE,
|
||||
UF_DONT_REQUIRE_PREAUTH,
|
||||
@@ -22,7 +20,6 @@ from impacket.dcerpc.v5.samr import (
|
||||
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
|
||||
@@ -30,8 +27,8 @@ 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
|
||||
from impacket.smbconnection import SMBConnection, SessionError
|
||||
from impacket.smbconnection import SessionError
|
||||
from impacket.ntlm import getNTLMSSPType1
|
||||
|
||||
from nxc.config import process_secret, host_info_colors
|
||||
from nxc.connection import connection
|
||||
@@ -42,6 +39,7 @@ from nxc.protocols.ldap.bloodhound import BloodHound
|
||||
from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB
|
||||
from nxc.protocols.ldap.kerberos import KerberosAttacks
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
from nxc.helpers.ntlm_parser import parse_challenge
|
||||
|
||||
ldap_error_status = {
|
||||
"1": "STATUS_NOT_SUPPORTED",
|
||||
@@ -136,7 +134,7 @@ class ldap(connection):
|
||||
self.server_os = None
|
||||
self.os_arch = 0
|
||||
self.hash = None
|
||||
self.ldapConnection = None
|
||||
self.ldap_connection = None
|
||||
self.lmhash = ""
|
||||
self.nthash = ""
|
||||
self.baseDN = ""
|
||||
@@ -163,15 +161,15 @@ class ldap(connection):
|
||||
}
|
||||
)
|
||||
|
||||
def get_ldap_info(self, host):
|
||||
def create_conn_obj(self):
|
||||
try:
|
||||
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
|
||||
ldap_url = f"{proto}://{host}"
|
||||
ldap_url = f"{proto}://{self.host}"
|
||||
self.logger.info(f"Connecting to {ldap_url} with no baseDN")
|
||||
try:
|
||||
ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host)
|
||||
if ldap_connection:
|
||||
self.logger.debug(f"ldap_connection: {ldap_connection}")
|
||||
self.ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host)
|
||||
if self.ldap_connection:
|
||||
self.logger.debug(f"ldap_connection: {self.ldap_connection}")
|
||||
except SysCallError as e:
|
||||
if proto == "ldaps":
|
||||
self.logger.fail(f"LDAPs connection to {ldap_url} failed - {e}")
|
||||
@@ -179,9 +177,9 @@ class ldap(connection):
|
||||
self.logger.fail("Even if the port is open, LDAPS may not be configured")
|
||||
else:
|
||||
self.logger.fail(f"LDAP connection to {ldap_url} failed: {e}")
|
||||
exit(1)
|
||||
return False
|
||||
|
||||
resp = ldap_connection.search(
|
||||
resp = self.ldap_connection.search(
|
||||
scope=ldapasn1_impacket.Scope("baseObject"),
|
||||
attributes=["defaultNamingContext", "dnsHostName"],
|
||||
sizeLimit=0,
|
||||
@@ -208,42 +206,18 @@ class ldap(connection):
|
||||
self.logger.debug("Exception:", exc_info=True)
|
||||
self.logger.info(f"Skipping item, cannot process due to error {e}")
|
||||
except OSError:
|
||||
return [None, None, None]
|
||||
return False
|
||||
self.logger.debug(f"Target: {target}; target_domain: {target_domain}; base_dn: {base_dn}")
|
||||
return [target, target_domain, base_dn]
|
||||
|
||||
def get_os_arch(self):
|
||||
try:
|
||||
string_binding = rf"ncacn_ip_tcp:{self.host}[135]"
|
||||
transport = DCERPCTransportFactory(string_binding)
|
||||
transport.setRemoteHost(self.host)
|
||||
transport.set_connect_timeout(5)
|
||||
dce = transport.get_dce_rpc()
|
||||
if self.args.kerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
dce.connect()
|
||||
try:
|
||||
dce.bind(
|
||||
MSRPC_UUID_PORTMAP,
|
||||
transfer_syntax=("71710533-BEBA-4937-8319-B5DBEF9CCC36", "1.0"),
|
||||
)
|
||||
except DCERPCException as e:
|
||||
if str(e).find("syntaxes_not_supported") >= 0:
|
||||
dce.disconnect()
|
||||
return 32
|
||||
else:
|
||||
dce.disconnect()
|
||||
return 64
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error retrieving os arch of {self.host}: {e!s}")
|
||||
|
||||
return 0
|
||||
self.target = target
|
||||
self.targetDomain = target_domain
|
||||
self.baseDN = base_dn
|
||||
return True
|
||||
|
||||
def get_ldap_username(self):
|
||||
extended_request = ldapasn1_impacket.ExtendedRequest()
|
||||
extended_request["requestName"] = "1.3.6.1.4.1.4203.1.11.3" # whoami
|
||||
|
||||
response = self.ldapConnection.sendReceive(extended_request)
|
||||
response = self.ldap_connection.sendReceive(extended_request)
|
||||
for message in response:
|
||||
search_result = message["protocolOp"].getComponent()
|
||||
if search_result["resultCode"] == ldapasn1_impacket.ResultCode("success"):
|
||||
@@ -254,48 +228,28 @@ class ldap(connection):
|
||||
return ""
|
||||
|
||||
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.hostname = self.target.split(".")[0].upper()
|
||||
self.remoteName = self.target
|
||||
self.domain = self.targetDomain
|
||||
# smb no open, specify the domain
|
||||
if not self.args.no_smb:
|
||||
self.local_ip = self.conn.getSMBServer().get_socket().getsockname()[0]
|
||||
|
||||
try:
|
||||
self.conn.login("", "")
|
||||
except BrokenPipeError as e:
|
||||
self.logger.fail(f"Broken Pipe Error while attempting to login: {e}")
|
||||
except Exception as e:
|
||||
if "STATUS_NOT_SUPPORTED" in str(e):
|
||||
self.no_ntlm = True
|
||||
if not self.no_ntlm:
|
||||
self.hostname = self.conn.getServerName()
|
||||
self.targetDomain = self.domain = self.conn.getServerDNSDomainName()
|
||||
self.server_os = self.conn.getServerOS()
|
||||
self.signing = self.conn.isSigningRequired() if self.smbv1 else self.conn._SMBConnection._Connection["RequireSigning"]
|
||||
self.os_arch = self.get_os_arch()
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
ntlm_challenge = None
|
||||
bindRequest = ldapasn1_impacket.BindRequest()
|
||||
bindRequest["version"] = 3
|
||||
bindRequest["name"] = ""
|
||||
negotiate = getNTLMSSPType1()
|
||||
bindRequest["authentication"]["sicilyNegotiate"] = negotiate.getData()
|
||||
try:
|
||||
response = self.ldap_connection.sendReceive(bindRequest)[0]["protocolOp"]
|
||||
ntlm_challenge = bytes(response["bindResponse"]["matchedDN"])
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Failed to get target {self.host} ntlm challenge, error: {e!s}")
|
||||
|
||||
if not self.domain:
|
||||
self.domain = self.hostname
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"
|
||||
if ntlm_challenge:
|
||||
ntlm_info = parse_challenge(ntlm_challenge)
|
||||
self.server_os = ntlm_info["os_version"]
|
||||
|
||||
try: # noqa: SIM105
|
||||
# DC's seem to want us to logoff first, windows workstations sometimes reset the connection
|
||||
self.conn.logoff()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Re-connect since we logged off
|
||||
self.create_conn_obj()
|
||||
|
||||
if not self.kdcHost and self.domain:
|
||||
if not self.kdcHost and self.domain and self.domain == self.remoteName:
|
||||
result = self.resolver(self.domain)
|
||||
self.kdcHost = result["host"] if result else None
|
||||
self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}")
|
||||
@@ -304,17 +258,10 @@ class ldap(connection):
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.debug("Printing host info for LDAP")
|
||||
if self.args.no_smb:
|
||||
self.logger.extra["protocol"] = "LDAP" if self.port == 389 else "LDAPS"
|
||||
self.logger.extra["port"] = self.port
|
||||
self.logger.display(f'{self.baseDN} (Hostname: {self.hostname.split(".")[0]}) (domain: {self.domain})')
|
||||
else:
|
||||
self.logger.extra["protocol"] = "SMB" if not self.no_ntlm else "LDAP"
|
||||
self.logger.extra["port"] = "445" if not self.no_ntlm else "389"
|
||||
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})")
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
self.logger.extra["protocol"] = "LDAP" if str(self.port) == "389" else "LDAPS"
|
||||
self.logger.extra["port"] = self.port
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
|
||||
|
||||
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
|
||||
self.username = username
|
||||
@@ -355,8 +302,8 @@ class ldap(connection):
|
||||
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
|
||||
ldap_url = f"{proto}://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [1]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
|
||||
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldap_connection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
|
||||
if self.username == "":
|
||||
self.username = self.get_ldap_username()
|
||||
|
||||
@@ -400,8 +347,8 @@ class ldap(connection):
|
||||
self.logger.extra["port"] = "636"
|
||||
ldaps_url = f"ldaps://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [2]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
|
||||
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldap_connection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
|
||||
if self.username == "":
|
||||
self.username = self.get_ldap_username()
|
||||
|
||||
@@ -457,8 +404,8 @@ class ldap(connection):
|
||||
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
|
||||
ldap_url = f"{proto}://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [3]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.check_if_admin()
|
||||
|
||||
# Prepare success credential text
|
||||
@@ -478,8 +425,8 @@ class ldap(connection):
|
||||
self.logger.extra["port"] = "636"
|
||||
ldaps_url = f"ldaps://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [4]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.check_if_admin()
|
||||
|
||||
# Prepare success credential text
|
||||
@@ -543,8 +490,8 @@ class ldap(connection):
|
||||
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
|
||||
ldaps_url = f"{proto}://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.check_if_admin()
|
||||
|
||||
# Prepare success credential text
|
||||
@@ -564,8 +511,8 @@ class ldap(connection):
|
||||
self.logger.extra["port"] = "636"
|
||||
ldaps_url = f"{proto}://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.check_if_admin()
|
||||
|
||||
# Prepare success credential text
|
||||
@@ -594,40 +541,6 @@ class ldap(connection):
|
||||
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):
|
||||
self.logger.debug("Creating smbv1 connection object")
|
||||
try:
|
||||
self.conn = SMBConnection(self.host, self.host, None, 445, preferredDialect=SMB_DIALECT)
|
||||
self.smbv1 = True
|
||||
if self.conn:
|
||||
self.logger.debug("SMBv1 Connection successful")
|
||||
except OSError as e:
|
||||
if str(e).find("Connection reset by peer") != -1:
|
||||
self.logger.debug(f"SMBv1 might be disabled on {self.host}")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error creating SMBv1 connection to {self.host}: {e}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def create_smbv3_conn(self):
|
||||
self.logger.debug("Creating smbv3 connection object")
|
||||
try:
|
||||
self.conn = SMBConnection(self.host, self.host, None, 445)
|
||||
self.smbv1 = False
|
||||
if self.conn:
|
||||
self.logger.debug("SMBv3 Connection successful")
|
||||
except OSError:
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error creating SMBv3 connection to {self.host}: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def create_conn_obj(self):
|
||||
return bool(self.args.no_smb or self.create_smbv1_conn() or self.create_smbv3_conn())
|
||||
|
||||
def get_sid(self):
|
||||
self.logger.highlight(f"Domain SID {self.sid_domain}")
|
||||
|
||||
@@ -692,12 +605,12 @@ class ldap(connection):
|
||||
|
||||
def search(self, searchFilter, attributes, sizeLimit=0) -> list:
|
||||
try:
|
||||
if self.ldapConnection:
|
||||
if self.ldap_connection:
|
||||
self.logger.debug(f"Search Filter={searchFilter}")
|
||||
|
||||
# Microsoft Active Directory set an hard limit of 1000 entries returned by any search
|
||||
paged_search_control = ldapasn1_impacket.SimplePagedResultsControl(criticality=True, size=1000)
|
||||
return self.ldapConnection.search(
|
||||
return self.ldap_connection.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter=searchFilter,
|
||||
attributes=attributes,
|
||||
@@ -1272,7 +1185,7 @@ class ldap(connection):
|
||||
searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=32)"
|
||||
try:
|
||||
self.logger.debug(f"Search Filter={searchFilter}")
|
||||
resp = self.ldapConnection.search(
|
||||
resp = self.ldap_connection.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter=searchFilter,
|
||||
attributes=[
|
||||
@@ -1400,7 +1313,7 @@ class ldap(connection):
|
||||
def gmsa(self):
|
||||
self.logger.display("Getting GMSA Passwords")
|
||||
search_filter = "(objectClass=msDS-GroupManagedServiceAccount)"
|
||||
gmsa_accounts = self.ldapConnection.search(
|
||||
gmsa_accounts = self.ldap_connection.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter=search_filter,
|
||||
attributes=[
|
||||
@@ -1453,7 +1366,7 @@ class ldap(connection):
|
||||
else:
|
||||
# getting the gmsa account
|
||||
search_filter = "(objectClass=msDS-GroupManagedServiceAccount)"
|
||||
gmsa_accounts = self.ldapConnection.search(
|
||||
gmsa_accounts = self.ldap_connection.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter=search_filter,
|
||||
attributes=["sAMAccountName"],
|
||||
@@ -1483,7 +1396,7 @@ class ldap(connection):
|
||||
gmsa_pass = gmsa[1]
|
||||
# getting the gmsa account
|
||||
search_filter = "(objectClass=msDS-GroupManagedServiceAccount)"
|
||||
gmsa_accounts = self.ldapConnection.search(
|
||||
gmsa_accounts = self.ldap_connection.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter=search_filter,
|
||||
attributes=["sAMAccountName"],
|
||||
|
||||
@@ -5,7 +5,6 @@ def proto_args(parser, parents):
|
||||
ldap_parser = parser.add_parser("ldap", help="own stuff using LDAP", parents=parents, formatter_class=DisplayDefaultsNotNone)
|
||||
ldap_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
|
||||
ldap_parser.add_argument("--port", type=int, default=389, help="LDAP port")
|
||||
ldap_parser.add_argument("--no-smb", action="store_true", help="No smb connection")
|
||||
|
||||
dgroup = ldap_parser.add_mutually_exclusive_group()
|
||||
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to")
|
||||
|
||||
@@ -269,7 +269,7 @@ class rdp(connection):
|
||||
if word in str(e):
|
||||
reason = self.rdp_error_status[word]
|
||||
self.logger.fail(
|
||||
(f"{domain}\\{username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} {f'({reason})' if reason else str(e)}"),
|
||||
(f"{domain}\\{username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} ({reason if reason else str(e)})"),
|
||||
color=("magenta" if ((reason or "CredSSP" in str(e)) and reason != "KDC_ERR_C_PRINCIPAL_UNKNOWN") else "red"),
|
||||
)
|
||||
elif "Authentication failed!" in str(e):
|
||||
@@ -284,7 +284,7 @@ class rdp(connection):
|
||||
if str(e) == "cannot unpack non-iterable NoneType object":
|
||||
reason = "User valid but cannot connect"
|
||||
self.logger.fail(
|
||||
(f"{domain}\\{username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} {f'({reason})' if reason else ''}"),
|
||||
(f"{domain}\\{username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} ({reason if reason else str(e)})"),
|
||||
color=("magenta" if ((reason or "CredSSP" in str(e)) and reason != "STATUS_LOGON_FAILURE") else "red"),
|
||||
)
|
||||
return False
|
||||
@@ -318,7 +318,7 @@ class rdp(connection):
|
||||
if str(e) == "cannot unpack non-iterable NoneType object":
|
||||
reason = "User valid but cannot connect"
|
||||
self.logger.fail(
|
||||
(f"{domain}\\{username}:{process_secret(password)} {f'({reason})' if reason else ''}"),
|
||||
(f"{domain}\\{username}:{process_secret(password)} ({reason if reason else str(e)})"),
|
||||
color=("magenta" if ((reason or "CredSSP" in str(e)) and reason != "STATUS_LOGON_FAILURE") else "red"),
|
||||
)
|
||||
return False
|
||||
@@ -353,7 +353,7 @@ class rdp(connection):
|
||||
reason = "User valid but cannot connect"
|
||||
|
||||
self.logger.fail(
|
||||
(f"{domain}\\{username}:{process_secret(ntlm_hash)} {f'({reason})' if reason else ''}"),
|
||||
(f"{domain}\\{username}:{process_secret(ntlm_hash)} ({reason if reason else str(e)})"),
|
||||
color=("magenta" if ((reason or "CredSSP" in str(e)) and reason != "STATUS_LOGON_FAILURE") else "red"),
|
||||
)
|
||||
return False
|
||||
|
||||
+13
-5
@@ -296,9 +296,10 @@ class smb(connection):
|
||||
self.logger.debug(f"Error logging off system: {e}")
|
||||
|
||||
# DCOM connection with kerberos needed
|
||||
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"
|
||||
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.targetDomain}"
|
||||
|
||||
if not self.kdcHost and self.domain:
|
||||
# using kdcHost is buggy on impacket when using trust relation between ad so we kdcHost must stay to none if targetdomain is not equal to domain
|
||||
if not self.kdcHost and self.domain and self.domain == self.targetDomain:
|
||||
result = self.resolver(self.domain)
|
||||
self.kdcHost = result["host"] if result else None
|
||||
self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}")
|
||||
@@ -548,7 +549,6 @@ class smb(connection):
|
||||
preferredDialect=SMB_DIALECT,
|
||||
timeout=self.args.smb_timeout,
|
||||
)
|
||||
self.smbv1 = True
|
||||
except OSError as e:
|
||||
if "Connection reset by peer" in str(e):
|
||||
self.logger.info(f"SMBv1 might be disabled on {self.host}")
|
||||
@@ -576,7 +576,6 @@ class smb(connection):
|
||||
self.port,
|
||||
timeout=self.args.smb_timeout,
|
||||
)
|
||||
self.smbv1 = False
|
||||
except (Exception, NetBIOSTimeout, OSError) as e:
|
||||
self.logger.info(f"Error creating SMBv3 connection to {self.host}: {e}")
|
||||
return False
|
||||
@@ -590,6 +589,8 @@ class smb(connection):
|
||||
|
||||
:param no_smbv1: If True, it will not try to create a SMBv1 connection
|
||||
"""
|
||||
no_smbv1 = self.args.no_smbv1 if self.args.no_smbv1 else no_smbv1
|
||||
|
||||
# Initial negotiation
|
||||
if not no_smbv1 and self.smbv1 is None:
|
||||
self.smbv1 = self.create_smbv1_conn()
|
||||
@@ -839,6 +840,7 @@ class smb(connection):
|
||||
temp_dir = ntpath.normpath("\\" + gen_random_string())
|
||||
temp_file = ntpath.normpath("\\" + gen_random_string() + ".txt")
|
||||
permissions = []
|
||||
write_check = bool(not self.args.no_write_check)
|
||||
|
||||
try:
|
||||
self.logger.debug(f"domain: {self.domain}")
|
||||
@@ -885,8 +887,14 @@ class smb(connection):
|
||||
except SessionError as e:
|
||||
error = get_error_string(e)
|
||||
self.logger.debug(f"Error checking READ access on share {share_name}: {error}")
|
||||
except (NetBIOSError, UnicodeEncodeError) as e:
|
||||
write_check = False
|
||||
share_info["access"].append("UNKNOWN (try '--no-smbv1')")
|
||||
error = get_error_string(e)
|
||||
self.logger.debug(f"Error checking READ access on share {share_name}: {error}. This exception always caused by special character in share name with SMBv1")
|
||||
self.logger.info(f"Skipping WRITE permission check on share {share_name}")
|
||||
|
||||
if not self.args.no_write_check:
|
||||
if write_check:
|
||||
try:
|
||||
self.conn.createDirectory(share_name, temp_dir)
|
||||
write_dir = True
|
||||
|
||||
@@ -16,6 +16,7 @@ def proto_args(parser, parents):
|
||||
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("--no-smbv1", action="store_true", help="Force to disable SMBv1 in connection")
|
||||
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")
|
||||
|
||||
Generated
+15
-2
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aardwolf"
|
||||
@@ -959,6 +959,19 @@ MarkupSafe = ">=2.0"
|
||||
[package.extras]
|
||||
i18n = ["Babel (>=2.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "jwt"
|
||||
version = "1.3.1"
|
||||
description = "JSON Web Token library for Python 3."
|
||||
optional = false
|
||||
python-versions = ">= 3.6"
|
||||
files = [
|
||||
{file = "jwt-1.3.1-py3-none-any.whl", hash = "sha256:61c9170f92e736b530655e75374681d4fcca9cfa8763ab42be57353b2b203494"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cryptography = ">=3.1,<3.4.0 || >3.4.0"
|
||||
|
||||
[[package]]
|
||||
name = "ldap3"
|
||||
version = "2.9.1"
|
||||
@@ -2494,4 +2507,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10.0"
|
||||
content-hash = "b102ff826faf73e87da291e242fcdb95294a641c8d8ab8590d9b47f73d6375b6"
|
||||
content-hash = "9af8efb9eb1cf1026dca8b5276ca23db2dbcdf6865fd61920a9daf1098646193"
|
||||
|
||||
@@ -44,6 +44,7 @@ bloodhound = "^1.7.2"
|
||||
dploot = "^3.0.3"
|
||||
dsinternals = "^1.2.4"
|
||||
impacket = { git = "https://github.com/fortra/impacket.git" }
|
||||
jwt = ">=1.3.1"
|
||||
lsassy = ">=3.1.11"
|
||||
masky = "^0.2.0"
|
||||
minikerberos = "^0.4.1"
|
||||
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --shares --no-smbv1
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --shares --filter-shares READ WRITE
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --pass-pol
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --disks
|
||||
@@ -84,6 +85,7 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M iis
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M install_elevated
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ioxidresolver
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M security-questions
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M remove-mic
|
||||
# currently hanging indefinitely - TODO: look into this
|
||||
#netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M keepass_discover
|
||||
#netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M keepass_trigger -o ACTION=ALL USER=LOGIN_USERNAME KEEPASS_CONFIG_PATH="C:\\Users\\LOGIN_USERNAME\\AppData\\Roaming\\KeePass\\KeePass.config.xml"
|
||||
|
||||
Reference in New Issue
Block a user