
Análisis detallado y prueba de concepto para CVE-2023-41564, una vulnerabilidad XSS almacenada en Cockpit CMS <=2.6.3, con causa raíz a nivel de código y guía de mitigación.
CVE-2023-415641 es una vulnerabilidad de XSS almacenado presente en el CMS Cockpit (<= v2.6.3).
Al cargar un archivo HTML con extensión .shtml durante la subida de archivos, el JavaScript se ejecuta en el momento en que se abre el archivo subido.
1: https://nvd.nist.gov/vuln/detail/CVE-2023-41564
Primero, explicaremos el método de ataque basado en el PoC2 publicado por quien parece ser el reportador de la vulnerabilidad.
Primero, inicia sesión en el sitio con una cuenta que tenga permisos para subir archivos.
Luego, sube un archivo shtml como el siguiente:
Copia la URL del archivo (Asset) subido.
Al acceder al enlace copiado, se ejecuta el script descrito en el HTML anterior.
2: https://github.com/LongHair00/Mitre_opensource_report/blob/main/CockpitCMS-StoredXSS.md
A partir de aquí, explico los resultados de mi investigación más profunda.
El proceso de carga de Assets comienza con una solicitud POST a la ruta /assets/upload.
El manejo de la solicitud de carga está definido en la línea 513 de modules/Assets/bootstrap.php.
A continuación se muestra el código.
// 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];
},
// 省略
]);
La función upload se almacena en la variable miembro registry['modules']['assets'] de la clase App, que contiene las funciones del módulo Assets.
Por lo tanto, al llamar a la función upload, se invoca como $this->module('assets')->upload('file', $file);
La función upload registrada aquí es llamada por la clase 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);
}
Puede ver que se está llamando a la función upload registrada en registry['modules']['assets'].
Ahora, veamos la función upload definida en bootstrap.php.
Aquí se definen las extensiones prohibidas en la variable $forbiddenExtension.
Sin embargo, falta la extensión shtml.
Por lo tanto, esto es un defecto que permite la subida de archivos 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
Dado que la causa es que shtml no está prohibido, se puede mitigar agregando shtml a forbiddenExtension.
$forbiddenExtension = ['bat', 'exe', 'sh', 'php', 'phar', 'phtml', 'phps', 'htm', 'html', 'xhtml', 'htaccess', 'shtml'];