Tags: web 

Rating: 5.0

The downloaded `tar.xz` file contains a Dockerfile and all resources needed in order to create a local setup for the challenge as well as a comment on top of the Dockerfile with the necessary commands for running the Docker container.

The web application running at port 8000 was written in PHP and only consists of a single `index.php` file.

When requesting `index.hmtl`, a session is created if necessary. Afterwards, the directory `/var/www/html/files/[sessionid]` gets created and the current working directory is changed to there. Users can upload files of any type that are smaller than 10 kilobyte. If the upload was successful, a database entry is created in the `uploads` table with an (autoincremented) id and a "description" of the file. Uploaded files get moved to `/var/www/html/files/[sessionid]/[id]`. Users can see a list of their uploaded files, e.g.:

<td>ASCII text </td>

The flag can be read from a file located at `/flag_XXXXXXXXXXXXXXXX.txt` (`X` being an arbitrary alphanumeric character) and can be read by everybody. We probably need RCE for reading the flag, because only being able to read arbitrary files on the server still means that we need to bruteforce the filename, which would take too many attempts to be feasible, except if we could use wildcards for filenames which usually doesn't work.

When taking a closer look at the `index.php` file, we immediately noticed the string concatenation in the SQL statement that inserts an entry for an uploaded file. We therefore suspected that SQL injection is possible:

if (isset($_FILES['file']) && $_FILES['file']['size'] < 10*1024 ){
$s = "INSERT INTO upload(info) VALUES ('" .(new finfo)->file($_FILES['file']['tmp_name']). " ');";
move_uploaded_file( $_FILES['file']['tmp_name'], $d . $db->lastInsertId()) || die('move_upload_file');

The statement `(new finfo)->file(...)` seems to have an output equal to the Linux `file` command and therefore depends on the content of the uploaded file (magic bytes, line endings, file encoding,...) rather that the file extension. Our goal is therefore to get user input into the output of that statement.

Unfortunately, we first tried to upload a plaintext file, where no user input gets included into the SQL statement. After ruling out other attack vectors, we finally uploaded a JPG image. Luckily enough, the `Comment` field from the exif metadata is part of the output of `(new finfo)->file(...)`. Furthermore, `PDO`, the database library in use, allows stacked queries, which means that we can insert other statements after ending the intended `INSERT` statement appropriately and commenting out the rest.

The database in use is sqlite. In combination with a PHP webserver, RCE can be obtained by attaching a new database (and thus generating a file under the webroot), creating a table inside that database with a single column of type `text` and inserting a single column with the PHP code that should get executed.

Due to the length of the `(new finfo)->file(...)` output being restricted, we splitted the necessary SQL and used two requests to generate our PHP file on the server.

We first generated two empty JPG files with a size of 1x1 pixel:

$ convert -size 32x32 xc:white empty.jpg
$ convert -size 32x32 xc:white empty2.jpg

We updated the exif metadata `Comment` field with the necessary SQL statements using `exiftool` with the following commands:

$ exiftool -Comment="'); ATTACH DATABASE '/var/www/html/files/l.php' AS l; CREATE TABLE l.p (d text); --" empty.jpg
$ exiftool -Comment=$'\');ATTACH DATABASE \'../l.php\' AS l;INSERT INTO l.p(d) VALUES (\'\');--' empty2.jpg

Afterwards, we simply uploaded `empty.jpg` and `empty2.jpg` to the server and issued an HTTP GET request to `/files/l.php` for obtaining the flag:

hxp{I should have listened to my mum about not trusting files about files}