Adds the Tier-3 exploit driver — an MSFExploitDriver that plugs into
EpisodeRunner.on_phase, fires a Metasploit module against a target VM
via msfrpcd, watches for the resulting session, and stamps each
transition (exploit_fire, session_open, session_landing_probe,
sample_executed, session_dormant, session_killed) into the episode's
events.jsonl on the orchestrator's monotonic clock.
What landed:
- exploits/msfrpc.py — minimal msgpack-over-HTTPS client (auth,
module.execute, job/session lifecycle) so we don't depend on a
third-party MSF wrapper.
- exploits/driver.py — phase-to-msfrpc adapter; idempotent fire,
session-open polling with timeout, workload start/stop, teardown.
- exploits/modules.py + exploits/modules/vsftpd_234_backdoor.toml —
TOML module configs with {{ target_ip }} placeholders, replacing the
imperative .rc-script approach the README previously hinted at.
- vm/launch_target.sh — SLIRP+restrict=on launcher for the
intentionally-vulnerable target VM (host can reach guest via
hostfwd, guest cannot reach host or internet).
- tools/run_tier3_demo.py — end-to-end runner mirroring run_real_vm_demo.
- tests/test_exploits.py — 12 new tests against a fake MSFRpcClient,
including an integration test that drives a real EpisodeRunner.
Plumbing changes:
- EpisodeRunner._emit_event → public emit_event, so external drivers
share the runner's monotonic clock and events.jsonl.
- mkdir for episode_dir moved to __init__ so emit_event is callable
before run() (driver_setup fires pre-schedule).
Status: driver + tests pass (40/40); end-to-end against a live msfrpcd
+ Metasploitable2 image is the next bring-up step.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
95 lines
3.1 KiB
Bash
Executable file
95 lines
3.1 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Boot the Tier-3 *target* VM (the intentionally-vulnerable guest the
|
|
# exploit fires against). Companion to ``launch_demo.sh``, which boots
|
|
# the *idle* Alpine guest used in Tiers 1-2.
|
|
#
|
|
# Networking note: this launcher uses SLIRP usermode networking with
|
|
# ``restrict=on`` plus an explicit ``hostfwd`` for each vulnerable port.
|
|
# That gives us:
|
|
# - the host can reach the guest's services (for msfrpcd + the
|
|
# exploit module to drive ``RHOSTS=127.0.0.1``)
|
|
# - the guest cannot reach the host or the internet (no NAT exit)
|
|
#
|
|
# The host-only ``br-malware`` bridge described in docs/architecture.md
|
|
# replaces SLIRP once the bridge-side pcap collector (source 4) lands —
|
|
# at which point payloads with ``reverse_tcp`` callbacks become viable
|
|
# too. Until then, we restrict module choices to ones that return a
|
|
# shell on the same socket they exploit (e.g. vsftpd_234_backdoor).
|
|
#
|
|
# Run-dir contract (read by run_tier3_demo.py):
|
|
# $RUN_DIR/qemu.pid
|
|
# $RUN_DIR/qmp.sock
|
|
# $RUN_DIR/monitor.sock
|
|
# $RUN_DIR/serial.sock
|
|
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
IMAGE="${IMAGE:-$REPO_ROOT/vm/images/metasploitable2.qcow2}"
|
|
RUN_DIR="${RUN_DIR:-/tmp/cis490-target}"
|
|
RAM_MIB="${RAM_MIB:-512}"
|
|
# Ports the host should forward to the guest. Comma-separated host:guest pairs.
|
|
# Default covers the vsftpd module's RPORT.
|
|
TARGET_PORTS="${TARGET_PORTS:-21:21}"
|
|
# KVM if the host can take it; otherwise fall back to TCG. Cross-arch
|
|
# images (Metasploitable2 is x86-only) on aarch64 hosts will need TCG.
|
|
ACCEL="${ACCEL:-}"
|
|
|
|
mkdir -p "$RUN_DIR"
|
|
QMP_SOCK="$RUN_DIR/qmp.sock"
|
|
MON_SOCK="$RUN_DIR/monitor.sock"
|
|
PID_FILE="$RUN_DIR/qemu.pid"
|
|
SERIAL_SOCK="$RUN_DIR/serial.sock"
|
|
|
|
if [[ ! -f "$IMAGE" ]]; then
|
|
cat >&2 <<EOF
|
|
no target image at $IMAGE
|
|
|
|
Drop a vulnerable Linux qcow2 there. The canonical choice is
|
|
Metasploitable2 — see docs/sources.md for the download + sha256.
|
|
|
|
If the image is x86 and your host is not, set ACCEL=tcg explicitly.
|
|
EOF
|
|
exit 1
|
|
fi
|
|
|
|
# Build the netdev string with one hostfwd= per requested port pair.
|
|
NETDEV="user,id=n0,restrict=on"
|
|
IFS=',' read -ra _PAIRS <<< "$TARGET_PORTS"
|
|
for pair in "${_PAIRS[@]}"; do
|
|
host_port="${pair%%:*}"
|
|
guest_port="${pair##*:}"
|
|
NETDEV+=",hostfwd=tcp:127.0.0.1:${host_port}-:${guest_port}"
|
|
done
|
|
|
|
# Pick acceleration: explicit override wins; otherwise use KVM if the
|
|
# device is present, else TCG.
|
|
if [[ -z "$ACCEL" ]]; then
|
|
if [[ -e /dev/kvm && -r /dev/kvm && -w /dev/kvm ]]; then
|
|
ACCEL="kvm"
|
|
else
|
|
ACCEL="tcg"
|
|
fi
|
|
fi
|
|
|
|
CPU_FLAGS=()
|
|
if [[ "$ACCEL" == "kvm" ]]; then
|
|
CPU_FLAGS=(-cpu host)
|
|
fi
|
|
|
|
# snapshot=on so the qcow2 is never mutated — every boot is identical.
|
|
exec qemu-system-x86_64 \
|
|
-name cis490-target \
|
|
-machine q35,accel="$ACCEL" \
|
|
"${CPU_FLAGS[@]}" \
|
|
-smp 1,sockets=1,cores=1,threads=1 \
|
|
-m "$RAM_MIB" \
|
|
-drive file="$IMAGE",format=qcow2,if=virtio,snapshot=on \
|
|
-netdev "$NETDEV" \
|
|
-device virtio-net-pci,netdev=n0 \
|
|
-nographic \
|
|
-serial unix:"$SERIAL_SOCK",server=on,wait=off \
|
|
-monitor unix:"$MON_SOCK",server=on,wait=off \
|
|
-qmp unix:"$QMP_SOCK",server=on,wait=off \
|
|
-pidfile "$PID_FILE" \
|
|
-display none
|