Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Draft] POC ldap3 implementation for schannel authentication #412

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion nxc/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,12 @@ def gen_cli_args():
kerberos_group.add_argument("--use-kcache", action="store_true", help="Use Kerberos authentication from ccache file (KRB5CCNAME)")
kerberos_group.add_argument("--aesKey", metavar="AESKEY", nargs="+", help="AES key to use for Kerberos Authentication (128 or 256 bits)")
kerberos_group.add_argument("--kdcHost", metavar="KDCHOST", help="FQDN of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter")


certificate_group = std_parser.add_argument_group("Certificate", "Options for Certificate authentication")
certificate_group.add_argument("-pfx", metavar="PFX", action="store", default=None, dest="pfx", help=".pfx file for certificate authentication")
certificate_group.add_argument("-key", metavar="KEY", action="store", default=None, dest="key", help=".key file for certificate authentication")
certificate_group.add_argument("-cert", metavar="CERT", action="store", default=None, dest="cert", help=".crt file fertificate authentication")

server_group = std_parser.add_argument_group("Servers", "Options for nxc servers")
server_group.add_argument("--server", choices={"http", "https"}, default="https", help="use the selected server")
server_group.add_argument("--server-host", type=str, default="0.0.0.0", metavar="HOST", help="IP to bind the server to")
Expand Down
20 changes: 20 additions & 0 deletions nxc/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ def __init__(self, args, db, target):
self.username = ""
self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey)
self.aesKey = None if not self.args.aesKey else self.args.aesKey[0]
self.pfx = self.args.pfx
self.key = self.args.key
self.cert = self.args.cert
self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache
self.admin_privs = False
self.failed_logins = 0
Expand Down Expand Up @@ -217,6 +220,9 @@ def plaintext_login(self, domain, username, password):

def hash_login(self, domain, username, ntlm_hash):
return

def schannel_login(self, domain, username="", password="", pfx=None, key=None, cert=None):
return

def proto_flow(self):
self.logger.debug("Kicking off proto_flow")
Expand Down Expand Up @@ -518,6 +524,20 @@ def login(self):
cred_type = []
data = [] # Arbitrary data needed for the login, e.g. ssh_key


## POC, skipping the DB stuff
if self.args.pfx:
domain = self.args.domain
username = self.args.username[0]
return self.schannel_login(domain, username, pfx=self.args.pfx, key=None, cert=None)


if self.args.key and self.args.cert:
domain = self.args.domain
username = self.args.username[0]
return self.schannel_login(domain, username, pfx=None, key=self.args.key, cert=self.args.cert)


if self.args.cred_id:
db_domain, db_username, db_owned, db_secret, db_cred_type, db_data = self.query_db_creds()
domain.extend(db_domain)
Expand Down
28 changes: 9 additions & 19 deletions nxc/modules/mssql_priv.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,6 @@ def update_priv(self, user: User, exec_as=""):
"""
if self.is_admin_user(user.username):
user.is_sysadmin = True
self.context.log.debug(f"Updated {user.username} to is_sysadmin")
return True
user.dbowner = self.check_dbowner_privesc(exec_as)
return user.dbowner
Expand Down Expand Up @@ -250,15 +249,11 @@ def is_admin(self, exec_as="") -> bool:
self.revert_context(exec_as)
is_admin = res[0][""]
self.context.log.debug(f"IsAdmin Result: {is_admin}")
try:
if int(is_admin):
self.context.log.debug("User is admin!")
self.admin_privs = True
return True
else:
return False
except ValueError:
self.logger.fail(f"Error checking if user is admin, got {is_admin} as response. Expected 0 or 1.")
if is_admin:
self.context.log.debug("User is admin!")
self.admin_privs = True
return True
else:
Comment on lines +252 to +256
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something is off here. Multiple lines are fixes introduced in the last months. Perhaps the commit containing this change reverted them.

return False

def get_databases(self, exec_as="") -> list:
Expand Down Expand Up @@ -447,15 +442,10 @@ def is_admin_user(self, username) -> bool:
"""
res = self.query_and_get_output(f"SELECT IS_SRVROLEMEMBER('sysadmin', '{username}')")
is_admin = res[0][""]
try:
if is_admin != "NULL" and int(is_admin):
self.admin_privs = True
self.context.log.debug(f"Updated: {username} is admin!")
return True
else:
return False
except ValueError:
self.context.log.fail(f"Error checking if user is admin, got {is_admin} as response. Expected 0 or 1.")
if is_admin:
self.admin_privs = True
return True
else:
Comment on lines +445 to +448
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with this

