diff --git a/nxc/modules/reauth_swap.py b/nxc/modules/reauth_swap.py new file mode 100644 index 00000000..d6788dbe --- /dev/null +++ b/nxc/modules/reauth_swap.py @@ -0,0 +1,129 @@ +import binascii +import contextlib + +from impacket import ntlm +from impacket.smb3 import SMB2SessionSetup, SMB2SessionSetup_Response +from impacket.smb3structs import SMB2_SESSION_SETUP +from impacket.spnego import SPNEGO_NegTokenInit, SPNEGO_NegTokenResp, TypesMech +from nxc.helpers.misc import CATEGORY +from nxc.helpers.smb import get_smb_connection + + +class NXCModule: + """ + Module made by: Lyderic Lefebvre (@aas_s3curity) - https://blackfield.ch + + Check if an SMB server still allows a cross-user re-authentication swap on an existing session: + log in as one account, then re-authenticate as a different one on the same connection. That swap + is the primitive an auth relay uses to keep a privileged session under a signing key it controls, + defeating enforced SMB signing. Recent Windows builds allow same-user re-auth only and refuse the + swap with ACCESS_DENIED. Not derivable from the build number, so it is probed live. + https://blackfield.ch/blog/defeating-enforced-smb-signing/ + """ + name = "reauth_swap" + description = "Check if the server allows a cross-user re-auth swap on an SMB session (used to defeat enforced signing); needs a 2nd account" + supported_protocols = ["smb"] + category = CATEGORY.ENUMERATION + + STATUS_SUCCESS = 0x00000000 + STATUS_MORE_PROCESSING_REQUIRED = 0xC0000016 + STATUS_ACCESS_DENIED = 0xC0000022 + + def options(self, context, module_options): + """ + PROBE_USER second account, must differ from the login account (required) + PROBE_PASS second account password + PROBE_NTHASH second account NT hash (instead of PROBE_PASS) + + The second account is probed over NTLM; its privileges do not matter. + """ + self.probe_user = module_options.get("PROBE_USER") + self.probe_pass = module_options.get("PROBE_PASS", "") + self.probe_nthash = module_options.get("PROBE_NTHASH", "") + if not self.probe_user: + context.log.fail("reauth_swap requires a second account: -o PROBE_USER= PROBE_PASS= (must differ from the account nxc logged in with)") + return False + if not self.probe_pass and not self.probe_nthash: + context.log.fail("reauth_swap requires PROBE_PASS or PROBE_NTHASH for the second account") + return False + + def on_login(self, context, connection): + if self.probe_user.lower() == (connection.username or "").lower(): + context.log.fail("PROBE_USER must differ from the account nxc logged in with: only a cross-user swap is refused, so the same account always succeeds and tests nothing") + return + + # without enforced signing there is nothing to bypass: a relayed auth is accepted directly + if not connection.signing: + context.log.info("SMB signing not enforced, nothing to bypass - skipping") + return + + try: + # fresh session so nxc's own connection is left untouched + new_conn = get_smb_connection(context, connection, timeout=connection.args.smb_timeout) + try: + status = self.reauth_as(new_conn.getSMBServer(), self.probe_user, self.probe_pass, connection.domain, self.probe_nthash) + finally: + with contextlib.suppress(Exception): + new_conn.close() + except Exception as e: + context.log.fail(f"could not probe this host: {e.__class__.__name__}: {e}") + return + + if status == self.STATUS_SUCCESS: + context.log.highlight("VULNERABLE - enforced SMB signing bypassable") + context.log.highlight("Next step: https://blackfield.ch/blog/defeating-enforced-smb-signing/") + elif status == self.STATUS_ACCESS_DENIED: + context.log.display("not vulnerable - cross-user re-auth swap refused (ACCESS_DENIED), same-user re-auth only") + else: + context.log.display(f"inconclusive - second session setup returned 0x{status:08X}") + + def reauth_as(self, smb, user, password, domain, nthash): + """Replay an NTLM (Type1/Type3) session setup as `user` on an EXISTING session; return the NTSTATUS.""" + # Create NTLM Type1 message (challenge) + type1 = ntlm.getNTLMSSPType1("", domain) + blob = SPNEGO_NegTokenInit() + blob["MechTypes"] = [TypesMech["NTLMSSP - Microsoft NTLM Security Support Provider"]] + blob["MechToken"] = type1.getData() + + setup = SMB2SessionSetup() + setup["Flags"] = 0 + setup["SecurityMode"] = 1 + setup["SecurityBufferLength"] = len(blob.getData()) + setup["Buffer"] = blob.getData() + pkt = smb.SMB_PACKET() + pkt["Command"] = SMB2_SESSION_SETUP + pkt["Data"] = setup + + ans = smb.recvSMB(smb.sendSMB(pkt)) + if ans["Status"] != self.STATUS_MORE_PROCESSING_REQUIRED: + return ans["Status"] + + resp = SMB2SessionSetup_Response(ans["Data"]) + try: + type2 = SPNEGO_NegTokenResp(resp["Buffer"])["ResponseToken"] + except Exception: + raw = resp["Buffer"] + type2 = raw[raw.index(b"NTLMSSP"):] + + lm = nt = "" + if nthash: + lm = binascii.unhexlify("aad3b435b51404eeaad3b435b51404ee") + nt = binascii.unhexlify(nthash) + + # Create NTLM Type3 message (authentication) + type3, _ = ntlm.getNTLMSSPType3(type1, type2, user, password, domain, lmhash=lm, nthash=nt) + blob2 = SPNEGO_NegTokenResp() + blob2["ResponseToken"] = type3.getData() + + setup2 = SMB2SessionSetup() + setup2["Flags"] = 0 + setup2["SecurityMode"] = 1 + setup2["SecurityBufferLength"] = len(blob2.getData()) + setup2["Buffer"] = blob2.getData() + + pkt2 = smb.SMB_PACKET() + pkt2["Command"] = SMB2_SESSION_SETUP + pkt2["Data"] = setup2 + + ans2 = smb.recvSMB(smb.sendSMB(pkt2)) + return ans2["Status"] diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index b0b5ee0f..325b097b 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -154,6 +154,7 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M putty netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdcman #netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdp -o ACTION=enable #netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdp -o ACTION=disable +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M reauth_swap -o PROBE_USER=SECOND_USERNAME PROBE_PASS=SECOND_PASSWORD netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M reg-query -o PATH=HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion KEY=DevicePath netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M runasppl netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M scuffy -o SERVER=127.0.0.1 NAME=test