Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement ignore feature #322

Merged
merged 3 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion wifite/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ def start(self):
from .util.crack import CrackHelper

if Configuration.show_cracked:
CrackResult.display()
CrackResult.display('cracked')

elif Configuration.show_ignored:
CrackResult.display('ignored')

elif Configuration.check_handshake:
Handshake.check()
Expand Down
5 changes: 5 additions & 0 deletions wifite/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,11 @@ def _add_command_args(commands):
dest='cracked',
help=Color.s('Print previously-cracked access points'))

commands.add_argument('--ignored',
action='store_true',
dest='ignored',
help=Color.s('Print ignored access points'))

commands.add_argument('-cracked',
help=argparse.SUPPRESS,
action='store_true',
Expand Down
32 changes: 24 additions & 8 deletions wifite/attack/all.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from enum import Enum
from .pmkid import AttackPMKID
from .wep import AttackWEP
from .wpa import AttackWPA
Expand All @@ -9,6 +10,11 @@
from ..model.target import WPSState
from ..util.color import Color

class Answer(Enum):
Skip = 1
ExitOrReturn = 2
Continue = 3
Ignore = 4

class AttackAll(object):
@classmethod
Expand Down Expand Up @@ -109,10 +115,14 @@ def attack_single(cls, target, targets_remaining):
except KeyboardInterrupt:
Color.pl('\n{!} {O}Interrupted{W}\n')
answer = cls.user_wants_to_continue(targets_remaining, len(attacks))
if answer is True:
if answer == Answer.Continue:
continue # Keep attacking the same target (continue)
elif answer is None:
elif answer == Answer.Skip:
return True # Keep attacking other targets (skip)
elif answer == Answer.Ignore:
from ..model.result import CrackResult
CrackResult.ignore_target(target)
return True # Ignore current target and keep attacking other targets (ignore)
else:
return False # Stop all attacks (exit)

Expand All @@ -126,9 +136,10 @@ def user_wants_to_continue(cls, targets_remaining, attacks_remaining=0):
"""
Asks user if attacks should continue onto other targets
Returns:
None if the user wants to skip the current target
True if the user wants to continue to the next attack on the current target
False if the user wants to stop the remaining attacks
Answer.Skip if the user wants to skip the current target
Answer.Ignore if the user wants to ignore the current target
Answer.Continue if the user wants to continue to the next attack on the current target
Answer.ExitOrReturn if the user wants to stop the remaining attacks
"""
if attacks_remaining == 0 and targets_remaining == 0:
return # No targets or attacksleft, drop out
Expand All @@ -152,6 +163,9 @@ def user_wants_to_continue(cls, targets_remaining, attacks_remaining=0):
prompt += ' {O}skip{W} to the next target,'
options += '{O}s{W}{D}, {W}'

prompt += ' skip and {P}ignore{W} current target,'
options += '{P}i{W}{D}, {W}'

if Configuration.infinite_mode:
options += '{R}r{W})'
prompt += ' or {R}return{W} to scanning %s? {C}' % options
Expand All @@ -163,8 +177,10 @@ def user_wants_to_continue(cls, targets_remaining, attacks_remaining=0):
answer = input().lower()

if answer.startswith('s'):
return None # Skip
return Answer.Skip
elif answer.startswith('e') or answer.startswith('r'):
return False # Exit/Return
return Answer.ExitOrReturn # Exit/Return
elif answer.startswith('i'):
return Answer.Ignore # Ignore
else:
return True # Continue
return Answer.Continue # Continue
11 changes: 8 additions & 3 deletions wifite/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class Configuration(object):
scan_time = None
show_bssids = None
show_cracked = None
show_ignored = None
show_manufacturers = None
skip_crack = None
target_bssid = None
Expand Down Expand Up @@ -209,6 +210,7 @@ def initialize(cls, load_interface=True):

# Commands
cls.show_cracked = False
cls.show_ignored = False
cls.check_handshake = None
cls.crack_handshake = False

Expand Down Expand Up @@ -257,6 +259,8 @@ def load_from_arguments(cls):
# Commands
if args.cracked:
cls.show_cracked = True
if args.ignored:
cls.show_ignored = True
if args.check_handshake:
cls.check_handshake = args.check_handshake
if args.crack_handshake:
Expand Down Expand Up @@ -356,10 +360,11 @@ def parse_settings_args(cls, args):
Color.pl('{+} {C}option: {O}ignoring ESSID(s): {R}%s{W}' %
', '.join(args.ignore_essids))

from .model.result import CrackResult
cls.ignore_cracked = CrackResult.load_ignored_bssids(args.ignore_cracked)

if args.ignore_cracked:
from .model.result import CrackResult
if cracked_targets := CrackResult.load_all():
cls.ignore_cracked = [item['bssid'] for item in cracked_targets]
if cls.ignore_cracked:
Color.pl('{+} {C}option: {O}ignoring {R}%s{O} previously-cracked targets' % len(cls.ignore_cracked))

else:
Expand Down
49 changes: 49 additions & 0 deletions wifite/model/ignored_result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from ..util.color import Color
from ..model.result import CrackResult
from contextlib import contextmanager, redirect_stderr, redirect_stdout
from os import devnull


@contextmanager
def suppress_stdout_stderr():
"""A context manager that redirects stdout and stderr to devnull"""
with open(devnull, 'w') as fnull:
with redirect_stderr(fnull) as err, redirect_stdout(fnull) as out:
yield err, out


class CrackResultIgnored(CrackResult):
def __init__(self, bssid, essid):
self.result_type = 'IGN'
self.bssid = bssid
self.essid = essid
super(CrackResultIgnored, self).__init__()
Comment on lines +19 to +23
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider adding validation for bssid and essid parameters in the constructor

It might be beneficial to add some basic validation for the bssid and essid parameters in the constructor. This could include checking for correct format of BSSID and ensuring that ESSID is not empty or too long.

    def __init__(self, bssid, essid):
        if not self._is_valid_bssid(bssid):
            raise ValueError("Invalid BSSID format")
        if not essid or len(essid) > 32:
            raise ValueError("ESSID must not be empty and not exceed 32 characters")
        self.result_type = 'IGN'
        self.bssid = bssid
        self.essid = essid
        super(CrackResultIgnored, self).__init__()


def dump(self):
if self.essid is not None:
Color.pl(f'{{+}} {"ESSID".rjust(12)}: {{C}}{self.essid}{{W}}')
Color.pl('{+} %s: {C}%s{W}' % ('BSSID'.rjust(12), self.bssid))

def print_single_line(self, longest_essid):
self.print_single_line_prefix(longest_essid)
Color.p('{G}%s{W}' % 'IGN'.ljust(9))
Color.pl('')

def to_dict(self):
with suppress_stdout_stderr():
print('@@@ to dict', self.__dict__)
return {
'type': self.result_type,
'date': self.date,
'essid': self.essid,
'bssid': self.bssid,
}


if __name__ == '__main__':
crw = CrackResultIgnored('AA:BB:CC:DD:EE:FF', 'Test Router')
crw.dump()
crw.save()
76 changes: 64 additions & 12 deletions wifite/model/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,28 +64,33 @@ def save(self):
saved_results.append(self.to_dict())
with open(name, 'w') as fid:
fid.write(dumps(saved_results, indent=2))
Color.pl('{+} saved crack result to {C}%s{W} ({G}%d total{W})'
Color.pl('{+} saved result to {C}%s{W} ({G}%d total{W})'
% (name, len(saved_results)))

@classmethod
def display(cls):
""" Show cracked targets from cracked file """
def display(cls, result_type):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider refactoring the 'display' method into smaller, more focused methods.

The display method has indeed become more complex with the addition of the result_type parameter and conditional logic. To improve readability and maintainability, consider refactoring this method into smaller, more focused methods:

  1. Extract the loading and filtering logic into a separate method:
def get_filtered_results(cls, result_type):
    targets = cls.load_all()
    if result_type == 'cracked':
        return [item for item in targets if item.get('type') != 'IGN']
    else:
        return [item for item in targets if item.get('type') == 'IGN']
  1. Simplify the display method by using the new get_filtered_results method:
@classmethod
def display(cls, result_type):
    name = cls.cracked_file
    if not os.path.exists(name):
        Color.pl('{!} {O}file {C}%s{O} not found{W}' % name)
        return

    filtered_results = cls.get_filtered_results(result_type)
    if len(filtered_results) == 0:
        Color.pl('{!} {R}no results found in {O}%s{W}' % name)
        return

    Color.pl('\n{+} Displaying {G}%d{W} %s target(s) from {C}%s{W}\n' % (
        len(filtered_results), 'cracked' if result_type == 'cracked' else 'ignored', cls.cracked_file))

    results = sorted([cls.load(item) for item in filtered_results], key=lambda x: x.date, reverse=True)
    cls.print_results_table(results, result_type == 'cracked')
  1. Create a new method for printing the results table:
@classmethod
def print_results_table(cls, results, show_key):
    longest_essid = max(len(result.essid or 'ESSID') for result in results)
    cls.print_table_header(longest_essid, show_key)
    for result in results:
        result.print_single_line(longest_essid)
    Color.pl('')

@classmethod
def print_table_header(cls, longest_essid, show_key):
    # ... (existing header printing code) ...

These changes separate concerns, making each method more focused and easier to understand. The display method now orchestrates the overall process, while smaller methods handle specific tasks. This approach improves readability and makes future modifications easier.

""" Show targets from results file """
name = cls.cracked_file
if not os.path.exists(name):
Color.pl('{!} {O}file {C}%s{O} not found{W}' % name)
return

with open(name, 'r') as fid:
cracked_targets = loads(fid.read())
targets = cls.load_all()
only_cracked = result_type == 'cracked'

if len(cracked_targets) == 0:
if only_cracked:
targets = [item for item in targets if item.get('type') != 'IGN']
else:
targets = [item for item in targets if item.get('type') == 'IGN']

if len(targets) == 0:
Color.pl('{!} {R}no results found in {O}%s{W}' % name)
return

Color.pl('\n{+} Displaying {G}%d{W} cracked target(s) from {C}%s{W}\n' % (
len(cracked_targets), name))
Color.pl('\n{+} Displaying {G}%d{W} %s target(s) from {C}%s{W}\n' % (
len(targets), result_type, cls.cracked_file))

results = sorted([cls.load(item) for item in cracked_targets], key=lambda x: x.date, reverse=True)
results = sorted([cls.load(item) for item in targets], key=lambda x: x.date, reverse=True)
longest_essid = max(len(result.essid or 'ESSID') for result in results)

# Header
Expand All @@ -98,9 +103,10 @@ def display(cls):
Color.p(' ')
Color.p('TYPE'.ljust(5))
Color.p(' ')
Color.p('KEY')
Color.pl('{D}')
Color.p(' ' + '-' * (longest_essid + 17 + 19 + 5 + 11 + 12))
if only_cracked:
Color.p('KEY')
Color.pl('{D}')
Color.p(' ' + '-' * (longest_essid + 17 + 19 + 5 + 11 + 12))
Color.pl('{W}')
# Results
for result in results:
Expand All @@ -118,6 +124,24 @@ def load_all(cls):
return []
return json

@classmethod
def load_ignored_bssids(cls, ignore_cracked = False):
json = cls.load_all()
ignored_bssids = [
item.get('bssid', '')
for item in json
if item.get('result_type') == 'IGN'
]

if not ignore_cracked:
return ignored_bssids

return ignored_bssids + [
item.get('bssid', '')
for item in json
if item.get('result_type') != 'IGN'
]

@staticmethod
def load(json):
""" Returns an instance of the appropriate object given a json instance """
Expand Down Expand Up @@ -149,10 +173,30 @@ def load(json):
essid=json['essid'],
pmkid_file=json['pmkid_file'],
key=json['key'])

else:
from .ignored_result import CrackResultIgnored
result = CrackResultIgnored(bssid=json['bssid'],
essid=json['essid'])

result.date = json['date']
result.readable_date = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(result.date))
return result

@classmethod
def ignore_target(cls, target):
ignored_targets = cls.load_all()

for ignored_target in ignored_targets:
is_ignored = ignored_target == 'IGN'
bssid_match = target.bssid == ignored_target.get('bssid')
essid_match = target.essid == ignored_target.get('essid')
if is_ignored and bssid_match and essid_match:
return

from .ignored_result import CrackResultIgnored
ignored_target = CrackResultIgnored(target.bssid, target.essid)
ignored_target.save()

if __name__ == '__main__':
# Deserialize WPA object
Expand All @@ -178,3 +222,11 @@ def load(json):
'"date": 1433403278, "type": "WPS"}')
obj = CrackResult.load(json)
obj.dump()

# Deserialize Ignored object
Color.pl('\nIgnored:')
json = loads(
'{"bssid": "AA:BB:CC:DD:EE:FF", "essid": "Test Router", '
'"date": 1433403278, "type": "IGN"}')
obj = CrackResult.load(json)
obj.dump()
Loading