Skip to content

Commit

Permalink
Merge branch 'develop' into neff-bugfixes
Browse files Browse the repository at this point in the history
  • Loading branch information
Marshall-Hallenbeck authored Nov 12, 2023
2 parents 75190da + 426e446 commit c9c676a
Show file tree
Hide file tree
Showing 29 changed files with 1,227 additions and 680 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ If applicable, add screenshots to help explain your problem.

**NetExec info**
- OS: [e.g. Kali]
- Version of nxc [e.g. v1.5.2]
- Version of nxc: [e.g. v1.5.2]
- Installed from: apt/github/pip/docker/...? Please try with latest release before openning an issue

**Additional context**
Expand Down
41 changes: 22 additions & 19 deletions nxc/connection.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import random
import socket
from socket import AF_INET, AF_INET6, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME
from socket import getaddrinfo
from os.path import isfile
from threading import BoundedSemaphore
from functools import wraps
from time import sleep
from ipaddress import ip_address
from socket import AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME, getaddrinfo

from nxc.config import pwned_label
from nxc.helpers.logger import highlight
Expand All @@ -22,15 +20,22 @@


def gethost_addrinfo(hostname):
try:
for res in getaddrinfo(hostname, None, AF_INET6, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
af, socktype, proto, canonname, sa = res
host = canonname if ip_address(sa[0]).is_link_local else sa[0]
except socket.gaierror:
for res in getaddrinfo(hostname, None, AF_INET, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
af, socktype, proto, canonname, sa = res
host = sa[0] if sa[0] else canonname
return host
is_ipv6 = False
is_link_local_ipv6 = False
address_info = {"AF_INET6": "", "AF_INET": ""}

for res in getaddrinfo(hostname, None, AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
af, _, _, canonname, sa = res
address_info[af.name] = sa[0]

# IPv4 preferred
if address_info["AF_INET"]:
host = address_info["AF_INET"]
else:
is_ipv6 = True
host, is_link_local_ipv6 = (canonname, True) if ip_address(address_info["AF_INET6"]).is_link_local else (address_info["AF_INET6"], False)

return host, is_ipv6, is_link_local_ipv6


def requires_admin(func):
Expand Down Expand Up @@ -78,6 +83,7 @@ def __init__(self, args, db, host):
self.args = args
self.db = db
self.hostname = host
self.port = self.args.port
self.conn = None
self.admin_privs = False
self.password = ""
Expand All @@ -91,10 +97,10 @@ def __init__(self, args, db, host):
self.logger = nxc_logger

try:
self.host = gethost_addrinfo(self.hostname)
self.host, self.is_ipv6, self.is_link_local_ipv6 = gethost_addrinfo(self.hostname)
if self.args.kerberos:
self.host = self.hostname
self.logger.info(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={ 'True' if self.args.kerberos else 'False' }")
self.logger.info(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
except Exception as e:
self.logger.info(f"Error resolving hostname {self.hostname}: {e}")
return
Expand Down Expand Up @@ -389,11 +395,8 @@ def try_credentials(self, domain, username, owned, secret, cred_type, data=None)
return False
if self.args.continue_on_success and owned:
return False
# Enforcing FQDN for SMB if not using local authentication. Related issues/PRs: #26, #28, #24, #38
if self.args.protocol == "smb" and not self.args.local_auth and "." not in domain and not self.args.laps and secret != "" and self.domain.upper() != self.hostname.upper():
self.logger.error(f"Domain {domain} for user {username.rstrip()} need to be FQDN ex:domain.local, not domain")
return False

if hasattr(self.args, "delegate") and self.args.delegate:
self.args.kerberos = True
with sem:
if cred_type == "plaintext":
if self.args.kerberos:
Expand Down
66 changes: 50 additions & 16 deletions nxc/helpers/bloodhound.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

def add_user_bh(user, domain, logger, config):
"""Adds a user to the BloodHound graph database.
Expand Down Expand Up @@ -41,21 +40,16 @@ def add_user_bh(user, domain, logger, config):
encrypted=False,
)
try:
with driver.session() as session, session.begin_transaction() as tx:
for info in users_owned:
if info["username"][-1] == "$":
user_owned = info["username"][:-1] + "." + info["domain"]
account_type = "Computer"
with driver.session().begin_transaction() as tx:
for user_info in users_owned:
distinguished_name = "".join([f"DC={dc}," for dc in user_info["domain"].split(".")]).rstrip(",")
domain_query = tx.run(f"MATCH (d:Domain) WHERE d.distinguishedname STARTS WITH '{distinguished_name}' RETURN d").data()
if not domain_query:
logger.debug(f"Domain {user_info['domain']} not found in BloodHound. Falling back to domainless query.")
_add_without_domain(user_info, tx, logger)
else:
user_owned = info["username"] + "@" + info["domain"]
account_type = "User"

result = tx.run(f'MATCH (c:{account_type} {{name:"{user_owned}"}}) RETURN c')

if result.data()[0]["c"].get("owned") in (False, None):
logger.debug(f'MATCH (c:{account_type} {{name:"{user_owned}"}}) SET c.owned=True RETURN c.name AS name')
result = tx.run(f'MATCH (c:{account_type} {{name:"{user_owned}"}}) SET c.owned=True RETURN c.name AS name')
logger.highlight(f"Node {user_owned} successfully set as owned in BloodHound")
domain = domain_query[0]["d"].get("name")
_add_with_domain(user_info, domain, tx, logger)
except AuthError:
logger.fail(f"Provided Neo4J credentials ({config.get('BloodHound', 'bh_user')}:{config.get('BloodHound', 'bh_pass')}) are not valid.")
return
Expand All @@ -64,6 +58,46 @@ def add_user_bh(user, domain, logger, config):
return
except Exception as e:
logger.fail(f"Unexpected error with Neo4J: {e}")
logger.fail("Account not found on the domain")
return
driver.close()


def _add_with_domain(user_info, domain, tx, logger):
if user_info["username"][-1] == "$":
user_owned = f"{user_info['username'][:-1]}.{domain}"
account_type = "Computer"
else:
user_owned = f"{user_info['username']}@{domain}"
account_type = "User"

result = tx.run(f"MATCH (c:{account_type} {{name:'{user_owned}'}}) RETURN c").data()

if len(result) == 0:
logger.fail("Account not found in the BloodHound database.")
return
if result[0]["c"].get("owned") in (False, None):
logger.debug(f"MATCH (c:{account_type} {{name:'{user_owned}'}}) SET c.owned=True RETURN c.name AS name")
result = tx.run(f"MATCH (c:{account_type} {{name:'{user_owned}'}}) SET c.owned=True RETURN c.name AS name").data()[0]
logger.highlight(f"Node {result['name']} successfully set as owned in BloodHound")


def _add_without_domain(user_info, tx, logger):
if user_info["username"][-1] == "$":
user_owned = user_info["username"][:-1]
account_type = "Computer"
else:
user_owned = user_info["username"]
account_type = "User"

result = tx.run(f"MATCH (c:{account_type}) WHERE c.name STARTS WITH '{user_owned}' RETURN c").data()

if len(result) == 0:
logger.fail("Account not found in the BloodHound database.")
return
elif len(result) >= 2:
logger.fail(f"Multiple accounts found with the name '{user_info['username']}' in the BloodHound database. Please specify the FQDN ex:domain.local")
return
elif result[0]["c"].get("owned") in (False, None):
logger.debug(f"MATCH (c:{account_type} {{name:'{result[0]['c']['name']}'}}) SET c.owned=True RETURN c.name AS name")
result = tx.run(f"MATCH (c:{account_type} {{name:'{result[0]['c']['name']}'}}) SET c.owned=True RETURN c.name AS name").data()[0]
logger.highlight(f"Node {result['name']} successfully set as owned in BloodHound")
18 changes: 10 additions & 8 deletions nxc/modules/daclread.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ class NXCModule:
"""

name = "daclread"
description = "Read and backup the Discretionary Access Control List of objects. Based on the work of @_nwodtuhs and @BlWasp_. Be carefull, this module cannot read the DACLS recursively, more explains in the options."
description = "Read and backup the Discretionary Access Control List of objects. Be careful, this module cannot read the DACLS recursively, see more explanation in the options."
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = False
Expand All @@ -208,16 +208,18 @@ def __init__(self, context=None, module_options=None):

def options(self, context, module_options):
"""
Be carefull, this module cannot read the DACLS recursively.
Be careful, this module cannot read the DACLS recursively.
For example, if an object has particular rights because it belongs to a group, the module will not be able to see it directly, you have to check the group rights manually.
TARGET The objects that we want to read or backup the DACLs, sepcified by its SamAccountName
TARGET_DN The object that we want to read or backup the DACL, specified by its DN (usefull to target the domain itself)
TARGET The objects that we want to read or backup the DACLs, specified by its SamAccountName
TARGET_DN The object that we want to read or backup the DACL, specified by its DN (useful to target the domain itself)
PRINCIPAL The trustee that we want to filter on
ACTION The action to realise on the DACL (read, backup)
ACE_TYPE The type of ACE to read (Allowed or Denied)
RIGHTS An interesting right to filter on ('FullControl', 'ResetPassword', 'WriteMembers', 'DCSync')
RIGHTS_GUID A right GUID that specify a particular rights to filter on
Based on the work of @_nwodtuhs and @BlWasp_.
"""
self.context = context

Expand Down Expand Up @@ -271,8 +273,8 @@ def options(self, context, module_options):
self.filename = None

def on_login(self, context, connection):
"""On a successful LDAP login we perform a search for the targets' SID, their Security Decriptors and the principal's SID if there is one specified"""
context.log.highlight("Be carefull, this module cannot read the DACLS recursively.")
"""On a successful LDAP login we perform a search for the targets' SID, their Security Descriptors and the principal's SID if there is one specified"""
context.log.highlight("Be careful, this module cannot read the DACLS recursively.")
self.baseDN = connection.ldapConnection._baseDN
self.ldap_session = connection.ldapConnection

Expand All @@ -292,7 +294,7 @@ def on_login(self, context, connection):
context.log.fail(f"Principal SID not found in LDAP ({_lookedup_principal})")
sys.exit(1)

# Searching for the targets SID and their Security Decriptors
# Searching for the targets SID and their Security Descriptors
# If there is only one target
if (self.target_sAMAccountName or self.target_DN) and self.target_file is None:
# Searching for target account with its security descriptor
Expand Down Expand Up @@ -383,7 +385,7 @@ def search_target_principal_security_descriptor(self, context, connection):
context.log.fail(f"Principal not found in LDAP ({_lookedup_principal}), probably an LDAP session issue.")
sys.exit(0)

# Attempts to retieve the SID and Distinguisehd Name from the sAMAccountName
# Attempts to retrieve the SID and Distinguisehd Name from the sAMAccountName
# Not used for the moment
# - samname : a sAMAccountName
def get_user_info(self, context, samname):
Expand Down
4 changes: 2 additions & 2 deletions nxc/modules/example_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ def on_login(self, context, connection):
# These are for more critical error handling
context.log.error("I'm doing something") # This will not be printed in the module context and should only be used for critical errors (e.g. a required python file is missing)
try:
raise Exception("Exception that might occure")
raise Exception("Exception that might have occurred")
except Exception as e:
context.log.exception(f"Exception occured: {e}") # This will display an exception traceback screen after an exception was raised and should only be used for critical errors
context.log.exception(f"Exception occurred: {e}") # This will display an exception traceback screen after an exception was raised and should only be used for critical errors

def on_admin_login(self, context, connection):
"""Concurrent.
Expand Down
2 changes: 1 addition & 1 deletion nxc/modules/keepass_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def options(self, context, module_options):
USER Targeted user running KeePass, used to restart the appropriate process
(used by RESTART action)
EXPORT_NAME Name fo the database export file, default: export.xml
EXPORT_NAME Name of the database export file, default: export.xml
EXPORT_PATH Path where to export the KeePass database in cleartext
default: C:\\Users\\Public, %APPDATA% works well too for user permissions
Expand Down
2 changes: 1 addition & 1 deletion nxc/modules/laps.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class NXCModule:
"""

name = "laps"
description = "Retrieves the LAPS passwords"
description = "Retrieves all LAPS passwords which the account has read permissions for."
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = False
Expand Down
4 changes: 2 additions & 2 deletions nxc/modules/ldap-checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ async def run_ldaps_noEPA(target, credential):

# Conduct a bind to LDAPS with channel binding supported
# but intentionally miscalculated. In the case that and
# LDAPS bind has without channel binding supported has occured,
# LDAPS bind has without channel binding supported has occurred,
# you can determine whether the policy is set to "never" or
# if it's set to "when supported" based on the potential
# error recieved from the bind attempt.
# error received from the bind attempt.
async def run_ldaps_withEPA(target, credential):
ldapsClientConn = MSLDAPClientConnection(target, credential)
_, err = await ldapsClientConn.connect()
Expand Down
Loading

0 comments on commit c9c676a

Please sign in to comment.