Clocky
Time is an illusion. - by toxicat0r
The following post by 0xb0b is licensed under CC BY 4.0
Recon
We start with a Nmap scan and discover four open ports. 22
on which SSH is available and on ports 80
, 8000
, and 8080
, we are dealing with web servers. On 80
with Apache HTTP 2.4.4
, and on 8000
with nginx 1.18.0
and 8080
Werkzeug/2.2.3 Python/3.8.10
.
Furthermore, we find three disallowed entries in the robots.txt
on the nginx webserver.
We take another look at the robots.txt
at endpoint 8000
and confirm the findings from Nmap. We also find the first flag here.
Since these entries are explicitly not allowed for crawlers, sensitive information could be hidden here. It is very likely that security by obscurity is the only protection here. We run a Gobuster scan to enumerate all directories, including the file endings sql
, zip
, and bak
. The image shows only the zip
scan. We find an index.zip
archive next to robots.txt
.
We download and extract the zip archive and find the second flag; we are heading in the right direction.
Even though we already have two flags that show us the possible route to take, let's take a look at the other end points. A directory scan on 8080
reveals a dashboard, an administrator page and an option to reset the password.
If we visit the index page, we find only a few anchor links and the current time, which is updated when the page is loaded.
If we want to access the /dashboard
, we are redirected to the /administrator
page and have to log in. Unfortunately, usernames cannot be enumerated here, and no SQL injection is possible at first glance.
A directory scan using gobuster did not produce any results on endpoint 80
. Even including the file extensions sql
, zip
and bak
did not yield any results for the time being, since each requested resource leads to a 403
.
Shell As Clarice
The app.py
is a flask application, maybe the one on port 8080
. It includes routes for user authentication, password reset, and an administrator dashboard. It utilizes a MySQL database for user management and password reset tokens. However, it lacks proper error handling, security measures like rate limiting, and some routes are not fully implemented, indicating areas for improvement in terms of functionality and security. There is also no input sanitization, which indicates a possible SQL injection vulnerabilities. Additionally, there are comments suggesting plans for future development, such as deploying a new application version and implementing rate limiting or using a Web Application Firewall.
We might deal with an older version of app.py
, since it seems like the debug mode is turned off on the live page.
We are able to extract the routes /
, /administrator
, /forgot_password
we already knew of and a new route /reset_password
. And also find at least three potential usernames: clocky_user
as the database user, and jane
and clarice
as the potential developers of the application.
Of interest for further abuse is the password reset process. The Users access the forgot password
page and enter their username. The application checks if the provided username exists in the database. If the username exists, a unique token is generated. The token generation algorithm combines the current timestamp with the username. This concatenation creates a unique string that is then hashed using the SHA-1 hashing algorithm. This token can then be used to reset the user's password via the password_reset
page.
So, if we have a valid username, we should be able to request a password reset for this user, since we can get the current time of the system from the index page. Furthermore, do we know exactly the format of time, since we can try in Python str(datetime.datetime.now())[:-4]
which yields a format of YYYY-MM-DD HH:ss:SS
. This lets us create a valid token SHA1(CURRENT_TIME . USERNAME)
.
A time synchronization is not possible with the server; even if you recognize the time zone and take it into account in your script and query it, you might be off in seconds, which requires you to brute force more. Hence the approach to get the timestamp from the index page.
Unfortunately, the password reset field does not offer the possibility to enumerate the users; for every username we enter, we receive a valid confirmation. So we have to request a reset for every user we have enumerated so far in the hope of catching an existing one.
When we access the password_reset
page, we are told that we have entered the wrong parameter. We have not entered one here.
The use of the TEMPORARY
parameter leads to the same note. It's probable that the parameter changed and the app.py
we received from index.zip
is outdated.
To enumerate the correct parameter, we can make sure of s0md3vs
tool called Arjun
. Or we could just guess the correct one in this case. As the site expects tokens, the parameter could be named like something related to tokens.
We run Arjun and see that token
is the correct parameter.
Upon accessing the page and providing an arbitrary token with the parameter, we get the hint that it's an invalid token instead of an invalid parameter.
We now have to create a script that uses the password reset process described before. To do this, we pass a lsit of usernames to the script, for which the password reset is triggered, and at the same time, the index page is called in order to obtain the current time stamp of the server.
Now we only have a small problem; the milliseconds are missing in the timestamp and cannot be determined with a separate call with the correct time. So we create 100 timestamps with all millisecond combinations, use them to create the tokens. Then we brute force the reset_password page with the created tokens. If we get a response without the content of Invalid token
, we have the correct token and query the reset with it.
Next, we craft a users.txt which contains all possible users we found so far, including standard usernames like admin
and administrator
.
We run the script and get a hit on Administrator
.
We use the found token to query a password reset for the user Administrator
.
Next, we log in as Administrator
with the new credentials...
... and are redirected to the dashboard, where we find the third flag.
We try to receive files on the system using file://etc/passwd
, but that does not work. We know we still have restricted access to the endpoint on port 80
, so try to access this via http://127.0.0.1/
. We get a message, that the action is not permitted
. We are dealing with SSRF with restrictions. So we should be able to somehow bypass it.
The following resource gives some payloads to bypass it.
With http://0:8080/
, we are able to access the endpoint 8080
.
Unfortunately, on port 80
, we get a 400 Bad Request
response—at least something.
Further trying leads to using the potential hostname, which gives us a 200
response with just the heading Internal dev storage
.
Ideally, this can also be automated and solved with the Burp Suite Intruder. In the initial solution, another possibility has been overlooked, namely that the resource on port 80
can also be accessed via http://0x7f000001/
. The steps in Burp Suite are broken down here: First catch the request at /dashboard
and pass it to Intruder.
Next, use the list of Hacktricks mentioned before, and paste or load it in the payload settings. Slight additions have to be made, remove the comments and the comments with =
.
Here is the edited list:
After starting the attack, two entries with a length of 270
bytes yield a successful bypass.
We recall our finding of robots.txt
at the endpint at 8000
. Maybe there are some files hidden, like they were there.
After a lot of guessing, we find a SQL file. It was mentioned in a comment inapp.py
.
Here we have the fourth flag and some credentials.
Recalling the users we found in app.py
and including some common ones, we attempt to brute-force the SSH login with Hydra, in case those credentials were reused.
We have a hit for the user clarice
.
We SSH into the system as clarice
and locate the fifth flag within the user's home directory.
Shell As Root
During system enumeration, we uncover the reference password for the database user mentioned in app.py
, located at /home/clarice/app/.env
.
With this information, we successfully enumerate the app's database. However, we find no additional entries.
Being desperate, we look into the mysql database and find some users with passwords. In MySQL, the authentication_string
column in the mysql.user
table contains the hashed password or authentication string for each user account. However, the format of these hashes does not precisely match any examples in hashcat.
We only have a partly match with $A$500
with the mode 7401. A specific hash type, that can only be found in specific systems.
Researching on the 7401
mode, we find a pull request adding 7401
. There, we can extract the method to get the hash valid for hashcat.
There, you can find a discussion on how that hash should be processed. The comments discuss challenges with encoding consistency when using SHA256Crypt and MySQL, noting that MySQL employs a non-standard base64 variant. It suggests options such as converting all data to hex for consistency, creating custom encoding/decoding functions, pre-processing/post-processing data, or exploring alternative storage solutions to address these challenges.
With the following query by philsmd the hash can be converted to one compliant with hashcat.
Using Hashcat with rockyou.txt
, we are able to crack the hash of user dev - with a hash starting with$mysql$A$005*1C1
- and retrieve the password of dev
.
It turns out the password was already in use for root. We change the user to root and find the final flag in /root/flag6.txt
.
Last updated