Files
c4ch3c4d3 d23ea56364 fix(portals,dns,radio1,capture): live-validation fixes, verified on Pager 24.10.1
- portals: replace zipfile with struct+zlib ZIP reader (python3-light has
  no urllib; import endpoint was dead on device)
- dns hijack: uci add_list/del_list for dhcp.@dnsmasq[0].address (list
  option; plain set was silently dropped from generated dnsmasq config)
- radio1: bridge attack APs into br-lan via network.brlan.ports so
  victims get DHCP/portal reach; wlan1ent runtime-bridged after hostapd
  verify (retry loop may recreate the iface)
- capture: auto-start pinned wlan1mon pcap on 5GHz WPA deploy, teardown
  on stop; loot flows via hc22000 export (crack-verified end-to-end)
- enterprise: pineapd restart after ctrl link + re-assert PineAPE toggles

Documented residual: pineapd refuses forwarding from foreign hostapd
instances (broken pipe), and daemon set_ap rejects radio1 names - so
hostap_handshake rows for radio1 twins and enterprise cred tables cannot
populate without a Hak5 firmware change.

New tests/test_validation_fixes.py covers each fix (TDD); full suite
(29 modules) green.
2026-08-23 21:44:21 -06:00

10 KiB

Mark VIII

A Mark VII-style web management UI that runs on the WiFi Pineapple Pager at http://172.16.52.1:8080/. Packaged as a native Pager payload.

Features: Dashboard (live), PineAP (settings, SSID pool, filters, clients/kick), Recon (scans from recon.db), Handshakes/Loot, Payloads (embedded stock Pager Portal), Logs, Settings (hostname/NTP/password/prefs), and a bottom-docked xterm terminal. Recon AP focus offers bulk deauth; an Evil Portal tab imports Hak5 EvilPortalNano-format portals (kleo/evilportals compatible), serves them to victims via DNS hijack on port 80, and captures form credentials.

  • Rogue AP on the second radio (5GHz / 6GHz Wi-Fi 6E): Open AP and Evil WPA (WPA2-PSK/WPA3-SAE/WPA3-OWE) on radio1, band-aware channel pickers, 6GHz requires WPA3. While a radio1 AP is enabled the stock monitor-hopping (wlan1mon) is paused and resumed on disable; 2.4GHz PineAP is untouched.

Requirements

  • WiFi Pineapple Pager, firmware Pineapple Pager 24.10.1
  • python3 on the device (factory 24.10.1 may not ship it; scripts/deploy.sh installs OpenWrt python3-light from offline ipks)
  • Python 3.11 on the development machine

Install (sideload)

macOS/Linux:

# Recommended: key authentication
./scripts/deploy.sh --ssh-key "$HOME/.ssh/pager_key"

# Password authentication requires sshpass
brew install hudochenkov/sshpass/sshpass
./scripts/deploy.sh --password '<device-password>'

Windows:

# deploy.ps1 needs either an SSH key or sshpass for password auth:
& .\scripts\deploy.ps1 -SshKey "$HOME\.ssh\pager_key"
# or set up a key and add it: ssh-copy-id root@172.16.52.1

The deployment scripts build build/pager-webui/payload-<b64>.zip, upload it, extract it to /root/payloads/user/remote_access/pager-webui/, and refresh the portal index.

Then on the Pager menu, run Mark VIII:

  • Yes to "Run as background service?" -> procd service (respawns on crash, boot-persistent via rc.d symlinks).
  • No -> foreground mode; press B to stop.
  • Re-run the payload while running to Stop the service.
  • PAYLOAD_GET_CONFIG pager_webui auto_mode/run_mode skip the prompt.

Every payload run (and every service startup) first runs an environment check that prints on the payload screen / /tmp/pagerwebui.log: daemon reachable, pineapd alive, monitor interfaces up, and recon DB readable. On a healthy pager it does not rewrite live PineAP or wireless UCI (SSID pool, hopping, dummy_radio0 STA, Open AP). Crash-prone settings are reported as warnings. Startup aborts only if a core dependency fails. The health monitor will restart a dead pineapd and re-raise dropped monitors; it will not clear the SSID list or disable pool broadcast.

Browse http://172.16.52.1:8080/ and log in with the device password.

Uninstall / recovery

Re-run the payload and confirm "Stop service?" (stops, disables, removes the init script), then delete the payload directory via the portal or: rm -rf /root/payloads/user/remote_access/pager-webui. No stock files are modified. After a firmware upgrade (which wipes the overlay), reinstall and run the payload to re-enable it—the same caveat as Nautilus.

Local dev loop

.\scripts\deploy.ps1 -SshKey "$HOME\.ssh\pager_key"   # deploy backend once
.\scripts\dev.ps1 -Tunnel                             # local SPA + API proxy
# open http://127.0.0.1:8000

