pfSense Plus MIM API 工具包 b4acd7ca - 高危 - 远程 - 权限残留 - example-add-devices.py JSON 转 SSH
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · pfsense-api-enroll-json-ssh
类别: 权限残留 可达性: 远程
pfSense Plus MIM API 工具包(b4acd7ca)- Netgate
example-add-devices.py 是文档中记载的、通过 SSH 在每台 pfSense Plus 设备上注册 Nexus 控制器的方式。它对 GET /mim/controller/identity 的结果执行 json.dumps,仅替换 ",然后将该数据块两次 .format 进一个双引号的 curl -d POSIX 脚本。Paramiko 的 exec_command 会以 SSHUSER(示例默认值为 admin)身份在 pfsense_addresses 中的每个地址上运行该脚本。
json.dumps 加上 replace('"', '\\"') 使引号保持闭合。$()、反引号和 ${...} 不是 JSON 元字符。它们仍然在仍然打开的双引号 -d 字符串内保持活跃。OpenSSH 的 exec 是一个 shell。这就是残留。
这不是第三方 CE REST 包。它是 pfSense Plus Nexus 多实例管理控制器的官方 OpenAPI 客户端和示例。我并未编写 Plus。实验环境是固定版本的插值加上一个模拟的 /var/run/pfnet-controller.sock。
| ID | 暂无 CVE |
| 类别 | 权限残留(控制器 JSON 进入注册 SSH) |
| 可达性 | 远程(在示例设置 verify_ssl=False 时位于 CONTROLLER_URL 路径上,或可写入 ControllerSummary 字符串字段) |
| CWE | CWE-78 |
| CVSS | 高危:8.1 CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 产品 | Netgate/pfsense-api(Plus MIM 工具包) |
| 受影响 | 公开 main b4acd7ca21dbca739a530403a1f62a7440f7726f example-add-devices.py |
| 认证 | 操作员运行文档中的示例。攻击者位于路径上,或可写入摘要字符串。仍需先登录控制器。 |
| 许可证 | GNU Affero GPL v3.0 |
| 实验环境 | 仅 127.0.0.1:19101。无 Plus 镜像。 |
Nexus MIM 残留系列的第 1 部分。同类项目:enroll-autoadd、unix-sock-noauth、example-ssl-verify(G9 说明了路径上的对等方如何塑造此 JSON)、fleet-jwt、export-key-get。
残留是仍然打开的双引号 -d 字符串内的命令替换。json.dumps 加上引号处理使字符串保持闭合。$()、反引号和 ${...} 得以保留,并在 OpenSSH exec 该脚本时以 SSHUSER 身份运行。
| 方法 | 汇聚点 | 权限 | 实验室验证或推断 |
|---|---|---|---|
在 ControllerSummary 字符串字段中放入 $()(或反引号 / ${...}),然后让操作员运行 example-add-devices.py | controller_info.replace('"', '\\"') 然后 device_script.format(...) 然后 sshclient.exec_command(device_script)(example-add-devices.py 约 104-161 行) | 在 pfsense_addresses 中每个地址上以 SSHUSER 身份获得 shell(示例默认 admin;pfSense 管理员 SSH 通常等同于 root) | 实验室验证 插值:INJECT vpn_listenaddr=$(printf %s PFSENSE-API-ENROLL-JSON-SSH-WITNESS >/oracle/witness) 在仅引号转义后存活(dollar-paren-survived=yes)并写入了见证文件(inject-oracle=yes)。Paramiko/SSH 本身不在此 compose 中。 |
对 GET /api/mim/controller/identity 进行 TLS 中间人攻击,而示例使用 verify_ssl=False | 相同的 ControllerSummary 解析,然后相同的脚本 | 相同的 shell。所有 ControllerSummary 字段都是 str 或 Unset,因此一个类型可解析的 MITM JSON 正文就足够了。 | 推断 自示例(example-add-devices.py 登录 Client 和 AuthenticatedClient 都设置了 verify_ssl=False)。实验室直接提交 summary 对象。同类:example-ssl-verify。 |
将替换字符写入控制器上的 name 或 vpn_listenaddr,等待下一次注册 | 相同的汇聚点 | 当操作员下次运行示例时,相同的 shell | 推断。需要事先写入这些字段(已经是控制器管理员,或 unix-sock-noauth 同类)。 |
相同的 shell,通过 ControllerSummary.additional_properties 附加额外键 | 相同的转储包含额外的 MITM 键(to_dict 在 from_dict 之后复制剩余项) | 额外字段进入 -d 并进入 shell | 推断 自 to_dict。实验室仅通过 vpn_listenaddr 注入。 |
| 方法 | 汇聚点 | 权限 | 实验室验证或推断 |
|---|---|---|---|
| 读取注册工作站的 stdout | print("CONTROLLER_SUMMARY:", controller_info) | 控制器身份 key、Netgard vpn_pubkey / vpn_netkey / 监听地址 | 推断 自示例打印。实验室不打印实时控制器数据块。 |
| 嗅探或 MITM SSH exec 通道 | exec_command(device_script) 载荷是 curl -d JSON | 在发往每台设备的线路上具有相同的控制器身份 | 推断。该通道上的 AutoAddPolicy 是同类报告。 |
| 对 gadget 进行 CONTROL 用例 | POST /case name=control | 诚实的 vpn_listenaddr 原样到达 unix socket | 实验室验证。 192.0.2.10:51820 原样提交,无 oracle(control-clean=yes)。 |
| 方法 | 汇聚点 | 权限 | 实验室验证或推断 |
|---|---|---|---|
以 SSHUSER 身份注入命令 | 相同的 exec_command | 停止 pfnet-controller、清除注册、重启,或以其他方式使设备脱离 MIM | 推断 的 shell 副作用。实验室仅写入见证文件。 |
要求: 官方示例路径(操作员注册)。一旦该示例运行,路径上对等方的 UI:N。登录控制器仍然首先发生。
在 pfSense 机器上以 admin 身份获得 shell 并不是“JWT 可以 php”。它是注册工作站通过 sh -c 将控制器数据块交给防火墙。从那里,整形后的 JSON 是设备在没有 JWT 的情况下 POST 到 /var/run/pfnet-controller.sock 的内容(unix-sock-noauth)。AutoAddPolicy 意味着接收此脚本的 SSH 对等方未被固定(enroll-autoadd)。G9 verify_ssl=False 正是路径上对等方最初写入 JSON 的方式。
| 攻击面 | 原因 |
|---|---|
生成的库默认 verify_ssl=True | 这个遗留项是示例,不是 pfapi.Client 的默认值。 |
curl -d "..." 的引号突破 | JSON 加上 " 替换使字符串保持闭合(quote-only-escape=yes)。 |
来自通过 from_dict / json.dumps 的 ControllerSummary 的原始换行脚本拆分 | U+000A 变成 \n。 |
| 注册工作站上的 RCE | 格式是本地。shell 是远程 exec。 |
| 到 Nexus 控制器的未认证 HTTPS | login.sync 仍然先运行。 |
add_controlled_device.sync | 将解析后的 JSON POST 到 HTTPS。不执行 DeviceIdentity.controller_add。 |
| CONTROL listenaddr | 没有 $() / 反引号。不写入 oracle。 |
INJECT 在 unix POST body 中的 $() | 展开发生在 sh 中,在 curl 之前(inject-meta-reached-unix 为 fail)。 |
Paramiko AutoAddPolicy() | 兄弟包。不要与此混用。 |
| 无 JWT 的设备 unix-socket MIM | 兄弟包。 |
| pfSense Plus 控制器/守护进程 | 不在此仓库中。未组合。 |
预期的 POST /diag/cmd/php 和 /diag/cmd/shell | 已是管理员的 Nexus 控制台。不是这个遗留项。 |
| 第三方 CE REST 包 | 不同的 GitHub 树。 |
我固定了 github.com/Netgate/pfsense-api b4acd7ca21dbca739a530403a1f62a7440f7726f(2026-05-25,Add Snort-3 API definitions)。没有标签。该树是 OpenAPI 3.0.3(pfapi_openapi.yml,486 个路径,755 个操作)加上生成的 Python 和示例。HTTPS 控制器和 pfnet-controller 是 Plus 专有,不在仓库中。
十二次搜寻。Login/JWT、诊断、MIM 注册、客户端 TLS、用户/证书、包、VPN/防火墙、OpenAPI 安全、设备 IDOR、git/固件/feed,加上对每个生成操作的环回 mock。755/755 个 _get_kwargs 发送到 127.0.0.1:55980。AuthenticatedClient 总是发送 Bearer。Client 不发送任何,包括 php/shell。路径参数对 ../ 进行百分号编码。库默认 verify_ssl=True。每个官方示例都设置为 False。
跳过:已公开的 CE WebGUI 遗留项(QinQ、OpenVPN 向导、CARP VHID、限流器、IPsec 登录横幅)。跳过 Diagnostics:Command PHP/shell 作为未公开的 RCE。Netgate 将其视为预期的管理员权限。跳过第三方 CE 包。跳过 OpenAPI security: MISSING 作为未认证 HTTPS。该规范在任何地方都没有 securitySchemes,包括登录。
注册示例是文档化的路径。README 将其呈现为如何在每个设备上注册控制器。我阅读了 dump、引号处理,以及 .format 进入 curl -d "{}"。然后我阅读了 POSIX 引用。
错误转向:期望 json.dumps 中和 $();期望解析后的摘要注入原始换行;将 -d 的引号突破视为 bug。实际类别是仍然闭合的双引号字符串内的 POSIX 命令替换。
戏剧性:反向 shell。oracle 是一个见证文件加上 dollar-paren-survived=yes。
python3 pfsense-api-enroll-json-ssh-Abraxas-Labs.py
或
cd lab
./run.sh
仅回环:127.0.0.1:19101。Compose 项目 pfsense-api-enroll-json-ssh。见证 PFSENSE-API-ENROLL-JSON-SSH-WITNESS。enroll.py 是固定的 json.dumps / 引号替换 / .format 序列。lab_server.py 模拟设备 unix socket,并在 bash 下运行该脚本。运行后栈拆除。Plus 未组合。
CONTROL:vpn_listenaddr=192.0.2.10:51820 原样提交,无见证。