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
The web application has a registration function which generates a Random UID.
async 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()
}
We are also able to craft query
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!"))
}
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>();
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);
I then copied the code, to create the hash of my current user with a simple rust script.
use sha1::{Digest, Sha1};
use hex;
fn main() {
let mut hasher = Sha1::new();
hasher.update(b"fearless_concurrency");
hasher.update(2629577642326695728u64.to_le_bytes());
let table_name = format!("tbl_{}", hex::encode(hasher.finalize()));
println!("{}", table_name);
}
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.