HTB CrossFitTwo

10.10.10.232 | 50 pts

PART 1 : INITIAL RECON

1.1 MACHINE INFORMATION

1.2 NMAP SCAN

PART 2 : PORT ENUMERATION

2.1 TCP PORT 22 (OpenSSH)

The SSH client supports authentication using a password for regular users:

2.2 TCP PORT 80 (HTTP)

Opening the service via a web browser brings you to:

A subdomain is found among the links within the landing page:

Updating the /etc/hosts file to be able to resolve the new finding:

Now, going to http://employees.crossfit.htb, it leads you to a login page:

Within the login page is a link to “Forgot Password?”. It rejects unregistered email addresses. This might be something to work on later in the machine:

Going back to the main site (10.10.10.232 or crossfit.htb) then running gobuster to look for other directories or files that weren’t previously seen. Non-existent pages are tagged as 403 (Forbidden) so I filtered them out during this run:

The path /ws seems to exist but it always times out when requested. This might be shorthand for websockets. This will be explored later on:

2.3 TCP PORT 8953 (Unbound Control DNS)

This service could be used to query/configure various things DNS-related but it is running with SSL and no valid certificate has been found as of yet:

PART 3: EXPLOITATION

3.1 CONNECTING TO /ws

Creating a simple python script to check if /ws is a websocket and if it’s possible to connect:

Running this script to check how the server will respond:

The server is actually running a websocket on /ws. There is a token and it seems like in order to submit an actual command like “help”, a valid token is required so I modified the script to take the token generated from an initial request to check for reuse and pass it along with the submitted command:

Running the script again to check if token is valid:

The reused token was able to send commands to the websocket and there are three available commands listed when sending “help” -- coaches, classes, and memberships. Showing the output of each command:

Showing the messages in beautified format:

3.2 FUZZING COMMANDS

Nothing is usable as of the moment and after fuzzing what other commands might turn out something from the websocket, I eventually tried availability andavailable. The first one didn’t turn out anything but the latter generated a possibly good lead. This might be related to the check_availability() function as seen from the output from memberships. A parameter, id, is required and was stated to be undefined in the output and the value of which must be either one from 1-4:

Passing the parameter, id, simply as a field in the JSON payload, it should be done through a parameter called, params. The updated python script now looks like this:

And after running the script above:

3.3 SQLi OVER WEBSOCKETS

The parameter, id, is usually tested for SQL Injection vulnerabilities. It should be fairly easy, but in this case, it will be passed through a websocket that needs a valid token in order to check for exploitability. I came across this article which provides information on how to be able to run sqlmap over a websocket. The goal is to locally start a web server that will take the output from the websocket into a locally hosted page. This will essentially serve as a middleware for sqlmap and the websocket.

I modified my python script for the final time. This will start a web server on port 6969 where it will take values through a GET parameter, id, and pass it to the websocket on 10.10.10.232/ws. The response of the websocket will then be written in the response body of the local server:

For the exploit to work, first start the server on another terminal:

Then run sqlmap to know what kind of injections you can do if possible. In this case, a boolean-based blind injection was returned:

Since the parameter, id, was considered vulnerable, the database could now be dumped:

employees.password_reset has no entries. It is possible that a token is saved there whenever a password reset is requested from http://employees.crossfit.htb/password-reset.php.

The table, employees.employees, contains the usernames and password hashes of all users. However, none of which are crackable:

id

username

email

password

1

administrator

fff34363f4d15e958f0fb9a7c2e7cc550a5672321d54b5712cd6e4fa17cd2ac8

2

wsmith

06b4daca29092671e44ef8fad8ee38783b4294d9305853027d1b48029eac0683

3

mwilliams

fe46198cb29909e5dd9f61af986ca8d6b4b875337261bdaa5204f29582462a9c

4

jparker

4de9923aba6554d148dbcd3369ff7c6e71841286e5106a69e250f779770b3648

Now that valid emails are found, we can now issue a password reset:

Which then responds with “Reset link sent, please check your email.

A reset token is indeed saved in the employees.password_reset table but it seems that it is deleted once the password reset link has been “clicked”. Given that the SQL Injection vulnerability is boolean-based blind, the exact value of the token can’t be extracted in one go and takes quite some time to fully extract. The race condition seems impossible based on the current parameters given.

3.4 VHOST ENUMERATION

What else can be done with the SQL injection vulnerability is to read files from the server:

The file is encoded in hex but when decoded, it shows the contents of the /etc/passwd file:

The files, httpd.conf and relayd.conf, were also read from the server which contains the configuration for virtual hosts deployed in OpenBSD:

The contents of /etc/httpd.conf reveals that there is another virtual host running called “chat”:

While the contents of /etc/relayd.conf reveals that there is another domain, “crossfit-club.htb”. What is interesting here is that both crossfit-club.htb and employees.crossfit.htb are prefixed with a wildcard (*) and all connections are routed to localhost:

Now, updating the /etc/hosts file to contain crossfit-club.htb:

Then, looking at the contents of http://crossfit-club.htb, it leads you to a sign-in page and trying to navigate to /home or /chat redirects you back to /login while the sign up functionality is disabled.

3.5 DNS REBINDING

Going back to previous findings, there is an open port for unbound-dns-control. Trying to read its configuration file using the SQL Injection vulnerability earlier:

Which when decoded shows the following:

Absolute paths for the certificate and key files are written in the configuration file and reading those files from the server using sqlmap:

The unbound_server.key file couldn’t be read but the other certificate files could be read:

Editing the unbound configuration that was exfiltrated from the server leaving only the remote-control section and replace the certificate and key files with the absolute path of where they were saved:

