How We Audited a High-Traffic PHP Enterprise Stack on AWS and Mitigated Remote Code Execution (RCE) via insecure file uploads
Initial Triage: Identifying the Attack Vector
Our engagement began with a critical alert: intermittent but significant spikes in outbound traffic from a high-traffic PHP enterprise application hosted on AWS. The pattern suggested data exfiltration, a common symptom of Remote Code Execution (RCE). The initial hypothesis pointed towards a vulnerability in the application’s file upload functionality, a perennial weak point in web applications, especially those handling user-generated content.
The application, a custom-built CRM with extensive file attachment capabilities, allowed users to upload various document types. While image and PDF uploads were expected, the system also permitted seemingly arbitrary file types, a red flag. Our first step was to examine the application’s logging infrastructure. We needed to correlate the outbound traffic spikes with specific user actions or system events.
Deep Dive into File Upload Logic
We requested access to the application’s source code and deployment configurations. The core of the file upload logic resided in a PHP class, let’s call it `FileUploadHandler.php`. A quick scan revealed a critical oversight:
The application performed client-side validation (easily bypassed) and a rudimentary server-side check on the file’s MIME type, but crucially, it did not validate the file’s actual content or execute it in a sandboxed environment. The uploaded files were stored directly in a publicly accessible S3 bucket, with their original filenames preserved. This meant an attacker could upload a PHP shell disguised as an image (e.g., `shell.php.jpg`).
Exploitation Scenario: The PHP Shell Upload
The typical exploitation path would involve:
- An attacker crafts a simple PHP web shell (e.g., `backdoor.php`) containing functions to execute arbitrary commands.
- The attacker uploads this file through the application’s interface, potentially renaming it to bypass basic extension checks (e.g., `shell.php.jpg`).
- The application, failing to properly validate the file type and content, stores `shell.php.jpg` in the S3 bucket.
- The attacker then directly accesses the uploaded file via its URL (e.g., `https://s3.amazonaws.com/your-bucket-name/uploads/shell.php.jpg`).
- If the web server (e.g., Nginx or Apache) is configured to interpret `.jpg` files as PHP (a common misconfiguration, though less so with modern setups), the shell executes. More likely, the attacker would find a way to trick the server into executing it as PHP, perhaps by exploiting a vulnerability in how the server handles specific file types or by finding a way to rename the file on the server after upload if there’s a separate processing step. A more direct approach is to upload a file with a double extension like `shell.php.jpg` and hope the server’s configuration or a subsequent processing step misinterprets it. A more robust attack would involve uploading a file with a valid image extension but with PHP code embedded within the image’s metadata or EXIF data, and then using a separate exploit to extract and execute that code. However, the simplest and most common RCE via file upload is directly uploading a script with a recognized executable extension. In this case, the attacker would likely attempt to upload `shell.php` and hope the application’s storage mechanism or a subsequent processing step doesn’t sanitize the extension, or they would upload `shell.php.jpg` and rely on a misconfiguration or a secondary exploit. For this audit, we focused on the direct upload of a `.php` file, assuming the application’s storage and retrieval mechanism was the primary vulnerability.
The outbound traffic spikes were likely the result of the uploaded PHP shell executing commands to scan the internal network, exfiltrate data, or connect to a command-and-control (C2) server.
Replicating the Vulnerability in a Controlled Environment
To confirm the vulnerability and understand its impact, we set up a staging environment mirroring the production stack on AWS. This included:
- An EC2 instance running the PHP application.
- An S3 bucket configured for file storage.
- A load balancer (ALB) directing traffic.
- Relevant security groups and NACLs.
We then crafted a basic PHP web shell:
<?php
// Simple PHP Web Shell
if(isset($_REQUEST['cmd'])){
echo "<pre>";
$cmd = ($_REQUEST['cmd']);
system($cmd);
echo "</pre>";
die;
}
?>
We attempted to upload this file, named `shell.php`, through the application’s interface. The application accepted the upload and stored it in the S3 bucket. We then accessed the file directly via its S3 URL. The web server, in this case, was configured to serve static files from S3. The vulnerability wasn’t in the web server executing the PHP, but in the application’s logic *allowing* the upload of a `.php` file to a location that could be directly accessed. The attacker would then need to find a way to execute it. A common method is to upload it to a path that *is* processed by PHP, or to exploit a secondary vulnerability. However, in this scenario, the primary risk was the *presence* of the executable code in an accessible location, which could be leveraged if any part of the application or its dependencies later processed files from that location in an unsafe manner, or if the attacker could trick a user into clicking a link that somehow triggered execution.
A more direct exploitation, if the application itself had an endpoint that *processed* uploaded files (e.g., for thumbnail generation) and that endpoint was vulnerable, would be to upload `shell.php` and then trigger that processing endpoint. For our audit, the critical finding was the insecure storage of executable code.
AWS Configuration Review: S3 and IAM
Our review extended to the AWS infrastructure supporting the application. We focused on the S3 bucket configuration and the IAM roles/policies associated with the EC2 instances and the application itself.
S3 Bucket Security
The S3 bucket was configured with public read access enabled. This was a significant misconfiguration, allowing anyone to list and retrieve objects. While not directly leading to RCE, it facilitated the exfiltration of any sensitive data stored in the bucket and made it easier for attackers to discover and access uploaded malicious files.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-sensitive-bucket-name/*"
}
]
}
The bucket policy above grants public read access. This must be removed or restricted.
IAM Policies
We examined the IAM role attached to the EC2 instance running the PHP application. The role had broad permissions, including `s3:*` access to the target bucket. While necessary for the application to function, overly permissive policies are a common security risk. In this case, if the application’s credentials were compromised, an attacker could potentially abuse these permissions to manipulate the bucket contents or access other AWS resources.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": "arn:aws:s3:::your-sensitive-bucket-name/*"
}
]
}
This policy grants excessive permissions. A more granular approach is required.
Mitigation Strategies and Implementation
Based on our findings, we recommended and implemented a multi-layered mitigation strategy:
1. Secure File Upload Handling in PHP
The most critical fix was to overhaul the file upload logic in the PHP application. This involved:
- Strict Extension Whitelisting: Instead of blacklisting dangerous extensions, we implemented a strict whitelist of allowed file extensions (e.g., `jpg`, `jpeg`, `png`, `pdf`, `doc`, `docx`). Any file with an extension not in this list is rejected.
- Content-Type Validation: We now use the `finfo_file` extension (or `mime_content_type` if `finfo` is unavailable) to determine the actual MIME type of the file based on its content, not just the extension. This is compared against a whitelist of expected MIME types for allowed file extensions.
- File Content Validation: For image uploads, we integrated a library like GD or Imagick to attempt to open and process the image. If the file is not a valid image, it’s rejected. This prevents PHP code from being embedded in image files and executed.
- Renaming Uploaded Files: Uploaded files are renamed to a unique, random string (e.g., using `uniqid()` or `random_bytes()`) with their original extension preserved. This prevents attackers from predicting file paths and directly accessing them.
- Storing Files Outside Web Root/Public S3 Access: Files are stored in a private S3 bucket. Access is then granted via pre-signed URLs generated by the application server, or through a dedicated download endpoint that streams the file after performing authentication and authorization checks.
Here’s an example of a more secure PHP upload handler snippet:
<?php
// Secure File Upload Handler Example
// Configuration
$allowed_extensions = ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx'];
$allowed_mime_types = [
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'pdf' => 'application/pdf',
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
];
$upload_dir = '/tmp/uploads/'; // Temporary local storage before S3 upload
$s3_bucket = 'your-secure-private-bucket';
$s3_client = new Aws\S3\S3Client([
'version' => 'latest',
'region' => 'your-region',
'credentials' => [
'key' => 'YOUR_AWS_ACCESS_KEY_ID',
'secret' => 'YOUR_AWS_SECRET_ACCESS_KEY',
],
]);
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['userfile'])) {
$file = $_FILES['userfile'];
// 1. Basic checks
if ($file['error'] !== UPLOAD_ERR_OK) {
die("Upload error: " . $file['error']);
}
$original_filename = basename($file['name']);
$file_extension = strtolower(pathinfo($original_filename, PATHINFO_EXTENSION));
// 2. Extension Whitelisting
if (!in_array($file_extension, $allowed_extensions)) {
die("Invalid file extension: .$file_extension");
}
// 3. MIME Type Validation (using finfo)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$actual_mime_type = $finfo->file($file['tmp_name']);
if (!isset($allowed_mime_types[$file_extension]) || $allowed_mime_types[$file_extension] !== $actual_mime_type) {
// Additional check for images: try to open with GD/Imagick
if ($file_extension === 'jpg' || $file_extension === 'jpeg' || $file_extension === 'png') {
if (!@getimagesize($file['tmp_name'])) {
die("Invalid image file content.");
}
} else {
die("MIME type mismatch: Expected " . $allowed_mime_types[$file_extension] . ", got " . $actual_mime_type);
}
}
// 4. Generate a unique filename
$new_filename = uniqid('', true) . '.' . $file_extension;
$destination_path = $upload_dir . $new_filename;
// Move uploaded file to a temporary local directory
if (!move_uploaded_file($file['tmp_name'], $destination_path)) {
die("Failed to move uploaded file.");
}
// 5. Upload to S3
try {
$result = $s3_client->putObject([
'Bucket' => $s3_bucket,
'Key' => 'uploads/' . $new_filename, // Store in a subfolder
'SourceFile' => $destination_path,
// 'ACL' => 'private' // Default is private, ensure this is set
]);
echo "File uploaded successfully to S3. Object URL: " . $result['ObjectURL'] . "\n";
// Clean up temporary local file
unlink($destination_path);
} catch (Aws\S3\Exception\S3Exception $e) {
error_log("S3 Upload Error: " . $e->getMessage());
die("Failed to upload file to storage.");
}
} else {
echo "Please upload a file via POST request.";
}
?>
2. S3 Bucket Security Hardening
We immediately removed the public read access from the S3 bucket. All objects are now private by default.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowBucketOwnerRead",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::YOUR_ACCOUNT_ID:root"
},
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::your-secure-private-bucket",
"arn:aws:s3:::your-secure-private-bucket/*"
]
}
]
}
This policy grants the bucket owner full control. Access for other services or users must be explicitly defined via IAM policies.
Access to files is now managed through:
- Pre-signed URLs: The application server generates temporary, time-limited URLs for accessing files. This is ideal for user-facing downloads.
- Internal Download Endpoint: A dedicated PHP endpoint on the application server that authenticates the user, retrieves the file from S3 (using IAM credentials), and streams it back to the client. This is useful for internal processing or when pre-signed URLs are not suitable.
3. Principle of Least Privilege (IAM)
The IAM role attached to the EC2 instance was updated to grant only the necessary permissions. Instead of `s3:*`, we specified `s3:PutObject` and `s3:GetObject` for the specific bucket and a designated prefix (e.g., `uploads/`).
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject" // If deletion is required
],
"Resource": "arn:aws:s3:::your-secure-private-bucket/uploads/*"
}
]
}
This significantly reduces the blast radius if the application’s credentials are compromised.
4. Web Application Firewall (WAF) and Intrusion Detection
To add an extra layer of defense, we configured AWS WAF on the Application Load Balancer (ALB). This included:
- Rate Limiting: To prevent brute-force attacks and excessive requests.
- IP Reputation Lists: Blocking known malicious IP addresses.
- Custom Rules: Specifically targeting patterns indicative of file upload exploits (e.g., suspicious characters in filenames, common shell commands in request bodies).
- Logging and Monitoring: Enabling detailed WAF logs for analysis and integration with CloudWatch for real-time alerting on suspicious activity.
Post-Mitigation Validation and Ongoing Monitoring
After implementing the changes, we re-tested the file upload functionality rigorously. We attempted to upload various malicious file types, including PHP shells, executables, and files with embedded scripts. All attempts were correctly rejected by the updated PHP logic.
We also performed vulnerability scans against the staging environment and reviewed CloudTrail logs for any unusual API activity related to S3 and IAM. The outbound traffic spikes observed initially were no longer present.
Ongoing monitoring is crucial. We established CloudWatch alarms for:
- S3 bucket access logs showing `GetObject` requests for non-image/document files (if such logging is enabled and analyzed).
- WAF alerts indicating blocked malicious requests.
- Unusual outbound network traffic from EC2 instances (monitored via VPC Flow Logs and CloudWatch Metrics).
- IAM policy changes.
This comprehensive approach, combining secure coding practices, robust AWS infrastructure configuration, and continuous monitoring, effectively mitigated the RCE vulnerability and significantly enhanced the overall security posture of the enterprise PHP application.