
تحليل مفصل وإثبات مفهوم لـ CVE-2023-41564، وهي ثغرة XSS مخزنة في Cockpit CMS <=2.6.3، مع السبب الجذري على مستوى الكود وإرشادات التخفيف.
CVE-2023-415641 هي ثغرة XSS مخزّنة (Stored XSS) موجودة في نظام إدارة المحتوى (CMS) المسمى Cockpit (بالإصدار <= v2.6.3).
وتتمثل هذه الثغرة في أنه عند رفع ملف HTML بامتداد .shtml أثناء رفع الملفات، يتم تنفيذ JavaScript فور فتح الملف المرفوع.
1: https://nvd.nist.gov/vuln/detail/CVE-2023-41564
في البداية، سأشرح أسلوب الهجوم استنادًا إلى كود الإثبات PoC2 الذي نشره الشخص الذي يُرجَّح أنه مبلّغ الثغرة.
أولاً، سجّل الدخول إلى الموقع باستخدام حساب يمتلك صلاحية رفع الملفات.
ثم ارفع ملف shtml مثل الملف الموضح أدناه.
انسخ عنوان URL للملف المرفوع (Asset).
عند الوصول إلى الرابط المنسوخ، يتم تنفيذ السكربت المكتوب في كود HTML الموضح أعلاه.
2: https://github.com/LongHair00/Mitre_opensource_report/blob/main/CockpitCMS-StoredXSS.md
من هنا سأشرح نتائج التحقيق الأعمق الذي أجريته حول الثغرة.
تبدأ عملية رفع الأصول (Assets) من طلب POST يُرسل إلى المسار /assets/upload.
تم تعريف معالجة طلب الرفع في السطر 513 من الملف modules/Assets/bootstrap.php.
فيما يلي الكود:
// assets api
$this->module('assets')->extend([
// 省略
'upload' => function(string|array $param = 'files', array $meta = [], bool $isUpload = true) {
$files = [];
if (is_string($param) && isset($this->app->request->files[$param])) {
$files = $this->app->request->files[$param];
} elseif (is_array($param) && isset($param['name'], $param['error'], $param['tmp_name'])) {
$files = $param;
}
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$uploaded = [];
$failed = [];
$_files = [];
$assets = [];
$allowed = $this->app->retrieve('assets/allowed_uploads', '*');
$allowed = $allowed == '*' ? true : str_replace([' ', ','], ['', '|'], preg_quote(is_array($allowed) ? implode(',', $allowed) : $allowed));
$max_size = $this->app->retrieve('assets/max_upload_size', 0);
$forbiddenExtension = ['bat', 'exe', 'sh', 'php', 'phar', 'phtml', 'phps', 'htm', 'html', 'xhtml', 'htaccess'];
$forbiddenMime = [
'application/x-httpd-php', 'application/x-php', 'text/x-php',
'text/html', 'application/xhtml+xml'
];
if (isset($files['name']) && is_array($files['name'])) {
$cnt = count($files['name']);
for ($i = 0; $i < $cnt; $i++) {
$_file = $this->app->path('#tmp:').'/'.$files['name'][$i];
$_mime = $finfo->file($files['tmp_name'][$i]);
$_isAllowed = $allowed === true ? true : preg_match("/\.({$allowed})$/i", $_file);
$_sizeAllowed = $max_size ? filesize($files['tmp_name'][$i]) < $max_size : true;
$extension = strtolower(pathinfo(parse_url($_file, PHP_URL_PATH), PATHINFO_EXTENSION));
if (!$extension) {
$_isAllowed = false;
}
// prevent uploading php / html files
if ($_isAllowed && (
in_array($extension, $forbiddenExtension) ||
in_array(strtolower($_mime), $forbiddenMime)
)) {
$_isAllowed = false;
}
if (!$files['error'][$i] && $_isAllowed && $_sizeAllowed && ($isUpload ? move_uploaded_file($files['tmp_name'][$i], $_file) : rename($files['tmp_name'][$i], $_file))) {
$_files[] = $_file;
$uploaded[] = $files['name'][$i];
if (\preg_match('/\.(svg|xml)$/i', $_file)) {
file_put_contents($_file, \SVGSanitizer::clean(\file_get_contents($_file)));
}
} else {
$failed[] = $files['name'][$i];
}
}
}
if (count($_files)) {
$assets = $this->add($_files, $meta, isset($meta['_id']));
foreach ($_files as $file) {
unlink($file);
}
}
return ['uploaded' => $uploaded, 'failed' => $failed, 'assets' => $assets];
},
// 省略
]);
يتم تخزين دالة upload في متغير عضو في صنف App يحتفظ بدوال وحدة Assets، ويُسمى هذا المتغير registry['modules']['assets'].
لذلك، عند استدعاء دالة upload، يتم ذلك باستخدام الصيغة: $this->module('assets')->upload('file', $file);
يتم استدعاء دالة upload المسجلة هنا بواسطة صنف Assets4.
public function upload() {
this->helper('session')->close();
this->hasValidCsrfToken(true);
if (!this->isAllowed('assets/upload')) {
return $this->stop(['error' => 'Upload not allowed'], 401);
}
$meta = ['folder' => $this->param('folder', '')];
return $this->module('assets')->upload('files', $meta);
}
يمكنك ملاحظة أن دالة upload المسجلة في registry['modules']['assets'] هي التي يتم استدعاؤها.
لنلقِ نظرة الآن على دالة upload المكتوبة في bootstrap.php.
يتم هنا تعريف قائمة الامتدادات المحظورة في المتغير $forbiddenExtension.
ولكن، امتداد shtml غير مدرج في القائمة.
لذلك، تُعد هذه نقطة الخلل التي تجعل رفع ملفات shtml ممكنًا.
3: https://github.com/Cockpit-HQ/Cockpit/blob/2.6.3/modules/Assets/bootstrap.php#L51
4: https://github.com/Cockpit-HQ/Cockpit/blob/2.6.3/modules/Assets/Controller/Assets.php#L140
بما أن سبب الثغرة هو أن امتداد shtml غير محظور، يمكن معالجتها بإضافة shtml إلى قائمة forbiddenExtension.
$forbiddenExtension = ['bat', 'exe', 'sh', 'php', 'phar', 'phtml', 'phps', 'htm', 'html', 'xhtml', 'htaccess', 'shtml'];