The flag in this challenge sits in /root/flag.txt, readable only by UID 0, on a container running as an unprivileged user with no sudo. That setup describes a huge share of real production images, and the technique that gets you there — a SETUID binary — is also the mechanism behind two of the most-cited Linux privilege escalation CVEs of the last decade. CVE-2021-4034, nicknamed PwnKit, is a memory-corruption bug in Polkit's pkexec that Qualys disclosed on January 25, 2022; because pkexec has shipped as a SETUID-root binary on nearly every major distribution since Polkit's first release in 2009, the flaw was exploitable on default installs going back over a decade. CVE-2019-5736, disclosed February 11, 2019, is a different class entirely: a flaw in runc (affecting versions through 1.0-rc6, and Docker before 18.09.2) that let a malicious container process overwrite the host's runc binary via /proc/self/exe and get code execution on the host itself the next time any container was started. One is a SETUID binary bug; the other shows what happens once you have SETUID-style root inside a container that isn't as isolated as you think. This writeup walks through the discovery, exploitation, and containment side of both, GTFOBins-style.
What Makes a SETUID Binary a Privilege Escalation Target?
A SETUID binary runs with the privileges of the file's owner, not the user who executes it, via a mode bit (04000, shown as an s in ls -l output — rwsr-xr-x) that the kernel checks at execve() time. /usr/bin/passwd is the textbook legitimate case: it needs root to write /etc/shadow, so it's owned by root and SETUID-root, and any user can run it safely because its logic only ever changes the calling user's own password. The danger appears when a SETUID-root binary either has a bug that lets you make it do something other than its intended narrow task, or was never designed to be run with elevated privileges in the first place — a custom deploy script, a debugging tool, or a language interpreter someone chmod'd 4755 to work around a permissions headache and forgot about. Inside a container, this matters doubly: even "container-local" root is still root as far as the kernel's own access checks are concerned, and if that container shares host namespaces, mounts, or a writable path back to the host filesystem, container-local root can become the pivot point for host-local root.
How Do You Find an Exploitable SETUID Binary Inside a Container?
The first move in almost any CTF box or real pentest is find / -perm -4000 -type f 2>/dev/null, which walks the filesystem for every file with the SETUID bit set regardless of owner. A stock Debian or Ubuntu image typically returns a short, boring list — passwd, su, mount, umount, chsh — all of which are intentionally SETUID and hardened against misuse. What you're hunting for is the extra entry: a project's custom binary, a leftover cp or python3 someone SETUID'd during debugging, or a package that ships a SETUID helper you don't recognize. GTFOBins.github.io catalogs which common Unix binaries can be abused for privilege escalation when they carry the SETUID bit, sudo rights, or specific capabilities — find, vim, python, perl, awk, and dozens of others each have documented one-liners. If find itself is SETUID-root, find . -exec /bin/sh -p \; -quit drops you into a root shell instantly, because find's -exec inherits the SETUID process's effective UID.
What Actually Happened in CVE-2021-4034 (PwnKit)?
PwnKit is the cleanest illustration of a SETUID binary bug turning into full root with zero credentials. Qualys researchers found that pkexec, Polkit's SETUID-root command, mishandled its argument count: when invoked with an empty argv (achievable via a crafted execve() call), it read past the end of the array while building an environment for the child process, allowing an attacker to inject a malicious environment variable — specifically a fake GCONV_PATH pointing at attacker-controlled shared library code that the dynamic loader would then execute as root. Because pkexec had carried the bug since its first commit and shipped SETUID-root by default across essentially every mainstream distribution, Qualys's January 25, 2022 disclosure affected an enormous installed base simultaneously. Inside a CTF container image with Polkit installed and an unpatched pkexec, the public proof-of-concept reproduces a root shell in a few seconds — no misconfiguration required beyond running an old package.
How Does Container Root Become Host Root?
This is where CVE-2019-5736 changes the stakes. Once an attacker had root inside a container — whether via a SETUID bug, a bad Dockerfile, or an exposed shell — the flaw let them overwrite the host's runc binary itself. runc opens its own executable via /proc/self/exe when re-executing itself as part of starting a container; the bug meant an attacker-controlled process running as root inside the container could race that file descriptor and get it to point at the container's own file, so that the next invocation of runc on the host (starting any container, by any user) executed attacker-controlled code with host privileges. Docker fixed this in 18.09.2, and it's the reason mode isolation matters: runc versions through 1.0-rc6 were affected industry-wide, not just in one vendor's product, per NVD and the coordinated disclosure that followed on February 11, 2019.
Why Does a Distroless Image Close This Off?
Distroless and scratch-based images remove the attack surface this entire writeup depends on: there's no /bin/sh, no find, no vim, no package manager, and often no SETUID binaries at all beyond what a single static application binary needs. GTFOBins-style escalation requires a shell or an interpreter to pivot into; if find / -perm -4000 returns nothing usable because there is no find, the technique dead-ends at reconnaissance. This doesn't eliminate risk from the application binary itself, but it collapses the "spray and pray" phase of a CTF box or a real intrusion into something that has to target the application logic directly, rather than the dozen convenience tools every general-purpose base image carries by default.
How Safeguard Helps
Neither PwnKit nor the runc file-descriptor bug required a novel technique to exploit once discovered — they required someone to still be running an unpatched version months or years after a fix existed upstream, which is the ordinary state of most fleets. Safeguard's self-healing containers close that gap on the base-image side: continuous scanning detects when a CVE like a Polkit or runc vulnerability lands on a component in your image, Griffin plans a patch strategy (upstream bump, backport, or a Gold-hardened substitution), and the image is rebuilt, tested, and promoted — typically with production rollout in under an hour, per Safeguard's own time-to-heal telemetry. That doesn't replace the harder work of auditing custom SETUID binaries baked into your own layers, which is a build-process discipline problem, not a patching one — but it does mean the well-known CVEs that give a CTF challenge (or an attacker) a reliable public exploit stop being live targets in your fleet once a fix exists.