CIS490/vm/targets/shellshock/verify.sh
Max Gorog 4d29b7236d PIPELINE §5 step 3: target VM build infrastructure + containment posture
§4.2 calls for target VMs we BUILD, not VMs we fetch. §4.13 demands
every target ship the same isolation posture (no upstream egress, no
host-shared FS, unprivileged QEMU, fresh snapshot per episode). This
commit lands the infrastructure for both.

New surface:
  * orchestrator/target_spec.py
      Loads + validates `vm/targets/<name>/spec.toml`. Containment
      fields are not knobs — each has exactly ONE safe value, and a
      spec asserting the unsafe value is rejected at load time. There's
      no `--containment-override`; weakening §4.13 requires amending
      PIPELINE.md and operator sign-off.

  * tools/build_target.py
      Orchestrates build → verify → publish for a single target. Spec
      invalid → exit 78 (sysadmin error). build.sh failure → image not
      published. verify.sh failure → image discarded; that's the §4.2
      acceptance gate. Publishes sha256 + the manifest.toml stanza the
      operator copies in to admit the image (§16 substantive amendment
      with sign-off per §15).

  * vm/targets/<name>/{spec.toml,build.sh,verify.sh}
      Template structure. spec.toml is the contract; build.sh produces
      $OUT_PATH; verify.sh boots the produced image under the §4.13
      containment posture and asserts every promise.

  * vm/targets/shellshock/
      First real working target. CVE-2014-6271 (Apache mod_cgi + bash
      4.2 mis-parsing function-export environment values). Replaces
      the SourceForge Metasploitable2 path that §3 evidence proved
      unverifiable. Bash 4.2 is built from sha256-pinned GNU source
      inside an Alpine 3.21 cloudinit guest; the build script asserts
      the produced bash actually triggers shellshock; the verifier
      re-asserts it under restrict=on with a real CVE-2014-6271 probe.

  * vm/targets/README.md
      How operators add a target. Walks the spec → build → verify →
      manifest amendment loop.

Containment regression tests (tests/test_containment.py) — 20 new
assertions, parameterized over every target with a build/verify trio:

  * verify.sh MUST contain `restrict=on` on its netdev (§4.13)
  * verify.sh MUST contain `snapshot=on` on the boot drive (§4.13)
  * verify.sh + build.sh MUST NOT contain -virtfs / -fsdev / 9pfs
  * verify.sh + build.sh MUST NOT wrap qemu-system in `sudo`
  * Every target must ship the complete spec.toml + build.sh + verify.sh
    trio — no half-built targets (§1 default-to-removal)

Spec validation tests (tests/test_target_spec.py): 13 new tests over
spec parse, name/dir mismatch, missing fields, out-of-range port, and
the §4.13 containment field validators (each unsafe value rejected
with a clear error).

The shellshock target's image is NOT yet published to manifest.toml's
[[targets.images]] — that's the §15 sign-off amendment that lands
after a successful operator-driven build_target.py run on a lab host
with KVM. Building takes ~10 min on x86_64; cannot run on the Pi
under TCG. Operator drives the first build, verifies the sha256, then
amends manifest.toml in a follow-up commit.

261 tests passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 01:31:40 -05:00

114 lines
4 KiB
Bash
Executable file

