Decloaked ESSIDs will have a "*" next to their name. For #78 While testing, I found that Wifite did not parse Airodump's CSV correctly. Specifically, ESSIDs with commas or trailing spaces. Fixed in this commit. Also fixed hidden ESSID detection introduced by the new CSV parsing logic.
325 lines
11 KiB
Python
Executable File
325 lines
11 KiB
Python
Executable File
#!/usr/bin/python2.7
|
|
# -*- coding: utf-8 -*-
|
|
|
|
from .tshark import Tshark
|
|
from .wash import Wash
|
|
from ..util.process import Process
|
|
from ..config import Configuration
|
|
from ..model.target import Target
|
|
from ..model.client import Client
|
|
|
|
import os, time
|
|
|
|
class Airodump(object):
|
|
''' Wrapper around airodump-ng program '''
|
|
|
|
def __init__(self, interface=None, channel=None, encryption=None,\
|
|
wps=False, target_bssid=None, output_file_prefix='airodump',\
|
|
ivs_only=False, skip_wps=False):
|
|
'''
|
|
Sets up airodump arguments, doesn't start process yet
|
|
'''
|
|
|
|
Configuration.initialize()
|
|
|
|
if interface is None:
|
|
interface = Configuration.interface
|
|
if interface is None:
|
|
raise Exception("Wireless interface must be defined (-i)")
|
|
self.interface = interface
|
|
|
|
self.targets = []
|
|
|
|
if channel is None:
|
|
channel = Configuration.target_channel
|
|
self.channel = channel
|
|
self.five_ghz = Configuration.five_ghz
|
|
|
|
self.encryption = encryption
|
|
self.wps = wps
|
|
|
|
self.target_bssid = target_bssid
|
|
self.output_file_prefix = output_file_prefix
|
|
self.ivs_only = ivs_only
|
|
self.skip_wps = skip_wps
|
|
|
|
# For tracking decloaked APs (previously were hidden)
|
|
self.decloaking = False
|
|
self.decloaked_bssids = set()
|
|
self.decloaked_times = {} # Map of BSSID(str) -> epoch(int) of last deauth
|
|
|
|
|
|
def __enter__(self):
|
|
'''
|
|
Setting things up for this context.
|
|
Called at start of 'with Airodump(...) as x:'
|
|
Actually starts the airodump process.
|
|
'''
|
|
self.delete_airodump_temp_files()
|
|
|
|
self.csv_file_prefix = Configuration.temp() + self.output_file_prefix
|
|
|
|
# Build the command
|
|
command = [
|
|
'airodump-ng',
|
|
self.interface,
|
|
'-a', # Only show associated clients
|
|
'-w', self.csv_file_prefix, # Output file prefix
|
|
'--write-interval', '1' # Write every second
|
|
]
|
|
if self.channel:
|
|
command.extend(['-c', str(self.channel)])
|
|
elif self.five_ghz:
|
|
command.extend(['--band', 'a'])
|
|
|
|
if self.encryption:
|
|
command.extend(['--enc', self.encryption])
|
|
if self.wps:
|
|
command.extend(['--wps'])
|
|
if self.target_bssid:
|
|
command.extend(['--bssid', self.target_bssid])
|
|
|
|
if self.ivs_only:
|
|
command.extend(['--output-format', 'ivs,csv'])
|
|
else:
|
|
command.extend(['--output-format', 'pcap,csv'])
|
|
|
|
# Start the process
|
|
self.pid = Process(command, devnull=True)
|
|
return self
|
|
|
|
|
|
def __exit__(self, type, value, traceback):
|
|
'''
|
|
Tearing things down since the context is being exited.
|
|
Called after 'with Airodump(...)' goes out of scope.
|
|
'''
|
|
# Kill the process
|
|
self.pid.interrupt()
|
|
|
|
# Delete temp files
|
|
self.delete_airodump_temp_files()
|
|
|
|
|
|
def find_files(self, endswith=None):
|
|
''' Finds all files in the temp directory that start with the output_file_prefix '''
|
|
result = []
|
|
for fil in os.listdir(Configuration.temp()):
|
|
if fil.startswith(self.output_file_prefix):
|
|
if not endswith or fil.endswith(endswith):
|
|
result.append(Configuration.temp() + fil)
|
|
return result
|
|
|
|
def delete_airodump_temp_files(self):
|
|
'''
|
|
Deletes airodump* files in the temp directory.
|
|
Also deletes replay_*.cap and *.xor files in pwd.
|
|
'''
|
|
# Remove all temp files
|
|
for fil in self.find_files():
|
|
os.remove(fil)
|
|
|
|
# Remove .cap and .xor files from pwd
|
|
for fil in os.listdir('.'):
|
|
if fil.startswith('replay_') and fil.endswith('.cap') or fil.endswith('.xor'):
|
|
os.remove(fil)
|
|
|
|
def get_targets(self, apply_filter=True):
|
|
''' Parses airodump's CSV file, returns list of Targets '''
|
|
|
|
# Find the .CSV file
|
|
csv_filename = None
|
|
for fil in self.find_files(endswith='-01.csv'):
|
|
# Found the file
|
|
csv_filename = fil
|
|
break
|
|
if csv_filename is None or not os.path.exists(csv_filename):
|
|
# No file found
|
|
return self.targets
|
|
|
|
# Parse the .CSV file
|
|
targets = Airodump.get_targets_from_csv(csv_filename)
|
|
|
|
# Check targets for WPS
|
|
if not self.skip_wps:
|
|
capfile = csv_filename[:-3] + 'cap'
|
|
try:
|
|
Tshark.check_for_wps_and_update_targets(capfile, targets)
|
|
except Exception as e:
|
|
# No tshark, or it failed. Fall-back to wash
|
|
Wash.check_for_wps_and_update_targets(capfile, targets)
|
|
|
|
if apply_filter:
|
|
# Filter targets based on encryption & WPS capability
|
|
targets = Airodump.filter_targets(targets, skip_wps=self.skip_wps)
|
|
|
|
# Sort by power
|
|
targets.sort(key=lambda x: x.power, reverse=True)
|
|
|
|
for old_target in self.targets:
|
|
for new_target in targets:
|
|
if old_target.bssid != new_target.bssid: continue
|
|
if new_target.essid_known and not old_target.essid_known:
|
|
# We decloaked a target!
|
|
new_target.decloaked = True
|
|
self.decloaked_bssids.add(new_target.bssid)
|
|
|
|
if self.pid.poll() is not None:
|
|
raise Exception('Airodump has stopped')
|
|
|
|
self.targets = targets
|
|
self.deauth_hidden_targets()
|
|
|
|
return self.targets
|
|
|
|
|
|
@staticmethod
|
|
def get_targets_from_csv(csv_filename):
|
|
'''
|
|
Returns list of Target objects parsed from CSV file
|
|
'''
|
|
targets = []
|
|
import csv
|
|
with open(csv_filename, 'rb') as csvopen:
|
|
lines = []
|
|
for line in csvopen:
|
|
if type(line) is bytes: line = line.decode('utf-8')
|
|
line = line.replace('\0', '')
|
|
lines.append(line)
|
|
csv_reader = csv.reader(lines,
|
|
delimiter=',',
|
|
quoting=csv.QUOTE_ALL,
|
|
skipinitialspace=True,
|
|
escapechar='\\')
|
|
|
|
hit_clients = False
|
|
for row in csv_reader:
|
|
# Each "row" is a list of fields for a target/client
|
|
|
|
if len(row) == 0: continue
|
|
|
|
if row[0].strip() == 'BSSID':
|
|
# This is the "header" for the list of Targets
|
|
hit_clients = False
|
|
continue
|
|
|
|
elif row[0].strip() == 'Station MAC':
|
|
# This is the "header" for the list of Clients
|
|
hit_clients = True
|
|
continue
|
|
|
|
if hit_clients:
|
|
# The current row corresponds to a "Client" (computer)
|
|
try:
|
|
client = Client(row)
|
|
except (IndexError, ValueError) as e:
|
|
# Skip if we can't parse the client row
|
|
continue
|
|
|
|
if 'not associated' in client.bssid:
|
|
# Ignore unassociated clients
|
|
continue
|
|
|
|
# Add this client to the appropriate Target
|
|
for t in targets:
|
|
if t.bssid == client.bssid:
|
|
t.clients.append(client)
|
|
break
|
|
|
|
else:
|
|
# The current row corresponds to a "Target" (router)
|
|
try:
|
|
target = Target(row)
|
|
targets.append(target)
|
|
except Exception:
|
|
continue
|
|
|
|
return targets
|
|
|
|
@staticmethod
|
|
def filter_targets(targets, skip_wps=False):
|
|
''' Filters targets based on Configuration '''
|
|
result = []
|
|
# Filter based on Encryption
|
|
for target in targets:
|
|
if Configuration.clients_only and len(target.clients) == 0:
|
|
continue
|
|
if 'WEP' in Configuration.encryption_filter and 'WEP' in target.encryption:
|
|
result.append(target)
|
|
elif 'WPA' in Configuration.encryption_filter and 'WPA' in target.encryption:
|
|
result.append(target)
|
|
elif 'WPS' in Configuration.encryption_filter and target.wps:
|
|
result.append(target)
|
|
elif skip_wps:
|
|
result.append(target)
|
|
|
|
# Filter based on BSSID/ESSID
|
|
bssid = Configuration.target_bssid
|
|
essid = Configuration.target_essid
|
|
i = 0
|
|
while i < len(result):
|
|
if result[i].essid is not None and Configuration.ignore_essid is not None and Configuration.ignore_essid.lower() in result[i].essid.lower():
|
|
result.pop(i)
|
|
elif bssid and result[i].bssid.lower() != bssid.lower():
|
|
result.pop(i)
|
|
elif essid and result[i].essid and result[i].essid.lower() != essid.lower():
|
|
result.pop(i)
|
|
else:
|
|
i += 1
|
|
return result
|
|
|
|
def deauth_hidden_targets(self):
|
|
'''
|
|
Sends deauths (to broadcast and to each client) for all
|
|
targets (APs) that have unknown ESSIDs (hidden router names).
|
|
'''
|
|
self.decloaking = False
|
|
|
|
# Do not deauth if requested
|
|
if Configuration.no_deauth: return
|
|
|
|
# Do not deauth if channel is not fixed.
|
|
if self.channel is None: return
|
|
|
|
# Reusable deauth command
|
|
deauth_cmd = [
|
|
'aireplay-ng',
|
|
'-0', # Deauthentication
|
|
str(Configuration.num_deauths), # Number of deauth packets to send
|
|
'--ignore-negative-one'
|
|
]
|
|
for target in self.targets:
|
|
if target.essid_known: continue
|
|
now = int(time.time())
|
|
secs_since_decloak = now - self.decloaked_times.get(target.bssid, 0)
|
|
# Decloak every AP once every 30 seconds
|
|
if secs_since_decloak < 30: continue
|
|
self.decloaking = True
|
|
self.decloaked_times[target.bssid] = now
|
|
if Configuration.verbose > 1:
|
|
from ..util.color import Color
|
|
verbout = " [?] Deauthing %s" % target.bssid
|
|
verbout += " (broadcast & %d clients)" % len(target.clients)
|
|
Color.pe("\n{C}" + verbout + "{W}")
|
|
# Deauth broadcast
|
|
iface = Configuration.interface
|
|
Process(deauth_cmd + ['-a', target.bssid, iface])
|
|
# Deauth clients
|
|
for client in target.clients:
|
|
Process(deauth_cmd + ['-a', target.bssid, '-c', client.bssid, iface])
|
|
|
|
if __name__ == '__main__':
|
|
''' Example usage. wlan0mon should be in Monitor Mode '''
|
|
with Airodump() as airodump:
|
|
|
|
from time import sleep
|
|
sleep(7)
|
|
|
|
from ..util.color import Color
|
|
|
|
targets = airodump.get_targets()
|
|
for idx, target in enumerate(targets, start=1):
|
|
Color.pl(' {G}%s %s' % (str(idx).rjust(3), target.to_str()))
|
|
|
|
Configuration.delete_temp()
|