CVE-2024-47611 — XZ Utils Windows CRT best-fit argument injection and directory traversal¶
Published 2026-06-06
Verified reproduction
This page documents a confirmed exploitation of CVE-2024-47611. The out-of-tree file write was observed through an independent channel and confirmed absent against the patched binary.
| Field | Detail |
|---|---|
| Project | XZ Utils |
| Affected component | xz.exe, xzdec.exe, lzmadec.exe, lzmainfo.exe — native Windows builds only |
| Severity | MEDIUM |
| CVSS | CVSS v4.0: 6.3 (Medium) |
| CWE | CWE-88, CWE-176 |
| Affected versions | XZ Utils ≤ 5.6.2, native Windows builds (MinGW-w64 or MSVC) only |
| Fixed version | 5.6.3 (released 2024-10-01) |
| Advisory | GHSA-m538-c5qw-3cg4 |
1. Vulnerability overview¶
XZ Utils is a collection of command-line compression tools and the liblzma library, available on Linux, macOS, and Windows. CVE-2024-47611 affects only the native-Windows executables: the builds produced with MinGW-w64 or MSVC, not Cygwin or MSYS2 variants, and not the liblzma library itself. An attacker who controls a filename passed to one of the affected tools can cause the tool to read from or write to a filesystem path entirely outside the directory it was launched from. On a system running the unpatched binary under a legacy Windows code page, a single crafted filename is enough to traverse directories the tool was never meant to reach.
Root cause¶
The Windows runtime converts the Unicode command line into the argv[] array that main() receives. Before XZ Utils 5.6.3, the Windows executables were built without an application manifest declaring activeCodePage = UTF-8. Without that declaration, the Windows startup code falls back to the system's active legacy code page (for example, Windows-1252) and applies "best-fit" character mapping during conversion. Best-fit mapping means that when a Unicode character cannot be directly encoded in the legacy code page, Windows silently substitutes the closest-looking ASCII character.
Several Unicode characters map to ASCII metacharacters that have meaning in the Windows command-line parser or in file-path handling:
- U+2215 DIVISION SLASH (
∕, UTF-8e2 88 95) maps to ASCII/(0x2F), enabling directory traversal. - Look-alike double-quote characters map to
"(0x22), breaking argument quoting and injecting extra arguments. - Look-alike question marks map to
?(0x3F), triggering wildcard expansion.
The patch (commit bf518b9ba446327a062ddfe67e7e0a5baed2394f) embeds a Windows application manifest in every executable by adding src/common/w32_application.manifest and wiring it into src/common/common_w32res.rc:
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
</windowsSettings>
</application>
The manifest entry tells the Windows loader to use UTF-8 for the command-line-to-argv[] conversion (available on Windows 10 version 1903 and later). With this in place, U+2215 reaches main() intact as a division slash, not rewritten to /, so no traversal occurs. The resource file conditionally includes the manifest only for non-Cygwin/MSYS2 Windows application builds:
#if MY_TYPE == VFT_APP && !defined(__CYGWIN__)
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "w32_application.manifest"
#endif
Older Windows
The activeCodePage=UTF-8 setting only takes effect on Windows 10 version 1903 and later. The fix does not protect binaries running on older Windows versions.
2. Vulnerable environment¶
The reproduction runs inside a single Docker container (linux/amd64) that cross-compiles both the vulnerable and patched XZ Utils for Windows using the MinGW-w64 toolchain and drives the resulting PE binaries under Wine.
What is in the container:
/opt/xz-vuln/bin/xz.exe— XZ Utils 5.6.2, compiled from the unmodifiedxz-5.6.2.tar.gz, no UTF-8 manifest. This is the vulnerable binary./opt/xz-patched/bin/xz.exe— XZ Utils 5.6.3, compiled from the unmodifiedxz-5.6.3.tar.gz, manifest embedded. This is the patched differential control.- Wine prefix configured with locale
en_US.CP1252at boot so the Windows Active Code Page (ACP) is set to 1252 (Windows-1252). This forces CRT best-fit argv mapping for the vulnerable binary. /lab/work— the confined working directory from whichxzis launched./lab/outside— the out-of-tree sibling directory, unreachable from/lab/workwithout a../traversal. The container entrypoint clears this directory on every boot, establishing an empty baseline before any exploit run.
Download the full environment: env.zip
Individual files:
Starting the environment:
Verifying the environment is correctly set up — the following smoke test confirms both binary versions, the legacy code page, and the directory layout:
docker exec cve-2024-47611-xz sh -c '
wine /opt/xz-vuln/bin/xz.exe --version | grep -i "5.6.2" &&
wine /opt/xz-patched/bin/xz.exe --version | grep -i "5.6.3" &&
wine reg query "HKLM\\System\\CurrentControlSet\\Control\\Nls\\CodePage" /v ACP | grep -i 1252 &&
test -d /lab/work && test -d /lab/outside &&
echo SMOKE-OK'
Expect output containing 5.6.2, 5.6.3, an ACP ... 1252 line, and SMOKE-OK. Wine may emit harmless fixme/err lines on stderr; these can be ignored.
3. How to exploit¶
The exploit passes a single crafted filename to the vulnerable xz.exe. The filename contains U+2215 DIVISION SLASH (∕, UTF-8 bytes e2 88 95) in place of each / in the path ../outside/payload. Because ACP=1252 is active, the Windows CRT rewrites each U+2215 to ASCII / before main() is called, so xz processes ../outside/payload, climbing out of the confined /lab/work into /lab/outside and writing the compressed output ../outside/payload.xz there.
Download the exploit: exploit.zip — exploit/run.sh
Step 1 — Copy the exploit script into the container¶
Step 2 — Run the exploit against the vulnerable binary¶
docker exec -w /lab/work cve-2024-47611-xz sh /tmp/run.sh /opt/xz-vuln/bin/xz.exe /lab/work outside payload "PWNED-BY-CVE-2024-47611"
run.sh constructs the crafted filename internally as .. + U+2215 + outside + U+2215 + payload (using POSIX octal escapes \342\210\225 for the three UTF-8 bytes of U+2215). The script seeds the attacker's plaintext input file at /lab/outside/payload (this is the attacker's INPUT, not the proof target) and invokes wine xz.exe -k -v <crafted_filename>. The -k flag keeps the input; -v makes xz print its progress including the resolved path.
The script prints the raw argument bytes before invoking xz so you can confirm the U+2215 bytes are present and no ASCII slash was introduced by the shell:
e2 88 95 at positions 3–5 and 13–15 are the U+2215 bytes — not 2f (ASCII /).
xz's stderr then shows the best-fit rewrite has fired:
The path printed contains a real ASCII /, not ∕. The argument arrived at main() already rewritten.
Step 3 — Observe the out-of-tree file through the independent channel¶
The proof is not xz's stdout. The verifier reads the filesystem directly from a host-side shell, independent of the exploit process:
docker exec cve-2024-47611-xz sh -c 'ls -la /lab/outside; find /lab/outside -type f -exec sh -c "echo FILE: \$1; od -An -tx1 \"\$1\" | head -2" _ {} \;'
What was observed in the verified run:
- A new file
/lab/outside/payload.xzappeared — 88 bytes, first bytesfd 37 7a 58 5a 00— the XZ container magic header. Onlyxzcould have produced a valid XZ container at that path. /lab/work(the confined launch directory) was empty —xzwrote nothing inside it; the output escaped entirely to/lab/outside.- Decompressing the proof target confirmed the marker:
xz -dc /lab/outside/payload.xz→PWNED-BY-CVE-2024-47611(exit 0).
Differential control — patched binary does not traverse¶
Running the same invocation against the patched binary confirms the effect is tied to the absent manifest:
docker exec -w /lab/work cve-2024-47611-xz sh /tmp/run.sh /opt/xz-patched/bin/xz.exe /lab/work outside payload "PWNED-BY-CVE-2024-47611"
What was observed:
- The argument bytes passed to xz are byte-for-byte identical (
... e2 88 95 ...). - xz stderr:
xz: ..∕outside∕payload: No such file or directory— U+2215 was left intact; no ASCII slash appears in the resolved path; no traversal occurred. - Independent channel after the patched run:
/lab/outsidecontains only the seededpayloadinput file.payload.xzis absent. The traversal is provably tied to the unpatched binary.
Environment teardown¶
4. Security advice¶
Remediation¶
Upgrade to XZ Utils 5.6.3 or later. This release embeds an activeCodePage=UTF-8 Windows application manifest in every affected executable, preventing the Windows CRT from applying best-fit character substitution during command-line parsing on Windows 10 version 1903 and later.
If an immediate upgrade is not possible, the following mitigations reduce exposure:
- Avoid passing untrusted filenames to
xz.exe,xzdec.exe,lzmadec.exe, orlzmainfo.exeon Windows. - Run on a UTF-8 system code page. On Windows 10 1903+ the system-wide UTF-8 beta setting (
Control Panel → Region → Administrative → Change system locale → Beta: Use Unicode UTF-8 for worldwide language support) also disables best-fit mapping, even without the manifest. This is a system-level change and should be evaluated in context. - Cygwin and MSYS2 builds handle argument encoding differently and are not affected. Switching to those toolchain variants is an option for environments that can accept the change.
- The fix provides no protection on Windows versions older than 10 1903, regardless of the manifest. Avoid running these tools with attacker-controlled filenames on legacy Windows.
References¶
- GHSA-m538-c5qw-3cg4 — GitHub Security Advisory — affected/fixed versions, CWE classifications, CVSS, discoverers (Orange Tsai and splitline, DEVCORE Research Team)
- NVD CVE-2024-47611 — CWE classifications, description, references
- Patch commit bf518b9 — tukaani-project/xz — full diff; root cause documentation
- XZ Utils v5.6.3 release notes — official changelog describing the security fix and its scope
- BUseclab/cve-genie exploit.py — community proof-of-concept using Wine and the U+2215 lookalike