pfSense Plus MIM API toolkit b4acd7ca - High - Remote - Privilege leftover - example-add-devices.py JSON into SSH
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · pfsense-api-enroll-json-ssh
Class: Privilege leftover Reach: Remote
pfSense Plus MIM API toolkit (b4acd7ca) - Netgate
example-add-devices.py is the documented way to register the Nexus controller on each pfSense Plus device over SSH. It json.dumpss GET /mim/controller/identity, replaces only ", then .formats that blob twice into a double-quoted curl -d POSIX script. Paramiko exec_command runs the script as SSHUSER (sample default admin) on every address in pfsense_addresses.
json.dumps plus replace('"', '\\"') keeps the quotes closed. $(), backticks, and ${...} are not JSON metacharacters. They stay live inside the still-open double-quoted -d string. OpenSSH exec is a shell. That is the leftover.
This is not the third-party CE REST package. It is the official OpenAPI client and samples for the pfSense Plus Nexus Multi-instance Management controller. I did not compose Plus. The lab is the pinned interpolation plus a mock /var/run/pfnet-controller.sock.
| ID | no CVE yet |
| Class | Privilege leftover (controller JSON into enrollment SSH) |
| Reach | Remote (on-path to CONTROLLER_URL while the sample sets verify_ssl=False, or a writer of ControllerSummary string fields) |
| CWE | CWE-78 |
| 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 MIM toolkit) |
| Affected | public main b4acd7ca21dbca739a530403a1f62a7440f7726f example-add-devices.py |
| Auth | operator runs the documented sample. Attacker is on-path, or can write summary strings. Login to the controller still happens first. |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1:19101 only. No Plus image. |
Part 1 of the Nexus MIM leftover series. Siblings: enroll-autoadd, unix-sock-noauth, example-ssl-verify (G9 is how an on-path peer shapes this JSON), fleet-jwt, export-key-get.
The leftover is command substitution inside the still-open double-quoted -d string. json.dumps plus the quote pass keep the string closed. $(), backticks, and ${...} survive and run as SSHUSER when OpenSSH execs the script.
| Method | Sink | Privilege | Lab-proven or inferred |
|---|---|---|---|
Put $() (or backticks / ${...}) in a ControllerSummary string field, then let the operator run example-add-devices.py | controller_info.replace('"', '\\"') then device_script.format(...) then sshclient.exec_command(device_script) (example-add-devices.py ~104-161) | Shell as SSHUSER (sample default admin; pfSense admin SSH is typically root-equivalent) on each address in pfsense_addresses | Lab-proven on the interpolation: INJECT vpn_listenaddr=$(printf %s PFSENSE-API-ENROLL-JSON-SSH-WITNESS >/oracle/witness) survived quote-only escape (dollar-paren-survived=yes) and wrote the witness (inject-oracle=yes). Paramiko/SSH itself is not in this compose. |
TLS MITM of GET /api/mim/controller/identity while the sample uses verify_ssl=False | Same ControllerSummary parse then the same script | Same shell. All ControllerSummary fields are str or Unset, so a MITM JSON body that type-parses is enough. | Inferred from the sample (example-add-devices.py login Client and AuthenticatedClient both set verify_ssl=False). Lab posts the summary object directly. Sibling: example-ssl-verify. |
Write substitution characters into name or vpn_listenaddr on the controller, wait for the next enroll | Same sink | Same shell when an operator next runs the sample | Inferred. Needs a prior write of those fields (already controller-admin, or the unix-sock-noauth sibling). |
Same shell, extra keys via ControllerSummary.additional_properties | Same dump includes extra MITM keys (to_dict copies leftovers after from_dict) | Extra fields ride into -d and into the shell | Inferred from to_dict. Lab injects through vpn_listenaddr only. |
| Method | Sink | Privilege | Lab-proven or inferred |
|---|---|---|---|
| Read the enrollment workstation stdout | print("CONTROLLER_SUMMARY:", controller_info) | Controller identity key, Netgard vpn_pubkey / vpn_netkey / listen addresses | Inferred from the sample print. Lab does not print a live controller blob. |
| Sniff or MITM the SSH exec channel | exec_command(device_script) payload is the curl -d JSON | Same controller identity on the wire to every device | Inferred. AutoAddPolicy on that channel is a sibling report. |
| CONTROL case on the gadget | POST /case name=control | Honest vpn_listenaddr reaches the unix socket unchanged | Lab-proven. 192.0.2.10:51820 posted as-is, no oracle (control-clean=yes). |
| Method | Sink | Privilege | Lab-proven or inferred |
|---|---|---|---|
Injected command as SSHUSER | Same exec_command | Stop pfnet-controller, wipe enrollment, reboot, or otherwise drop the device off MIM | Inferred side effect of the shell. Lab only writes the witness file. |
Requires: the official sample path (operator enroll). UI:N for the on-path peer once that sample is running. Login to the controller still happens first.
A shell as admin on a pfSense box is not "the JWT can php." It is the enrollment workstation handing the firewall a controller blob through sh -c. From there the shaped JSON is what the device POSTs to /var/run/pfnet-controller.sock with no JWT (unix-sock-noauth). AutoAddPolicy means the SSH peer that receives this script is not pinned (enroll-autoadd). G9 verify_ssl=False is how the on-path peer writes the JSON in the first place.