CVE-2024–55371 & CVE-2024–55372: Malicious File Upload to RCE in Wallos Application
Overview
Wallos is an open-source, self-hosted web application that makes it easy to manage your personal finances.
Today, I’m sharing details about two CVEs that represent the same vulnerability with different attack vectors, discovered in Wallos. I discovered two critical vulnerabilities, affecting all versions up to and including 2.38.2. These flaws allow both authenticated and unauthenticated attackers to upload malicious files, leading to remote code execution on the system. These vulnerabilities are now identified as CVE-2024–55371 (Authenticated RCE) and CVE-2024–55372 (Unauthenticated RCE).
CVE-2024–55371 — Authenticated Malicious File Upload Lead to RCE
Description
The vulnerability exists in the “\endpoints\db\restore.php” file (Restore Backup feature), which lets authenticated users upload a ZIP file to restore backups. The problem is that when the ZIP file is extracted, the files never get cleaned up whether the backup succeeds or fails. This means an attacker can sneak in a malicious file inside a ZIP and get remote code execution on the system.
Attack Vector
To conduct this attack, an attacker must be authenticated to the web application, being an administrator is not required.
The bug
Consider the PHP code below
Feature: Restore Backup
File: \endpoints\db\restore.php
<?php
…
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
die(json_encode([
"success" => false,
"message" => translate('session_expired', $i18n)
]));
}
…
The first part of the code only performs an authentication check but skips authorization, allowing non-administrative users to access this function.
…
if (isset($_FILES['file'])) {
$file = $_FILES['file'];
$fileTmpName = $file['tmp_name'];
$fileError = $file['error'];
if ($fileError === 0) {
$fileDestination = '../../.tmp/restore.zip';
move_uploaded_file($fileTmpName, $fileDestination);
$zip = new ZipArchive();
if ($zip->open($fileDestination) === true) {
$zip->extractTo('../../.tmp/restore/');
$zip->close();
} else {
die(json_encode([
"success" => false,
"message" => "Failed to extract the uploaded file"
]));
}
if (file_exists(‘../../.tmp/restore/wallos.db’)) {
if (file_exists('../../db/wallos.db')) {
unlink('../../db/wallos.db');
}
rename('../../.tmp/restore/wallos.db', '../../db/wallos.db');
if (file_exists(‘../../.tmp/restore/logos/’)) {
$dir = '../../images/uploads/logos/';
$di = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS);
$ri = new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST);
foreach ($ri as $file) {
if ($file->isDir()) {
rmdir($file->getPathname());
} else {
unlink($file->getPathname());
}
}
$dir = new RecursiveDirectoryIterator(‘../../.tmp/restore/logos/’);
$ite = new RecursiveIteratorIterator($dir);
$allowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp'];
foreach ($ite as $filePath) {
if (in_array(pathinfo($filePath, PATHINFO_EXTENSION), $allowedExtensions)) {
$destination = str_replace('../../.tmp/restore/', '../../images/uploads/', $filePath);
$destinationDir = pathinfo($destination, PATHINFO_DIRNAME);
if (!is_dir($destinationDir)) {
mkdir($destinationDir, 0755, true);
}
copy($filePath, $destination);
}
}
}
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator('../../.tmp', RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $fileinfo) {
$removeFunction = ($fileinfo->isDir() ? 'rmdir' : 'unlink');
$removeFunction($fileinfo->getRealPath());
}
echo json_encode([
"success" => true,
"message" => translate("success", $i18n)
]);
} else {
die(json_encode([
"success" => false,
"message" => "wallos.db does not exist in the backup file"
]));
}
} else {
echo json_encode([
"success" => false,
"message" => "Failed to upload file"
]);
}
…
It can be noticed that the uploaded ZIP file will be extracted to “../../.tmp/restore/”. The code then looks for a file (wallos.db) in that directory if it’s there, the restore process starts and the files get cleaned up afterward. But if the wallos.db file is missing, the restore and cleanup processes are skipped, leaving all the extracted files sitting in the “../../.tmp/restore folder”.
Proof Of Concept
- I logged into the Wallos web application as a user (not admin privilege) and collected session cookie.
2. I archived the web shell (pwn.php) into pwn.zip.
3. I used Curl command to upload a pwn.zip to the server. The server responded an error message cause from the wallos.db is missing, which mean the ZIP file was extracted on the server and the web shell should now be in the “/.tmp/restore/” directory.
4. I gained remote code execution through the web shell.
CVE-2024–55372 Unauthenticated Malicious File Upload Lead to RCE
Description
The vulnerability exists in the “\endpoints\db\import.php” file (Registration feature), which lets unauthenticated users upload a ZIP file to restore databases. The problem is that when the ZIP file is extracted, the files never get cleaned up whether the backup succeeds or fails. This means an attacker can sneak in a malicious file inside a ZIP and get remote code execution on the system.
Attack Vector
To exploit this vulnerability the Wallos web application must not have any users in the database.
The Bug
Since it uses the same code, the bug refers to the previous analysis in the CVE-2024–55372.
Feature: Restore database on registration feature
File: \endpoints\db\import.php
Proof Of Concept
- Navigated to the web application. Since there were no users in the database, it allowed unauthenticated users to either create an account or restore the database.
2. I used Curl command to upload a pwn.zip to the server. The server responded an error message cause from the wallos.db is missing, which mean the ZIP file was extracted on the server and the web shell should now be in the “/.tmp/restore/” directory.
3. I gained remote code execution through the web shell.
Conclusion
CVEs like these pose a serious security risk to users. As always, keeping systems up-to-date and following security best practices can help reduce exposure to this and other vulnerabilities. That’s the details on the two CVEs I discovered — hope you like it. Stay tuned for more articles coming soon.
References: