Bagel — HackTheBox

7 min readJun 3


Bagel is a medium Linux machine from HackTheBox. It involves exploiting file read vulnerability to read the application source code, fuzzing another dotnet application’s PID to download its DLL, and reversing it to find and exploit JSON deserialization vulnerability leading to foothold on the machine. Then, running the dotnet as root leads to getting shell as root.


PORT     STATE SERVICE  REASON  VERSION                                                                                                                                                                                                       
22/tcp open ssh syn-ack OpenSSH 8.8 (protocol 2.0)
| ssh-hostkey:
| 256 6e4e1341f2fed9e0f7275bededcc68c2 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEwHzrBpcTXWKbxBWhc6yfWMiWfWjPmUJv2QqB/c2tJDuGt/97OvgzC+Zs31X/IW2WM6P0rtrKemiz3C5mUE67k=
| 256 80a7cd10e72fdb958b869b1b20652a98 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINnQ9frzL5hKjBf6oUklfUhQCMFuM0EtdYJOIxUiDuFl
5000/tcp open upnp? syn-ack
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 400 Bad Request
| Server: Microsoft-NetCore/2.0
| Date: Wed, 31 May 2023 13:21:24 GMT
|_ Connection: close
8000/tcp open http-alt syn-ack Werkzeug/2.2.2 Python/3.10.9
|_http-server-header: Werkzeug/2.2.2 Python/3.10.9
|_http-title: Did not follow redirect to http://bagel.htb:8000/?page=index.html

Port 8000 (HTTP)

Add the domain bagel.htb entry in the /etc/hosts file.

Navigating to website on port 8000, we get the following page. Looking at the URL, there’s a page parameter. We can test this later.

On the orders page, there a list of order details.

Path Traversal

Now, testing the page parameter that we saw on first page, it is vulnerable to path traversal. We can read the system files.

There are two users on the system. But can’t read their SSH keys.

So next, let’s get the source code on the application and analyze it.

Looking at order function, we now know that a dotnet websocket application is running on port 5000.

In this function, first we connect to WebSocket at port 5000. Next, a dictionary is created and converted into JSON and sent to the server. Then the response from server is parsed and returned.

In summary, the code establishes a WebSocket connection, sends a request to read the contents of the file “orders.txt” to the WebSocket server, and retrieves and prints the server’s response.

If we try to read any other file except order.txt, it returns order not found. Which essentially means there’s some kind of filtering or restriction in place.

We can also interact with it using wsc utility.

Fuzzing PID

So, the next step is to try to extract the source code of this dotnet order app. Since, we don’t know the path to the dotnet application, we can try to find it’s PID by fuzzing from directory traversal and look at the command used to run the application.

First, we generate a list of PIDs and then fuzz the /proc directory which contains information about running processes and system resources and find dotnet process. Then, we read the cmdline file in the process directory which contains the command-line arguments passed to the process.

Let’s see this in action.

# Generate a list of numbers from 1 to 5000
for i in $(seq 1 5000); do echo $i >> pids.txt; done

# Fuzz PIDs in /proc/ and match dotnet word
ffuf -c -w pids.txt -u http://bagel.htb:8000/?page=../../../../proc/FUZZ/cmdline -fw 3 -fs 0 -mr dotnet

Doing this we get a list of PIDs. Testing the first one, we get the path for dotnet application DLL.

Lets download the DLL from browser using directory traversal.

Reversing DLL

Transfer the DLL to Windows and decompile it using dnSpy tool for analysis.

Starting from MessageReceived function, a handler object is created, and used to deserialize the received JSON. Then the result is passed back to serialize and the resulting string is sent back.

Looking at the Handler class, it has two methods. The problem arise here in the deserialize method when setting TypeNameHandling to 4. According to the Microsoft documentation it means:

Include the .NET type name when the type of the object being serialized is not the same as its declared type. Note that this doesn’t include the root serialized object by default. To include the root object’s type name in JSON you must specify a root type object with SerializeObject(Object, Type, JsonSerializerSettings) or Serialize(JsonWriter, Object, Type).

This just basically means it is vulnerable to JSON .NET insecure deserialization.

Looking at the Orders class, it has three methods. The ReadOrder method is filtering the given input as we also tried to read file other than orders.txt and it did not work.

There’s also a File class which has four methods. The ReadFile and WriteFile set getter and setter functions and calls the other two functions. Here, we can see the is passed to the functions which is defined below. So, it makes the path like this.

There’s also a connection string in the DB class where we also find the credentials. But, using these credentials we can’t login via SSH.

JSON Deserialization

The issue arises where the JsonSerializerSettings sets the TypeNameHandling to 4, which is Auto. The following article explains it in better details.

We know from the main function of the DLL that the code always deserializes the input we give it no matter what. Looking at the methods in Order class, it seems that ReadOrder does check for LFI, so that’s not exploitable. WriteOrder does not seem to do much, but RemoveOrder is suspiciously short and does nothing. For our JSON deserialisation exploit, perhaps we should use this as the main function for exploitation.

For our payload, we would first need to add the the RemoveOrder function, then nest our payload within it. Since, TypeNameHandling is 4, the $type variable has to call the root object's type, in this case it would be bagel_server.file,bagel as per the DLL object names. The final payload will look like this:

"RemoveOrder": {
"$type": "bagel_server.File, bagel",
"ReadFile": "../../../etc/passwd"

Using this payload, we can read the content of /etc/passwd file.

Now, we can read the SSH private key of phil user.

Using the key, we can login as phil user and get the user flag.

Privilege Escalation

Using the DB credentials found while reversing the DLL, we can switch to developer user.

Looking at the sudo permissions, we can run dotnet command as root.

Running the following commands, we get a shell as root.

  • sudo dotnet fsi: Launches the F# Interactive (FSI) environment with root privileges, allowing interactive execution of F# code and exploration of F# language features using the .NET framework.
  • System.Diagnostics.Process.Start("/bin/bash").WaitForExit();: Starts a new /bin/sh process as root. The program waits for the process to exit before continuing execution.
sudo dotnet fsi