Post

Next.js Middleware Auth Bypass and Server Side JavaScript Injection (ASEAN OPEN CTF 2025 - ThankYou Next)

Write-up for ThankYou Next web challenge from ASEAN OPEN CTF 2025.

Next.js Middleware Auth Bypass and Server Side JavaScript Injection (ASEAN OPEN CTF 2025 - ThankYou Next)

The Challenge

The gist is that the intern made a project timeline management system, surely it’s bug free :)

Source code was not given for this challenge.

Next.js Middleware Authorization Bypass (CVE-2025-29927)

First thing when we visit the challenge site is the register/login page. Now the register feature doesn’t really do anything (it’s supposed to be unavailable atm), and if we take a closer look, it’s not actually making a request to the server too.

The login feature does, and we need a set of working credentials.

Quick search through the JS files, we see only 2 API endpoints – /api/login , and /api/timeline which needs a valid token.

Now this part of the code is checking for our token in localStorage, and so my first instinct was to set a value here and see what the admin’s panel looks like.

1
2
3
4
5
6
7
8
...[SNIP]...
case 8:
    (a = e.sent).isAdmin && a.token ? (w(!0),
    j("Welcome Admin!"),
    H(a.token),
    window.localStorage.setItem("token", a.token)) : j("Access denied. Admin privileges required."),
    e.next = 13;
    break;

Nothing much, but this feature does make the request to /api/timeline, but we still need a valid token for the server to respond.

Now, knowing that the site is running on Nextjs, we can check for the famous middleware auth bypass. The challenge is running on 12.2.0 which is vulnerable to CVE-2025-29927, and for this version we can set x-middleware-subrequest: src/middleware to get a valid token.

Server Side JavaScript Injection

Going back to /api/timeline with a valid token, we see… that we don’t get anything 🗿 I was able to trigger some errors to give me some hints on what technology was running on the server, but I sort of tunnel visioned here (more on that in a bit).

If we insert a ' in timezone, it triggers an error, and judging by the message, it’s a JavaScript error.

Odd, because I normally wouldn’t expect to see JavaScript errors for a database error, moreso when testing for SQL injection.

Now this is where I went down a rabbit hole, and in hindsight I should’ve did more enumeration to ACTUALLY CONFIRM what kind of database was running. At this point suspecting that timezone was executing JavaScript, I could escape the query properly with ')// which further confirms my assumptions.

  1. If you try SQL operators it throws an error
  2. You can throw JavaScript syntax errors
  3. // comments out the remaining query, no SQL database uses this.
  4. The server was doing something funky when you insert multiple {} objects. I was thinking something like ...rest

To my knowledge, the only database that would execute user supplied JavaScript is MongoDB with its $where operator, so I was going with the assumption that this was a MongoDB application.

I knew I had JavaScript execution, but I couldn’t get a working query to return output. I also tried using fetch() for sanity checks, but I’m assuming it either breaks the original query or CORS is blocking. My idea was that since “flag” was being blocked, it was in this and I could leak it using this["fla"+"g"] character by character using boolean expressions/errors/sleep.

Fast forward to after the CTF ended, I tried playing with the other parameters, and got an SQL error 💀

Ok now I know it’s SQL, and there’s no SQL injection. Finally, I got working output by sending {"startDate":{}, "endDate":{"timezone":""},"timezone":"UTC"}. I think this had something to do with their values ending up being nulled, but in hindsight if I had set the dates to something in a very large range I would’ve gotten the output…

We can now test our server side JavaScript injection, and voila it works. The code is being applied to the date values too.

Then, we can just use code execution to find the flag on the server. Note that the challenge does block “flag” anywhere in your request, simple bypass did the trick.

Payload:

1
) + btoa(process.mainModule.require('child_process').execSync('cat /fla*.txt').toString()) //

The intended solution was mysql2 code injection in timezone parameter.

Flag: flag{9b138d6f547df777dc0a15589bc6aae6}

This post is licensed under CC BY 4.0 by the author.