solved by hartmannsyg
Rust is the most safest, fastest and bestest language to write web app! The code compiles, therefore it is impossible for bugs! PS: This is my first rust project (real) 🦀🦀🦀🦀🦀
Author: jro
http://challs.nusgreyhats.org:33333
Full source code
1 | use std::{collections::HashMap, sync::Arc}; |
There are a few endpoints we can use:
/register
this adds a randomuser_id
to their hashmapusers
26
27
28
29
30struct AppState { users: Arc<RwLock<HashMap<u64, User>>>, pool: Arc<Pool> }
70
71
72
73
74
75
76async fn register(State(state): State<AppState>) -> impl IntoResponse { let uid = rand::random::<u64>(); let mut users = state.users.write().await; let user = User::new(); users.insert(uid, user); uid.to_string() }
/query
You give it youruser_id
and aquery_string
and it does quite a few things:locks the
user
based on theuser_id
provided (so you can’t race condition with the sameuser_id
)93
94
95
96// 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;
Creates an “Unguessable table name”, which contains
sha1(user_id)
and a randoom number:99
100
101
102
103
104// 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);
Creates a new table and stores the user secret inside:
109
110
111
112
113
114
115
116// 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")?;
It then queries your query from
info
, which only containsHello, World!
. Note that it is vulnerable to SQL injection:119
120
121
122
123// 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;It then deletes the unguessable table:
125
126
127
128// You'll never get the secret! conn.exec_drop( format!("DROP TABLE {}", table_name), () ).await.map_err(|_| "Failed to drop table")?;
My initial thoughtss
Well obviously we have SQL injection, so we can use 'UNION SELECT ...
to get other data we want
My inital idea was trying to get the table name from select table_name from information_schema.tables where table_name LIKE '...'
, then using that result to create a select statement. Potentially something like:
select * from magically_turn_into_the_table(select table_name from information_schema.tables where table_name LIKE '...') |
Spoiler alert: It didn’t work
Apparently doing something like that requires dynamic SQL or something, and is not feasible within normal SQL
I then kinda tried naively playing around with race conditions unsuccessfully; thankfully I managed to find a solution:
My (potentially overcomplicated) solution
My solve script
1 | import asyncio |
In my case, I used a time-consuming cartesian product:
SELECT * FROM info WHERE body LIKE ''union select i.table_name from information_schema.tables,information_schema.tables as i;#' |
since for some reason there were tons and tons of undropped tables in the database, even doing a cartesian product of information_schema.tables,information_schema.tables
is more than enough to lag it enough. (When I added even more information_schema.tables
to the cartesian product I get an error message lol)
As it turned out, more normal and sane people would have use SLEEP
, truly a bruh moment.
Other solutions/ideas
- Leaking out the table name one character at a time (this uses more requests though)
- somehow using sqlmap to get the flag (idk how that worked)