
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/ में मौजूद है: