Gynvael Web Challenge #6
We’re back again with another NodeJS web challenge from Gynvael. Let’s get into it!
Description
Just like the previous challenges, we are given the source code for the NodeJS application:
const http = require('http')
const express = require('express')
const fs = require('fs')
const path = require('path')
const PORT = 5006
const FLAG = process.env.FLAG || "???"
const SOURCE = fs.readFileSync(path.basename(__filename))
const app = express()
const checkSecret = (secret) => {
return
[
secret.split("").reverse().join(""),
"xor",
secret.split("").join("-")
].join('+')
}
app.get('/flag', (req, res) => {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain;charset=utf-8')
if (!req.query.secret1 || !req.query.secret2) {
res.end("You are not even trying.")
return
}
if (`<${checkSecret(req.query.secret1)}>` === req.query.secret2) {
res.end(FLAG)
return
}
res.end("Lul no.")
})
app.get('/', (req, res) => {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain;charset=utf-8')
res.write("Level 6\n\n")
res.end(SOURCE)
})
app.listen(PORT, () => {
console.log(`Example app listening at port ${PORT}`)
})
Clearly the endpoint of interest here is the /flag
endpoint so let’s see what happens when we interact with it.
Request
GET /flag?secret1=test&secret2=test HTTP/1.1
Host: challenges.gynvael.stream:5006
Response
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/plain;charset=utf-8
Date: Sun, 12 Jul 2020 19:53:23 GMT
Connection: close
Content-Length: 7
Lul no.
My first thought was that my input needed to be structed in such a way that when transformed by the checkSecret()
function, the 2 secrets would match. I started debugging a bit via print statements and got the following output: [+] DEBUG: Final return tset+xor+t-e-s-t
After looking closer at the source code, I realized this checkSecret()
function was a red herring and the solution was much more subtle and simple. Look closer at the way the checkSecret()
function is structured…
This is dead code underneath the return line!
[
secret.split("").reverse().join(""),
"xor",
secret.split("").join("-")
].join('+')
Which means the first check in this code block
if (`<${checkSecret(req.query.secret1)}>` === req.query.secret2) {
res.end(FLAG)
return
}
actually evaluates to the string literal: <undefined>
. This gives us our final payload to get the flag out: http://challenges.gynvael.stream:5006/flag?secret1=test&secret2=%3Cundefined%3E
Flag
CTF{***e*****O***eS*******}
This challenge was arguably easier than the previous ones, but I enjoyed the subtly here! Kept me scratching my head for longer than I’d like to admit :)