#!/usr/bin/env bash
# Verify the produced shellshock target satisfies its spec.toml
# promises (PIPELINE.md §4.2 acceptance).
#
# Inputs (env, set by tools/build_target.py):
# IMAGE_PATH — staged qcow2 we just built
# EXPECTED_SERVICE_NAME — "apache"
# EXPECTED_SERVICE_PORT — 80
# EXPECTED_SERVICE_PROTO — "tcp"
# EXPECTED_VULN_SOFTWARE — "bash"
# EXPECTED_VULN_VERSION — "4.2"
#
# Containment (§4.13) during verification:
# - SLIRP with restrict=on → no upstream egress (build is done; the
# image must be self-contained from here on). One hostfwd for the
# promised port.
# - snapshot=on so we don't mutate the image.
# - QEMU as the calling user (whoever ran build_target.py); we're not
# a root-owned daemon.
# - No -virtfs / -fsdev shared mounts.
set -euo pipefail
if [[ -z "${IMAGE_PATH:-}" ]]; then
echo "IMAGE_PATH not set" >&2
exit 2
fi
PORT="${EXPECTED_SERVICE_PORT:?}"
PROTO="${EXPECTED_SERVICE_PROTO:?}"
VULN_SW="${EXPECTED_VULN_SOFTWARE:?}"
VULN_VER="${EXPECTED_VULN_VERSION:?}"
if [[ "$PROTO" != "tcp" ]]; then
echo "[verify:shellshock] only TCP services supported in this verifier; got $PROTO" >&2
exit 1
fi
# Choose an unprivileged host port that maps to guest:$PORT.
HOST_PORT=$((20000 + RANDOM % 5000))
RUN_DIR="$(mktemp -d)"
trap 'qemu-down; rm -rf "$RUN_DIR"' EXIT
qemu-down() {
if [[ -f "$RUN_DIR/qemu.pid" ]]; then
local pid
pid=$(cat "$RUN_DIR/qemu.pid" 2>/dev/null || echo "")
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
sleep 1
kill -9 "$pid" 2>/dev/null || true
fi
fi
}
echo "[verify:shellshock] booting under §4.13 containment posture"
qemu-system-x86_64 \
-name cis490-verify-shellshock \
-machine q35,accel=kvm \
-cpu host \
-smp 1 \
-m 512 \
-drive file="$IMAGE_PATH",format=qcow2,if=virtio,snapshot=on \
-netdev "user,id=n0,restrict=on,hostfwd=tcp:127.0.0.1:${HOST_PORT}-:${PORT}" \
-device virtio-net-pci,netdev=n0 \
-nographic \
-display none \
-serial unix:"$RUN_DIR/serial.sock",server=on,wait=off \
-monitor unix:"$RUN_DIR/monitor.sock",server=on,wait=off \
-qmp unix:"$RUN_DIR/qmp.sock",server=on,wait=off \
-pidfile "$RUN_DIR/qemu.pid" \
-daemonize
# Wait for the service to come up. Apache + first-boot init can take
# 60-90s on cold start; budget 180s.
echo "[verify:shellshock] waiting for service on 127.0.0.1:${HOST_PORT}"
deadline=$((SECONDS + 180))
while (( SECONDS < deadline )); do
if curl -sf -m 2 "http://127.0.0.1:${HOST_PORT}/cgi-bin/test.cgi" -o "$RUN_DIR/probe.body" 2>/dev/null; then
break
fi
sleep 2
done
if (( SECONDS >= deadline )); then
echo "[verify:shellshock] FAIL: service never came up on 127.0.0.1:${HOST_PORT}" >&2
exit 1
fi
echo "[verify:shellshock] service responded; checking version + vulnerability"
# The CGI script prints `bash --version`. Assert the running bash is
# the promised vulnerable version (4.2).
if ! grep -q "bash, version ${VULN_VER}" "$RUN_DIR/probe.body"; then
echo "[verify:shellshock] FAIL: probe body does not show ${VULN_SW} ${VULN_VER}:" >&2
cat "$RUN_DIR/probe.body" >&2
exit 1
fi
# Confirm the actual shellshock vulnerability is present. CVE-2014-6271:
# bash mis-parses environment values that LOOK like function definitions
# and execute trailing commands.
echo "[verify:shellshock] confirming CVE-2014-6271 reachable via CGI"
RESP=$(curl -s -m 5 \
-H 'User-Agent: () { :;}; echo VULN_OK_MARKER_$$' \
"http://127.0.0.1:${HOST_PORT}/cgi-bin/test.cgi" \
-D "$RUN_DIR/headers" -o "$RUN_DIR/body")
if ! grep -q "VULN_OK_MARKER" "$RUN_DIR/body" "$RUN_DIR/headers" 2>/dev/null; then
echo "[verify:shellshock] FAIL: shellshock probe didn't trigger payload echo" >&2
echo " headers:" >&2; cat "$RUN_DIR/headers" >&2
echo " body:" >&2; cat "$RUN_DIR/body" >&2
exit 1
fi
echo "[verify:shellshock] PASS: ${VULN_SW} ${VULN_VER} on ${EXPECTED_SERVICE_NAME}:${PORT} confirmed exploitable"