HTB - Crossfit
Zweilosec's write-up on the insane-difficulty Linux machine from https://hackthebox.eu
Overview

This Insane-difficulty machine from Hack The Box took far longer to root than I would have liked, mostly due to getting hung up on the the final exploit. I took a break from it, after getting the user.txt, due to frustration and wanting to make progress elsewhere. This machine challenged me in a number of areas, from creative enumeration methods, to code and binary analysis, to "exploit" writing in a foreign language (JavaScript and C!). After taking a break for a few months, I came back with a fresh perspective and was able to quickly discover the errors I had been making. (Along with fresh patience with the quick-clean script the authors used!). A script to automate all of the moving pieces of the final exploit solved my issues and I was able to root the machine.
Useful Skills and Tools
Connecting to Secure FTP using lftp
NOTE: If the server is making use of self signed certificates you may need to add this as well:
Enumeration
Nmap scan
I started my enumeration with an nmap scan of 10.10.10.208. The options I regularly use are:
Flag
Purpose
-p-
A shortcut which tells nmap to scan all ports
-vvv
Gives very verbose output so I can see the results as they are found, and also includes some information not normally shown
-sC
Equivalent to --script=default and runs a collection of nmap enumeration scripts against the target
-sV
Does a service version scan
-oA $name
Saves all three formats (standard, greppable, and XML) of output with a filename of $name
The scan showed that there were only three TCP ports open, 21 - FTP, 22 - SSH, and 80 - HTTP.
Port 21 - FTP
My first target was any potential low-hanging fruit that may have been accessed through FTP.
In my nmap output for port 21 I found two hostnames, crossfit.htb and gym-club.crossfit.htb, which I added to my /etc/hosts/ file.
Without credentials, the first thing I check is whether or not the server accepts anonymous login. I was not able to log in to FTP using anonymous in this case.
Port 80 - HTTP
Next, I saw that there was an Apache web server being hosted on port 80. I opened my web browser and navigated to crossfit.htb to see what I could find.

crossfit.htb only led to the default apache page, meaning there was no default page configured at this address.

However, gym-club.crossfit.htb led to a CrossFit gym website.

The Wappalyzer Firefox plugin showed me the technologies that were in use on this site. I did a quick search for each of the ones that showed a version number but none led to any useable vulnerabilities.

The site included a schedule of classes. I took down the names Candy, Murph, Chelsea, and Annie as potential usernames.

The link to "Join the club" led to a "coming soon" page, but there was nothing useful there.
Since I had already found one virtual host for this IP address, as in HTB - Forwardslash I tried to do vhost enumeration using gobuster. I ran this in the background while enumerating the website.
This did not come up with anything, however. I tried with ffuf as well, but did not find any more subdomains.

On the "About-Us" page I found four more possible usernames: Becky Taylor, Noah Leonard, Evelyn Fields, and Leroy Guzman. As the Manager, Leroy seemed like the most likely target.
Cross-site Scripting (XSS)

Since there wasn't anything obvious to go by, I started doing some basic vulnerability testing on the submission boxes. The first one at /contact.php did not seem to be vulnerable to either XSS or SQL injection.

However, the second one I found at /blog-single.php gave a warning about XSS.

The warning claimed that browser information will be sent to the admin. I thought maybe I could smuggle something that would be executed by the admin through the "browser information": AKA User-Agent.

Using Burp I sent a request with a link to my machine in the User-Agent field. I made sure to send the same XSS attempt so this would be forwarded to the admin.
I received an HTTP GET request to my waiting netcat listener.

In the Referer field I saw /security_threat/report.php in the response headers, but was not able to access it. The code in this PHP file must have been what had checked the XSS request I had made, and somehow executed the <script> tags I had embedded, while creating the report for the admin.
In the same spirit I tried to get the admin to download a PHP reverse shell from me, but I couldn't find where it had been uploaded nor figure out how to execute it.

Origin Header with Access-Control-Allow-Origin response header
ftp.crossfit.htb
I was familiar with fuzzing for vhosts as well as directories and files, but fuzzing for a response in a header was something new. I did some research on more advanced uses of ffuf and found a few interesting articles.
With the information from these articles I was able to craft a set of parameters to fuzz the Origin header. In order to do this I needed to use the -H flag to include the custom header selection, as well as use the -mr flag to match using a custom regular expression.
Match on Regular ExpressionIn some cases, however, you may be fuzzing for more complex bugs and want to filter based on a regular expression. For example, if you’re filtering for a path traversal bug you may wish to pass a value of
-mr "root:"to FFUF to only identify successful responses that indicate a successful retreival of/etc/passwd.
I knew if the response from the server included the words "Allow-Origin" that it was a valid request using the specified Origin header. Using this information, I was able to find another virtual host: ftp.crossfit.htb.

