CTF Writeup - GlacierCTF - GlacierChat
TLDR: Reset password function leaks reset code, whereas you then can
perform sql injections while setting a new password. You can blindly
leak the table schema and the TOTP token for the admin. After
authentication, you need to exploit a race condition between creating a
media post
and a text post
, thus you can gain remote code execution on
the server. As /flag.txt
is only readable by root
you need to escalate
privileges by using the cronjob. As the file executed by the cronjob is
owned by www-data
you can overwrite the file and make /flag.txt
executable
by using chmod 644 /flag.txt
. You then wait for the cronjob to run and can
read the flag.
Password Reset Exploit
You can reset the user password in reset.php
, however if you use a prefix in
getResetCode
it shows you a warning that prefixes are deprecated and also leaks
you the reset code
. So you can reset the password in set_new_password.php
.
As the login requires a TOTP token
you need to get that as well.
Leak TOTP token
There is a SQL injection in set_new_password.php
.
The variable $username
is not properly escaped and vulnerable to SQL injection.
As you don't get any output, you must perform queries blindly, however you can't
use randomblob
, and hence not use tools like SQLMap.
You can leak tables and columns by making use of the password_cost
field.
The higher the value for the password cost, the longer takes the query.
Therefore, you can leak data:
attack = f"SELECT MAX(CASE WHEN {field} like '{valueToTry}%' AND substr({field}, 1, {len(valueToTry)}) = '{valueToTry}' THEN 12 ELSE 5 END) FROM {table} {subquery}"
If a value exists, you can return 12
as password_cost
, otherwise 5
. As the
request then takes significantly longer, you know, that the value exists then.
With that you can leak the TOTP token
for the admin
and authenticate to the website.
Race Condition in post creation
Users can directly create text posts
directly displayed on the website.
However for creating media posts
approval is needed. The uri
in the media post
has to be an URL, whereas with curl the url is downloaded and the response is
directly written back to the database in the content
field.
Exploiting the URL is (hopefully) not possible as we use PHP's internal email
validation.
However, the implementation is a bit wacky. After each database operation, the
a connection is opened and immediately released afterwards. This is especially bad
for getLastInsertPost
as some can exploit a race condition there.
The regular process of the media post would look like this:
insertMediaContent
id = getLastInsertPost
requirePostApproval id
So we have a race condition between insertMediaContent
and
getLastInsertPost
:
Request 1: insertMediaContent
Request 2: insertTextContent // Executed due to race Condition
Request 1: id = getLastInsertPost
Request 1: requirePostApproval id
You can now approve the post and you then have code execution.
Note, that you won't know the approval id, as only posts having which approval
require are displayed. So you need to create a regular media post first to know
the approvalID
. Then you perform the race condition and approve it with
approvalID + 1
(so from the previous post).
You can read the response of the command on the page then. You will find a broken image, which has the command output base64 encoded.
Run arbitrary code and read flag
While inserting a text post
, htmlspecialchars
is performed on the text content.
You therefore haven't full arbitrary code execution, which you however can bypass
easily.
You encode your payload with base64
and use the command:
encoded = base64.b64encode(raw_command.encode()).decode()
command = f"echo {encoded} | base64 -d | /bin/sh"
Therefore you don't use any special character and can run arbitrary code.
The flag is located at /flag.txt
, however only root
can read it.
Luckily, there is a cronjob performing periodically as you can see in
entrypoint.sh
and service/cron.sh
.
The root cronjob runs a file, which the user can control:
/usr/local/bin/php /var/www/cron.php 2>&1 &
Therefore, you can make the flag readble:
runCommand("echo \"<?php exec('chmod 644 /flag.txt'); ?>\" > /var/www/cron.php")
And waiting after the cronjob has been performed you cat the flag:
runCommand("cat /flag.txt")
The challenge sources and the solution script are available here,