# Pentest Results — ESP32 CSI Sensor Firmware **Date:** 2026-02-14 **Target:** amber-maple (192.168.129.30), ESP32-D0WD-V3 rev 3.1 **Firmware:** v1.11.0-11-ga4bd2a6-dirty (v1.12-dev security hardening) **ESP-IDF:** v5.5.2 **Tester:** Raspberry Pi 5 (192.168.129.11), same LAN --- ## Results Matrix ### Phase 1: Passive Reconnaissance | # | Test | Status | Notes | |---|------|--------|-------| | 1a | mDNS service discovery | **PASS** | No `_esp-csi._udp` service advertised. Only hostname.local resolves. | | 1b | Port scan (UDP) | **PASS** | Only 5353/udp (mDNS) and 5501/udp (cmd) open. 5500/udp closed (outbound-only). | | 1b | Port scan (TCP) | **PASS** | All 1000 TCP ports closed. Zero TCP attack surface. | | 1c | Firmware strings | **PASS** | No hardcoded secrets, passwords, or embedded private keys. | | 1c | Firmware strings | **WARN** | Hardcoded default IP `192.168.129.11` and hostname `amber-maple` in binary. | | 1c | Entropy analysis | **INFO** | Firmware not flash-encrypted (low entropy). Expected for dev boards. | | 1c | PEM markers | **INFO** | mbedTLS parser constants only, not actual embedded keys. | | 1d | eFuse: JTAG | **WARN** | `JTAG_DISABLE = False` — JTAG debug accessible via GPIO 12-15. | | 1d | eFuse: Secure Boot | **WARN** | `ABS_DONE_0 = False`, `ABS_DONE_1 = False` — no secure boot. | | 1d | eFuse: Flash Encryption | **WARN** | `FLASH_CRYPT_CNT = 0` — flash encryption disabled. | | 1d | eFuse: Download Mode | **WARN** | `DISABLE_DL_ENCRYPT/DECRYPT/CACHE = False` — UART download mode open. | | 1d | eFuse: Coding Scheme | **INFO** | `CODING_SCHEME = NONE` — full 256-bit key space available. | ### Phase 2: Network Protocol Analysis | # | Test | Status | Notes | |---|------|--------|-------| | 2a | Unauth STATUS info leak | **PASS** | Minimal response (hostname, uptime, rssi, channel, version). No secrets. | | 2a | CONFIG info disclosure | **PASS** | Auth secret not exposed in unauthed CONFIG response. | | 2a | HMAC on wire | **INFO** | HMAC tags visible in plaintext UDP (expected — no DTLS). | | 2a | HELP disclosure | **INFO** | Command list visible without auth (by design). | | 2b | HMAC timing oracle | **PASS** | Median diff 144us between test cases — within WiFi jitter. Constant-time comparison effective. | | 2c | Null byte injection | **PASS** | All 17 unauthed injection payloads handled safely. | | 2c | Format string injection | **PASS** | `%x%x%n` payloads rejected; no stack leak. | | 2c | Newline/CRLF injection | **PASS** | Rejected by auth requirement. | | 2c | Oversized packets | **PASS** | MTU-sized and 191-byte packets rejected gracefully. | | 2c | Binary garbage | **PASS** | Random byte payloads handled without crash. | | 2c | HOSTNAME format string | **PASS** | Authed: format strings rejected (timeout, likely sanitized). | | 2c | HOSTNAME oversized | **PASS** | `ERR HOSTNAME length 1-31` — proper validation. | | 2c | HOSTNAME bad chars | **PASS** | `ERR HOSTNAME chars: a-z 0-9 -` — strict allowlist. | | 2c | TARGET format string | **PASS** | `ERR TARGET invalid IP` — proper validation. | | 2c | RATE boundary values | **PASS** | All out-of-range values rejected: `ERR RATE range 10-100`. | | 2d | Valid HMAC command | **PASS** | Authed STATUS returns full response. | | 2d | Immediate replay | **PASS** | `ERR AUTH replay rejected` — nonce dedup works. | | 2d | Expired timestamp (-10s) | **PASS** | `ERR AUTH expired (drift=10s)` — window enforced. | | 2d | Future timestamp (+10s) | **PASS** | `ERR AUTH expired (drift=-10s)` — window enforced. | | 2d | Wrong secret | **PASS** | `ERR AUTH failed` — incorrect HMAC rejected. | | 2d | Nonce cache overflow (9 cmds) | **PASS** | Replay still rejected after flooding cache with 9 unique commands. | ### Phase 3: Static Analysis | # | Test | Status | Notes | |---|------|--------|-------| | 3a | NVS auth_secret storage | **FAIL** | Stored as plaintext string: `86bd963b07858d5b10db839d55b409df` at offset 0x1ba8. | | 3a | NVS WiFi credentials | **PASS** | No WiFi SSID/password found in NVS dump (compiled-in via sdkconfig). | | 3a | NVS integrity | **WARN** | 2 of 6 pages have invalid CRC (uninitialized, not corruption). | | 3a | Boot history | **INFO** | `boot_count = 25` visible — leaks reboot frequency. | | 3b | CVE exposure (12 checked) | **PASS** | 8 CVEs not applicable (unused components). 4 LOW risk (BLE scan-only mitigates). | | 3b | CVE-2025-27840 (HCI cmds) | **LOW** | Hidden HCI commands; mitigated by scan-only BLE mode. | | 3b | Unused `#include "esp_now.h"` | **INFO** | Dead include — remove to avoid link to CVE-2025-52471. | | 3c | Stack canaries | **FAIL** | `CONFIG_COMPILER_STACK_CHECK_MODE_NONE=y` — no stack overflow protection. | | 3c | Heap poisoning | **WARN** | `CONFIG_HEAP_POISONING_DISABLED=y` — no heap corruption detection. | | 3c | PMF configuration | **PASS** | `CONFIG_ESP_WIFI_PMF_REQUIRED=y` — 802.11w enforced. | | 3c | Debug symbols in ELF | **INFO** | ELF has full debug info (11.9 MB). `.bin` for OTA is stripped. | | 3c | cmd_task size | **INFO** | 9,877 bytes compiled — large function handling untrusted input. | | 3c | Watchdog | **PASS** | Bootloader, interrupt, and task WDTs all enabled. | --- ## Risk Assessment ### Critical (requires physical access) | Risk | Impact | Mitigation | |------|--------|------------| | No flash encryption | Auth secret + WiFi creds readable from flash | Enable `CONFIG_FLASH_ENCRYPTION_ENABLED` (irreversible eFuse) | | No secure boot | Arbitrary firmware flashable via UART/OTA | Enable Secure Boot V2 (irreversible eFuse) | | JTAG enabled | Live memory inspection, breakpoints | Burn `JTAG_DISABLE` eFuse (irreversible) | | NVS plaintext | `auth_secret` in cleartext NVS | Flash encryption covers this | ### Medium (network-accessible) | Risk | Impact | Mitigation | |------|--------|------------| | No DTLS | HMAC tokens visible to LAN sniffers | Implement DTLS for command channel | | No stack canaries | Buffer overflow in cmd_task could be exploitable | Enable `CONFIG_COMPILER_STACK_CHECK_MODE_NORM` | | OTA over HTTP | MITM firmware injection on LAN | Embed CA cert, enforce HTTPS OTA | ### Low / Informational | Risk | Impact | Mitigation | |------|--------|------------| | Hardcoded default IP | Network topology leak in binary | Move to Kconfig or NVS-only default | | Version string leaks git hash | Aids targeted attacks | Use clean tag-only version strings | | HELP visible without auth | Command enumeration | By design — acceptable | | Uptime in unauthed STATUS | Aids HMAC timestamp prediction | Already in minimal STATUS by design | --- ## Security Hardening Scorecard | Category | Score | Notes | |----------|-------|-------| | **Authentication** | 9/10 | HMAC-SHA256, replay protection, nonce cache, rate limiting. Only gap: no mutual auth. | | **Input Validation** | 10/10 | All 27 injection tests passed. Strict allowlists on HOSTNAME, RATE, TARGET, POWER. | | **Network Exposure** | 8/10 | Minimal ports, PMF required, service ads removed. Gap: plaintext UDP. | | **Physical Security** | 2/10 | No secure boot, no flash encryption, JTAG open, UART download mode open. | | **Binary Security** | 4/10 | No stack canaries, no heap poisoning. WDTs present. | | **CVE Exposure** | 9/10 | Minimal attack surface; unused components disabled; v5.5.2 patches applied. | **Overall: Strong network security, weak physical security.** The firmware is well-hardened against remote/network attacks. Physical access remains the primary threat vector. --- ## Recommendations (Priority Order) 1. **P1** — Enable stack canaries: `CONFIG_COMPILER_STACK_CHECK_MODE_NORM=y` 2. **P1** — Enable heap poisoning: `CONFIG_HEAP_POISONING_LIGHT=y` 3. **P2** — Enable WDT panic: `CONFIG_ESP_TASK_WDT_PANIC=y` 4. **P2** — Remove unused `#include "esp_now.h"` 5. **P2** — Remove hardcoded default IP from binary 6. **P3** — Flash encryption (requires eFuse planning) 7. **P3** — Secure Boot V2 (requires eFuse planning) 8. **P3** — DTLS for command channel (significant effort) 9. **P3** — OTA certificate pinning