However, loading up this URL in my browser just led to another default Apache page. Next, I decided to see if I could use the same Cross Origin Request Forgery to get the headers of the internal page to see if there was a different view from the proper origin.
Cross Origin Request Forgery with JavaScript
TODO: add screenshot of sending request with this payload in Burp
Since the <script> tag that triggered the cross-site scripting exploit was using JavaScript (alert()), I decided that was the best language to write a payload in to try to get the server to access the page I wanted for me. I did a bit of research and found an easy way to make HTTP requests using JavaScript.
I wrote a JavaScript payload to reach out to the ftp.crossfit.htb site then send the response back to my waiting Python HTTP server. I used this instead of netcat so the server would stay active and I wouldn't have to restart it each time the server closed the connection.
It took me a few tries, but I was able to get the server to download my script and execute it. The final response I got contained the encoded HTML for a web page.
After decoding the response I had the webpage at http://ftp.crossfit.htb as viewed internally. I could tell right away that this was not the same default Apache server page.

I saved the HTML code to a file and opened it in my browser. The page turned out to be an account management page for FTP. The "Create Account" button was a link to a site that sounded interesting: http://ftp.crossfit.htb/accounts/create. I modified my JavaScript payload to see what was at this page.
I got back a response with the encoded HTML code for the /accounts/create website.

With this page it looked as if I could create a new account. I would need to send a POST request to http://ftp.crossfit.htb/accounts with a username, password, and the value from the hidden field _token.
I modified my script to retrieve the hidden _token value. At first, I tried just reading the value of the _token element out of the response, but after some troubleshooting and more reading I found that the response text wasn't being properly parsed.
Once I was able to correctly parse this from the output I was able to send my POST request to the server to create a new user. First though, I had the test script send just the token back to me so I could see that it worked.
I received the reply back, this time with only the hidden _token value.
Next, I modified my payload to actually create the new FTP user.
Got a response on my HTTP server with the parameters I was sending to the server, then again with HTML code of the response. I hoped that it included some kind of way to tell if I had been successful at creating a user.
I decoded the HTML response, then saved it to a file.

