diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..fa4b8d4a2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,79 @@ +# Contributing to wifite2 + +Thank you for your interest in contributing. Please follow these guidelines to keep the project consistent and maintainable. + +--- + +## Development Environment + +```bash +# Clone your fork +git clone https://github.com//wifite2.git +cd wifite2 + +# Create and activate a virtual environment +python3 -m venv .venv +source .venv/bin/activate + +# Install in editable mode with all dependencies +pip install -e . +pip install -r requirements.txt +``` + +--- + +## Running the Test Suite + +```bash +pytest tests/ -v +``` + +All tests must pass before submitting a pull request. If you add new functionality, add corresponding tests in `tests/`. + +Some tests require external tools (`tshark`, `aircrack-ng`, `cowpatty`). Install them on your system or mock them in your test fixtures. + +--- + +## Code Style + +- Follow **PEP 8** for all Python code. +- Use the existing `Color` and `Output` utilities (`wifite/util/color.py`) for any user-facing output. Do not use bare `print()` calls. +- Use `log_debug()` / `log_info()` from `wifite/util/logger.py` for internal diagnostic messages. +- Keep functions focused. Refactor large blocks into helper methods rather than adding deeply nested logic. +- Type hints are welcome but not required for existing modules; new modules should include them where practical. + +--- + +## Submitting a Pull Request + +1. **Fork** the repository and create a feature branch off `master`: + ```bash + git checkout -b feature/my-improvement + ``` +2. Make your changes, following the style guidelines above. +3. Run the test suite and confirm it passes. +4. Commit with a clear, concise message describing *what* changed and *why*. +5. Push your branch to your fork and open a **Pull Request against `master`**. +6. Fill in the PR description: what problem it solves, how it was tested, and any known limitations. + +PRs that break existing tests or lack a description will not be merged. + +--- + +## Testing Requirements + +Contributions that touch attack logic, capture parsing, or tool integrations **must** be tested on real hardware or with mocked interfaces. Specifically: + +- Use `.cap` / `.pcapng` fixture files in `tests/files/` for capture-related changes. +- Mock external process calls (`aircrack-ng`, `tshark`, etc.) using `unittest.mock` where live hardware is not available. +- Document in your PR description how you verified the change (hardware model, driver, OS, or mock strategy). + +--- + +## Responsible Disclosure + +If you discover a security vulnerability in wifite2 itself (not a vulnerability in a third-party tool it wraps), **do not open a public GitHub issue**. + +Instead, email the maintainer directly. Provide a clear description of the issue, reproduction steps, and potential impact. Allow reasonable time for a fix before any public disclosure. + +Public issues are appropriate for bugs, feature requests, and usage questions, not for 0-day disclosures. diff --git a/requirements.txt b/requirements.txt index 4b0dae117..09dcf1092 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ chardet>=7.4.3 requests>=2.33.1 rich>=15.0.0 -scapy>=2.7.1rc1; python_version < '4' +scapy>=2.6.0; python_version < '4' setuptools>=82.0.1 # Development dependencies (optional) diff --git a/wifite/args.py b/wifite/args.py index a877d2725..73e68b2b1 100644 --- a/wifite/args.py +++ b/wifite/args.py @@ -647,6 +647,17 @@ def _add_wep_args(self, wep): 'Seconds to wait before failing (default: {G}%d sec{W})' % self.config.wep_timeout)) wep.add_argument('-wept', help=argparse.SUPPRESS, action='store', dest='wep_timeout', type=int) + wep.add_argument('--wep-fakeauth-time', + action='store', + dest='wep_fakeauth_time', + metavar='[seconds]', + type=int, + help=self._verbose( + 'Seconds to wait for fake-authentication (default: {G}%d sec{W})' + % self.config.wep_fakeauth_time)) + wep.add_argument('-wep-fakeauth-time', help=argparse.SUPPRESS, action='store', + dest='wep_fakeauth_time', type=int) + wep.add_argument('--wepca', action='store', dest='wep_crack_at_ivs', @@ -1106,6 +1117,12 @@ def _add_command_args(commands): dest='crack_handshake', help=Color.s('Show commands to crack a captured handshake')) + commands.add_argument('--print-crack-cmd', + action='store_true', + dest='show_cracking_command', + help=Color.s('Print the exact cracking command before running it, ' + 'for easy copy/pasting into a terminal (default: {G}off{W})')) + commands.add_argument('--syscheck', action='store_true', dest='syscheck', diff --git a/wifite/attack/wep.py b/wifite/attack/wep.py index 7fe93d391..332794b02 100755 --- a/wifite/attack/wep.py +++ b/wifite/attack/wep.py @@ -21,7 +21,7 @@ class AttackWEP(Attack): Contains logic for attacking a WEP-encrypted access point. """ - fakeauth_wait = 5 # TODO: Configuration? + fakeauth_wait = 5 def __init__(self, target): super(AttackWEP, self).__init__(target) @@ -206,7 +206,6 @@ def run(self): if not xor_file: # If .xor is not there, the process failed. Color.pl('\n{!} {O}%s attack{R} did not generate a .xor file' % attack_name) - # XXX: For debugging Color.pl('{?} {O}Command: {R}%s{W}' % ' '.join(aireplay.cmd)) Color.pl('{?} {O}Output:\n{R}%s{W}' % aireplay.get_output()) break @@ -381,14 +380,15 @@ def fake_auth(self): Returns: True if successful, False is unsuccessful. """ Color.p('\r{+} attempting {G}fake-authentication{W} with {C}%s{W}...' % self.target.bssid) - fakeauth = Aireplay.fakeauth(self.target, timeout=AttackWEP.fakeauth_wait) + fakeauth_wait = Configuration.wep_fakeauth_time or AttackWEP.fakeauth_wait + fakeauth = Aireplay.fakeauth(self.target, timeout=fakeauth_wait) if fakeauth: Color.pl(' {G}success{W}') else: Color.pl(' {R}failed{W}') if Configuration.require_fakeauth: # Fakeauth is required, fail - raise Exception('Fake-authenticate did not complete within %d seconds' % AttackWEP.fakeauth_wait) + raise Exception('Fake-authenticate did not complete within %d seconds' % fakeauth_wait) # Warn that fakeauth failed Color.pl('{!} {O} unable to fake-authenticate with target (%s){W}' % self.target.bssid) Color.pl('{!} continuing attacks because {G}--require-fakeauth{W} was not set') diff --git a/wifite/config/__init__.py b/wifite/config/__init__.py index b43a7ac49..093d71366 100644 --- a/wifite/config/__init__.py +++ b/wifite/config/__init__.py @@ -53,6 +53,7 @@ class Configuration: random_mac = None random_mac_vendor = None require_fakeauth = None + show_cracking_command = False scan_time = None show_bssids = None show_cracked = None @@ -97,6 +98,7 @@ class Configuration: wep_attacks = None wep_crack_at_ivs = None wep_filter = None + wep_fakeauth_time = 5 wep_keep_ivs = None wep_pps = None wep_restart_aircrack = None @@ -216,6 +218,8 @@ def load_from_arguments(cls): cls.check_handshake = args.check_handshake if args.crack_handshake: cls.crack_handshake = True + if hasattr(args, 'show_cracking_command') and args.show_cracking_command: + cls.show_cracking_command = True if args.update_db: cls.update_db = True if hasattr(args, 'syscheck') and args.syscheck: diff --git a/wifite/config/defaults.py b/wifite/config/defaults.py index a1fac73d8..07ede6f9e 100644 --- a/wifite/config/defaults.py +++ b/wifite/config/defaults.py @@ -61,7 +61,7 @@ def initialize_defaults(cls): cls.interface_primary = None # Primary interface name cls.interface_secondary = None # Secondary interface name cls.auto_assign_interfaces = True # Auto-assign interfaces (default True) - cls.prefer_dual_interface = True # Prefer dual over single when available (default True) + cls.prefer_dual_interface = False # Prefer dual over single when available (default False) cls.use_hcxdump = False # Use hcxdumptool for dual interface WPA capture (default False) # WEP variables @@ -70,6 +70,7 @@ def initialize_defaults(cls): cls.wep_timeout = 600 # Seconds to wait before failing cls.wep_crack_at_ivs = 10000 # Minimum IVs to start cracking cls.require_fakeauth = False + cls.wep_fakeauth_time = 5 # Seconds to wait for fake-authentication to complete cls.wep_restart_stale_ivs = 11 # Seconds to wait before restarting # Aireplay if IVs don't increase. # '0' means never restart. @@ -152,6 +153,7 @@ def initialize_defaults(cls): cls.wps_timeout_threshold = 100 # Max number of timeouts # Commands + cls.show_cracking_command = False # Print exact cracking command to stdout (--print-crack-cmd) cls.show_cracked = False cls.show_ignored = False cls.check_handshake = None diff --git a/wifite/config/parsers/wep.py b/wifite/config/parsers/wep.py index 461a32009..7f51556fb 100644 --- a/wifite/config/parsers/wep.py +++ b/wifite/config/parsers/wep.py @@ -18,6 +18,11 @@ def parse_wep_args(cls, args): cls.wep_timeout = args.wep_timeout Color.pl('{+} {C}option:{W} WEP attack timeout set to {G}%d seconds{W}' % args.wep_timeout) + if args.wep_fakeauth_time: + cls.wep_fakeauth_time = args.wep_fakeauth_time + Color.pl('{+} {C}option:{W} WEP fake-authentication timeout set to {G}%d seconds{W}' + % args.wep_fakeauth_time) + if args.require_fakeauth: cls.require_fakeauth = True Color.pl('{+} {C}option:{W} fake-authentication is {G}required{W} for WEP attacks') diff --git a/wifite/model/handshake.py b/wifite/model/handshake.py index d0eb2c282..5a3fceedb 100755 --- a/wifite/model/handshake.py +++ b/wifite/model/handshake.py @@ -40,12 +40,38 @@ def divine_bssid_and_essid(self): if not self.essid and not self.bssid and len(pairs) > 0: # We do not know the bssid nor the essid - # TODO: Display menu for user to select from list - # HACK: Just use the first one we see - self.bssid = pairs[0][0] - self.essid = pairs[0][1] - Color.pl('{!} {O}Warning{W}: {O}Arbitrarily selected ' + - '{R}bssid{O} {C}%s{O} and {R}essid{O} "{C}%s{O}"{W}' % (self.bssid, self.essid)) + if len(pairs) == 1: + self.bssid = pairs[0][0] + self.essid = pairs[0][1] + else: + # Multiple networks in capture; no prior context to choose from. + # TODO: Display menu for user to select from list + chosen_bssid, chosen_essid = pairs[0] + Color.pl( + '{!} {O}Warning{W}: Multiple networks found in capture file. ' + 'Using first: {C}%s{W} ({C}%s{W}). ' + 'Use {G}--bssid{W} to target a specific network.' % (chosen_bssid, chosen_essid) + ) + self.bssid = chosen_bssid + self.essid = chosen_essid + elif not self.essid and self.bssid and len(pairs) > 1: + # BSSID is already set (e.g. from attack context or filename); try to match it. + matched = next( + ((bssid, essid) for (bssid, essid) in pairs if bssid.lower() == self.bssid.lower()), + None, + ) + if matched: + self.essid = matched[1] + else: + # Pre-set BSSID not found among captured pairs; warn and fall back to first. + chosen_bssid, chosen_essid = pairs[0] + Color.pl( + '{!} {O}Warning{W}: Multiple networks found in capture file. ' + 'BSSID {C}%s{W} not matched; using first: {C}%s{W} ({C}%s{W}). ' + 'Use {G}--bssid{W} to target a specific network.' % (self.bssid, chosen_bssid, chosen_essid) + ) + self.bssid = chosen_bssid + self.essid = chosen_essid if not self.bssid: # We already know essid diff --git a/wifite/util/crack.py b/wifite/util/crack.py index b44210211..399174b18 100755 --- a/wifite/util/crack.py +++ b/wifite/util/crack.py @@ -18,7 +18,22 @@ from ..util.sae_crack import SAECracker -# TODO: Bring back the 'print' option, for easy copy/pasting. Just one-liners people can paste into terminal. +def print_cracking_command(tool_name, command_parts): + """ + Print the exact shell command that would be run for cracking, + so users can copy-paste it into a terminal. + + Used by cracking helpers that need a consistently formatted command line. + + Args: + tool_name: Human-readable label for the tool (e.g. 'aircrack-ng'). + command_parts: List of strings forming the shell command (argv style). + """ + import shlex + from ..util.color import Color + cmd_str = ' '.join(shlex.quote(str(p)) for p in command_parts) + Color.pl('{+} {C}[crack-cmd]{W} {G}%s{W}: {W}%s' % (tool_name, cmd_str)) + def decode_hex_essid_if_needed(essid): """ @@ -383,6 +398,9 @@ def crack_4way(cls, hs, tool): Color.pl('{!} {R}Error: {O}%s{W}' % e) return None + # show_command is True when verbose or when --print-crack-cmd is set + show_cmd = Configuration.show_cracking_command or Configuration.verbose > 0 + wordlists_to_try = Configuration.wordlists if Configuration.wordlists else [Configuration.wordlist] for wordlist in wordlists_to_try: @@ -390,13 +408,13 @@ def crack_4way(cls, hs, tool): Color.pl('{+} Trying wordlist: {C}%s{W}' % wordlist_name) if tool == 'aircrack': - key = Aircrack.crack_handshake(handshake, show_command=True, wordlist=wordlist) + key = Aircrack.crack_handshake(handshake, show_command=show_cmd, wordlist=wordlist) elif tool == 'hashcat': - key = Hashcat.crack_handshake(handshake, target_is_wpa3_sae=False, show_command=True, wordlist=wordlist) + key = Hashcat.crack_handshake(handshake, target_is_wpa3_sae=False, show_command=show_cmd, wordlist=wordlist) elif tool == 'john': - key = John.crack_handshake(handshake, show_command=True, wordlist=wordlist) + key = John.crack_handshake(handshake, show_command=show_cmd, wordlist=wordlist) elif tool == 'cowpatty': - key = Cowpatty.crack_handshake(handshake, show_command=True, wordlist=wordlist) + key = Cowpatty.crack_handshake(handshake, show_command=show_cmd, wordlist=wordlist) if key is not None: return CrackResultWPA(hs['bssid'], hs['essid'], hs['filename'], key) @@ -408,12 +426,15 @@ def crack_pmkid(cls, hs, tool): if tool != 'hashcat': Color.pl('{!} {O}Note: PMKID hashes can only be cracked using {C}hashcat{W}') + # show_command is True when verbose or when --print-crack-cmd is set + show_cmd = Configuration.show_cracking_command or Configuration.verbose > 0 + wordlists_to_try = Configuration.wordlists if Configuration.wordlists else [Configuration.wordlist] for wordlist in wordlists_to_try: wordlist_name = os.path.basename(wordlist) Color.pl('{+} Trying wordlist: {C}%s{W}' % wordlist_name) - key2 = Hashcat.crack_pmkid(hs['filename'], verbose=True, wordlist=wordlist) + key2 = Hashcat.crack_pmkid(hs['filename'], verbose=show_cmd, wordlist=wordlist) if key2 is not None: return CrackResultPMKID(hs['bssid'], hs['essid'], hs['filename'], key2) @@ -426,6 +447,9 @@ def crack_sae(cls, hs, tool): Color.pl('{!} {O}Note: WPA3-SAE handshakes can only be cracked using {C}hashcat{W}') return None + # show_command is True when verbose or when --print-crack-cmd is set + show_cmd = Configuration.show_cracking_command or Configuration.verbose > 0 + # Create SAEHandshake object sae_handshake = SAEHandshake( capfile=hs['filename'], @@ -441,8 +465,8 @@ def crack_sae(cls, hs, tool): key = SAECracker.crack_sae_handshake( sae_handshake, wordlist=wordlist, - show_command=True, - verbose=True + show_command=show_cmd, + verbose=show_cmd ) if key is not None: return CrackResultSAE(hs['bssid'], hs['essid'], hs['filename'], key) diff --git a/wifite/wifite.py b/wifite/wifite.py index 1906a6b9a..b1a6e6a1c 100644 --- a/wifite/wifite.py +++ b/wifite/wifite.py @@ -932,19 +932,24 @@ def _target_from_state(target_state): if power > 0: power = power - 100 + # Use the current datetime for first/last seen so restored targets sort + # and display with a reasonable timestamp instead of a year-2000 stub. + from datetime import datetime + now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + fields = [ target_state.bssid, # 0: BSSID - '2000-01-01 00:00:00', # 1: First time seen (placeholder) - '2000-01-01 00:00:00', # 2: Last time seen (placeholder) + now_str, # 1: First time seen (current time; original not stored in session) + now_str, # 2: Last time seen (current time; original not stored in session) str(target_state.channel), # 3: channel - '54', # 4: Speed (placeholder) + '54', # 4: Speed: airodump-ng "speed" field; not stored in session, 54 Mbps is a safe neutral default target_state.encryption, # 5: Privacy/Encryption - 'CCMP', # 6: Cipher (placeholder) - '', # 7: Authentication (will be derived from encryption) + 'CCMP', # 6: Cipher: not stored in session; CCMP is the standard WPA2/WPA3 cipher and a safe default + '', # 7: Authentication (will be derived from encryption below) str(power), # 8: Power - '10', # 9: beacons (placeholder) - '0', # 10: IV (placeholder) - '0. 0. 0. 0', # 11: LAN IP (placeholder) + '10', # 9: beacons (not stored in session; any non-zero value is a safe placeholder) + '0', # 10: IV (not stored in session; 0 is correct for WPA targets) + '0. 0. 0. 0', # 11: LAN IP (not stored in session; airodump-ng format placeholder) str(len(target_state.essid)) if target_state.essid else '0', # 12: ID-length target_state.essid if target_state.essid else '', # 13: ESSID '' # 14: Key (empty) @@ -1362,9 +1367,14 @@ def interface_cleanup(): from .util.logger import log_debug log_debug('Wifite', f'Interface cleanup thread error: {e}') - # Delete Reaver .pcap quickly + # Delete Reaver .pcap quickly. + # reaver_output.pcap is the legacy bare filename written by older reaver versions + # directly into the cwd. The current Reaver tool class writes to Configuration.temp() + # instead, but we still clean up the legacy name here using an absolute path + # (os.path.abspath) so the removal works regardless of cwd. try: - subprocess.run(["rm", "-f", "reaver_output.pcap"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=2) + reaver_pcap = os.path.abspath('reaver_output.pcap') + subprocess.run(["rm", "-f", reaver_pcap], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=2) except Exception as e: from .util.logger import log_debug log_debug('Wifite', f'Reaver cleanup error: {e}')