Then, check if the unbound server could now be reached:

The new unbound config successfully reached the server and this should be enough to perform a DNS rebind and if you recall from what was written in relayd.conf wherein employees.crossfit.htb and crossfit-club.htb are prefixed with a wildcard and are then routed to localhost.

If a new zone is forwarded into the unbound server like fake-employees.crossfit.htb, it should be routed to the same destination as employees.crossfit.htb.

To verify this, a new subdomain, fake-employees.crossfit.htb, will be forwarded using unbound-controlthen start a DNS proxy with dnschef and finally, check if a successful request could be made to the same endpoints of employees.crossfit.htb using fake-employees.crossfit.htb:

Now, starting a DNS proxy:

Then, making a request to /password-reset.php which is an endpoint of employees.crossfit.htb:

An error message is encountered -- “Only local hosts are allowed.” But the remote machine is needed to make a request from our local machine (via the --fakeip option from dnschef).

Maybe this could be done by first requesting a password reset by setting the --fakeip option to 127.0.0.1 after which, cancel the proxy then start a new one at my local IP (10.10.14.28). But first, checking if the password reset will succeed:

Starting a DNS proxy but this time the --fakeip is 127.0.0.1:

Finally, making a request to /password-reset.php which is an endpoint of employees.crossfit.htb:

The password reset was successfully done using the fake domain and a bash script was then created that will handle the race condition when changing the DNS proxy for this exploit:

Before running this script, a netcat listener running on port 80 was started on the local machine:

Finally, running the bash script with the following arguments:

The machine then made a request to the web server running on the attacker machine. The machine is looking for /password-reset.php from the local server based on the data intercepted via netcat:

There are a few interesting things here from the intercepted HTTP request. First, a valid token has been leaked. However, when you attempt to use it to reset the password of [email protected], you get the following response -- “We are sorry, but password reset has been temporarily disabled.”:

Another thing is that the referrer of the HTTP request came from http://crossfit-club.htb. Some user that is currently logged in to the service is clicking on the password reset links. This might be an opening for a CSRF (Cross-site Request Forgery) Attack.

3.6 CSRF (password-reset.php)

Looking at the page source of http://crossfit-club.htb, there are a bunch of javascript files embedded:

Reviewing /js/app~748942c6.ead68abe.js, it is apparent that the application was written using VueJS and the interesting thing here are the functions for the chat feature of the service:

The xe object seems to be derived from socket.io given that there are emit() and connect() function calls. To add, socket.io is often integrated in VueJS applications when handling messaging features. This could be verified by checking the default path of socket.io:

The application indeed uses socket.io. Since the server now looks for /password-reset.php from the local web server, I can create one with a CSRF payload that will let me join a chat room and intercept the messages sent by other uses when triggered:

To use this, start a PHP web server running on port 80 where the created password-reset.php file is:

Then run the DNS rebind exploit script again:

Looking back at the started PHP web server, messages inside the chat room are being intercepted:

One particular message contains credentials for the user, david (password: NWBFcSe3ws4VDhTB):

PART 4 : SHELL AS david

4.1 LOGGING IN AS david

Using the credentials found for the user, david, to login via SSH:

4.2 GROUP ENUMERATION

The current user, david, is a member of the sysadmins group as well as the user, john, which means that the contents of /opt/sysadmin are shared and writable by both users:

Checking for SUID binaries, there is one executable that can’t be run globally, /usr/local/bin/log, it belongs to the root user and the staff group wherein the user, john, is a member of:

It seems that pivoting to the user john is needed in order to escalate privileges.

PART 5 : PIVOT (david -> john)

5.1 REVIEWING statbot.js

Examining the contents of /opt/sysadmin, there is a js file called statbot.js:

The contents of which seems to be a logger that checks whether the websocket is up or down :

5.2 NODEJS LIBRARY HIJACK

Given that this is in the /opt/sysadmin directory and the current user, david, has write permissions, maybe one of the required libraries by the javascript application could be hijacked.

Another thing to note, based on the generated file by statbot.js, the script seems to run every minute:

The first thing to do is to create an app.js file which contains a reverse shell that will replace the one on the required library such that the code will execute when statbot.js triggers:

Starting a listener on port 6969 using netcat:

Now setting everything up within the machine:

Going back to the netcat listener once statbot.js triggers:

PART 6 : PRIVILEGE ESCALATION

6.1 YUBIKEY ENUMERATION

The executable, /usr/local/bin/log, should now be executable via the user, john:

It can’t read from the root directory but it can read files in from /var. Within /var/db, there is a yubikey directory which contains the config files for generating an OTP (One-Time Password).

How is this relevant? Upon checking the file, /etc/login.conf, authentication via SSH could be done using yubikey:

Using /usr/local/bin/log to read the yubikey configuration files:

Now to use the exfiltrated values to generate an OTP back in the local machine:

Attempting to login via SSH as root generates an error message that authentication requires a public key:

6.2 OpenBSD changelist

Since reading from inside the /root directory is prohibited using /usr/local/bin/log, I eventually came across the documentation of changelist for OpenBSD:

For example, the system shell database, /etc/shells, is held as /var/backups/etc_shells.current. When this file is modified, it is renamed to /var/backups/etc_shells.backup and the new version becomes /var/backups/etc_shells.current. Thereafter, these files are rotated.

So if the root SSH private key is backed up from /root/.ssh/idrsa, based on the changelist documentation, it should be in /var/backups/root.ssh_id_rsa.current. Verifying this:

6.3 LOGGING IN AS root

The RSA private key indeed exists in /var/backups. Now using this private key to login via SSH and using a generated token from yubikey as a password:

The box has now been rooted.

Last updated