After recreating the webpage from the response, I could see that my account was created successfully! Next I tried to log into the server using FTP.
Port 21 - Revisited (using LFTP)
I tried logging in with FTP, but got an error 530 Non-anonymous sessions must use encryption.
NOTE: If the server is making use of self signed certificates you may need to add this
setas well:
Success! I was able to log into FTP with the user I had created. I immediately found four folders.
Unfortunately the most interesting sounding folder development-test/ was empty.
Annoyingly, the server seemed to delete my user account after a few minutes. I had to recreate it and log in again each time during my enumeration.
In the /gym-club directory there was a file db.php. The password I found here unfortunately did not work with the username crossfit for either SSH or FTP.
In the ftp/database/factories/ folder there was a file called UserFactory.php.
This file contained a password hash, which I loaded into hashcat. It cracked almost instantly to reveal ... 'password'. This was most likely a placeholder.
While I was looking at the four main folders' permissions, I realized that the permissions for /development-test allowed for writing to the directory.
Using the FTP command PUT I was able to upload files. I used this to upload a PHP backdoor that would send me a reverse shell when executed.
I once again modified my original JavaScript exploit from earlier, this time simply requesting access to my PHP backdoor. I hoped that this would execute the code within and send me a reverse shell. On this one I took a logical leap. I assumed that since each of the other folders was running a virtually hosted web domain that development-test would be the same (even though there was currently no web page hosted there).
I received a connection request for my JavaScript code, and Burp, which I had used to send the request, showed that the server was stuck while trying to send me a response. This was good sign as this tends to happen when sending a reverse shell this way.
Initial Foothold
After a little bit of waiting, I got a shell back on my waiting netcat listener! The first thing I did was upgrade to a full PTY using Python.
Enumeration as www-data
www-dataHmm...ps showed that this was a busy machine... It looked like I had company (only one of those shells is mine!).
I inspected /etc/passwd for local user accounts and found that root, isaac, and hank could login with a shell. These were my most likely targets.
In the /home directory there were only two user folders: hank and isaac.
I saw that hank had user.txt in his home folder, so now I knew I needed to move laterally to that user to get it.
TODO: insert output of search for hank in files (I may have deleted this since because of too many files?)
After searching for files that had mentioned the user hank, I found adduser-hank.yml in the/etc/ansible/playbooks directory. This file had a password hash in it that I copied to my machine for cracking.
Password hashes with $6 at the beginning are most likely Unix sha512crypt encrypted. I used this information to quickly crack the hash using hashcat. It revealed the password to be powerpuffgirls.
User.txt
After cracking the hash to get the password I fired up SSH and logged in as hank. The first thing I did was collect my hard-earned proof.
Path to Power (Gaining Administrator Access)
Enumeration as hank
hankThe user hank was in the group admins which sounded interesting.
Well, this was odd... It told me that it could not find the sudo command. I went and checked inside /usr/sbin and there was no binary for sudo installed.
The program ifconfig was also missing.
I decided to check and see if this was a strange distribution, or a BSD system, but uname -a told me that this was a Debian-based system. Curiouser and curiouser...
I checked for running processes and noticed a few things were running from the /opt/selenium folder. I wasn't sure what that was so I looked it up. I also noticed that I was unable to see any processes from other users.
Selenium automates browsers. That's it!
While checking for folders in the /var/www directory I noticed they looked suspiciously familiar...I also noticed the permissions for the user and group. The vsftpd group was what had allowed me to PUT files in the development-test folder using FTP. Interestingly, I could have also done this through a web interface if there had been a site hosted here because it was owned by www-data. This is probably why the folder was empty.
After looking at the folders in /var/www I remembered that I had found credentials for logging into MySQL there. I used the credentials to log into the database, but there was no useful information to be found. It only held information that was displayed on the website.
In the /security_threat folder there was a file called reports.php that held the code for reporting the detected XSS events to the admin. Apparently there was also another file that saved the report to the database, because this one only retrieved it, displayed it to the reports.php page when viewed, then deleted it from the database.
I searched for files that hank could access as a member of the admins group, and found a bunch of them that were in the /etc/pam.d directory. This was quite interesting since PAM deals with user authentication.
In /etc/pam.d the file vsftpd contained the password for the user ftpadm.
ftpadm
ftpadmUnfortunately, the SSH configuration was set to explicitly deny ftpadm from logging in through SSH. I could also see that it was configured to run the subsystem for SFTP.
I tried instead to log in through FTP once again. This time, there was only one folder messages, which I had read-write access to, but was empty. Once again I uploaded a reverse shell and tried to execute it, but I wasn't sure where the folder was in the filesystem. I decided to keep this in mind as I continued my enumeration.
send_updates.php
In /etc/crontab there was a cron running every minute. This cron ran the script /home/isaac/send_updates/send_updates.php as the user isaac.
In the folder /home/isaac/send_updates I found the send_updates.php file. Reading through the code, it seemed as if it was created to email the "CrossFit Club Newsletter" automatically to all users in the database (once a minute?! talk about spam!). The mail command was being run by the mikehaertl\shellcommand\Command library, which after a short search I found on GitHub.
php-shellcommand provides a simple object oriented interface to execute shell commands.
The file composer.json showed the version of this library was 1.6.0. A web search showed that this was vulnerable to command injection.
Since this library is used to execute arbitrary commands, and there was no kind of filtering being done on the email parameter, this could be used to inject other commands.
Looking at the mail program's help, I noticed that there was a flag -E that allowed for execution of commands. Since I already had the credentials to the database, it seemed likely that I could create an entry in the user's table that contained code I wanted to execute as isaac in the email field of the database to be loaded and run withinsend_updates.php. isaac did not have a .ssh folder to insert my public key to, so I needed to craft a reverse shell instead.
I logged into MySQL using the same credentials as before, and checked for the field names in the user table so I knew how to frame my query to insert my reverse shell into the correct field. I assumed that the id field was set to AUTO_INCREMENT, but just in case I set it to the value "1" to ensure my code would be the first entry. The script from earlier said it pulled only the first entry.
I got a connection back from my injected code, however it also immediately ran two commands ( I did not type these: test and exit) which caused it to disconnect. I also could not get the same injected command to work later for some reason. I had no idea what was broken here, or why those commands got run.
I looked back through the code in send_updates.php looking for clues for how to proceed. I noticed that these loops look through everything in $msgdir and check to see if there was a file, and if so, it would run the rest of the code.
After some testing I found out that I needed to trigger the message by uploading a file into the messages folder after logging in with ftpadm. It did not seem to matter what the file was.
I changed ports and tried inserting my reverse shell again, and this time got a steady connection.
I lost my connection again due to an "unplanned network outage", so had to try again. I forgot about the -E flag when I came back to this and found another way to get my code to execute. Chaining bash commands using & also worked.
Enumeration as isaac
isaacI finally got a shell back after triggering it by uploading a file to the ftp server.
There was a new group staff that had access to a bunch of files related to selenium.
Next, I upgraded to full PTY shell.
I did some investigating to figure out why I had to upload a file to get the code to execute
In the includes folder there was a file config.php that pointed to /srv/ftp/messages. This was the variable I had seen in the send_updates.php script.
The file functions.php explained why my query would be deleted from the database every so often.
In the process output I could see my reverse shells (I tried two different methods), but nothing else that was useful.
dbmsg
I tried using pspy (with the -f flag to see files as they are accessed) to see if it was just ps that was being restricted, and I noticed the program dbmsg that I didn't recognize.
There was no man page, and after looking around on the internet for awhile and not finding anything I figured it must be a home-brewed application.
A quick peek into the strings inside the file proved this to be true.
I exfiltrated the binary to my system and opened the program in ghidra. After locating the main() function, I saw that it ran as root. This looked to be a good bet for escalation of privileges. The program appeared to check the current system time, create a random number using the time as a seed, then run the process_data() function.
The process_data() function opened a connection to the MySQL database and logged in. Then it pulled all of the data from the messages table and stored the result in a variable. It then opened the file /var/backups/mariadb/comments.zip. After opening the messages table and the zip file, it appeared that the program took each entry in the messages table and created a file in /var/local using the md5sum of the random number it creates as the filename, then added each file to the zip.
If I could create a file with the correct "random" filename in the /var/local directory and link it to a file of my choice before the program executed the write action, the output of my database entry in message would be written to the file (and therefore to the linked file). Whew, this was a bit complicated!
I tried to see what was in the backup zip file to see if I could validate my analysis of the program, but I was unable to access the directory that file was stored in.
I did some research into creating a random number seed using C and then printing that number to a file.
I wrote a short function in C that emulated what the program was doing: using the current time to generate a pseudo-random number, then outputting that number to the terminal. I would use a script to use md5sum on the generated number and write it to a file since that would be much easier than writing a whole C program to do this.
I compiled the small program, then created a script to run it
TODO:fix this paragraph., linking the file to root's authorized_users and (hopefully!) writing my public SSH key to it.test script to see if I can write to random location and see if my output appears. update: test worked... automating process because its a pain, and deletes every thing so fast...The '1' after running my random number program is the ID field value from my SQL statement.
My random files were generated, and then suddenly and swiftly removed by the cleaning crew. I tried to do a test run, but the files were deleted so fast that I couldn't get the testing file to stick around. I thought about trying to get the output to redirect to /dev/tcp to show up at a netcat listener on my machine, but I was confident that I had made it work and pushed on with trying to get root access.
I compiled my random file generator then made it executable (removing the evidence of its creation). After running it, (note: this is from before I automated all of this with my script later!)
I added a line to my script that would insert my message into the database, ran my script, and saw the file linked to root's authorized_keys file in the proper folder. However, I was not able to SSH into the machine.
I scrolled up to check the error messages output from my script, and saw the error that had stopped it from working. My database query that I put in the script was only inserting my public key into the messages field. The database was not set up to function with NULL values in the other fields so it was not working. It required values in the other fields.
I was also receiving the second error message above that was caused by a cleanup script on the machine which regularly deleted all files in the /tmp, /var/local, and /dev/shm directories (and maybe more).
After going back to my shell as hank and checking mysql I realized my problem. I was getting an error while inserting my key into the database, but since there was a huge spam of output from my script I didn't see it. I used the DESCRIBE command to see the columns in the message table so I could tailor my input better.
I had to try a number of different entries, but in the end I had to break the public key into its three components and put them in the fields as seen above.
Field
Value
name
Key encryption type
Username (on attacker machine)
message
Public key content
My SQL entry into the message database was based off of this section of the code from the process_data() function. It wrote the second field, the fourth field, then the third field to the file. This means that it wrote them in the order: name, message, email. These were separated by adding by a space (0x20) when it wrote to the file. I would need to match my entries to this format.
I created a shell script to automate everything. It wrote the C source code to a file, compiled it into a binary, gave it the executable permission, inserted my properly formatted SQL statement into the database, and finally linked /root/.ssh/authorized_keys to my randomly generated file.
I verified that the randomly generated file was symlinked to /root/.ssh/authorized_keys and hoped that everything worked as planned.
Root.txt
Finally! I had to keep trying to log in through SSH, as it wasn't until my files got deleted that I was successful. I was able to tell that my files got deleted because I suddenly began getting spammed with an error message from my script.
As you can see...I got tired of even typing out test.sh and simply called my script t. I almost was about to write another script that would send my files over, chmod +x them, and run the exploit script, but just before I did that the exploit finally worked!

Thanks to polarbearer & GibParadox for creating such a fun and challenging machine. I am grateful that while it involved reverse engineering a C binary file, it did not involve advanced buffer overflows or anything like that (looking at you Rope and Rope2...). I had to learn a number of new things to complete this machine such as writing code in JavaScript and C, both of which I am not that familiar. Overall this was a great challenge and I definitely look forward to more like this!
If you like this content and would like to see more, please consider buying me a coffee!
Last updated
Was this helpful?