
pfSense Plus MIM API toolkit b4acd7ca - High - Local - Privilege leftover - unix sock enroll no JWT
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · pfsense-api-unix-sock-noauth
Class: Privilege leftover Reach: Local
pfSense Plus MIM API b4acd7ca - Netgate
github.com/Netgate/pfsense-api is the public OpenAPI plus generated Python for pfSense Plus Nexus Multi-instance Management. It is not the CE REST package. The official enroll sample logs into HTTPS :8443 with a Bearer JWT, then SSHs a POSIX script onto each firewall. That script talks to /var/run/pfnet-controller.sock with no Authorization.
curl --unix-socket /var/run/pfnet-controller.sock -H "Content-Type: application/json" -d "{controller JSON}" http://unix/api/mim/controllers
curl --unix-socket /var/run/pfnet-controller.sock http://unix/api/mim/device/identity
Deprecated fallbacks /api/system/controller and /api/system/mim/devid are the same: no Bearer. HTTPS login still issues a JWT. That token is not applied to the on-device sock. This is a separate trust boundary from :8443 plus mim_* groups.
Part 3 of the Nexus MIM series. After G1 / G2 land on the box, this sock is the local plane.
A local process that can connect(2) the sock can enroll a controller and dump device identity with no JWT. Plus was not executed. Real sock DAC is unknown.
| ID | no CVE yet |
| Class | Privilege leftover |
| Reach | Local (connect(2) to /var/run/pfnet-controller.sock) |
| CWE | CWE-306 |
| CVSS | High: 7.8 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H if sock mode lets another local uid connect. Honest: DAC unknown, Plus not executed. |
| Product | Netgate/pfsense-api (Plus Nexus MIM toolkit) |
| Affected | public main b4acd7ca21dbca739a530403a1f62a7440f7726f (2026-05-25) |
| Auth | none on the official unix-socket enroll curls |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1:19121 only. Unix HTTP mock. No Plus image. |
Actor is any process on the pfSense device that can connect(2) to /var/run/pfnet-controller.sock. Not remote TCP by this example (curl --unix-socket only). HTTPS :8443 JWT and mim_* groups do not apply on this fd.
| Method | Sink | Privilege |
|---|---|---|
Official device_script POST with no Authorization | POST http://unix/api/mim/controllers | Integrity: mock accepted enroll JSON. Hit log authorization_present=false. |
Official device_script GET with no Authorization | GET http://unix/api/mim/device/identity | Recon: mock returned DeviceIdentity-shaped JSON including PFSENSE-API-UNIX-SOCK-NOAUTH-WITNESS. |
Same GET with Authorization: Bearer lab-jwt | same identity path | Lab control: 401, witness not in body. Distinguishes present vs absent. Not a Plus result. |
These are the same OpenAPI ops the official sample and the mim tag put on the device. Proven only as protocol, not as a Plus 200.
| Method | Sink | Privilege |
|---|---|---|
POST /mim/controllers (fallback POST /api/system/controller) with attacker ControllerIdentity (key, vpn_pubkey, vpn_listenaddr, vpn_netkey, cert) | SetController | Integrity: enroll a hostile controller. Spec: device "will continue to issue a connection request every 10 seconds" (Netgard VPN to the attacker). |
GET /mim/device/identity (fallback GET /api/system/mim/devid) | GetDeviceIdentity | Recon: device Noise/VPN public keys, address, controller_add curl text. |
GET /mim/controllers | GetControllers | Recon: list controllers plus device info (ControllerInfo.noise_secret, keys). |
GET /mim/controllers/id/{key} | GetMIMController | Recon: one controller record. |
DELETE /mim/controllers/id/{key} | DeleteMIMController | Integrity / availability: unenroll the real controller (key hex or base64-URL). |
POST /mim/device/pubkey with attacker privkey / pubkey | SetDevicePublicKey (DevicePublicKeyOption includes privkey) | Integrity: replace the device ED25519 identity. Controller handshake then trusts the attacker key. |
Does not grant a host root shell by itself. Grants the MIM plane: who the device trusts as controller, and the device Noise private key if pubkey POST is honored.
Requires: local connect(2) to /var/run/pfnet-controller.sock. DAC (mode, owner, group, SO_PEERCRED) is not in the public client tree. If the daemon binds 0600 root, this is already-root local (confused deputy for a helper, package, or escape that can see /var/run). If the sock is reachable to another uid, CVSS 7.8 as scored.
/mim/controllers on Nexus HTTPS :8443. That port is documented as User Manager JWT. This leftover is the unix fd.POST /login still issues a Bearer JWT. AuthenticatedClient still sends it to CONTROLLER_URL+"/api".verify_ssl=True. Path params percent-encode ../.security: MISSING on all 755 ops is a schema gap, not a proven unauth HTTPS skip.Same pin, remote enrollment plane: pfsense-api-enroll-json-ssh and pfsense-api-enroll-autoadd. Series map: pfsense-api-nexus-mim.
I pinned Netgate/pfsense-api b4acd7ca21dbca739a530403a1f62a7440f7726f and read the official enrollment loop: login, GET /mim/controller/identity, stringify, SSH exec of unix-socket POST/GET, POST /mim/devices. HTTPS uses AuthenticatedClient(token=...). The sock curls do not.
Wrong turns: treating missing securitySchemes as unauth HTTPS (login is missing it too; examples always Bearer first). Assuming the sock is world-writable (mode is not in this repo). Claiming a Plus 200. The leftover proven here is the published enroll protocol.
Theatre: a reverse shell. The oracle is the official curl shape plus a witness in the identity body plus a Bearer 401 control.
python3 pfsense-api-unix-sock-noauth-Abraxas-Labs.py
or
cd lab
./run.sh
Loopback only: 127.0.0.1:19121. Compose project pfsense-api-unix-sock-noauth. Mock unix HTTP at /var/run/pfnet-controller.sock. Witness PFSENSE-API-UNIX-SOCK-NOAUTH-WITNESS. Docker unix-socket mock. No Plus image.
Expected last line:
SUCCESS PFSENSE-API-UNIX-SOCK-NOAUTH auth-absent=yes identity-witness=yes bearer-401=yes PFSENSE-API-UNIX-SOCK-NOAUTH-WITNESS