return False

def revert_context(self, exec_as):
Expand Down
16 changes: 9 additions & 7 deletions nxc/modules/spider_plus.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from os.path import abspath, join, split, exists, splitext, getsize, sep
from os import makedirs, remove, stat
import time
import traceback
from nxc.paths import TMP_PATH
from nxc.protocols.smb.remotefile import RemoteFile
from impacket.smb3structs import FILE_READ_DATA
Expand Down Expand Up @@ -158,8 +159,8 @@ def read_chunk(self, remote_file, chunk_size=CHUNK_SIZE):
remote_file.__smbConnection = self.smb.conn
return self.read_chunk(remote_file)

except Exception as e:
self.logger.exception(e)
except Exception:
traceback.print_exc()
break

return chunk
Expand Down Expand Up @@ -213,13 +214,13 @@ def spider_shares(self):
# Start the spider at the root of the share folder
self.results[share_name] = {}
self.spider_folder(share_name, "")
except SessionError as e:
self.logger.exception(e)
except SessionError:
traceback.print_exc()
self.logger.fail("Got a session error while spidering.")
self.reconnect()

except Exception as e:
self.logger.exception(e)
traceback.print_exc()
self.logger.fail(f"Error enumerating shares: {e!s}")

# Save the metadata.
Expand Down Expand Up @@ -254,7 +255,7 @@ def spider_folder(self, share_name, folder):
# Check file-dir exclusion filter.
if any(d in next_filedir.lower() for d in self.exclude_filter):
self.logger.info(f'The {result_type} "{next_filedir}" has been excluded')
self.stats[f"num_{result_type}s_filtered"] += 1
self.stats[f"{result_type}s_filtered"] += 1
continue

if result_type == "folder":
Expand Down Expand Up @@ -411,7 +412,8 @@ def print_stats(self):
self.logger.display(f"Total folders found: {num_folders}")
num_folders_filtered = self.stats.get("num_folders_filtered", 0)
if num_folders_filtered:
self.logger.display(f"Folders Filtered: {num_folders_filtered}")
num_filtered_folders = len(num_folders_filtered)
self.logger.display(f"Folders Filtered: {num_filtered_folders}")

# File statistics.
num_files = self.stats.get("num_files", 0)
Expand Down
1 change: 1 addition & 0 deletions nxc/netexec.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ def main():

protocol_object = getattr(p_loader.load_protocol(protocol_path), args.protocol)
nxc_logger.debug(f"Protocol Object: {protocol_object}, type: {type(protocol_object)}")
nxc_logger.debug(f"Protocol Object dir: {dir(protocol_object)}")
protocol_db_object = p_loader.load_protocol(protocol_db_path).database
nxc_logger.debug(f"Protocol DB Object: {protocol_db_object}")

Expand Down
12 changes: 7 additions & 5 deletions nxc/parsers/ldap_results.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from impacket.ldap import ldapasn1 as ldapasn1_impacket

def parse_result_attributes(ldap_response):
parsed_response = []
for entry in ldap_response:
# SearchResultReferences may be returned
if not isinstance(entry, ldapasn1_impacket.SearchResultEntry):
if entry["type"] != "searchResEntry":
continue
attribute_map = {}
for attribute in entry["attributes"]:
val = [str(val) for val in attribute["vals"].components]
attribute_map[str(attribute["type"])] = val if len(val) > 1 else val[0]
if "description" in attribute:
attribute_map[str(attribute)] = "" if entry['attributes'][attribute] == [] else str(entry['attributes'][attribute][0])
elif "pwdLastSet" in attribute:
attribute_map[str(attribute)] = str(entry['attributes'][attribute])
else:
attribute_map[str(attribute)] = entry['attributes'][attribute]
parsed_response.append(attribute_map)
return parsed_response
Loading