abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · pfsense-api-acme-key-dump
类别: 权限残留 可达性: 远程
pfSense Plus Nexus 多实例管理 OpenAPI 工具包 b4acd7ca - Netgate
GET /services/acme(GetACMEConfig)返回 ACMEConfig,且没有 write-only 或脱敏标志。200 schema 包含 ACME 账户私钥、每一张已签发的叶子私钥、任何自定义叶子密钥,以及用于验证的 DNS 提供商令牌。证书管理器至少还有一个专用的 GET .../export-key。ACME 则完全没有。配置 GET 就是转储。
一个能够 GET 此路径的控制器会话,会复制防火墙注册的每一个主机名密钥,以及证明对这些名称拥有控制权的 DNS 凭据。
此仓库是公开的 OpenAPI 加上一个生成的 Python 客户端。它不是 CE REST 包。MIM 控制器仅限 Plus,且不在 GitHub 仓库中。我没有与实时控制器交互过。plus-not-executed=1。
| ID | 暂无 CVE |
| 类别 | 权限残留(配置 GET 上的机密转储) |
| 可达性 | 远程(已认证的 MIM 控制器;未证明未认证可达) |
| CWE | CWE-200 |
| CVSS | 高危:6.5 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N,前提是 services-read 角色能收到响应体。如果只有超级用户能 GET 它,这种转储对于配置列表来说仍然是错误的形态。 |
| 产品 | Netgate/pfsense-api 用于 pfSense Plus Nexus MIM 的 OpenAPI |
| 受影响 | 公开 main b4acd7ca21dbca739a530403a1f62a7440f7726f(2026-05-25,Add Snort-3 API definitions)。无标签。 |
| 认证 | 来自 POST /login 的控制器 JWT。示例发送 Bearer。规范中没有 securitySchemes。 |
| 许可证 | GNU Affero GPL v3.0 |
| 实验环境 | 仅 127.0.0.1:19151。无 Plus 镜像。 |
系列中心:pfSense Plus MIM leftovers。这是第 5 部分。可与 G9 窃取的 JWT(示例设置 verify_ssl=False)和 G6 车队令牌(一个登录 Bearer 被复用于每个 device_id)串联。同类转储:G4 cert-manager export-key。
需要一个能够 GET /services/acme 的控制器会话。文档将其映射到 mim_services_read / mim_services / mim_superuser。规范并未将这些角色附加到该路径上。Plus RBAC 未被执行。
来自 G9 的窃取 JWT(对官方示例的 MITM)或来自 G6 的 helpdesk 车队令牌,足以将此 GET 指向每一台已注册的设备。该链条是推断的。此残留就是 GET 响应体。
| 方法 | 数据泄露点 | 权限 |
|---|---|---|
GET /services/acme | ACMEConfig.account_keys[].account_key(format: byte) | 已在公开规范和模拟环境中验证: 200 响应模式包含 account_key;模拟响应体往返验证了 PFSENSE-API-ACME-KEY-DUMP-WITNESS。在 Plus 上推断: 复制 Let's Encrypt(或其他 ACME)账户私钥。在带外与 ACME 账户通信。 |
| 同一 GET | certs[].issued_cert.privateKey(format: byte) | 已在规范和模拟环境中验证。 在 Plus 上推断: 复制防火墙注册的每个已签发叶子证书私钥。对这些域名进行带外 TLS 冒充。 |
| 同一 GET | certs[].custom_key(format: byte) | 已在规范和模拟环境中验证。 在 Plus 上推断: 当 key_type=custom 时复制自定义叶子证书密钥。 |
| 同一 GET | certs[].domain_list[].config.credentials(additionalProperties: string) | 已在规范和模拟环境中验证。 在 Plus 上推断: 复制 DNS 验证密钥(Cloudflare 令牌、AWS 密钥、RFC2136 密钥)。这些可证明对域名的控制权。 |
| 同一 GET | 账户邮箱、自定义服务器 URL、action_list.command | 已验证这些字段存在于模式中。 钩子执行属于修改路径,而非此 GET。 |
GET 本身无影响。后续使用窃取的密钥和 DNS 令牌属于带外操作(签发恶意证书、与 ACME 账户通信、更改 DNS)。未实际测试。
无影响。配置列表不会吊销或重写证书。
前提条件: 已认证的 MIM 控制器访问权限,且允许 GET ACME 标签。未证明可未认证访问。为此操作生成的 _get_kwargs 不添加 Authorization;示例仍会登录并发送 Bearer。
| 攻击面 | 原因 |
|---|---|
| 在线 pfSense Plus / Nexus 控制器 | 未执行。plus-not-executed=1。 |
| 未认证的互联网到 8443 端口 | 示例需要 POST /login。缺少 OAS security 是规范缺陷,而非服务器无 JWT 返回 200 的证明。 |
POST /services/acme/cert/{name}/issue 和 .../renew | 预期的服务修改操作。 |
POST /services/acme/genprivatekey | 预期的生成并返回新账户密钥。并非此遗留问题。 |
POST /diag/cmd/php / POST /diag/cmd/shell | 预期的管理控制台。独立攻击面。厂商将 Diagnostics: Command 视为预期功能(CVE-2025-69691 有争议)。 |
GET /system/certificates/{refid}/export-key | 独立的遗留问题:证书管理器上的专用密钥导出。ACME 没有等效的导出操作。 |
GET /services/acme/validmethods | 方法目录。无密钥。 |
CE jaredhendrickson/pfsense-api | 不同产品。 |
在 CE 2.9.0-DEVELOPMENT 浪潮之后,我锁定了公开的 Netgate/pfsense-api b4acd7ca。此代码树是 OpenAPI 3.0.3(pfapi_openapi.yml,486 个路径,755 个操作)加上为 :8443 上的 Plus MIM 控制器生成的 Python 客户端。无标签。克隆当天最新的公开提交是 2026-05-25,"Add Snort-3 API definitions."
围绕登录/JWT、诊断、MIM 注册、客户端 TLS、用户/证书、软件包、VPN/防火墙、OpenAPI 安全、设备 IDOR、git/固件/源,以及每个生成操作的环回模拟,进行了十二次搜寻。
先列跳过清单:已发送邮件的五个 CE 遗留问题(QinQ parent-if、OpenVPN 向导 CRLF、CARP VHID、限制器编号、IPsec 登录横幅)。第三方 CE REST 包。诊断 PHP/shell 作为未公开的 RCE。缺少 securitySchemes 作为已证明的未认证访问。POST /system/firmware 没有 URL。Ping/DNS/traceroute 属于诊断功能。
我阅读了 GET /services/acme。200 响应的 $ref 是 ACMEConfig。遍历属性会命中 account_key、privateKey、custom_key 和 credentials,密钥字段带有 format: byte。生成的客户端 get_acme_config.py 调用 GET /services/acme 并解析 ACMEConfig。
走过的弯路:将 POST /services/acme/genprivatekey 视为遗留问题(该生成功能是特性);将缺少 securitySchemes 视为已证明的未认证访问(示例仍发送 Bearer);声称非超级用户角色在 Plus 上实际能收到密钥。
演示:一台在线 Plus 设备。判定依据是公开的模式加上环回模拟的 200 响应。
pfapi_openapi.yml GET /services/acme:
/services/acme:
get:
tags:
- services
summary: Get ACME Config
operationId: GetACMEConfig
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/ACMEConfig'
ACMEAccountKey.account_key:
account_key:
type: string
description: |
if key_type is not "custom", system will overwrite this field with generated private key
format: byte
ACMEIssuedCert.privateKey:
privateKey:
type: string
format: byte
ACMECertificate.custom_key 是 format: byte。ACMEValidationMethodConfig.credentials 是 additionalProperties: string。
生成的客户端 py/pfapi/api/services/get_acme_config.py _get_kwargs:
_kwargs: dict[str, Any] = {
"method": "get",
"url": "/services/acme",
}
python3 pfsense-api-acme-key-dump-Abraxas-Labs.py
或:
cd lab
./run.sh
仅回环:http://127.0.0.1:19151。Compose 项目 pfsense-api-acme-key-dump。见证 PFSENSE-API-ACME-KEY-DUMP-WITNESS。在运行时克隆或复制固定提交 b4acd7ca21dbca739a530403a1f62a7440f7726f。需要 Docker。无需 Plus 镜像。
该 PoC 遍历固定提交的 OpenAPI 以查找这四个密钥字段,加载生成的 _get_kwargs(GET /services/acme,无 Authorization),并通过模拟的 200 响应往返验证见证。
IOC plus-not-executed=1 mock=http://127.0.0.1:19151 pin=b4acd7ca21dbca739a530403a1f62a7440f7726f
IOC schema-get-acme account_key=1 privateKey=1 custom_key=1 credentials=1
IOC mock-get http=200 account_key=1 privateKey=1 custom_key=1 credentials=1
SUCCESS pfsense-api-acme-key-dump schema=account_key,privateKey,custom_key,credentials mock-http=200 plus-not-executed=1 PFSENSE-API-ACME-KEY-DUMP-WITNESS
从 GET /services/acme 中移除私钥和 DNS 凭据。仅在显式的超级用户导出操作中返回它们。其形态与专用的导出密钥操作相同,而非配置列表。在 GET schema 中将那些字段标记为 writeOnly,以便生成的客户端不再从列表解析它们。