dev.ps1 serves www/ locally, proxies /api/* to the Pager, and points the terminal at the Pager's daemon WS (-Tunnel opens the :1471 SSH tunnel). The live WebSocket falls back to 5s polling through the dev proxy.

API tests

Python unit tests (stdlib unittest, runnable on Windows with mocks). Run each module in its own process—the tests monkeypatch module-level helpers and do not restore them, so a single discover process leaks state between files:

$py = "$env:LOCALAPPDATA\Programs\Python\Python311\python.exe"
Get-ChildItem tests\test_*.py | ForEach-Object {
    $mod = "tests." + [IO.Path]::GetFileNameWithoutExtension($_.Name)
    & $py -m unittest $mod -v
}

On-device smoke tests cover every page, background vs foreground, terminal I/O, and reboot persistence.

Architecture

  • server.py — Mark VIII's pure-socket HTTP + JSON API + minimal RFC6455 WS on 0.0.0.0:8080, written to run on the device's python3-light (no urllib/http.server/sqlite3 stdlib modules there); talks to the Hak5 daemon (127.0.0.1:1471) over a raw-socket HTTP client, hak5cmd, uci, iwinfo, and recon.db read-only (via the sqlite3 CLI).
  • www/ — vanilla JS SPA (no build step) + bundled xterm.js.
  • payload.sh + pagerwebui.init — Nautilus-style installer / procd service.

Stability notes (Pager 24.10.1)

Reliability Core (v1.4.0)

The factory /etc/config/pineapd ships every crash source below enabled, so guards that only live inside Mark VIII revert on every reset/upgrade. v1.4.0 makes the fixes structural (payload-only — no firmware changes):

  • Boot guard (mk8-guard, START=49): enforces the known-good UCI set before the S50 pineapple stack starts; idempotent, diff-only commits, logged to /tmp/mk8-guard.log and the event journal.
  • Profiles + rollback watchdogs: every risky operation (AP deploy, client-mode change, profile restore) first snapshots UCI under /mmc/mk8/profiles/, then spawns a detached watchdog probing local liveness (127.0.0.1:8080 + monitor presence). Sustained failure → automatic snapshot restore + wifi reload; recovery → promotes lastknown-good. Watchdogs self-exit after 120 quiet ticks.
  • RF plan: phy0 is always 2.4GHz ops; phy1 role-switches uplink/attack/idle (POST /api/rfplan/role). The uplink STA lives on radio1, so client-mode no longer blinds 2.4GHz recon. Hop governance belongs to the role manager, not the boot guard.
  • Supervisor: passive sampler (pidof/iw//proc only — never pineapd's socket), bounded JSONL journal at /mmc/mk8/events.log, boot-marker unexpected-reboot detection, memory watermark alerts. Dashboard shows events feed + reliability counters + guard sync chip.
  • Atomic deploys: scripts/deploy.sh stamps VERSION into build copies only, sha256-verifies the upload, swaps /mmc/mk8/releases/<ts> with a current symlink, polls local health, and auto-rolls back to the previous release on failure.
  • Smoke suite: scripts/smoke.sh on-device (read-only checks always; --write adds reconcile/watchdog drills; SMOKE_UPLINK_SSID=... enables the RF-role drill). All checks verified against live hardware including reboot persistence.

Persistent state lives in /mmc/mk8/ and survives reboots and firmware upgrades (overlay wipes). A pre-reliability config backup is captured at first deploy.

pineapd crash sources found and fixed on this firmware

pineapd crash sources found and fixed on this firmware (verified on-device, zero crashes over sustained watches):

  1. SSID-pool broadcast — segfaults pineapd (~15s cadence). Kept disabled.
  2. wlan2mon — a 6GHz monitor this hardware never creates; hopping the missing iface segfaults pineapd. Disabled.
  3. Large refilled pool — the pool list itself crashes pineapd even with broadcast disabled. The health monitor clears it (collect refills).
  4. wlan1mon fast-hopping 6GHz — stalls pineapd's command socket; the stock daemon's watchdog then SIGTERMs pineapd every ~30s. Bands pinned to 5GHz (2.4GHz only on wlan0mon).
  5. Socket collisions — actively pinging pineapd from a health monitor collides with the stock daemon's own socket writes. The monitor now checks pidof only.

GET /api/health reports pineapd/monitor state; the top bar shows a PINEAP OK / POOL OFF / PINEAPD DOWN chip.

Live validation findings (v1.4.x, Pager 24.10.1)

Fixed after an on-hardware attack validation pass:

  1. Evil Portal import no longer uses zipfile (pulls pathlib → urllib, absent from python3-light). A minimal struct+zlib ZIP reader handles stored/deflate entries.
  2. Portal DNS hijack now uses uci add_list/del_list (dhcp.@dnsmasq[0].address is a list option; a plain uci set was silently dropped from the generated dnsmasq config).
  3. 5 GHz attack APs are bridged into br-lan (network.brlan.ports) so victims get DHCP/portal reach; the standalone enterprise AP (wlan1ent) is runtime-bridged after its hostapd instance verifies ENABLED (the retry loop may recreate it).
  4. 5 GHz WPA deploys auto-start a pinned wlan1mon capture and the matching stop tears it down: loot flows via pcap → .hc22000 export instead of the dead daemon path below.

Residual firmware limitation (not fixable in-process): pineapd refuses handshake/PineAPE forwarding from hostapd instances it did not provision itself (PINEAP: could not send ... Broken pipe), and the stock daemon's set_ap rejects radio1 interface names ("Invalid access point interface"). Consequences: hostap_handshake rows never populate for radio1 evil twins (use the auto-capture + .hc22000 export, which is crack-verified end-to-end), and enterprise credentials never reach hostap_basic/hostap_chalresp even though the AP terminates PEAP/MSCHAPv2 successfully. Fixing these requires a Hak5 pineapd change.

Security notes

  • Auth via device password validated against the daemon; HttpOnly session cookie AUTH_<serverid>; all state-changing endpoints gated.
  • Commands run with argument lists (no shell interpolation).
  • Binds 0.0.0.0:8080 — same exposure class as the stock :1471/:7681.
  • Same-origin validation protects browser writes and WebSocket upgrades; the daemon token is stored in a root-only temporary session file.

Out of scope (v1)

:1471 takeover, Mark VII-only features (Campaigns/Modules/Cloud C2/EAP), physical-display mirroring, and a PR to hak5/wifipineapplepager-payloads (packaging is drop-in ready for that PR).

License

Mark VIII source code is available under the MIT License. See LICENSE and THIRD_PARTY_NOTICES.md for bundled-component and trademark notices.