Fearless Concurrency

Challenge Description

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

Author: jro

http://challs.nusgreyhats.org:33333

https://storage.googleapis.com/greyctf-challs/dist-fearless-concurrency.zip


Code Analysis

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.

https://discord.com/channels/969232688521281606/969232688911360035/1231460020412223589

Last updated