During our security audits, we are regularly confronted with vulnerabilities that allow commands to be executed on a system. These can take various forms depending on the type of application and the functionality impacted. You will find in this article an example of a RCE vulnerability encountered during a penetration test of a web application coded in PHP.
What is a RCE (Remote Code Execution)?
In computer security, arbitrary code execution (ACE) is the ability of an attacker to execute any command or code of his choice on a target machine or in a target process. The ability to trigger an arbitrary code execution over a network (especially via a wide area network such as the internet) is often referred to as remote code execution, or RCE.
A RCE is particularly dangerous, as it often provides privileged access to a system. For example, a RCE vulnerability on a web application will often allow to execute commands on the server that hosts it and therefore to break into it. This will give the attacker access to all or part of the server’s files.
Presentation of the RCE vulnerability
The purpose of the functionality tested was to allow the user to upload files to a platform so that they can be reused elsewhere. When the uploaded file is an audio or video file, the PHP application will run a command on the server to retrieve the duration of the file and thus be able to communicate it to the user.
Here is the code (simplified and modified for the example) of the application:
<?php
$message = "";
function upload($file): string
{
$base_dir = __DIR__ . "/../upload/";
$ext = strtolower(pathinfo("/" . $file["name"], PATHINFO_EXTENSION));
$filepath = $base_dir . uniqid() . '.' . $ext;
$filetype = mime_content_type($file["tmp_name"]);
move_uploaded_file($file["tmp_name"], $filepath);
if (str_starts_with($filetype, "video/") || str_starts_with($filetype, "audio/")) {
$command = sprintf("ffprobe -i \"%s\" -show_entries format=duration -v quiet -of csv=\"p=0\"", $filepath);
$duration = shell_exec($command);
return "Media file of duration " . $duration . " uploaded.";
} else {
return "File uploaded";
}
}
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$file = $_FILES["formFile"] ?? null;
$message = $file === null ? "Missing file." : upload($file);
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Upload media file</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
</head>
<body>
<div class="container">
<header>
<h1>Upload media file</h1>
<hr>
</header>
<div class="row justify-content-md-center">
<div class="col col-lg-6">
<form action="" method="post" enctype="multipart/form-data">
<div class="mb-3">
<label for="formFile" class="form-label">File to upload</label>
<input class="form-control" type="file" name="formFile" id="formFile">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary mb-3">Upload</button>
</div>
<?php if (!empty($message)) { ?>
<div class="alert alert-primary" role="alert">
<?= htmlentities($message, ENT_QUOTES) ?>
</div>
<?php } ?>
</form>
</div>
</div>
</div>
</body>
</html>
What we can see at first is that a command is built on line 13 thanks to the PHP function “sprintf” with the path of the previously uploaded file as parameter. This command is then executed on line 14 and its result is sent back to the user to be displayed on the page.
The file path is built on line 7 from the following elements:
- The variable $basedir: its value is hardcoded in the code, so it cannot be manipulated
- The output of the PHP function uniqid: its value is not manipulable
- The output of the PHP pathinfo function which is asked to return the extension of the uploaded file: this is where the vulnerability lies
Indeed, the pathinfo function, when asked for the file extension (with the PATHINFO_EXTENSION flag), will simply return everything after the last point of the path passed to it. It is therefore possible to inject malicious code in the file extension, so that it is inserted in the command built on line 13.
However, given the way the PHP function “pathinfo” works, our command must not contain a dot or a slash. So, we must be cunning to exploit this vulnerability.
Exploitation of the RCE vulnerability
To exploit this vulnerability, we will start by trying to inject a simple command into the file name. We’ll add a \” to get out of the double quotes in which our command is located, then we’ll add a semicolon (;), then our command, and we’ll add another semicolon and a # to comment out the rest of the line so that it doesn’t interfere with us.
So, our payload is as follows:
\";id;#
We will replay the request, injecting this payload into the file extension. The full name of the file is therefore:
02.mp3\";id;#
We see that the output of the “id” command is returned. We will then try to read the “/etc/passwd” file. However, as mentioned above, because of the way the function that retrieves the extension works, we will have to be tricky to insert commands containing dots and slashes. There are many ways to do this (for example by encoding the characters). As our application is in PHP, the php executable is certainly present on the server. So, we will use it to execute PHP code from the command line.
We are going to create variables that contain the characters ” / ” and ” . ” generated from their respective charcode, then we are going to use them to build our commands. The PHP code will be as follows:
$sl=chr(47); // Character code of /
$dot=chr(46); // Character code of .
echo shell_exec(\"cat ${sl}etc${sl}passwd\"); // Launch and retrieve with echo the system command using the previous variables in place of the characters / et .
The complete payload will be as follows:
02.mp3\";php -r '$sl=chr(47);$dot=chr(46);echo shell_exec(\"cat ${sl}etc${sl}passwd\");';#
Here is a complete example of a command to read the file “/etc/passwd”:
Here is another example to retrieve a file containing a “.” in its path.
The payload is as follows:
02.mp3\";php -r '$sl=chr(47);$dot=chr(46);echo shell_exec(\"cat ${sl}etc${sl}resolv${dot}conf\");';#
From here we can run any command on the server. These will be executed with the rights of the user running the web service (e.g. www-data), and we are mostly not root, but the rights are often sufficient to compromise the system and the data it hosts.
How to fix this RCE?
To fix this problem, the first recommendation would obviously be to never use data that can be manipulated by users in commands to the system.
However, if it is necessary, it is important to make sure that the data that is used is clean and properly secured. For example, in our case, we could simply set up a whitelist of authorized extensions and not run the command if it is not in the list:
$allowedExtensions = ["mp3", "mpeg"];
if (!in_array($ext, $allowedExtensions)) {
return "File not allowed";
}
It is also possible, for example, to use a regex in the PHP “filter_var” function to allow only letters and numbers:
if (!filter_var($ext, FILTER_VALIDATE_REGEXP, ["options"=>array("regexp"=>"/^\w+$/")])) {
return "Invalid extension";
}
Conclusion
When developing an application, it is important to understand where the data we are manipulating comes from and especially what the output of the functions we are using will be. Indeed, in this example, the PHP function “pathinfo” is quite basic and is not made to ensure that what it returns is really what we could expect for an extension (only letters and numbers, quite short…).
It is also important to consider whether, when using data in a feature, it can pose a security problem, especially when that data is returned to the user (e.g. for XSS), or when it is used, for example, in system commands (RCE) or SQL (SQL injections). You can consult our article on security best practices for PHP regarding injections and XSS. It is therefore important to filter this data as much as possible and/or to encode it in such a way that malicious content cannot be interpreted.
Auteur : Romain Garcia