Files
Mark-VIII/docs/superpowers/specs/2026-08-23-deauth-evilportal-capture-design.md
T

5.6 KiB

Mark VIII — Deauth UX, Evil Portal, Monitor Capture Fixes

Date: 2026-08-23 Status: approved (user confirmed design in session)

Goals

  1. Make deauth obvious: "Deauth All Clients" from Recon's AP focus sidebar, "Deauth All" on the Deauth Targeting card — both gated by an engagement-scope confirmation.
  2. New top-level Evil Portal tab compatible with Hak5 EvilPortalNano portals (kleo/evilportals layout): import zips, serve pages to victims, capture credentials, DNS-hijack delivery.
  3. OpenAP gains an "Evil Portal" card: dropdown of imported templates, greyed out when none exist; activating an OpenAP with a template selected starts the portal.
  4. Fix Monitor Capture never appearing to work.

Non-goals

  • No PHP interpreter; the backend shims only the trivial PHP patterns stock portals use.
  • No TLS interception (HTTPS requests are not redirected).
  • .enable/.disable portal scripts are stored but not executed (divergence from stock EvilPortalNano, documented here deliberately).

1. Bulk deauth endpoint

POST /api/attacks/deauth/bulk body {targets: [{bssid, client, channel?}, ...]} (max 32). Shared helper _deauth_one(bssid, client, channel) extracted from h_attacks_deauth; per-target results returned {results: [...], sent, failed}. Uses the existing band→inject-iface logic and PINEAPPLE_DEAUTH_CLIENT.

Recon sidebar

In renderFocus() (www/js/views.js), when the focused AP has confirmed clients, add a danger button "Deauth All Clients". Clicking shows confirm("Deauthenticate N client(s) of <SSID>? Confirm this target is IN SCOPE for your engagement."), then posts one bulk call.

Deauth Targeting card

deauthPanel() gains a danger "Deauth All" button that bulk-deauths every device in the current result list against the selected AP, behind the same scope confirmation.

2. Evil Portal

Storage & import

  • Root: /mmc/mk8/portals/<name>/ ([A-Za-z0-9._-]{1,64} names, mk8_profiles rules).
  • POST /api/portals/import {data: <base64 zip>} (≤10 MB decoded). Zip-slip guarded (reject absolute paths and ..). If every entry shares a single top-level directory, that directory becomes the portal root. index.php must exist at the root.
  • Other endpoints: GET /api/portals, DELETE /api/portals/{name}, POST /api/portals/{name}/activate|deactivate, GET /api/portals/captures, DELETE /api/portals/captures, GET /api/portals/{name}/logs (Download), GET /api/portals/{name}/download (zip of the portal folder).

Serving engine (port 80)

Second listener thread in server.py, own connection handler (no auth, no same-origin — victims are unauthenticated). Admin UI on :8080 untouched.

  • GET anything → active portal:
    • Path resolves inside the portal dir (_safe_join); missing paths fall back to index.php; asset files served raw with the standard mime map.
    • index.php rendered through the PHP shim.
  • PHP shim: replaces getClientMac($_SERVER['REMOTE_ADDR']) / getClientHostName(...) with values resolved from DHCP leases for the requester IP, $_SERVER['REMOTE_ADDR'] with the requester IP, $destination with the request URL; strips all other <?php ... ?> / <?= ... ?> blocks.
  • POST anything → credential capture: parse urlencoded fields, enrich with client ip/mac/hostname, append:
    • <portal>/.logs in the exact MyPortal.php text format (stock-tool compatible), and
    • /mmc/mk8/portals/captures.jsonl (one JSON object per line) for the UI. Response: simple authorization-success HTML page.

Delivery: DNS hijack

Activation writes dhcp.@dnsmasq[0].address='/#/<lan-ip>' (UCI), commits, restarts dnsmasq; deactivation deletes the option and restarts. LAN IP discovered via ip -4 addr show br-lan (env-overridable PAGER_LAN_IFACE, fallback 172.16.52.1). Active portal name persists at /mmc/mk8/portals/.active; on service start the hijack is re-applied best-effort if a portal was left active.

Tab UI

Rail entry + route #/evilportal + views.evilportal: Active Portal status card, Templates card (list with Activate/Stop/Delete/Download, Import via zip file input → base64), Captured Credentials table (time, portal, fields) with Clear and Download.

3. OpenAP Evil Portal card

attackLauncher('open', {..., portal: true}): card with a template <select> (populated from GET /api/portals), disabled with hint text when no templates exist. Deploy body carries portal: <name>; h_attacks_deploy activates it after a successful open-AP deploy; h_attacks_stop for kind open deactivates any active portal. Status card shows the active binding.

4. Monitor Capture fix

Root cause: views.js:1314 polls /api/attacks/capture {action:'status'} without iface; the backend defaults to wlan0mon, so a live wlan1mon capture flips back to "Not capturing" within one 5 s poll.

  • Backend status with no iface: evaluates both pidfiles and reports whichever capture is actually running (else the default). Start failures now mkdir -p the pcap dir first and tee tcpdump stderr to /tmp/mk8_capture_<iface>.log, surfaced in the 502 detail.
  • Frontend: when a status/start response reports a running capture, adopt its iface for subsequent Stop clicks.

Testing

  • New tests/test_portals.py: import validation (bad name, zip-slip, oversize, missing index.php, nested top-dir flattening), PHP shim substitutions, capture log formats.
  • Extend tests/test_attacks.py: bulk deauth validation/looping (mocked device_run), capture status dual-iface resolution.
  • Existing suite must stay green (run per-file as documented in README).