2024. 2. 3. 19:24ㆍ정보보안/CTFLOG
dice와 block이 정확히 9번째 움직임에서 만나면 되는 문제.
wasd로 움직일수 있다.
dice가 아래 방향으로 9번 움직일때 block도 왼쪽으로 9번 오면 flag가 출력된다
하지만 dice를 움직일 때 block도 랜덤하게 움직이게 되는데, 이를 해결하기위해서
적용된 js code를 조금 수정해주면 된다.
아래는 원본 코드
var icons = [
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgBAMAAACBVGfHAAAAD1BMVEUAAADDGBgwMDD%2F%2F%2F%2FNMzNpjlSGAAAAAXRSTlMAQObYZgAAAJJJREFUKM%2Bd0dEJgyEQA2DpBncuoKELyI3Q%2FXdqzO%2FVVv6nBgTzEXyx%2FBsw7as%2FYD5t924M8QPqMWzJHNSwMGANXmzBDVyAJ2EG3Zsg3%2BxG0JOQwRbMO6NdAuOwA3odB8Qd4IIaBPkBdcMQRIIlDIG2ZhyOXCjg8XIADvB2AC5AX6CBBAIHIYUrAL8%2Fl32PWrnNGwkeH2HFm0NCAAAAAElFTkSuQmCC",
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgAgMAAAAOFJJnAAAADFBMVEUAAADDGBgwMDD%2F%2F%2F8aApG0AAAAAXRSTlMAQObYZgAAAJZJREFUGNNtzzEOgzAMBdAoI0fpfX6QvLAW9o4ILkFnllTk3yfqSdq1tZMipAovfrIs2d%2BdFtfaG3IruCCUmY8AOCuANhsadM%2F3Y9WVT5flruAICFbnmYDeAC6IuOquMCwFGIgKpPaHftox7rgVTBCMBfkfneJlsJt6q4mGMDufDKIf0jBYmqjYahwJs8FTxNWE5BH%2BqC8RZ01veWxOMgAAAABJRU5ErkJggg%3D%3D",
];
var imageIdx = 0;
function run() {
for (let item of document.getElementsByClassName("dice")) {
item.style.backgroundImage = "url('" + icons[imageIdx++ % 2] + "')";
}
}
setInterval(run, 200);
let X = 11;
let Y = 20;
let player = [0, 1];
let goose = [9, 9];
let walls = [];
for (let i = 0; i < 9; i++) {
walls.push([i, 2]);
}
let history = [];
history.push([player, goose]);
const log = (msg) => {
console.log(msg);
fetch("/log?log=" + encodeURIComponent(msg));
};
const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
window.onerror = (e) => log(e);
for (let i = 0; i < X; i++) {
var row = document.createElement("div");
row.className = "row";
for (let i = 0; i < Y; i++) {
var elem = document.createElement("div");
elem.className = "grid";
row.appendChild(elem);
}
game.appendChild(row);
}
function redraw() {
for (let item of document.getElementsByClassName("grid")) {
item.className = "grid";
item.style.backgroundImage = "";
}
game.children[player[0]].children[player[1]].className = "grid dice";
game.children[goose[0]].children[goose[1]].className = "grid goose";
for (let item of document.getElementsByClassName("dice")) {
item.style.backgroundImage = "url('" + icons[imageIdx++ % 2] + "')";
}
for (const wall of walls) {
game.children[wall[0]].children[wall[1]].className = "grid wall";
}
}
function isValid(pos) {
if (pos[0] < 0 || pos[0] >= X) return false;
if (pos[1] < 0 || pos[1] >= Y) return false;
for (const wall of walls) {
if (pos[0] === wall[0] && pos[1] === wall[1]) return false;
}
return true;
}
redraw();
let won = false;
document.onkeypress = (e) => {
if (won) return;
let nxt = [player[0], player[1]];
switch (e.key) {
case "w":
nxt[0]--;
break;
case "a":
nxt[1]--;
break;
case "s":
nxt[0]++;
break;
case "d":
nxt[1]++;
break;
}
if (!isValid(nxt)) return;
player = nxt;
if (player[0] === goose[0] && player[1] === goose[1]) {
win(history);
won = true;
return;
}
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();
};
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;
}
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) + "}");
}
const replayCode = new URL(location.href).searchParams.get("code");
if (replayCode !== null) replay();
let validated = false;
async function replay() {
if (!(await validate())) {
log("Failed to validate");
return;
}
won = true;
const replay = atob(replayCode.split(";")[0]);
const name = replayCode.split(";")[1];
title.innerText =
"DDG: The Replay (" + name + ") " + (validated ? "" : "[unvalidated]");
let idx = 0;
setInterval(() => {
player = [replay.charCodeAt(idx), replay.charCodeAt(idx + 1)];
goose = [replay.charCodeAt(idx + 2), replay.charCodeAt(idx + 3)];
redraw();
idx += 4;
if (idx >= replay.length) idx = 0;
}, 500);
scoreboard.innerHTML = "<b>" + winner.name + "</b>: " + winner.score;
}
let winner = {
score: 4,
name: "warley the winner winner chicken dinner",
};
async function validate() {
if (typeof Mojo === "undefined") {
return true;
}
try {
log("starting " + Math.random());
const ptr = new blink.mojom.OtterVMPtr();
Mojo.bindInterface(
blink.mojom.OtterVM.name,
mojo.makeRequest(ptr).handle,
);
await sleep(100);
const replay = atob(replayCode.split(";")[0]);
const name = replayCode.split(";")[1];
let data = new Uint8Array(code.length);
for (let i = 0; i < code.length; i++) data[i] = code.charCodeAt(i);
await ptr.init(data, entrypoint);
data = new Uint8Array(1_024 * 11);
data[0] = 1;
idx = 8;
data[idx++] = 0xff;
data[idx++] = 0;
data[idx++] = 0;
data[idx++] = 0;
idx += 4;
idx += 32;
idx += 32;
idx += 8;
data_idx = idx;
LEN = 8 + 4 + winner.name.length;
data[idx++] = LEN;
idx += 7;
data[idx] = winner.score;
idx += 8;
data[idx] = winner.name.length;
idx += 4;
for (let i = 0; i < winner.name.length; i++) {
data[idx + i] = winner.name.charCodeAt(i);
}
idx += winner.name.length;
idx += 1024 * 10;
idx += (8 - (idx % 8)) % 8;
idx += 8;
LEN = 4 + name.length + 4 + replay.length;
data[idx++] = LEN;
idx += 7;
data[idx++] = name.length;
idx += 3;
for (let i = 0; i < name.length; i++) {
data[idx + i] = name.charCodeAt(i);
}
idx += name.length;
data[idx++] = replay.length;
idx += 3;
for (let i = 0; i < replay.length; i++) {
data[idx + i] = replay.charCodeAt(i);
}
idx += replay.length;
idx += 32; // pubkey
data[idx] = 0;
var resp = (await ptr.run(data)).resp;
if (resp.length === 0) {
return false;
}
data_idx += 8;
let num = 0;
let cnter = 1;
for (let i = data_idx; i < data_idx + 8; i++) {
num += cnter * resp[i];
cnter *= 0x100;
}
data_idx += 8;
winner.score = num;
num = 0;
cnter = 1;
for (let i = data_idx; i < data_idx + 4; i++) {
num += cnter * resp[i];
cnter *= 0x100;
}
data_idx += 4;
len = num;
let winnerName = "";
for (let i = data_idx; i < data_idx + len; i++) {
winnerName += String.fromCharCode(resp[i]);
}
winner.name = winnerName;
return true;
} catch (e) {
log("error");
log(": " + e.stack);
}
}
이 코드를 console창에 복붙하면 패치가 되고 s를 9번 누르면 flag가 출력된다.
document.onkeypress = (e) => {
if (won) return;
nxt = [player[0], player[1]];
switch (e.key) {
case "w":
nxt[0]--;
break;
case "a":
nxt[1]--;
break;
case "s":
nxt[0]++;
break;
case "d":
nxt[1]++;
break;
}
if (!isValid(nxt)) return;
player = nxt;
if (player[0] === goose[0] && player[1] === goose[1]) {
win(history);
won = true;
return;
}
nxt = [goose[0], goose[1]];
switch (e.key) {
case "w":
nxt[0]--;
break;
case "a":
nxt[1]++;
break;
case "s":
nxt[1]--;
break;
case "d":
nxt[1]++;
break;
}
while (!isValid(nxt));
goose = nxt;
history.push([player, goose]);
redraw();
};
flag: dice{pr0_duck_gam3r_AAEJCQEBCQgCAQkHAwEJBgQBCQUFAQkEBgEJAwcBCQIIAQkB
일반적인 sql injection인줄 알았으나, prototype pollution까지 이용해야 풀리는 문제였다.
주어지는 파일에서 확인할 바이너리는 한개 뿐이다.
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}`));
user 라는 상수형 변수에 10만 길이의 배열을 생성한다.
그리고 생성되는 테이블에는 user, password 뿐 아니라 랜덤한 값의 id를 저장한다.
그리고, 페이로드를 전송하게 되면 서버 측에서 넘겨받는 값은 user와 pass라는 파라미터 뿐이다.
테이블에는 id,user,password 3개의 컬럼이 존재하지만 query라는 변수를 봤을땐 id가 들어갈 부분이 존재하지 않는다.
즉, id라는 파라미터를 넘겨받지 못하여 에러가 발생한다.
해당 조건을 통과했다면, 다음 조건문을 통과해야한다.
user[id]와 isAdmin[user] 모두 참이어야 flag가 출력된다.
이 부분은 pp로 해결하여야 한다.
잠시 위쪽 코드를 확인해보면 다음과 같은 부분을 확인할 수 있다.
따라서 다음과 같은 내용을 전달하면 id는 참이되고 pass에는 sqli로 문제가 해결된다.
dice{i_l0ve_java5cript!}
'정보보안 > CTFLOG' 카테고리의 다른 글
[CTF] DEF CON CTF 2024 Qualifiers - Gilroy (0) | 2024.05.09 |
---|---|
[CTF] insomni'hack Teaser 2024 (0) | 2024.01.21 |
[CTF] uoftctf-2024 (0) | 2024.01.17 |
[CTF] Wacon 2023 ( junior division ) (0) | 2023.09.05 |
[CTF] SSTF 2023 (0) | 2023.08.20 |