Gynvael Web Challenge #6

1 minute read

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 :)

Updated: