diff --git a/wifite/__main__.py b/wifite/__main__.py index 314bcb1f6..2f7e5c1d9 100755 --- a/wifite/__main__.py +++ b/wifite/__main__.py @@ -72,14 +72,26 @@ def scan_and_attack(self): from .util.scanner import Scanner from .attack.all import AttackAll + attacked_targets = 0 + Color.pl('') # Scan s = Scanner() + do_continue = s.find_targets() targets = s.select_targets() - # Attack - attacked_targets = AttackAll.attack_multiple(targets) + if Configuration.infinite_mode: + while do_continue: + AttackAll.attack_multiple(targets) + do_continue = s.update_targets() + if not do_continue: + break + targets = s.select_targets() + attacked_targets = s.get_num_attacked() + else: + # Attack + attacked_targets = AttackAll.attack_multiple(targets) Color.pl('{+} Finished attacking {C}%d{W} target(s), exiting' % attacked_targets) diff --git a/wifite/args.py b/wifite/args.py index af6b6183a..c8e2782c3 100755 --- a/wifite/args.py +++ b/wifite/args.py @@ -70,6 +70,12 @@ def _add_global_args(self, glob): dest='five_ghz', help=self._verbose('Include 5Ghz channels (default: {G}off{W})')) + glob.add_argument('-inf', + '--infinite', + action='store_true', + dest='infinite_mode', + help=Color.s('Enable infinite attack mode. Modify scanning time with ' + '{C}-p{W} (default: {G}off{W})')) glob.add_argument('-mac', '--random-mac', @@ -95,6 +101,14 @@ def _add_global_args(self, glob): help=Color.s('Kill processes that conflict with Airmon/Airodump ' + '(default: {G}off{W})')) + glob.add_argument('-pow', + '--power', + action='store', + dest='min_power', + metavar='[min_power]', + type=int, + help=Color.s('Attacks any targets with at least {C}min_power{W} signal strength')) + glob.add_argument('-b', action='store', dest='target_bssid', diff --git a/wifite/attack/all.py b/wifite/attack/all.py index 6db4d3718..36a4af81c 100755 --- a/wifite/attack/all.py +++ b/wifite/attack/all.py @@ -81,6 +81,8 @@ def attack_single(cls, target, targets_remaining): return True # Keep attacking other targets (skip) while len(attacks) > 0: + # Needed by infinite attack mode in order to count how many targets were attacked + target.attacked = True attack = attacks.pop(0) try: result = attack.run() @@ -110,7 +112,9 @@ def user_wants_to_continue(cls, targets_remaining, attacks_remaining=0): ''' Asks user if attacks should continue onto other targets Returns: - True if user wants to continue, False otherwise. + None if the user wants to skip the current target + True if the user wants to continue to the next attack on the current target + False if the user wants to stop the remaining attacks ''' if attacks_remaining == 0 and targets_remaining == 0: return # No targets or attacksleft, drop out @@ -128,22 +132,27 @@ def user_wants_to_continue(cls, targets_remaining, attacks_remaining=0): if attacks_remaining > 0: prompt += ' {G}continue{W} attacking,' - options += '{G}C{W}{D}, {W}' + options += '{G}c{W}{D}, {W}' if targets_remaining > 0: prompt += ' {O}skip{W} to the next target,' options += '{O}s{W}{D}, {W}' - options += '{R}e{W})' - prompt += ' or {R}exit{W} %s? {C}' % options + if Configuration.infinite_mode: + options += '{R}r{W})' + prompt += ' or {R}return{W} to scanning %s? {C}' % options + else: + options += '{R}e{W})' + prompt += ' or {R}exit{W} %s? {C}' % options from ..util.input import raw_input - answer = raw_input(Color.s(prompt)).lower() + Color.p(prompt) + answer = raw_input().lower() if answer.startswith('s'): return None # Skip - elif answer.startswith('e'): - return False # Exit + elif answer.startswith('e') or answer.startswith('r'): + return False # Exit/Return else: return True # Continue diff --git a/wifite/attack/wep.py b/wifite/attack/wep.py index cef71cfd9..3096f5d61 100755 --- a/wifite/attack/wep.py +++ b/wifite/attack/wep.py @@ -288,7 +288,8 @@ def user_wants_to_stop(self, current_attack, attacks_remaining, target): attack_index += 1 Color.pl(' {G}%d{W}: {R}Stop attacking, {O}Move onto next target{W}' % attack_index) while True: - answer = raw_input(Color.s('{?} Select an option ({G}1-%d{W}): ' % attack_index)) + Color.p('{?} Select an option ({G}1-%d{W}): ' % attack_index) + answer = raw_input() if not answer.isdigit() or int(answer) < 1 or int(answer) > attack_index: Color.pl('{!} {R}Invalid input: {O}Must enter a number between {G}1-%d{W}' % attack_index) continue diff --git a/wifite/attack/wps.py b/wifite/attack/wps.py index ca4169c88..45f7dfa15 100755 --- a/wifite/attack/wps.py +++ b/wifite/attack/wps.py @@ -33,14 +33,14 @@ def run(self): return False if not Configuration.wps_pixie and self.pixie_dust: - Color.pl('\r{!} {O}--no-pixie{R} was given, ignoring WPS PIN Attack on ' + - '{O}%s{W}' % self.target.essid) + Color.pl('\r{!} {O}--no-pixie{R} was given, ignoring WPS Pixie-Dust Attack ' + + 'on {O}%s{W}' % self.target.essid) self.success = False return False if not Configuration.wps_pin and not self.pixie_dust: - Color.pl('\r{!} {O}--no-pin{R} was given, ignoring WPS Pixie-Dust Attack ' + - 'on {O}%s{W}' % self.target.essid) + Color.pl('\r{!} {O}--pixie{R} was given, ignoring WPS PIN Attack on ' + + '{O}%s{W}' % self.target.essid) self.success = False return False diff --git a/wifite/config.py b/wifite/config.py index 9759ff5b5..df4c56917 100755 --- a/wifite/config.py +++ b/wifite/config.py @@ -8,7 +8,7 @@ class Configuration(object): ''' Stores configuration variables and functions for Wifite. ''' - version = '2.2.5' + version = '2.2.6' initialized = False # Flag indicating config has been initialized temp_dir = None # Temporary directory @@ -38,12 +38,15 @@ def initialize(cls, load_interface=True): cls.tx_power = 0 # Wifi transmit power (0 is default) cls.interface = None + cls.min_power = 0 # Minimum power for an access point to be considered a target. Default is 0 cls.target_channel = None # User-defined channel to scan cls.target_essid = None # User-defined AP name cls.target_bssid = None # User-defined AP BSSID cls.ignore_essid = None # ESSIDs to ignore cls.clients_only = False # Only show targets that have associated clients cls.five_ghz = False # Scan 5Ghz channels + cls.infinite_mode = False # Attack targets continuously + cls.inf_wait_time = 60 cls.show_bssids = False # Show BSSIDs in targets list cls.random_mac = False # Should generate a random Mac address at startup. cls.no_deauth = False # Deauth hidden networks & WPA handshake targets @@ -197,6 +200,15 @@ def parse_settings_args(cls, args): cls.five_ghz = True Color.pl('{+} {C}option:{W} including {G}5Ghz networks{W} in scans') + if args.infinite_mode: + cls.infinite_mode = True + Color.p('{+} {C}option:{W} ({G}infinite{W}) attack all neighbors forever') + if not args.scan_time: + Color.p('; {O}pillage time not selected{W}, ' + 'using default {G}%d{W}s' % cls.inf_wait_time) + args.scan_time = cls.inf_wait_time + Color.pl('') + if args.show_bssids == True: cls.show_bssids = True Color.pl('{+} {C}option:{W} showing {G}bssids{W} of targets during scan') @@ -211,6 +223,11 @@ def parse_settings_args(cls, args): Color.pl('{+} {C}option:{W} send {G}%d{W} deauth packets when deauthing' % ( cls.num_deauths)) + if args.min_power and args.min_power > 0: + cls.min_power = args.min_power + Color.pl('{+} {C}option:{W} Minimum power {G}%d{W} for target to be shown' % + cls.min_power) + if args.target_essid: cls.target_essid = args.target_essid Color.pl('{+} {C}option:{W} targeting ESSID {G}%s{W}' % args.target_essid) diff --git a/wifite/model/target.py b/wifite/model/target.py index 9f3422095..63b5463f8 100755 --- a/wifite/model/target.py +++ b/wifite/model/target.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- from ..util.color import Color +from ..config import Configuration import re @@ -10,6 +11,43 @@ class WPSState: NONE, UNLOCKED, LOCKED, UNKNOWN = range(0, 4) +class ArchivedTarget(object): + ''' + Holds information between scans from a previously found target + ''' + def __init__(self, target): + self.bssid = target.bssid + self.channel = target.channel + self.decloaked = target.decloaked + self.attacked = target.attacked + self.essid = target.essid + self.essid_known = target.essid_known + self.essid_len = target.essid_len + + def transfer_info(self, other): + ''' + Helper function to transfer relevant fields into another Target or ArchivedTarget + ''' + other.attacked = self.attacked + + # If both targets know the essid, keep decloacked value + if self.essid_known and other.essid_known: + other.decloaked = self.decloaked + + # The destination target does not know the essid but the source + # does, copy that information + if self.essid_known and not other.essid_known: + other.decloaked = self.decloaked + other.essid = self.essid + other.essid_known = self.essid_known + other.essid_len = self.essid_len + + def __eq__(self, other): + # Check if the other class type is either ArchivedTarget or Target + return (isinstance(other, self.__class__) or isinstance(other, Target)) \ + and self.bssid == other.bssid + + class Target(object): ''' Holds details for a 'Target' aka Access Point (e.g. router). @@ -51,6 +89,7 @@ def __init__(self, fields): self.power = int(fields[8].strip()) if self.power < 0: self.power += 100 + self.max_power = self.power self.beacons = int(fields[9].strip()) self.ivs = int(fields[10].strip()) @@ -69,10 +108,42 @@ def __init__(self, fields): self.decloaked = False # If ESSID was hidden but we decloaked it. + # Will be set to true once this target will be attacked + # Needed to count targets in infinite attack mode + self.attacked = False + self.clients = [] self.validate() + def __eq__(self, other): + # Check if the other class type is either ArchivedTarget or Target + return (isinstance(other, self.__class__) or isinstance(other, ArchivedTarget)) \ + and self.bssid == other.bssid + + def transfer_info(self, other): + ''' + Helper function to transfer relevant fields into another Target or ArchivedTarget + ''' + other.wps = self.wps + other.attacked = self.attacked + + if other.max_power < self.max_power: + other.max_power = self.max_power + + # If both targets know the essid, keep decloacked value + if self.essid_known and other.essid_known: + other.decloaked = self.decloaked + + # The destination target does not know the essid but the source + # does, copy that information + if self.essid_known and not other.essid_known: + other.decloaked = self.decloaked + other.essid = self.essid + other.essid_known = self.essid_known + other.essid_len = self.essid_len + + def validate(self): ''' Checks that the target is valid. ''' if self.channel == '-1': @@ -145,6 +216,8 @@ def to_str(self, show_bssid=False): wps = Color.s('{R}lock') elif self.wps == WPSState.UNKNOWN: wps = Color.s('{O} n/a') + else: + wps = ' ERR' clients = ' ' if len(self.clients) > 0: diff --git a/wifite/tools/airmon.py b/wifite/tools/airmon.py index a2a45381b..c694beb50 100755 --- a/wifite/tools/airmon.py +++ b/wifite/tools/airmon.py @@ -55,6 +55,8 @@ class Airmon(Dependency): dependency_name = 'airmon-ng' dependency_url = 'https://www.aircrack-ng.org/install.html' + chipset_table = 'https://wikidevi.com/wiki/Wireless_adapters/Chipset_table' + base_interface = None killed_network_manager = False @@ -107,6 +109,19 @@ def get_interfaces(): return interfaces + @staticmethod + def get_iface_info(interface_name): + ''' + Get interface info (driver, chipset), based on interface name. + Returns an AirmonIface if interface name is found by airmon-ng or None + ''' + for iface in Airmon.get_interfaces(): + if iface.interface == interface_name: + return iface + + return None + + @staticmethod def start_bad_driver(iface): ''' @@ -308,8 +323,8 @@ def ask(): choice = 1 else: # Multiple interfaces found - question = Color.s('{+} Select wireless interface ({G}1-%d{W}): ' % (count)) - choice = raw_input(question) + Color.p('{+} Select wireless interface ({G}1-%d{W}): ' % (count)) + choice = raw_input() iface = a.get(choice) diff --git a/wifite/tools/airodump.py b/wifite/tools/airodump.py index 0092af117..698987138 100755 --- a/wifite/tools/airodump.py +++ b/wifite/tools/airodump.py @@ -48,7 +48,6 @@ def __init__(self, interface=None, channel=None, encryption=None,\ # 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 self.delete_existing_files = delete_existing_files @@ -83,6 +82,9 @@ def __enter__(self): if self.ivs_only: command.extend(['--output-format', 'ivs,csv']) else: command.extend(['--output-format', 'pcap,csv']) + # Store value for debugging + self.command = command + # Start the process self.pid = Process(command, devnull=True) return self @@ -138,7 +140,7 @@ def delete_airodump_temp_files(cls, output_file_prefix): if fil.startswith('replay_') and fil.endswith('.cap') or fil.endswith('.xor'): os.remove(os.path.join(temp_dir, fil)) - def get_targets(self, old_targets=[], apply_filter=True): + def get_targets(self, old_targets=[], apply_filter=True, target_archives={}): ''' Parses airodump's CSV file, returns list of Targets ''' # Find the .CSV file @@ -150,40 +152,46 @@ def get_targets(self, old_targets=[], apply_filter=True): if csv_filename is None or not os.path.exists(csv_filename): return self.targets # No file found - targets = Airodump.get_targets_from_csv(csv_filename) - for old_target in old_targets: - for target in targets: - if old_target.bssid == target.bssid: - target.wps = old_target.wps + new_targets = Airodump.get_targets_from_csv(csv_filename) + + # Check if one of the targets is also contained in the old_targets + for new_target in new_targets: + just_found = True + for old_target in old_targets: + # If the new_target is found in old_target copy attributes from old target + if old_target == new_target: + # Identify decloaked targets + if new_target.essid_known and not old_target.essid_known: + # We decloaked a target! + new_target.decloaked = True + + old_target.transfer_info(new_target) + just_found = False + break; + + # If the new_target is not in old_targets, check target_archives + # and copy attributes from there + if just_found and new_target.bssid in target_archives: + target_archives[new_target.bssid].transfer_info(new_target) + # Check targets for WPS if not self.skip_wps: capfile = csv_filename[:-3] + 'cap' try: - Tshark.check_for_wps_and_update_targets(capfile, targets) + Tshark.check_for_wps_and_update_targets(capfile, new_targets) except ValueError: # No tshark, or it failed. Fall-back to wash - Wash.check_for_wps_and_update_targets(capfile, targets) + Wash.check_for_wps_and_update_targets(capfile, new_targets) if apply_filter: - # Filter targets based on encryption & WPS capability - targets = Airodump.filter_targets(targets, skip_wps=self.skip_wps) + # Filter targets based on encryption, WPS capability & power + new_targets = Airodump.filter_targets(new_targets, skip_wps=self.skip_wps) # Sort by power - targets.sort(key=lambda x: x.power, reverse=True) + new_targets.sort(key=lambda x: x.power, reverse=True) - # Identify decloaked targets - 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) - - self.targets = targets + self.targets = new_targets self.deauth_hidden_targets() return self.targets @@ -255,6 +263,12 @@ def filter_targets(targets, skip_wps=False): result = [] # Filter based on Encryption for target in targets: + # Filter targets if --power + # TODO Filter a target based on the current power - not on the max power + # as soon as losing targets in a single scan does not cause excessive output + if Configuration.min_power > 0 and target.max_power < Configuration.min_power: + continue + if Configuration.clients_only and len(target.clients) == 0: continue if 'WEP' in Configuration.encryption_filter and 'WEP' in target.encryption: diff --git a/wifite/tools/tshark.py b/wifite/tools/tshark.py index f7ab6f395..831da2427 100755 --- a/wifite/tools/tshark.py +++ b/wifite/tools/tshark.py @@ -179,7 +179,12 @@ def check_for_wps_and_update_targets(capfile, targets): try: p.wait() lines = p.stdout() - except: + except Exception as e: + # Manually check for keyboard interrupt as only python 3.x throws + # exceptions for subprocess.wait() + if isinstance(e, KeyboardInterrupt): + raise KeyboardInterrupt + # Failure is acceptable return diff --git a/wifite/tools/wash.py b/wifite/tools/wash.py index 2ef48382b..ef6d78a40 100755 --- a/wifite/tools/wash.py +++ b/wifite/tools/wash.py @@ -31,7 +31,12 @@ def check_for_wps_and_update_targets(capfile, targets): try: p.wait() lines = p.stdout() - except: + except Exception as e: + # Manually check for keyboard interrupt as only python 3.x throws + # exceptions for subprocess.wait() + if isinstance(e, KeyboardInterrupt): + raise KeyboardInterrupt + # Failure is acceptable return @@ -47,7 +52,9 @@ def check_for_wps_and_update_targets(capfile, targets): wps_bssids.add(bssid) else: locked_bssids.add(bssid) - except: + except Exception as e: + if isinstance(e, KeyboardInterrupt): + raise KeyboardInterrupt pass # Update targets diff --git a/wifite/util/scanner.py b/wifite/util/scanner.py index 29b11db28..a619c45d1 100755 --- a/wifite/util/scanner.py +++ b/wifite/util/scanner.py @@ -3,12 +3,14 @@ from ..util.color import Color from ..tools.airodump import Airodump +from ..tools.airmon import Airmon from ..util.input import raw_input, xrange -from ..model.target import Target, WPSState +from ..model.target import Target, WPSState, ArchivedTarget from ..config import Configuration from time import sleep, time + class Scanner(object): ''' Scans wifi networks & provides menu for selecting targets ''' @@ -16,40 +18,48 @@ class Scanner(object): UP_CHAR = '\x1B[1F' def __init__(self): + self.previous_target_count = 0 + self.target_archives = {} + self.targets = [] + self.target = None # Target specified by user (based on ESSID/BSSID) + self.err_msg = None + self.airodump_iface = None + + def check_running(self, airodump): + exit_code = airodump.pid.poll() + if exit_code is not None: + self.err_msg = "{!} {O}Airodump crashed with status code {R}%s{O}." % exit_code + self.err_msg += " Airodump was started with: {C}%s{W}" % airodump.command + return False # Airodump process died + return True + + def find_targets(self): ''' Scans for targets via Airodump. Loops until scan is interrupted via user or config. Note: Sets this object's `targets` attrbute (list[Target]) upon interruption. ''' - self.previous_target_count = 0 - self.targets = [] - self.target = None # Target specified by user (based on ESSID/BSSID) - max_scan_time = Configuration.scan_time - self.err_msg = None - # Loads airodump with interface/channel/etc from Configuration try: with Airodump() as airodump: # Loop until interrupted (Ctrl+C) scan_start_time = time() + self.airodump_iface = airodump.interface while True: - if airodump.pid.poll() is not None: - return # Airodump process died + if not self.check_running(airodump): + return True # Airodump process died - self.targets = airodump.get_targets(old_targets=self.targets) + self.targets = airodump.get_targets(old_targets=self.targets, + target_archives=self.target_archives) if self.found_target(): - return # We found the target we want - - if airodump.pid.poll() is not None: - return # Airodump process died + return True # We found the target we want - for target in self.targets: - if target.bssid in airodump.decloaked_bssids: - target.decloaked = True + if not self.check_running(airodump): + return True # Airodump process died self.print_targets() @@ -67,13 +77,52 @@ def __init__(self): Color.p(outline) if max_scan_time > 0 and time() > scan_start_time + max_scan_time: - return + return True sleep(1) except KeyboardInterrupt: - pass + if not Configuration.infinite_mode: + return True + + from ..util.input import raw_input + + options = '({G}s{W}{D}, {W}{R}e{W})' + prompt = '{+} Do you want to {G}start attacking{W} or {R}exit{W}%s?' % options + + self.print_targets() + Color.clear_entire_line() + Color.p(prompt) + answer = raw_input().lower() + if answer.startswith('e'): + return False + + return True + + def update_targets(self): + ''' + Archive all the old targets + Returns: True if user wants to stop attack, False otherwise + ''' + self.previous_target_count = 0 + for target in self.targets: + self.target_archives[target.bssid] = ArchivedTarget(target) + + self.targets = [] + do_continue = self.find_targets() + return do_continue + + def get_num_attacked(self): + ''' + Returns: number of attacked targets by this scanner + ''' + attacked_targets = 0 + for target in self.target_archives.values(): + if target.attacked: + attacked_targets += 1 + + return attacked_targets def found_target(self): ''' @@ -169,6 +218,7 @@ def select_targets(self): ''' Returns list(target) Either a specific target if user specified -bssid or --essid. + If the user used pillage or infinite attack mode retuns all the targets Otherwise, prompts user to select targets and returns the selection. ''' @@ -179,16 +229,23 @@ def select_targets(self): if len(self.targets) == 0: if self.err_msg is not None: Color.pl(self.err_msg) + self.err_msg = None + + message = "{!} {O}No targets found. You may need to wait longer or you " \ + "may have issues with your wifi card.\n" + if self.airodump_iface is not None: + info = Airmon.get_iface_info(self.airodump_iface) + message += "{!} {O}Airodump was ran on:\n" + message += "\t iface - {G}%s{O}\n" % info.interface + message += "\t driver - {G}%s{O}\n" % info.driver + message += "\tchipset - {G}%s{O}\n" % info.chipset + message += "{!} {O}Check if your chipset/driver supports monitor mode{O}: {C}%s{W}" % Airmon.chipset_table - # TODO Print a more-helpful reason for failure. - # 1. Link to wireless drivers wiki, - # 2. How to check if your device supporst monitor mode, - # 3. Provide airodump-ng command being executed. - raise Exception('No targets found.' - + ' You may need to wait longer,' - + ' or you may have issues with your wifi card') + Color.pl(message) + return [] # Return all targets if user specified a wait time ('pillage'). + # A scan time is always set if run in infinite mode if Configuration.scan_time > 0: return self.targets @@ -198,6 +255,7 @@ def select_targets(self): if self.err_msg is not None: Color.pl(self.err_msg) + self.err_msg = None input_str = '{+} select target(s)' input_str += ' ({G}1-%d{W})' % len(self.targets) @@ -206,7 +264,8 @@ def select_targets(self): chosen_targets = [] - for choice in raw_input(Color.s(input_str)).split(','): + Color.p(input_str) + for choice in raw_input().split(','): choice = choice.strip() if choice.lower() == 'all': chosen_targets = self.targets @@ -226,8 +285,10 @@ def select_targets(self): if __name__ == '__main__': # 'Test' script will display targets and selects the appropriate one Configuration.initialize() + targets = [] try: s = Scanner() + s.find_targets() targets = s.select_targets() except Exception as e: Color.pl('\r {!} {R}Error{W}: %s' % str(e))