Lab 11 — Reproduce the runc Breakout, Then Detect It¶
Variant D · build-first. ← Back to the module concept
Setup¶
This lab has two halves, both one command away.
Half A — the real CVE. You reproduce CVE-2019-5736 against a pinned vulnerable runc using
Vulhub's environment. This is the genuine exploit — the host
runc binary is actually overwritten from inside the container — not a simulation.
git clone https://github.com/vulhub/vulhub
cd vulhub/runc/CVE-2019-5736 # pinned vulnerable runc + a target container
docker compose up -d --build # bring up the vulnerable runtime <!-- VALIDATE compose vs docker-compose path -->
Half B — detection. You deploy and tune Falco using the companion reference environment in
plaintext-labs, which ships a privileged
container, a co-located target, and a Falco sensor:
git clone https://github.com/plaintext-security/plaintext-labs
cd plaintext-labs/cloud/11-container-escape-runtime
make up # privileged attacker + target + Falco sensor
make logs-falco # (second terminal) tail Falco's JSON alerts
make shell # shell into the attacker container
make demo # run escape + detection end-to-end
make down # stop and clean up
Be honest about what's real. Half A overwrites a host binary — run it on a disposable
VM you own, never a machine you care about, and docker compose down after. Half B's --privileged
escape is also real (it mounts and reads a host-side path); the two halves are split only because
pinning vulnerable runc and running Falco's eBPF probe have different host requirements.
Only test systems you own or have explicit written permission to test. Everything here runs locally against disposable containers and targets you launch yourself.
Scenario¶
The target account's platform team got a Dependabot alert: a data-processing job pulled a base image
with a vulnerable runc, and a second job in the same cluster runs --privileged "because it fixed a
permissions error." You are both halves of the response. Red team: reproduce the host-level escape
so the risk is undeniable, not theoretical. Detection engineer: stand up Falco, prove it catches
the behavior, and tune one false positive out so the rule is something the SOC will actually keep on.
Do¶
Half A — Reproduce CVE-2019-5736 (the real escape)
-
[ ] Bring up Vulhub's vulnerable-runc environment and confirm the runc version is in the affected range (cross-check against the NVD record). Hint: the README in the Vulhub directory names the exploit steps; follow them.
-
[ ] Run the exploit: place the malicious payload inside the container so that when the operator (you, playing the admin)
execs in, the/proc/self/exerace overwrites the hostrunc. Confirm the escape: the payload you control executes on the host as root on the next container operation. Hint: the published PoC works by replacing the container's entrypoint with a/proc/self/exesymlink and racing the open; the Vulhub README points to a working PoC. -
[ ] Record the chain. In your report, write the hop in the module's mental-model language: which shared resource was abused (the host
runcbinary the runtime reaches in with), and why no namespace, cgroup, or capability stopped it — the kernel and that host binary were shared.
Half B — Deploy and tune Falco
-
[ ]
make up, thenmake logs-falcoin a second terminal.make shellinto the privileged attacker. Confirm you're a container PID 1 (cat /proc/1/cgroupshows the container ID). -
[ ] Trigger the escape behavior and watch Falco fire. From the attacker shell, write to a host-binary-class path /
/etc/passwdand read the host-side secret (make demodoes both deterministically). In the Falco terminal you'll see CRITICAL/WARNING JSON alerts carryingcontainer,image,pid, and the rule name. Copy the alert text into your report. -
[ ] Settle the prediction. Open
data/falco-rules.yaml. Compare the rule that fires on the write to the sensitive file (low-noise: nothing benign writes a host binary) against the rule that fires onmount(2)inside a container (Rule 3). Note which one you'd build the detection around — and whymountis the one that will generate your false positive. -
[ ] Force the false positive. A legitimate workload mounts a volume at runtime (or a DB writes a path that matches a broad rule). Reproduce one benign event that trips a rule — e.g. a non-attack
mountfrom an allowed process, or a benign write under a data dir matched too broadly. Capture the spurious alert. -
[ ] Tune it out. Edit the rule (add an
exception, or tighten thecondition— e.g. exclude the known image/process/path for the benign case) so it no longer fires on the benign event but still fires on the escape. Re-run both the benign action andmake demoand show the alert flips: silent on benign, CRITICAL on the attack. Save the result asfalco-rules-tuned.yaml.
Success criteria — you're done when¶
- [ ] You reproduced CVE-2019-5736: a payload you control executed on the host as root, originating from inside the container (real exploit, not a description).
- [ ] You can state the escape in the module's terms: the shared host resource abused, and why namespaces/caps didn't stop it.
- [ ] You have a Falco alert (JSON) for the escape behavior showing container, image, pid, and rule.
- [ ] You produced one benign false positive and then tuned it out — your
falco-rules-tuned.yamlis silent on the benign event and still CRITICAL on the escape, demonstrated by re-running both.
Deliverables¶
escape-report.md— the CVE-2019-5736 reproduction (steps, commands, the host-root proof), the shared-resource explanation, and the Falco alert text.falco-rules-tuned.yaml— the tuned rule with yourexception/tightenedcondition, plus a one-line comment per change saying which false positive it removes and why it still catches the attack.
Commit these two. Container/runtime state, the overwritten runc, host artifacts, and any captured
secrets stay out of the commit.
Automate & own it¶
Required — the guardrail is the tuned detection itself. Your judgment ("mount is noisy; the
write-to-host-binary is the sharp signal; here is the one benign case to except") is encoded in
falco-rules-tuned.yaml as detection-as-code — a rule that fails the bad state and passes the
fix: it fires on the escape and stays silent on the benign workload. Prove the flip in CI-style: a
small script (or make demo) that runs the benign action and the escape and asserts the tuned rule
alerted on exactly one. Have a model draft rule variants and the exception; you read every line and
confirm each variant still fires on the real escape before keeping it — a rule tuned until it's silent
on everything is worse than no rule. (Optional: also ship audit-privileged.sh — docker inspect
across running containers flagging Privileged: true, /dev mounts, or docker.sock mounts — the
prevention companion to the detection.)
AI acceleration¶
Feed the escape's Falco alert JSON to a model and ask it to reconstruct the kill chain: technique, ATT&CK
ID, likely next move. It writes a solid triage narrative — validate it against the actual rule
condition and the T1611 card. Then paste your tuned rule
and ask it to find an attack variant that now sneaks past your exception. If it finds one, your
exception is too broad — tighten it. That adversarial loop is the whole skill.
Connects forward¶
- Module 13 (K8s admission & runtime) moves this exact problem into Kubernetes:
--privilegedbecomes a pod-spec field a Kyverno admission policy blocks before it ever runs — prevention in front of the Falco detection you wrote here, which catches what slips past. - Module 15 (cloud logging & detection) generalizes the move: today you tuned a Falco rule against benign syscalls; there you'll tune a Sigma rule against benign CloudTrail. Same signal-vs-noise craft, different log source.
Marketable proof¶
"I reproduced CVE-2019-5736 — the runc host-binary breakout — end to end, can explain why a container's shared kernel makes it a process-in-a-jail and not a VM, and I deployed and tuned a Falco rule that fires on the escape but stays silent on a benign workload I had to engineer around."
Stretch¶
- Reproduce a configuration escape with no CVE: in a container with
/var/run/docker.sockmounted,docker run --privileged -v /:/host alpine chroot /hostfor host root — and write the Falco rule that catches a container talking to the Docker socket. - Read CVE-2021-30465 (runc symlink race) and explain,
in your report, how it reaches a host filesystem write without
--privileged— a different door, same shared-kernel hallway.
Comments
Sign in with GitHub to comment. Choose the type: Feedback (errors or suggestions on this page) · Hints (help for fellow learners — no spoilers) · General (anything else).