Next.jsSSR权限错位链分析"

admin 2026-10-18 17:41:51 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

cover_image

Next.js SSR 权限错位链分析

原创

YMsora
YMsora

YMs0ra的安全漫路

2026年7月4日 18:47
安徽

在小说阅读器读本章

去阅读

Next.js SSR 权限错位链分析 Cookie 覆盖 + Query 解析差异 + Data Route 绕过

对手搓很友好的一集 取材sekaictf 2026

首先看flag位置,可以看出被分为两半了

import fsfrom'node:fs/promises'exportasyncfunctionreadFirstFlagHalf() {   const flag = (await fs.readFile('/flag.txt','utf8')).trim()   return flag.slice(0,Math.ceil(flag.length /2)) }

可以追溯其调用的函数

exportasyncfunctiongetServerSideProps({ req, res }) {   const ticketUser =awaitfindByTicketUuid(req.cookies.ticket_uuid ||'')   if (ticketUser?.id !==1) {     res.statusCode =403    return {       props: {         backstageNote:'',         denied:true,       },     }   }   return {     props: {       backstageNote:awaitreadFirstFlagHalf(),       denied:false,     },   } }

需要ticketUser为1,可以看看原生用户miku

await db.run(   `     INSERT INTO users (id, username, password_hash, tier, ticket_uuid)     VALUES (?, ?, ?, ?, ?)     ON CONFLICT(id) DO UPDATE SET       password_hash = excluded.password_hash,       ticket_uuid = excluded.ticket_uuid   `,   [1,'miku',await bcrypt.hash(crypto.randomUUID(),12),'VIP', crypto.randomUUID()] )

密码的hash是强加密,没法破解,怎么拿到ticket uuid是个问题

在全局查找ticket uudi的时候看到了row函数

functionrowToUser(row) {   if (!row)returnnull  return {     id: row.id,     username: row.username,     passwordHash: row.password_hash,     tier: row.tier,     ticketUuid: row.ticket_uuid,   } }

查看其调用位置

其中findusername是在login时鉴权采用,而findById在

