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) 🦀🦀🦀🦀🦀 Comment Suggest edit
asyncfnquery(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;letmut 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>();letmut 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 lockifletSome(result) = res {returnOk(result); }Ok(String::from("No results!"))}
Looking at this function, we can see that it is vulerable to SQL Injection.
let res = conn.exec_first::<String, _, _>(format!("SELECT * FROM info WHERE body LIKE '{}'", qs), () ).await;
Exploit
I used SQL Map, and was able to perform the sql injection
Next, I attempted to dump the table.
Looking at the source code, I was able to identify how they created the random strings for the table.
// Unguessable table name (requires knowledge of user id and random table id)let table_id = rand::random::<u32>();letmut 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);
I then copied the code, to create the hash of my current user with a simple rust script.
Upon inspecting the dumped table, I noticed that my current user's table is not being deleted for some reason. To gain a better understanding, I proxied my SQL Map to Burp Suite.
It appears that there were multiple tables containing my user hashes. I attempted to retrieve the secret from the last table on the list.
With the retrieved secret, I was able to get the flag from the /flag endpoint.
Flag: grey{ru57_c4n7_pr3v3n7_l061c_3rr0r5}
Further Discussion
However, this is definitely not the intended solution. Looking at the source code, it will attempt to drop the table after the query ran.
// You'll never get the secret! conn.exec_drop(format!("DROP TABLE {}", table_name), () ).await.map_err(|_|"Failed to drop table")?;
The challenge name Fearless Concurrency also hinted at it being a Race Condition or something similar, where you have 2 payload running concurrently.
After the CTF, while discussing solution in the discord server, the author revealed that the intended solution was to inject a sleep, and use a seperate user account to perform the SQLI to leak the secret.
Lorenz has also kindly shared his proof of concept script in the message linked below.