pfSense Plus MIM API toolkit b4acd7ca - High - Remote - IDOR - ACB userkey capability URL
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · pfsense-api-acb-userkey
Class: IDOR Reach: Remote
pfSense Plus MIM API (Netgate/pfsense-api b4acd7ca) - Netgate
GET /services/acb and GET /services/acb/bkp take userkey as an unconstrained query parameter. The generated Python client puts that key on the query string. The YAML has no securitySchemes. Possession of the Auto Config Backup device key is the check. That is a capability URL.
A stolen or leaked ACB userkey lists that device's hosted backups and fetches a revision body. Full config sits in those blobs. Same class as CE ACB key CVE-2024-57273 / SA-25_03 if the key leaks. That CVE was SSH-banner derivation. This leftover is the Plus MIM API query-param sibling. I am not re-filing the banner derivation.
This is the public OpenAPI + generated client for pfSense Plus Nexus Multi-instance Management (controller on :8443). It is not the CE REST package (jaredhendrickson/pfsense-api / pfrest.org). The server is Plus-only (paid). I did not execute pfSense Plus.
| ID | no CVE yet |
| Class | IDOR (capability URL) |
| Reach | Remote |
| CWE | CWE-639 |
| CVSS | High: 7.5 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N if those GETs honor userkey without binding a JWT to the device |
| Product | Netgate/pfsense-api (pfSense Plus MIM OpenAPI + generated client) |
| Affected | pin b4acd7ca21dbca739a530403a1f62a7440f7726f (2026-05-25). No tags. |
| Auth | Spec: none (securitySchemes absent). userkey is the query secret. Plus JWT/RBAC was not executed. |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1:19171 only. No Plus image. |
Series hub: pfsense-api-nexus-mim (this is part 7).
Requires a stolen or leaked ACB userkey (spec: "hexidecimal encoded 32-byte random bytes", 64 hex). The spec does not bind that key to a JWT or to the caller's device. G11-class GET /services/acb/config returns the live userkey. G9 samples set verify_ssl=False, so a MITM of :8443 can see the key on the wire. Those are leak paths. This leftover is what the key buys once it is out.
| Method | Sink | Privilege |
|---|---|---|
GET /services/acb?userkey=<stolen> | GetACBList (pfapi_openapi.yml). Generated get_acb_list.py params["userkey"]. | Lab-proven (mock): list that device's hosted backups. Witness in backups[].reason. |
GET /services/acb/bkp?userkey=<stolen>&revision= | GetACB -> RetrievedBackup.backup. Generated get_acb.py params["userkey"] + params["revision"]. | Lab-proven (mock): fetch the revision body. Witness in backup. |
| Same backup string on a live controller | hosted ACB blob | Inferred: full config.xml (users, keys, packages) once readable. Mock returned plaintext witness XML. I did not talk to acb.netgate.com or decrypt a live blob. |
GET /services/acb/config | ACBConfigInfo.userkey, ACBConfig.encryption_password | Spec, not this lab: live capability secret and decrypt password. G11 secrets-on-GET class. Not exercised. |
| Method | Sink | Privilege |
|---|---|---|
POST /services/acb/restore | RestoreACB body { timestamp } | Adjacent closed: samples still send a JWT. Not this leftover. Not executed. |
POST /services/acb/config replace userkey | SaveACBConfig | Adjacent closed: not a GET capability URL. Not executed. |
GET list / GET bkp do not delete. DELETE /services/acb/bkp/{timestamp} is a different op. Not exercised.
Wrong userkey and missing userkey returned no witness (lab-proven).
Lab-proven vs inferred: stolen-key list and bkp returned the witness. Wrong key and missing key did not. Plus was not executed. Live config.xml contents and decrypt are inferred from the public schema (RetrievedBackup.backup is a string; encryption_password sits on ACBConfig).
Requires: network path to the MIM controller ACB GETs, plus a stolen/leaked userkey. CVSS 7.5 if those GETs honor the query key without binding a JWT to the device. Honest: this pack proves the public spec models that contract, on loopback.
| Surface | Why |
|---|---|
| pfSense Plus controller | not executed. No Plus image in this pack. |
| CE REST package | different server (jaredhendrickson/pfsense-api / pfrest.org). |
| SSH-banner ACB key derivation | CVE-2024-57273 / SA-25_03. Do not re-file. |
POST /services/acb/restore as this finding | samples still send a JWT. |
Unauth php/shell from omitted OAS security | docs leftover. Mock is not a server 200-without-JWT proof. |
Missing userkey | lab 403, no witness. |
Wrong userkey | lab list empty / bkp 403, no witness. |
Generated library verify_ssl | default True. G9 is the samples turning it off, not the package default. |
I pinned Netgate/pfsense-api main b4acd7ca (2026-05-25, "Add Snort-3 API definitions"). Twelve hunts over login/JWT, diagnostics, MIM enrollment, client TLS, users/certs, packages, VPN/firewall, OpenAPI security, device IDOR, git/firmware/feeds, plus a loopback mock of every generated operation.
Skip list first: CE WebGUI leftovers already mailed (QinQ parent if, OpenVPN wizard CRLF, CARP VHID, limiter number, IPsec login banner). Diagnostics PHP/shell as intended admin power. The third-party CE REST package. CVE-2024-57273 banner derivation.
I read GetACBList and GetACB. userkey is a query parameter, not a bound device id. Generated Python copies it onto the query string. No securitySchemes in the YAML. ACBConfigInfo.userkey is "hexidecimal encoded 32-byte random bytes". GET /services/acb/config returns that key. G9 samples disable TLS verify, so a MITM can leak it. G11 is the GET dump. This leftover is the capability URL that dump feeds.
Wrong turns: treating missing OAS security as unauth php/shell; re-filing CVE-2024-57273; claiming a live Plus dump. Theatre: a Plus controller. The oracle is the public spec plus a loopback mock.
python3 pfsense-api-acb-userkey-Abraxas-Labs.py
or
cd lab
./run.sh