pfSense Plus MIM API toolkit b4acd7ca - High - Remote - Privilege leftover - Paramiko AutoAddPolicy enroll
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · pfsense-api-enroll-autoadd
Class: Privilege leftover Reach: Remote
pfSense Plus MIM API (Netgate/pfsense-api) b4acd7ca - Netgate
example-add-devices.py is the documented way to register the Nexus controller to each pfSense device. It never load_system_host_keys(). It sets paramiko.AutoAddPolicy() then connect then exec_command(device_script). The comment says to drop AutoAdd when devices are already in known_hosts. Shipping default is AutoAdd.
First-contact SSH to a new box accepts whatever host key answers. Password or key auth is presented to that peer. stdout is json.loads'd and POST /mim/devices'd to the real controller.
This is not the third-party CE REST package. This is the official OpenAPI plus generated Python client for pfSense Plus Nexus Multi-instance Management. Server is Plus-only. The lab is a Paramiko SSH gadget. I did not talk to a live Plus controller.
| ID | no CVE yet |
| Class | Privilege leftover (Paramiko AutoAddPolicy on enrollment SSH) |
| Reach | Remote (on-path to device SSH on first contact) |
| CWE | CWE-322 |
| CVSS | High: 8.1 CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| Product | Netgate/pfsense-api (Plus Nexus MIM client toolkit) |
| Affected | main b4acd7ca21dbca739a530403a1f62a7440f7726f example-add-devices.py |
| Auth | Operator runs the official sample; attacker is on-path to device SSH |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1:19111 only. No Plus image. |
AC:H is first-contact / on-path. A workstation that already has the real device keys in known_hosts and has the AutoAdd line removed is not this leftover.
Same pin leftovers: JSON interpolation of ControllerSummary into the enrollment shell (G1) and samples force verify_ssl=False (G9). Series hub: pfsense-api-nexus-mim.
Sit on-path to a first-contact SSH address in pfsense_addresses while the operator runs the sample. AutoAdd is still on.
| Method | Sink | Privilege |
|---|---|---|
Sit on-path to a first-contact SSH address in pfsense_addresses | set_missing_host_key_policy(paramiko.AutoAddPolicy()) then connect (example-add-devices.py:151-160) | Integrity (lab-proven): unknown host key is accepted. Lab: AutoAddPolicy connected (autoadd=yes). RejectPolicy failed (reject-control=yes). |
Same session, answer exec_command(device_script) | stdout parsed as DeviceIdentity (example-add-devices.py:161-187) | Integrity (lab-proven): exec returned identity JSON containing PFSENSE-API-ENROLL-AUTOADD-WITNESS (identity-json=yes). |
| Same MITM, read the exec payload | device_script is leftover G1's curl -d of ControllerSummary | Recon (inferred from source): controller identity key, Netgard vpn_pubkey / vpn_netkey / listen addrs. Not dumped by this lab. |
| Same MITM, password or key auth | SSHUSER / SSHPASS or agent/keys (example-add-devices.py:156-160) | Recon (inferred): credentials or keys presented to the fake host. Lab used password auth only as the sample SSHPASS path. |
Same stdout JSON, add_controlled_device.sync | POST /mim/devices on the real controller | Integrity (inferred): registers attacker Noise/VPN keys and address. Plus handshake not executed in this lab. |
| MITM is the SSH server | whatever exec the client sent, as SSHUSER | Integrity / availability (inferred): run, drop, or skip the enrollment script. Unix-socket calls on the real device happen only if the MITM forwards them. |
Lab-proven vs inferred: AutoAdd connect, RejectPolicy fail, and identity JSON with the witness are lab. Controller-key dump, credential theft, POST /mim/devices of a fake device, and running the real unix-socket curls are inferred from the same sample path.
Requires: operator runs the official sample against an SSH address whose host key is not already pinned, with AutoAdd left on. Network path to that SSH. AC:H is first-contact / on-path.
| Surface | Why |
|---|---|
RejectPolicy on the same unknown host | Lab: reject-control=yes. Paramiko refuses the key. |
Workstation with real device keys in known_hosts and AutoAdd line removed | The comment's intended mode. Sample never loads those keys unless you add the call. |
| HTTPS controller login | verify_ssl=False is a sibling sample leftover (G9), not this sink. |
| Unix-socket MIM on the real firewall | Only if the MITM forwards exec. This lab never talks to /var/run/pfnet-controller.sock. |
Generated Client default verify_ssl=True | Library default is not the leftover. Samples force False. |
Plus RBAC / live :8443 | This lab is a Paramiko gadget. I did not talk to a Plus controller. |
| Unauthenticated Internet-to-8443 | Login still issues a JWT. Not this finding. |
| Diagnostics PHP/shell | Intended admin power for a fleet JWT. Not this finding. |
I pinned github.com/Netgate/pfsense-api main b4acd7ca21dbca739a530403a1f62a7440f7726f after the CE WebGUI leftovers. This tree is OpenAPI plus generated Python for Plus Nexus MIM. I read the enrollment loop in example-add-devices.py. Host-key policy is AutoAdd. Secrets sit on the same exec channel. The sample never calls load_system_host_keys().
Wrong turns already recorded: treating AutoAdd as intended-lab-only (the comment is opt-out, not opt-in); assuming known_hosts is loaded; folding this into the ControllerSummary shell interpolation leftover; live-labbing Plus; claiming this is unauth HTTPS to :8443.
Theatre: a reverse shell. The oracle is RejectPolicy fail plus AutoAdd connect plus identity JSON with the witness.
python3 pfsense-api-enroll-autoadd-Abraxas-Labs.py
or
cd lab
./run.sh
Loopback SSH 127.0.0.1:19111. Compose project pfsense-api-enroll-autoadd. CONTROL: RejectPolicy fails unknown host. INJECT: official-sample AutoAddPolicy, connect, exec_command of the enrollment script; stdout is identity JSON with the witness. No live Plus controller. Witness PFSENSE-API-ENROLL-AUTOADD-WITNESS. Stack tears down after the run.
SUCCESS PFSENSE-API-ENROLL-AUTOADD autoadd=yes reject-control=yes identity-json=yes PFSENSE-API-ENROLL-AUTOADD-WITNESS