
Analisi dettagliata e proof-of-concept per CVE-2023-41564, una vulnerabilità XSS memorizzata in Cockpit CMS <=2.6.3, con causa principale a livello di codice e guida alla mitigazione.
CVE-2023-415641 è una vulnerabilità di tipo XSS persistente presente nel CMS Cockpit (<= v2.6.3).
Caricando un file HTML con estensione .shtml durante l'upload, JavaScript viene eseguito nel momento in cui si apre il file caricato.
1: https://nvd.nist.gov/vuln/detail/CVE-2023-41564
Prima di tutto, spiegheremo la tecnica di attacco basandoci sul PoC2 pubblicato dalla persona che sembra essere la segnalatrice della vulnerabilità.
Per prima cosa, accedi al sito con un account che ha i permessi di caricamento dei file.
Successivamente, carica un file shtml come il seguente.
Copia l'URL del file caricato (Asset).
Accedendo al link copiato, lo script scritto nell'HTML sopra viene eseguito.
2: https://github.com/LongHair00/Mitre_opensource_report/blob/main/CockpitCMS-StoredXSS.md
Da qui in poi, spiegherò i risultati di un'indagine più approfondita che ho condotto.
Il processo di caricamento degli Assets ha origine da una richiesta POST al percorso /assets/upload.
La gestione della richiesta di caricamento è definita alla riga 513 di modules/Assets/bootstrap.php.
Di seguito è mostrato il codice.
// 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 funzione upload viene salvata nella variabile membro registry['modules']['assets'] della classe App, la quale memorizza le funzioni del modulo Assets.
Pertanto, per chiamare la funzione upload, si usa $this->module('assets')->upload('file', $file);.
La funzione upload registrata qui viene chiamata dalla classe 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);
}
Come si può notare, viene chiamata la funzione upload registrata in registry['modules']['assets'].
Ora osserviamo la funzione upload descritta in bootstrap.php.
In questa funzione, le estensioni vietate sono definite nella variabile $forbiddenExtension.
Tuttavia, l'estensione shtml è assente.
Di conseguenza, questa è la falla che rende possibile il caricamento dei file 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
Poiché la causa è che shtml non è escluso, è possibile porre rimedio aggiungendo shtml a forbiddenExtension.
$forbiddenExtension = ['bat', 'exe', 'sh', 'php', 'phar', 'phtml', 'phps', 'htm', 'html', 'xhtml', 'htaccess', 'shtml'];