saiger.dev

CTF Writeup - GlacierCTF - GlacierChat

Posted on:

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,