
Healthy Hijack 1 (Infra) - Writeup
On this page
Challenge Overview
| Event | KPMG CTF 2026 Campus Edition |
|---|---|
| Challenge | Healthy Hijack 1 |
| Category | Web / Command Injection |
| Points | 10 |
| Target | kicyber-...-healthyhijack-0-0.chals.io |
"The application allows execution of limited network commands with strict filtering in place. Or so it seems... Explore the system, identify weaknesses, and escalate your privileges to root."

The target is a "Safe Command Runner", a web form that claims to only allow "network debugging commands." The name "Hijack" and the phrase "or so it seems" tell us the filter is beatable. This is a command injection challenge: get the runner to execute something it wasn't meant to.

Step 1: Map the Filter
Random input gets flatly rejected, submitting to /run returns "Command not allowed", so there's an allowlist on the first word:

Testing the allowed tools shows what's permitted and reveals the execution environment:
pingruns but can't create its raw socket ("Operation not permitted... missing cap_net_raw+p capability"), so the container is unprivileged.tracerouteruns fine with normal arguments.

So the allowlist validates the first token (ping, traceroute, …) and then runs the tool. The question is how the rest of our input reaches the system.
Step 2: Rule Out Semicolon Injection
The classic first attempt is a shell metacharacter, chain a second command with ;:
ping -V; id
traceroute -m 1 127.0.0.1; id
Both fail to inject. With traceroute, the error is the tell:
Cannot handle "packetlen" cmdline arg 'id' on position 2 (argc 4)

id was handed to traceroute as a literal argument, not run as a separate command. That means the app isn't passing our string to a shell like sh -c "..."; it's splitting the input into an argument list (or stripping ;). Semicolons are a dead end here, and importantly, this rules out the simplest injection model and tells us to look at how the input is split.
Step 3: The Bypass — Newline Separator
If semicolons are filtered/neutralised but the app still joins or processes multi-line input, a newline may act as the separator the filter forgot about. Entering two separate lines in the textarea:
traceroute -m 1 127.0.0.1
id
This works:

127.0.0.1 : Name or service not known
...
uid=1000(appuser) gid=1000(appuser) groups=1000(appuser)
The second line executed as its own command. The flaw: the validator only checks the first line (it sees traceroute, an allowed tool, and approves), but the backend passes the entire multi-line payload to a shell, where the newline is a perfectly valid command terminator. Semicolons were handled; newlines were not. Incomplete blocklist, classic.
We now have arbitrary command execution as appuser.
Step 4: Read the Flag
With the newline primitive, put an allowed command on line 1 to pass validation and the real payload on line 2. Reading the flag file:
traceroute -m 1 127.0.0.1
cat flag.txt
The flag prints below the traceroute error:

KPMG_CTF{uw_cezWpRozUg0wvT1Ah0eM5kDffaoewXOLrUAGZ8Ij9}
Summary
| Step | Input | Result |
|---|---|---|
| 1 | ping / traceroute | First-word allowlist; unprivileged container |
| 2 | traceroute ... ; id | ; not injected (id parsed as arg) |
| 3 | traceroute ... ⏎ id | Newline injects → uid=appuser |
| 4 | traceroute ... ⏎ cat flag.txt | Flag |
Key Takeaways
- Validate exactly what you execute. The filter approved the input by looking at the first line/token, but the backend ran the whole multi-line string in a shell. Any gap between what is checked and what is run is an injection.
- Blocklists miss separators. Handling
;but not\n(newline) is a textbook incomplete filter. Shells treat newlines,;,&&,||,|,$()and backticks all as command boundaries, block one and attackers use the next. - Let error messages classify the bug. The
traceroute ... Cannot handle ... arg 'id'error proved the input was being split into arguments, not run viash -c. That single observation redirected the whole approach from semicolons to newlines. - The right fix is no shell at all. Pass arguments as an exec array (
execve-style), never concatenate user input into a shell command line, and allowlist on the parsed argv, not a substring of the raw request. - This is the foothold. We're
appuser, not root. The challenge's "escalate to root" goal is the next stage (Healthy Hijack 2), enumeratingsudo -land SUID binaries from this RCE.
Flag: KPMG_CTF{uw_cezWpRozUg0wvT1Ah0eM5kDffaoewXOLrUAGZ8Ij9}