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