初ブログです、自分用に整理のため。
お偉いさんが
「成長したいなら書きなさいっ!」
って言ってたんだもん。。。
DiceCTF 2024
一人で参加、主にwebに取り掛かりました。
もう少し解けるようになりたいな。。。
dicedicegoose [web 445 solves]
鬼ごっこ形式
wasdでさいころが上下する。黒マスに到達したら勝ちだが、黒マスはランダムに移動する。
最短回数(9回)で到達したらflagを獲得できる。
ソースコードにスクリプトの詳細あり
// 省略
do {
nxt = [goose[0], goose[1]];
switch (Math.floor(4 * Math.random())) {
case 0:
nxt[0]--;
break;
case 1:
nxt[1]--;
break;
case 2:
nxt[0]++;
break;
case 3:
nxt[1]++;
break;
}
} while (!isValid(nxt));
goose = nxt;
history.push([player, goose]);
redraw();
};
サイコロ(player)と黒枠(goose)を行動ごとにhistoryにpushしているようだ。
function win(history) {
const code = encode(history) + ";" + prompt("Name?");
const saveURL = location.origin + "?code=" + code;
displaywrapper.classList.remove("hidden");
const score = history.length;
display.children[1].innerHTML = "Your score was: <b>" + score + "</b>";
display.children[2].href =
"https://twitter.com/intent/tweet?text=" +
encodeURIComponent(
"Can you beat my score of " + score + " in Dice Dice Goose?",
) +
"&url=" +
encodeURIComponent(saveURL);
if (score === 9) log("flag: dice{pr0_duck_gam3r_" + encode(history) + "}");
}
最短回数、すなわち score=9 のときのみ行動履歴であるhistoryをencodeしたのちflagとして表示する。
思いついた考え方は2つ。
-
Math.floor(4 * Math.random()) を変えて、黒枠が左に移動するように固定して実行
開発ツールの使い方を把握しきれていない...
開発ツール上でスクリプト書き換えて、書き換えたものをもとに実行する方法ってあるんですかね(あるとは思うんですが ; ;) -
encode(history) をごり押し作成
最短ケースのhistoryは次の通りになるはず
history =[
[[0, 1], [9, 9]],
[[1, 1], [9, 8]],
[[2, 1], [9, 7]],
[[3, 1], [9, 6]],
[[4, 1], [9, 5]],
[[5, 1], [9, 4]],
[[6, 1], [9, 3]],
[[7, 1], [9, 2]],
[[8, 1], [9, 1]]
]
これを用いてencode(history)にあてはめたら、flagを獲得できる。
function encode(history) {
const data = new Uint8Array(history.length * 4);
let idx = 0;
for (const part of history) {
data[idx++] = part[0][0];
data[idx++] = part[0][1];
data[idx++] = part[1][0];
data[idx++] = part[1][1];
}
let prev = String.fromCharCode.apply(null, data);
let ret = btoa(prev);
return ret;
}
やり方はいろいろあるっぽいが、コンソールを用いて実行。
pythonで書き換えた場合は以下の通りで同様の結果。
import base64
history = []
for i in range(9):
history.append([[i, 1], [9, 9-i]])
def encode(history):
data = bytearray(len(history) * 4)
idx = 0
for part in history:
data[idx] = part[0][0]
data[idx + 1] = part[0][1]
data[idx + 2] = part[1][0]
data[idx + 3] = part[1][1]
idx += 4
ret = base64.b64encode(data).decode()
return ret
print(("flag: dice{pr0_duck_gam3r_" + encode(history) + "}"))
実行結果
flag: dice{pr0_duck_gam3r_AAEJCQEBCQgCAQkHAwEJBgQBCQUFAQkEBgEJAwcBCQIIAQkB}
funnylogin [web 269 solves]
ソースコードは次の通り。
const express = require('express');
const crypto = require('crypto');
const app = express();
const db = require('better-sqlite3')('db.sqlite3');
db.exec(`DROP TABLE IF EXISTS users;`);
db.exec(`CREATE TABLE users(
id INTEGER PRIMARY KEY,
username TEXT,
password TEXT
);`);
const FLAG = process.env.FLAG || "dice{test_flag}";
const PORT = process.env.PORT || 3000;
const users = [...Array(100_000)].map(() => ({ user: `user-${crypto.randomUUID()}`, pass: crypto.randomBytes(8).toString("hex") }));
db.exec(`INSERT INTO users (id, username, password) VALUES ${users.map((u,i) => `(${i}, '${u.user}', '${u.pass}')`).join(", ")}`);
const isAdmin = {};
const newAdmin = users[Math.floor(Math.random() * users.length)];
isAdmin[newAdmin.user] = true;
app.use(express.urlencoded({ extended: false }));
app.use(express.static("public"));
app.post("/api/login", (req, res) => {
const { user, pass } = req.body;
const query = `SELECT id FROM users WHERE username = '${user}' AND password = '${pass}';`;
try {
const id = db.prepare(query).get()?.id;
if (!id) {
return res.redirect("/?message=Incorrect username or password");
}
if (users[id] && isAdmin[user]) {
return res.redirect("/?flag=" + encodeURIComponent(FLAG));
}
return res.redirect("/?message=This system is currently only available to admins...");
}
catch {
return res.redirect("/?message=Nice try...");
}
});
app.listen(PORT, () => console.log(`web/funnylogin listening on port ${PORT}`));
順々なIDに加え、ランダムなユーザー&パスワードを100000個もつデータベースを作成する。また、1つのユーザーをadminとして登録する。
usernameとpasswordの2つが入力として必要であり、users[id] && isAdmin[user]
が満たさればflagが獲得できる。
user[id]はidに1~100000のどれかが入ればいいのでSQLインジェクションでなんとかなりそう。すなわちpasswordの方でSQLインジェクションを実施。
usernameの方でisAdmin[user]の存在さえ証明できればいいが...?
終了後にJavascriptではプロトタイプというものがあることを理解。
空のオブジェクトを定義したとしても、いくつかのメソッドが自動的に定義されるというものである。
つまり、const isAdmin = {};
の部分でtoString
メソッドを含めた複数のメソッドが自動生成される。生成されたメソッドを用いることで、trueの判定を得られる。
以上より、次のように入力を行うことでflagを獲得できる。
Username: toString
Password: 1' or id=1; --
実行結果
dice{i_l0ve_java5cript!}