exportasyncfunctiongetServerSideProps({ query }) {   const user =awaitfindById(query.id)   if (!user)return {notFound:true }   const qrDataUrl =awaitQRCode.toDataURL(user.ticketUuid, {     errorCorrectionLevel:'M',     margin:1,     width:220,     color: {       dark:'#06111a',       light:'#ffffffff',     },   })

的时候只需要传入query.id就可以row返回所有的信息。这在access-card.js中

因为是nextjs的框架,看看对于这个板块中间件的处理

exportasyncfunctionmiddleware(request) {   const session =awaitverifyJwt(request.cookies.get('session')?.value)   if (!session?.sub) {     returndeny(request)   }   if (request.nextUrl.pathname ==='/access-card') {     const checkedId = request.nextUrl.searchParams.get('id')          if (checkedId !== session.sub) {       returndeny(request)     }   }

当然,这里的session最先一定是自身的,所以可以先建立一个用户

接下来就是过中间件。request.nextUrl.searchParams.get('id')需要和session字段相等

但是同时query.id需要等于1

这里可以清晰看见这里是存在解析差异的

query.id是直接取,但是searchParams.get('id')是nextjs库中的一个方法

Nextjs的源码有这个常量

export const NEXT\_QUERY\_PARAM\_PREFIX = 'nxtP'

以及这个函数

exportfunctionnormalizeNextQueryParam(key: string):null | string {   const prefixes = [NEXT_QUERY_PARAM_PREFIX,NEXT_INTERCEPTION_MARKER_PREFIX]   for (const prefixof prefixes) {     if (key !== prefix && key.startsWith(prefix)) {       return key.substring(prefix.length)     }   }   returnnull}

可以看到会对前缀进行去除

这样先传nxtPid,再传id就可以返回id=1的ticket uuid

再看backroom的鉴权

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

以及

exportasyncfunctiongetServerSideProps({ req, res }) {   const ticketUser =awaitfindByTicketUuid(req.cookies.ticket_uuid ||'')   if (ticketUser?.id !==1) {     res.statusCode =403    return {       props: {         backstageNote:'',         denied:true,       },     }   }   return {     props: {       backstageNote:awaitreadFirstFlagHalf(),       denied:false,     },   } }

条件有几个

req.cookies.ticket\_uuid为miku的ticket

session.ticketUuid和request.cookies.get('ticket\_uuid')需要相等(中间件鉴权)

session是没法改得,所以request.cookies.get('ticket\_uuid')需要取到和我们用户一样得ticket

但是req.cookies.ticket\_uuid为miku的ticket

可以看到这两者用的也是不一样的方法

request.cookie.get()逻辑如下

exportclassRequestCookies {   _parsed:Map =newMap()   constructor(requestHeaders: Headers) {     const header = requestHeaders.get('cookie')     if (header) {       const parsed =parseCookie(header)       for (const [name, value]of parsed) {         this._parsed.set(name, { name, value })       }     }   }   get(name) {     returnthis._parsed.get(name)   } }

可以看到这键值对会进行覆盖。

反复的this.\_parsed.set(name, { name, value })

会对上一对相同name的进行覆盖

而 req.cookies.ticket\_uuid

const obj = {}const key = ...// only assign onceif (obj[key] ===undefined) {   obj[key] = value }

源码注释也有 // only assign once

所以并不会被覆盖

所以就可以在对于覆盖的cookie进行操作,如下

Cookie: ticket\_uuid=MIKU\_TICKET; session=YOUR\_SESSION; ticket\_uuid=YOUR\_TICKET

也就可以拿到第一部分的flag

再者看第二部分

import fsfrom'node:fs/promises'asyncfunctionreadSecondFlagHalf() {   const flag = (await fs.readFile('/flag.txt','utf8')).trim()   return flag.slice(Math.ceil(flag.length /2)) }exportasyncfunctiongetServerSideProps() {   return {     props: {       backstageNote:awaitreadSecondFlagHalf(),     },   } }exportdefaultfunctionCdnHome({ backstageNote }) {   return (     <mainclassName=\"accepted-background\"aria-label=\"Accepted\">      {backstageNote}    </main>  ) }

貌似访问首页就可以了,看中间件

import {NextResponse }from'next/server'exportfunctionmiddleware(request) {   const remoteAddress =     request.headers.get('x-real-migu') ||''  if (remoteAddress !=='1.3.3.7') {     const url =newURL('/rejected', request.url)     returnNextResponse.redirect(url,302)   }   returnNextResponse.next() }exportconst config = {   matcher: ['/'], }

也非常短,限定了remoteAddress

刚想着直接改,但是不行,nginx发力了,看看.conf

server {   listen 8081;   server_name _;   proxy_http_version 1.1;   proxy_set_header Host $http_host;   proxy_set_header X-Forwarded-Host $http_host;   proxy_set_header X-Real-Migu $remote_addr;   proxy_set_header X-Forwarded-Proto $scheme;   location /assets/ {     alias /app/public/;   }   location / {     proxy_pass http://127.0.0.1:3002;   } }

可以注意到对于请求头都经过了二次规范,改是没用了

看看中间件

exportconst config = {   matcher: ['/'], }

这里只对/进行了防护,这里会想到nextjs的特性

在服务build之后,对于函数的请求资源都会去额外构建一个分区, data route

也就是说,可以去哪里拿到json,也就绕过了/的封锁

接口在/\_next/data//index.json

访问未受保护的页面拿到,然后去index.json拿flag

并且因为assetPrefix: 'cdn'

所以

/ -> /cdn/\_next/data//index.json

over


Next.jsSSR权限错位链分析" 网络安全文章

Next.jsSSR权限错位链分析"

本文分析Next.jsSSR中的权限错位漏洞链,涉及Cookie覆盖、Query解析差异和DataRoute绕过。通过利用nxtP前缀处理差异获取miku的ticket\\_uuid,并利用RequestCookies的覆盖特性与req.cookies的只赋值一次特性绕过中间件鉴权,最终通过DataRoute绕过根路径限制获取完整flag。建议开发者统一解析逻辑,避免依赖特定实现细节。
2026年5月十大网络安全事件" 网络安全文章

2026年5月十大网络安全事件"

2026年5月全球网络安全事件呈现高频复杂特征,十大事件涵盖嘉年华邮轮等数据泄露、兰州高校服务器被入侵、富士康遭攻击等。攻击入口以身份凭证和社会工程学为主,ShinyHunters等组织形成可复制攻击链条,数据勒索成为核心目标。企业需加强身份管理、数据分类分级和供应链安全,同时关注国内治理中的组织责任与法定义务。
网安利器|ServerScan:一款高并发内网资产扫描工具,渗透测试信息收集效率提升利器" 网络安全文章

网安利器|ServerScan:一款高并发内网资产扫描工具,渗透测试信息收集效率提升利器"

ServerScan是一款基于Go语言开发的开源内网资产扫描工具,结合了高速端口扫描、服务识别与Banner获取能力,旨在提升渗透测试信息收集效率。它支持多平台运行,通过高并发协程实现快速扫描,可识别HTTP、SSH、MySQL等多种服务,并获取Web标题与状态码。工具提供轻巧版、专业版及CobaltStrike版,适用于内网资产梳理。使用需注意合法授权,避免对目标网络造成压力。
评论:0   参与:  0