pfSense Plus MIM API toolkit b4acd7ca - 高 - リモート - ファイル読み取り - Snortルール {file} デコード後結合
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · pfsense-api-snort-file-join
クラス: ファイル読み取り 到達範囲: リモート
pfSense Plus MIM API (Netgate/pfsense-api b4acd7ca) - Netgate
GET /services/snort/rules/{name}/{file} は 2 つのパスパラメータを取ります。どちらも type: string で、pattern はありません。説明: ルールセット内のファイルを検査します。SnortRuleFileContents.filename は「ファイルのパス」です。生成された get_snort_ruleset_file._get_kwargs は quote(str(...), safe="") を実行するため、../ はワイヤ上で ..%2F になります。URL デコードしてからルールセットディレクトリに結合するサーバーは、依然として外に抜け出します。
そのパスを GET でき、かつフリート JWT を持つ呼び出し元は、登録されたすべてのボックス上で MIM エージェントとして、指定されたルールセット外のファイルを読み取ります。さらに join は実行されませんでした。このラボは、公開パスのデコードしてから結合するガジェットです。
これは 9 件中の残り 8 件目です。マップ: pfsense-api-nexus-mim。同じピンの残り: fleet JWT。これは jaredhendrickson/pfsense-api ではありません。
| ID | CVE はまだありません |
| クラス | ファイル読み取り (Snort {file} のデコードしてから結合) |
| 到達範囲 | リモート (認証済み GET、フリート JWT はすべての device_id で同じパスを再利用します) |
| CWE | CWE-22 |
| CVSS | High: 6.5 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N (Snort ルールセットファイルを GET できる可能性のある JWT に対して) |
| 製品 | Netgate/pfsense-api (pfSense Plus Nexus MIM コントローラー OpenAPI + 生成クライアント) |
| 影響を受けるバージョン | main b4acd7ca21dbca739a530403a1f62a7440f7726f (2026-05-25、Add Snort-3 API definitions) |
| 認証 | 例では POST /login の後にコントローラー JWT を送信します。OpenAPI には securitySchemes がありません。未認証は証明されていません。 |
| ライセンス | GNU Affero GPL v3.0 |
| ラボ | 127.0.0.1:19181 のみ。デコードしてから結合するガジェット。さらに join は実行されませんでした。 |
GET /services/snort/rules/{name}/{file} を実行できる呼び出し元が必要です。公式の例では POST /login の後に Authorization: Bearer を使用し、そのトークンを GET /mim/devices のすべての id に対して base_url=.../api/device/pfsense/{device_id}/api として再利用します。生成された操作は依然として AuthenticatedClient | Client と型付けされています。この GET では CSRF は関係しません。
| Method | Sink | Privilege | Lab vs inferred |
|---|---|---|---|
生成されたクライアントによる GET({file}=../../oracle/WITNESS、ワイヤ上は ..%2F..%2Foracle%2FWITNESS) | unquote(file) をデコードし、指定された ruleset ディレクトリに結合して読み取り | Recon: HTTP 200、filename は /oracle/WITNESS、ボディは PFSENSE-API-SNORT-FILE-JOIN-WITNESS。本来はその ruleset 内の .rules ファイルであるべき。 | ラボで実証済み(ガジェット)。なお join は実行されなかった。 |
同じ GET で {file}=../../../etc/passwd | コントローラがデバイスのファイルシステムにマッピングした場合、同じ join | Recon: MIM エージェントとしてホストファイルを読み取る(Plus WebGUI/PHP は通常 root)。ラボでは /oracle/WITNESS を使用し、実際の /etc/passwd は使用していない。 | 推測(同じ join、ホストパス)。 |
fleet JWT 取得後の同じ GET(POST /login を一度実行し、/api/device/pfsense/{id}/api でリプレイ) | 同じ制約のない {file} のデバイスプロキシ | Recon: ヘルプデスク向けのコントローラトークン1つで、ログイン対象の1台だけでなく、登録されたすべてのボックス上のファイルを読み取れる。 | 推測(G6 契約 + このパス)。なお RBAC は実行されていない。 |
{name} 上の同じ残存箇所 | GET|DELETE /services/snort/rules/{name} および同種の address/suppress {name} も制約のない文字列 | Recon / 整合性: 名前がパスである ruleset をリネームまたは削除できる。 | 仕様からの推測。ラボでは {file} のみを操作した。 |
| 同じワイヤパスの制限された制御 | デコード後の relative_to(ruleset_dir.resolve()) | クローズ。HTTP 400、witness なし。 | ラボで実証済み |
良性の {file}=local.rules | 意図された ruleset の読み取り | クローズ。200、sid:1、witness なし、naive と control の両方。 | ラボで実証済み |
隣接するクローズ済み項目: ../ のクライアント側パーセントエンコーディング(encoding=%2F)。デコード後の制御 join(control-confined=yes)。Plus コントローラの join(plus-join-not-executed)。ruleset foo 内の local.rules の意図された読み取り。未認証 GET(未証明。例では Bearer を送信)。POST /services/snort/rules のアーカイブ / 植え付けられた .so(意図された IDS 管理者であり、この残存箇所ではない)。Diagnostics PHP/shell。ファイル書き込み。ホストシェル。この残存箇所は RCE ではない。
Netgate/pfsense-api の b4acd7ca21dbca739a530403a1f62a7440f7726f を固定した。2026-10-14 時点の HEAD にはタグがなく、最新コミットが「Add Snort-3 API definitions」だったためである。これがこのパスが存在する理由である。YAML にはどこにも pattern: キーが1つもない。{name} と {file} は制約のない文字列である。quote(..., safe="") はクライアント側のエンコーダであり、サーバー側の basedir チェックではない。
/services/snort/rules/{name}/{file}:
parameters:
- in: path
name: name
required: true
schema:
type: string
- in: path
name: file
required: true
schema:
type: string
get:
description: Obtain the contents of the specified file in the ruleset. This is intended for inspecting scripts provided by it.
operationId: GetSnortRulesetFile
"url": "/services/snort/rules/{name}/{file}".format(
name=quote(str(name), safe=""),
file=quote(str(file), safe=""),
),
誤った方向性として既に記録済み: クライアントエンコーディングをクロージングトラバーサルとして扱うこと、pfSense Plus の構成、Snort アーカイブ .so をこの残存物としてランク付けすること、未認証 GET の主張。演目: リバースシェル。オラクルは /oracle/WITNESS のガジェット 200 に制御 400 を加えたもの。
python3 pfsense-api-snort-file-join-Abraxas-Labs.py
または
cd lab
./run.sh
のみ http://127.0.0.1:19181 を対象とする。Compose プロジェクト pfsense-api-snort-file-join。公開パスの decode-then-join ガジェット。Plus join は実行されない。実行後にスタックは破棄される。
SUCCESS PFSENSE-API-SNORT-FILE-JOIN encoding=%2F decode-then-join=yes control-confined=yes plus-join-not-executed PFSENSE-API-SNORT-FILE-JOIN-WITNESS
../../../etc/passwd のクライアントエンコーディングは ..%2F..%2F..%2Fetc%2Fpasswd です。/services/snort/rules/foo/..%2F..%2Foracle%2FWITNESS への素朴な GET は 200 を返し、ファイル名は /oracle/WITNESS、ボディは PFSENSE-API-SNORT-FILE-JOIN-WITNESS でした。ルールセットルート配下に解決される制限付きコントロール下での同一リクエストは 400 を返し、witness は得られませんでした。良性の local.rules は foo 内に留まりました。
デコード後にルールセットルート配下で解決します。.. と絶対パスを拒否します。読み取り前に Path.resolve() の後に relative_to(ruleset_dir.resolve())、または同等の処理を行います。OpenAPI で {file}(および {name})をルールセット安全なパターンに制約し、ジェネレータがパス形状の文字列を送信しないようにします。
b4acd7ca21dbca739a530403a1f62a7440f7726fpfapi_openapi.yml /services/snort/rules/{name}/{file}get_snort_ruleset_file.pyGNU Affero GPL v3.0。LICENSE を参照してください。ループバックラボ専用。無保証。