Clocky — TryHackMe Writeup
Nmap revealed SSH (22), Apache (80), Nginx (8000), and a Flask/Werkzeug app (8080). The robots.txt on port 8000 disclosed disallowed file extensions (.sql, .zip, .bak).
Directory fuzzing on port 8000 uncovered index.zip containing the Flask application source (app.py), revealing usernames, routes, and a critically weak password-reset token generation scheme.
The password reset token was generated from SHA-1(timestamp + username). By recording the server response time and brute-forcing the millisecond component, we forged a valid token to reset the administrator password.
The admin dashboard's location feature was vulnerable to SSRF but blocked direct localhost requests. Bypassed the filter by redirecting through an attacker-controlled Flask server to reach internal services and exfiltrate database.sql.
Recovered SSH credentials from the database, discovered MySQL credentials in a .env file via LinPEAS, extracted caching_sha2_password hashes, cracked the dev user's password with Hashcat, and used it to su to root.
Introduction
Clocky is a medium-rated TryHackMe machine that exposes multiple web services across four ports. The attack chain begins with source code recovery from a carelessly exposed zip archive, progresses through a predictable password-reset token forgery, exploits a Server-Side Request Forgery (SSRF) vulnerability with a redirect-based filter bypass, and culminates in MySQL credential extraction and hash cracking to achieve root access.
This room contains six flags scattered across the attack path, testing enumeration, web exploitation, lateral movement, and privilege escalation skills.
Reconnaissance
::Nmap Scan
A full TCP port scan reveals four listening services:
Four ports are open. Apache on port 80 and Nginx on port 8000 both return 403 Forbidden. The
Werkzeug/Python server on port 8080 is a Flask application — this will be our primary attack
surface. Nmap also detected a robots.txt on port 8000 with interesting disallowed extensions.
Enumeration
::
The robots.txt file on port 8000 reveals file extension restrictions and our first flag:
The robots.txt explicitly disallows indexing of .sql, .zip, and .bak files — a strong
indicator that sensitive backup or database files exist on this server.
::Directory Fuzzing
Using Feroxbuster with the leaked extensions to discover hidden files:
Key finding:
- ▹
http://10.114.156.245:8000/index.zip— 200 OK (3,280 bytes)
::Source Code Recovery
Downloading and extracting the archive reveals the Flask application source:
Source Code Analysis
::Key Findings in app.py
Reviewing the Flask application source reveals several critical details:
1. Debug Mode Enabled — The application runs with debug=True, exposing the Werkzeug interactive debugger (potential RCE vector).
2. Known Usernames — Three hardcoded users: jane, clarice, and administrator.
3. Exposed Routes — /administrator, /forgot_password, /password_reset.
4. Predictable Password Reset Tokens — The most critical vulnerability:
Critical Vulnerability: The password reset token is derived from SHA-1(server_timestamp + username). Since the timestamp is predictable (obtained from the HTTP response Date header) and
the username is known, an attacker can reconstruct the exact token by brute-forcing only the
millisecond component (0–99 range after truncation).
5. Password Reset Flow:
- ▹
POST /forgot_password— accepts ausernameparameter, generates a token, and stores it in the database. - ▹
GET /password_reset?token=<TOKEN>— validates the token and allows the user to set a new password.
Initial Foothold — Token Forgery
::Step 1: Request a Password Reset
Send a password reset request for the administrator account and capture the server's response timestamp from the Date header using Burp Suite:

The server's Date response header provides the base timestamp. The token generation uses
datetime.datetime.now() with microsecond precision, but the code truncates the last 4
characters, leaving only 2 significant digits of sub-second precision to brute-force.
::Step 2: Generate Candidate Tokens
Using the captured timestamp, generate all possible SHA-1 hashes for the 100-value millisecond range:
Parameter Discovery: The source code uses TEMPORARY as the query parameter name, but testing
reveals the actual endpoint accepts token as the parameter name. Always verify parameter names
against the live application.
::Step 3: Brute-Force the Valid Token
Using wfuzz to identify which candidate hash is the valid token:
::Step 4: Reset Password & Login
Navigate to the password reset URL with the valid token:
Set a new password, then log in at /administrator with the credentials administrator:<new_password>.

SSRF — Dashboard Exploitation
::Identifying the SSRF Vector
The admin dashboard contains a "location" input field. After testing for common injection types (command injection, LFI, template injection) with no results, testing for SSRF confirms the application makes outbound HTTP requests:

::SSRF Filter Bypass via Open Redirect
Direct requests to localhost or 127.0.0.1 are blocked with an Action not permitted error. To bypass this restriction, we use a redirect-based SSRF bypass: our attacker-controlled server receives the request and responds with a 301 redirect to the target's localhost.
Redirect Server (redi.py):
How this works: The application's SSRF filter only validates the initial URL (our attacker
server). When the server follows the 301 redirect to http://127.0.0.1/..., the filter has
already been bypassed. This is a classic redirect-based SSRF filter evasion technique.

::Exfiltrating the Database
From the source code analysis, we know a database.sql file exists on the server. Using the SSRF redirect to fetch it:

The database dump also reveals a plaintext password: Th1s_1s_4_v3ry_s3cur3_p4ssw0rd.
SSH Access — Credential Reuse
Testing the recovered password against the three known usernames (jane, clarice, administrator) via SSH reveals it belongs to clarice:
Privilege Escalation
::Enumeration with LinPEAS
Upload and run LinPEAS for automated enumeration:
LinPEAS discovers a .env file with MySQL credentials:
From app.py, we know the MySQL username is clocky_user.
::MySQL Credential Extraction
Hash Format Issue: MySQL 8.0+ uses caching_sha2_password by default. These hashes are not
directly compatible with Hashcat. They must be converted to a specific format before cracking.
::Converting Hashes for Hashcat
MySQL's caching_sha2_password hashes require a format conversion documented on the Hashcat example hashes page for mode 7401:
This produces hashes in the correct $mysql$A$0005*salt*hash format.
::Cracking with Hashcat
The dev user's password cracks to: armadillo
::Root Access
The cracked password works for su root:
Summary
Clocky demonstrates a realistic attack chain where multiple small vulnerabilities compound into full system compromise:
- ▹Information Disclosure — A
robots.txtfile hinted at hidden file types, leading to source code recovery from an exposedindex.zip. - ▹Predictable Token Forgery — Password reset tokens generated from
SHA-1(timestamp + username)allowed brute-forcing with only 100 candidates. - ▹SSRF with Filter Bypass — A redirect-based bypass evaded localhost restrictions, enabling internal service enumeration and database exfiltration.
- ▹Credential Reuse — A password from the database dump granted SSH access as
clarice. - ▹MySQL Hash Cracking — Extracting and cracking
caching_sha2_passwordhashes revealed the root password.
References
- ▹OWASP — Server-Side Request Forgery (SSRF)
- ▹OWASP — Insufficient Entropy / Predictable Tokens
- ▹PortSwigger — SSRF with Filter Bypass via Open Redirection
- ▹Hashcat — Example Hashes (mode 7401: MySQL caching_sha2_password)
- ▹HackTricks — SSRF Bypass Techniques
- ▹Linux Privilege Escalation: Enumeration & Recon
- ▹Linux Privilege Escalation: Basics & Exploitation

Red Team Consultant · Penetration Tester · Bug Bounty Hunter
Offensive security professional with 250+ vulnerabilities reported across 50+ organizations including Atlassian, Vimeo, and AT&T. Sharing research, tools, and field notes.