Skip to content

Fix Evil Twin attack: make it actually work, and harden the portal#527

Merged
kimocoder merged 1 commit into
masterfrom
eviltwin-fixes
Jun 3, 2026
Merged

Fix Evil Twin attack: make it actually work, and harden the portal#527
kimocoder merged 1 commit into
masterfrom
eviltwin-fixes

Conversation

@kimocoder

Copy link
Copy Markdown
Owner

The Evil Twin subsystem had several issues — some that prevented the attack from working at all, several dead/unwired components, and security/robustness gaps. Found via audit + a live debug run against an authorized AP.

Core correctness (attack couldn't work before):

  • Rogue AP is now OPEN. hostapd was unconditionally configured WPA2 with a secret passphrase, so victims could never associate to reach the portal. generate_config() now emits an open AP (auth_algs=1, no wpa lines) unless a passphrase is explicitly supplied.
  • dnsmasq now starts: added except-interface=lo so it no longer tries to bind 127.0.0.1:53 and collide with the host resolver ("Address already in use"). dnsmasq startup errors are now read from stderr (were read from stdout, so failures were blank/undiagnosable).
  • Dual-interface mode now actually engages: _get_interface_assignment() imported InterfaceAssignment from a non-existent module (ModuleNotFoundError), silently falling back to single-interface every run. run() also ran _setup() AND _run_dual_interface(), double-starting hostapd/dnsmasq/portal; run() now picks the mode first and only sets up once.
  • Added the missing Airmon helpers the dual path calls (put_interface_down, set/get_interface_mode, set/get_interface_channel) — they didn't exist, so the dual path raised AttributeError. The deauth NIC is now put in monitor mode AND locked to the target channel, and restored to managed on cleanup.

Credential handling / honesty:

  • Captured credentials are now validated against the real AP (CredentialValidator, honoring eviltwin_validate_credentials); a confirmed-wrong password is rejected and the attack continues, and unverifiable captures are clearly flagged rather than reported as a confirmed key.
  • CredentialHandler is now wired into the portal POST path, adding server-side input validation and per-client rate limiting (was dead code).

Portal / templates:

  • Configurable templates are now applied (PortalServer was always built without a renderer, so only the hardcoded generic page was served); --eviltwin-template now takes effect. Template variable substitution is HTML-escaped (the untrusted SSID was injected raw).
  • Captive portal now uses ThreadingHTTPServer + per-request socket timeout so one stalled client can't block every other victim.
  • POST Content-Length is bounds-checked (negative/non-numeric rejected; oversized 413) — a negative value previously caused rfile.read(-1) to drain until EOF.
  • Credential/log callbacks are wrapped in staticmethod when bound to the handler class, so plain-function callbacks aren't turned into bound methods (extra arg).
  • del no longer does network/thread teardown (fragile at GC); added context manager support.

hostapd SSID injection:

  • SSID is written safely: printable-ASCII SSIDs use ssid=, anything with control chars / non-ASCII uses ssid2=, so an SSID with a newline can't inject hostapd directives. Clamped to the 32-byte 802.11 limit; channel is int-coerced.

Config:

  • Fixed eviltwin_/evil_twin_ naming drift so configured options actually apply (deauth interval, portal template) and added a real eviltwin_timeout (+ --eviltwin-timeout); the attack previously never timed out.

The Evil Twin subsystem had several issues — some that prevented the attack
from working at all, several dead/unwired components, and security/robustness
gaps. Found via audit + a live debug run against an authorized AP.

Core correctness (attack couldn't work before):
- Rogue AP is now OPEN. hostapd was unconditionally configured WPA2 with a
  secret passphrase, so victims could never associate to reach the portal.
  generate_config() now emits an open AP (auth_algs=1, no wpa lines) unless a
  passphrase is explicitly supplied.
- dnsmasq now starts: added except-interface=lo so it no longer tries to bind
  127.0.0.1:53 and collide with the host resolver ("Address already in use").
  dnsmasq startup errors are now read from stderr (were read from stdout, so
  failures were blank/undiagnosable).
- Dual-interface mode now actually engages: _get_interface_assignment()
  imported InterfaceAssignment from a non-existent module (ModuleNotFoundError),
  silently falling back to single-interface every run. run() also ran _setup()
  AND _run_dual_interface(), double-starting hostapd/dnsmasq/portal; run() now
  picks the mode first and only sets up once.
- Added the missing Airmon helpers the dual path calls (put_interface_down,
  set/get_interface_mode, set/get_interface_channel) — they didn't exist, so
  the dual path raised AttributeError. The deauth NIC is now put in monitor
  mode AND locked to the target channel, and restored to managed on cleanup.

Credential handling / honesty:
- Captured credentials are now validated against the real AP (CredentialValidator,
  honoring eviltwin_validate_credentials); a confirmed-wrong password is rejected
  and the attack continues, and unverifiable captures are clearly flagged rather
  than reported as a confirmed key.
- CredentialHandler is now wired into the portal POST path, adding server-side
  input validation and per-client rate limiting (was dead code).

Portal / templates:
- Configurable templates are now applied (PortalServer was always built without
  a renderer, so only the hardcoded generic page was served); --eviltwin-template
  now takes effect. Template variable substitution is HTML-escaped (the untrusted
  SSID was injected raw).
- Captive portal now uses ThreadingHTTPServer + per-request socket timeout so one
  stalled client can't block every other victim.
- POST Content-Length is bounds-checked (negative/non-numeric rejected; oversized
  413) — a negative value previously caused rfile.read(-1) to drain until EOF.
- Credential/log callbacks are wrapped in staticmethod when bound to the handler
  class, so plain-function callbacks aren't turned into bound methods (extra arg).
- __del__ no longer does network/thread teardown (fragile at GC); added context
  manager support.

hostapd SSID injection:
- SSID is written safely: printable-ASCII SSIDs use ssid=, anything with control
  chars / non-ASCII uses ssid2=<hex>, so an SSID with a newline can't inject
  hostapd directives. Clamped to the 32-byte 802.11 limit; channel is int-coerced.

Config:
- Fixed eviltwin_*/evil_twin_* naming drift so configured options actually apply
  (deauth interval, portal template) and added a real eviltwin_timeout
  (+ --eviltwin-timeout); the attack previously never timed out.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 3, 2026 17:56
@kimocoder kimocoder merged commit 113014e into master Jun 3, 2026
1 check was pending
@kimocoder kimocoder deleted the eviltwin-fixes branch June 3, 2026 17:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR repairs and hardens the Evil Twin attack flow so the rogue AP + captive portal path works end-to-end (association, DNS/DHCP, portal submission), while also addressing several robustness and injection issues (hostapd SSID config injection, portal HTML/template injection, and safer request handling).

Changes:

  • Make hostapd emit an open AP by default (unless a passphrase is explicitly provided) and harden SSID handling to prevent hostapd config injection.
  • Improve dnsmasq startup reliability and error visibility (avoid loopback binding conflict; read stderr/combined output on failure).
  • Wire portal templating + add portal-side hardening (ThreadingHTTPServer, socket timeout, POST bounds checks) and add Evil Twin timeout config plumbing.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
wifite/tools/hostapd.py Default to open AP; safe SSID directive formatting; channel int coercion.
wifite/tools/dnsmasq.py Prevent loopback binding; surface startup errors via stderr/out.
wifite/tools/airmon.py Add missing interface up/down/mode/channel helper methods used by dual-interface logic.
wifite/config/parsers/eviltwin.py Parse and apply --eviltwin-timeout.
wifite/config/defaults.py Add eviltwin_timeout default.
wifite/config/init.py Add eviltwin_timeout config attribute.
wifite/attack/portal/templates.py HTML-escape template variable substitution values.
wifite/attack/portal/server.py Threaded server + timeouts; POST Content-Length bounds checks; wire credential handler; staticmethod-wrap callbacks; safer lifecycle semantics.
wifite/attack/portal/credential_handler.py Add check_and_record() for synchronous admission control (validate + rate limit + record).
wifite/attack/eviltwin.py Fix config key drift; enable template renderer; dual-interface flow correction; add credential verification path; cleanup restores interface mode.
wifite/args.py Add --eviltwin-timeout CLI option.
tests/test_eviltwin_unit.py Add/adjust unit tests for open AP default + SSID injection hardening.
tests/test_eviltwin_e2e.py Update config attribute names to eviltwin_*.
tests/test_adaptive_deauth_integration.py Update config attribute name for deauth interval.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread wifite/attack/eviltwin.py
Comment on lines +1284 to +1290
if self.client_monitor:
self.client_monitor.record_credential_attempt(client_ip, success=False)
self.credential_attempts.append({
'client_ip': client_ip,
'mac': client_ip,
'ssid': ssid,
'password': password,
Comment thread wifite/attack/eviltwin.py
Comment on lines 1299 to 1313
if self.client_monitor:
self.client_monitor.record_credential_attempt(client_ip, success=True)

# Store the result — the monitoring loop checks self.crack_result
self.crack_result = self.create_result(password)
# Annotate verification status for downstream reporting.
self.crack_result.verified = (verified is True)
self.credential_attempts.append({
'client_ip': client_ip,
'mac': client_ip,
'ssid': ssid,
'password': password,
'success': True,
'timestamp': time.time(),
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants