
pfSense Plus MIM API toolkit b4acd7ca - High - Remote - IDOR - one controller JWT is a fleet token
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · pfsense-api-fleet-jwt
Class: IDOR Reach: Remote
pfSense Plus MIM API (Netgate) pin b4acd7ca - github.com/Netgate/pfsense-api
One POST /login Bearer is reused for every enrolled device_id. Official examples copy that token onto /api/device/pfsense/{device_id}/api for every id from GET /mim/devices. LoginResponse fields are only token, user, version, and alerts. LDAP MIM roles are global. They are not bound to a device. A controller login that can list devices talks to firewall B with a token issued for a user who was meant for firewall A.
This is one customer tenancy, not a SaaS cross-tenant leftover. Unauthenticated calls stay 401. Plus RBAC was not executed. Series hub: pfsense-api-nexus-mim (this is leftover 6). Stolen JWT from leftover 9 (verify_ssl=False samples) is this fleet token.
| ID | no CVE yet |
| Class | IDOR |
| Reach | Remote (authenticated controller JWT) |
| CWE | CWE-639 |
| CVSS | High: 8.1 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N |
| Product | Netgate/pfsense-api (pfSense Plus Nexus Multi-instance Management controller OpenAPI + generated client) |
| Affected | pin b4acd7ca21dbca739a530403a1f62a7440f7726f (2026-05-25). No tags. Server is Plus-only (paid). Not the CE REST package. |
| Auth | controller JWT from POST /login. Guessing device_id is not required if GET /mim/devices is allowed. |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1:19161 only. Python mock of the published client contract. No Plus image. |
I pinned github.com/Netgate/pfsense-api main b4acd7ca21dbca739a530403a1f62a7440f7726f (2026-05-25, Add Snort-3 API definitions). This is not jaredhendrickson/pfsense-api / pfrest.org.
A controller login that can list devices talks to every enrolled instance with the same JWT. CSRF is not in play (Bearer API). PHP / shell / export-key on every box is intended admin power once the proxy accepts the fleet token. The leftover is the missing per-device bind, not Diagnostics: Command.
| Privilege | Method / path | Lab-proven or inferred |
|---|---|---|
List every enrolled device_id (name, address, state) | GET /mim/devices with the login Bearer | Recon (lab-proven). Mock returned both site-a and site-b. Unauth is 401. |
| Read system status of firewall A and firewall B with one token | GET /api/device/pfsense/{device_id}/api/system | Recon (lab-proven). device-a=200, device-b=200. Witness only on B. |
| POST the diagnostics PHP wrapper on both ids | POST /api/device/pfsense/{device_id}/api/diag/cmd/php | Integrity (lab on mock only). Both 200. Plus RBAC was not executed, so a live role gate is unproven. |
Run PHP / shell / file edit on every enrolled box if the login includes console or system | POST /diag/cmd/php, POST /diag/cmd/shell, GET/POST /diag/editfile, download/upload | Integrity (inferred from OAS + examples). Intended admin power once the proxy accepts the fleet token. Typical pfSense PHP-FPM is root. |
| Export CA / leaf private keys and PKCS12 on every listed instance | GET /system/certificates/{refid}/export-key, GET /system/certauth/{refid}/export-key, GET /system/certificates/{refid}/export-p12 | Recon (inferred). Same JWT, same schema as leftover 4. Plus RBAC not executed. |
| Dump ACME account and issued keys on every listed instance | GET /services/acme and related account/cert GETs | Recon (inferred). Same JWT, leftover 5 schema. |
| Change aliases, firewall, interfaces, VPN, services on every listed instance | same per-device prefix, remaining 700+ ops | Integrity (inferred). Same JWT, same schema as the local instance. |
| Reboot / halt / restore config on every listed instance | POST /diag/control, POST /diag/backup/restore | Availability + integrity (inferred). Same proxy. Not exercised. |
| Cross-customer SaaS IDOR | n/a | Closed. One customer tenancy. Netgate runs the controller in that tenancy. |
| Unauthenticated Internet-to-:8443 | GET /mim/devices with no Bearer | Closed in this lab. 401. Wrong Bearer is also 401. |
| Offline devices | samples skip state != online | Closed by samples. Not an auth boundary. |
Blind numeric walk of device_id | GET /api/device/pfsense/1/api/system | Closed as a guess requirement. Ids are long strings (ED25519-shaped). Listing removes the guess. |
Requires: a controller login that is allowed onto the device proxy. Network path to the MIM controller (:8443 in product, loopback :19161 in this lab). Not unauth. Not CSRF.
| Surface | Why |
|---|---|
| Unauth listing / php / shell | Mock 401 without Bearer and with a wrong Bearer. Spec silence on securitySchemes is not a server 200. |
| SaaS cross-tenant | One customer tenancy. Tenancy clusters were still planned on the marketing page. |
mim_user / no Nexus group | Documented as closed if the login gate holds. |
Functional mim_* role names | They exist (view, logs, usermgmt, services, firewall, interfaces, devices, system, superuser). They are global, not per device_id. Live Plus checks were not executed (plus-rbac=not-executed). |
| Enrollment SSH JSON interpolation | Leftover 1. Different pack. |
Enrollment AutoAddPolicy | Leftover 2. Different pack. |
| Device unix socket without JWT | Leftover 3. Local, different pack. |
| CE REST package | jaredhendrickson/pfsense-api is a different server. |
| CE WebGUI QinQ / OpenVPN wizard / CARP / limiter / IPsec banner | Already mailed as CE leftovers. Field names in this OAS are the same sinks, not new CVEs. |
Intended admins / mim_superuser fleet console | Product model for the controller owner. The leftover is the missing object ACL for everyone else. |
I pinned b4acd7ca and read the official examples against the OpenAPI. Every sample that talks to managed firewalls does this:
devApi = AuthenticatedClient(
base_url=CONTROLLER_URL+"/api/device/pfsense/{}/api".format(device.device_id),
verify_ssl=False,
headers={"Content-Type": "application/json"},
cookies=cookies,
token=token,
)