- 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.
219 lines
10 KiB
Markdown
219 lines
10 KiB
Markdown
# 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:
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```powershell
|
|
# 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
|
|
|
|
```powershell
|
|
.\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:
|
|
|
|
```powershell
|
|
$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.
|