
pfSense Plus MIM API toolkit b4acd7ca - 中 - リモート - 権限の残存 - 公式サンプル verify_ssl=False
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · pfsense-api-example-ssl-verify
クラス: 特権の置き忘れ 到達範囲: リモート
pfSense Plus MIM API (Netgate/pfsense-api b4acd7ca) - Netgate
生成された Client / AuthenticatedClient はデフォルトで verify_ssl=True となり、それを httpx に渡します。ドキュメントではすでに verify_ssl=False をセキュリティリスクと呼び、PEM バンドルパスを先に示しています。公式の Python サンプルと helper_funcs.py はすべて、依然として verify_ssl=False でこれらを構築しています。example.js は NODE_TLS_REJECT_UNAUTHORIZED=0 を設定するため、Node プロセス全体が検証をスキップします。ログイン JSON はユーザー名とパスワードを base64 で格納します。これは OpenAPI の契約であり、暗号化ではありません。TLS が機密性の層です。
:8443 の on-path ピアは任意の証明書を提示します。サンプルはそれでも /login に POST します。なりすましサーバーは base64 の管理者ログインを取得し、JWT を発行します。そのトークンが連鎖の起点です: G4 export-key、G5 ACME dump、G6 fleet JWT、G7 ACB userkey、意図された php/shell、そして G1 が SSH に補間する JSON。
これは pfSense Plus MIM API toolkit の Part 9 です。Medium ですが、まずこの置き忘れを読んでください。盗まれた JWT こそが、example.py をコピーしたワークステーションから残りに到達可能にする手段です。
| ID | CVE はまだなし |
| クラス | 特権の置き忘れ (サンプルの TLS 検証オフ) |
| 到達範囲 | リモート (:8443 への on-path) |
| CWE | CWE-295 |
| CVSS | Medium: 6.8 CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N |
| 製品 | Netgate/pfsense-api (Plus Nexus MIM クライアント。CE REST パッケージではない) |
| 影響を受けるもの | 公開 main b4acd7ca21dbca739a530403a1f62a7440f7726f (2026-05-25) の公式サンプルと helper_funcs.py |
| 認証 | オペレーターがサンプルをコピーしてコントローラーに対して実行する。追加のコントローラーロールは不要 |
| ライセンス | GNU Affero GPL v3.0 |
| ラボ | 127.0.0.1:19191 のみ。pfSense Plus を構成しない。 |
AC:H はオペレーターワークステーションと MIM コントローラーの間の on-path です。UI:R はサンプルをコピー/実行することです。
被害者: https://controller:8443 に対して公式サンプルまたは helper_funcs.py を実行しているオペレーターワークステーション。攻撃者: その HTTPS への on-path (LAN、不正 DHCP、侵害された VPN ホップ、ARP)。未認証のインターネットからファイアウォールへの攻撃ではありません。
| Method | Sink | Privilege |
|---|---|---|
サンプルが verify_ssl=False / NODE_TLS_REJECT_UNAUTHORIZED=0 で /login または /api/login に POST している間に任意の証明書を提示する | httpx verify=False(Python サンプル)または Node TLS reject-unauthorized 0 | 機密性(ラボ実証済み): ログイン JSON を読み取る。ラボ: 代替サーバーが base64 の admin / lab-controller-secret をキャプチャ(captured_b64_admin=yes)。デコードは base64 -d 一発。 |
同じ MITM、200 LoginResponse | サンプルは任意の証明書を受け入れる | 機密性(ラボ実証済み): アクセス JWT を窃取する。ラボ: INJECT は PFSENSE-API-EXAMPLE-SSL-VERIFY-WITNESS を運ぶトークンを取得した。 |
同じ MITM、ログイン時の Set-Cookie | サンプルは client.get_httpx_client().cookies を AuthenticatedClient にコピーする | 機密性(ラボ実証済みの Cookie、推測されるリプレイ): 24 時間有効なリフレッシュ JWT。その Cookie と base64 ユーザー名を使ってリフレッシュ TTL まで POST /login/refresh をリプレイする。ラボは Set-Cookie: refresh=... をキャプチャした。稼働中のコントローラーに対するリフレッシュは実行していない。 |
| 窃取した Bearer を実際のコントローラーにリプレイする(攻撃者がそこへもルーティングできる場合) | さらに MIM /api と /api/device/pfsense/{device_id}/api | 完全性(推測): この API 上でログイン済みユーザーと同等の管理者権限。スキーマには POST /diag/cmd/php、POST /diag/cmd/shell、POST /diag/editfile、POST /diag/backup/restore、POST /diag/control の halt/reboot が含まれる。ここでは実行していない。このラボはコントローラーの代替サーバーであり、Plus ではない。 |
| 窃取した Bearer を leftover 6(フリートトークン)としてリプレイする | 1 つの POST /login JWT をすべての device_id で再利用する | 完全性(推測): ファイアウォール A 向けのユーザーとして認証した後、ファイアウォール B と通信する。LoginResponse にはデバイスごとの許可リストがない。パック: pfsense-api-fleet-jwt。Plus RBAC は実行していない。 |
| 窃取した Bearer を leftover 4 としてリプレイする | GET /system/certauth/{refid}/export-key、GET /system/certificates/{refid}/export-key、GET /system/certificates/{refid}/export-p12 | 機密性(推測): CA とリーフの秘密鍵を取得する。PKCS12 はパスワード保護なしで文書化されており、その後 VPN / WebGUI TLS / IPsec をなりすます。パック: pfsense-api-export-key-get。 |
| 窃取した Bearer を leftover 5 としてリプレイする | GET /services/acme | 機密性(推測): ファイアウォールが登録するすべてのホスト名について、Let's Encrypt アカウントキーと発行済み証明書キーを窃取する。パック: pfsense-api-acme-key-dump。 |
窃取した Bearer をリプレイする、または ACB を GET するサンプルから userkey を漏洩させ、leftover 7 として使う | GET /services/acb?userkey= と GET /services/acb/bkp?userkey= | 機密性(推測): Auto Config Backup のブロブ(完全な設定)を一覧表示して取得する。パック: pfsense-api-acb-userkey。 |
ControllerSummary JSON を leftover 1 に向けて整形する | verify_ssl=False の HTTPS を MITM し、example-add-devices.py が引用符エスケープしてから .format で SSH に埋め込む JSON を供給する | 完全性(推測): コントローラーフィールド内の $() / バッククォート / 改行が、pfsense_addresses 内のすべてのアドレス上で SSHUSER として実行される。パック: pfsense-api-enroll-json-ssh。 |
| クライアントに向けてレスポンスを注入する | skip-verify は任意の証明書を受け入れる | 完全性(ラボ実証済みトークン、推測されるフリート): サンプルが保持し続ける JWT を鋳造する。ラボ: witness 付きの INJECT トークン。稼働中のサンプルに対する偽のデバイスリスト / アップグレード進捗 / ログイン成功も同じゲートである。 |
POST /diag/control、ファームウェア POST、壊れたバックアップの復元(窃取した Bearer 取得後) | TLS が失われた後の意図された管理者サーフェス | 可用性(推測): halt / reboot / 壊れたイメージ。同じ Bearer。ラボ未実施。 |
ラボ実証済み vs 推測: CONTROL ハンドシェイク失敗、INJECT トークン、キャプチャされた base64 管理者ログインは 127.0.0.1:19191 上でラボ実証済みである。その Bearer を稼働中の Plus コントローラーへリプレイすること、diag php/shell、G4-G7、G1 JSON 整形、および新しいアクセス JWT を鋳造するリフレッシュ Cookie は、OpenAPI とサンプルから推測される。
必要なもの: オペレーターが example.py、example-add-devices.py、example-upgrade-devices.py、example-set-alias.py / example-upgrade-devices-async.py を helper_funcs.py 経由で、または example.js をコピーして実行する。攻撃者は :8443 への経路上にいる。
閉じたままの隣接する攻撃面
| 攻撃面 | 閉じている理由 |
|---|---|
kwargs なしで生成された Client(base_url=...) | verify_ssl=True。ラボでの対照: CERTIFICATE_VERIFY_FAILED、HTTP キャプチャはゼロ。 |
| クロスホスト 3xx の資格情報転送 | 生成された follow_redirects=False。 |
| このクライアントがログインとして受理する偽造 JWT | クライアントはリソースサーバーではない。ペイロードのパースは exp の表示のみ。 |
| オペレータークライアントなしでの 8443 への未認証インターネット到達 | Plus の MIM バインド/ACL が閉じたサーバーであり、このツールキットではない。 |
example.conf にコミットされたパスワード | PASSWORD 行はコメントアウトされている。 |
| CE REST パッケージ (jaredhendrickson / pfrest.org) | 別製品。 |
| pfSense Plus 自体 | この GitHub ツリーには含まれない。このラボは Plus を構成しない。 |
verify をスキップするサンプルは、それ自体では pfSense ボックス上での未認証 RCE にはつながらない。これらがもたらすのは、サンプルをコピーしたオペレーターに対する MITM である。Shell/PHP の診断は、管理者 Bearer を窃取した後の推測されたスキーマ権限である。
client = Client(base_url=CONTROLLER_URL+"/api",
verify_ssl=False,
headers={"Content-Type": "application/json"})
example.py:47、example.py:78、example.py:120、helper_funcs.py:135、helper_funcs.py:167、および createDeviceApiChild / clone、example-add-devices.py:64、example-add-devices.py:94、example-upgrade-devices.py:57、:86、:132。helper_funcs.get_settings() には VERIFY_SSL 環境変数がありません(PASSWORD とは異なります)。
// To be able to use with self-signed Controller TLS certificates:
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0;
サンプルがオーバーライドする生成されたデフォルト:
_verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl")
py/pfapi/client.py:42 と AuthenticatedClient 上の同じフィールド。どちらも verify=self._verify_ssl を httpx に渡している。py/README.md では verify_ssl="/path/to/certificate_bundle.pem" が最初に示され、次に False が これはセキュリティリスクです とともに示されている。