深入Vite任意文件读取与分析复现

admin 2025年4月8日00:07:11评论2 views字数 3959阅读13分11秒阅读模式

深入Vite任意文件读取与分析复现

作者:Ting

https://xz.aliyun.com/news/17562

文章转载自 先知社区

环境搭建&漏洞复现

首先创建Vite项目

npm create [email protected] vitetest -- --template vue  

这次复现用的就是这个版本

深入Vite任意文件读取与分析复现

然后 进入项目

cd vitetest

安装相应环境

npm install

运行 即搭建成功

npm run dev
深入Vite任意文件读取与分析复现

测试漏洞 两种payload都可

http://localhost:5173/C:/Windows/win.ini?import&raw??
深入Vite任意文件读取与分析复现
http://localhost:5173/@fs/C:/Windows/win.ini?import&raw??
深入Vite任意文件读取与分析复现

这两种都可以 因为处理的url的时候会自动删除 @fs所以这两个都可以

漏洞分析

在分析之前会讲解几个前置知识 以便大家更好的理解这个漏洞 并尝试站在发现者的角度来挖掘这个漏洞

我们要分析这个漏洞,首先要了解一下Vite的执行过程,要了解Vite执行过程就要找到Vite的入口,而cli.js就是Vite的入口,再具体一点createServer就是他的入口

深入Vite任意文件读取与分析复现

然后来分析一下createServer 在这个JS里面 

深入Vite任意文件读取与分析复现

在这里定义 他调用了_createServer

深入Vite任意文件读取与分析复现

刚好在他的下面 这个函数很长 主要的功能就是 初始化服务器配置、注册中间件(Middleware)、处理热更新(HMR)、管理模块依赖图(Module Graph)等

深入Vite任意文件读取与分析复现

我们主要看的是注册中间件 这里注册了一个中间件集合

深入Vite任意文件读取与分析复现

而这个集合是有顺序的 如下我们需要记住这个顺序

servePublicMiddleware->transformMiddleware->serveRawFsMiddleware->serveStaticMiddleware
深入Vite任意文件读取与分析复现

那么就先去servePublicMiddleware中间件看 我添加了这几个代码

深入Vite任意文件读取与分析复现

发现无论有没有cleanUrl 最终执行next的时候都是不变的 /C:/Windows/win.ini?import&raw?? 而这个next()其实就是去下一个中间件 所以刚刚记的顺序就有用了

深入Vite任意文件读取与分析复现

再执行以下错误的payload 发现也是去到了下一个中间件 说明servePublicMiddleware对于这个漏洞作用并不大

深入Vite任意文件读取与分析复现

那么现在去看transformMiddleware 确实进入了这里

深入Vite任意文件读取与分析复现我们来看这段代码

深入Vite任意文件读取与分析复现

removeTimestampQuery顾名思义,从请求的 URL 中移除时间戳查询参数

深入Vite任意文件读取与分析复现

第一个replace即匹配形如t=1234567890123的字符串,后面可能跟着一个可选的&符号,也可能不跟

深入Vite任意文件读取与分析复现

第二个replace即匹配结尾部分的&、? 有的则替换为空 

深入Vite任意文件读取与分析复现

所以如果是http://example.com/path?t=1234567890123处理之后就只有http://example.com/path了,

如果是/C:/Windows/win.ini?import&raw??处理之后也就少了一个?变成了/C:/Windows/win.ini?import&raw?深入Vite任意文件读取与分析复现

然后这部分就不用看了,他如果结尾是.map的话就无法造成任意文件读取了

深入Vite任意文件读取与分析复现

继续往下到上面那个try结束的位置 61816行 如果以public开头也就会进入warnAboutExplicitPublicPathInUrl

深入Vite任意文件读取与分析复现

linux可能会有public文件夹,那么我们尝试一下进入这个函数

深入Vite任意文件读取与分析复现

第一个判断 相关代码如下 也就是判断url里面是否有import有的话就会将“import=value&”清除掉 同时将url末尾的一个?、&也清理了

const isImportRequest = (url) => importQueryRE.test(url);
const importQueryRE = /(?|&)import=?(?:&|$)/;
function removeImportQuery(url) {
  return url.replace(importQueryRE, "$1").replace(trailingSeparatorRE, "");
}
//前面讲过
const trailingSeparatorRE = /[?&]$/;
深入Vite任意文件读取与分析复现

其他地方也没任何对url的实质性操作,也没用return赋值, 只是打印警告而已 ,也就不用看了所以warnAboutExplicitPublicPathInUrl也没啥作用深入Vite任意文件读取与分析复现

继续来看这部分

深入Vite任意文件读取与分析复现

如果用错误的payload 也就直接return了 因为他没用raw也没用url

深入Vite任意文件读取与分析复现

const urlRE = /(?|&)url(?:&|$)/;
const rawRE = /(?|&)raw(?:&|$)/;

function ensureServingAccess(url, server, res, next) {
  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://vitejs.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 {

    next();
  }
  return false;
}

function isFileServingAllowed(url, server) {
  if (!server.config.server.fs.strict) return true;
  const file = fsPathFromUrl(url);
  if (server._fsDenyGlob(file)) return false;
  if (server.moduleGraph.safeModulesPath.has(file)) return true;
  if (server.config.server.fs.allow.some(
    (uri) => isSameFileUri(uri, file) || isParentDirectory(uri, file)
  ))
    return true;
  return false;
}

function isFileReadable(filename) {
  if (!tryStatSync(filename)) {
    return false;
  }
  try {
    fs__default.accessSync(filename, fs__default.constants.R_OK);
    return true;
  } catch {
    return false;
  }
}

前面我们有raw但是raw后面有?,不是直接接的&或者直接raw结束什么也没有导致(rawRE.test(url) || urlRE.test(url))为false,就压根不需要官ensureServingAccess了不会执行了。而只有后面payload有两个?或者更多?的时候才能满足括号为false,因为如果为没有?或者有一个?,这都会导致(rawRE.test(url) || urlRE.test(url))为true,就会执行ensureServingAccess。

深入Vite任意文件读取与分析复现

继续看  一个满足isJSRequest(url) 一个满足isImportRequest(url) 所以linux的没后缀就行这一步 windows的必须有import的

深入Vite任意文件读取与分析复现

isJSRequest就是正常的请求或者有合法后缀的例如vue js 要么就是没有后缀且末尾没用/这个条件很容易满足isImportRequest前面也讲过了有import就可以了

const knownJsSrcRE = /.(?:[jt]sx?|m[jt]s|vue|marko|svelte|astro|imba|mdx)(?:$|?)/;


const isJSRequest = (url) => {
  url = cleanUrl(url);
  if (knownJsSrcRE.test(url)) {
    return true;
  }
  if (!path$n.extname(url) && url[url.length - 1] !== "/") {
    return true;
  }
  return false;
};

继续跟进 unwrapId也是没用的因为不是以那个开头的

深入Vite任意文件读取与分析复现
深入Vite任意文件读取与分析复现

继续跟进到了这里

深入Vite任意文件读取与分析复现

跟进去 直接看doTransform 因为前面的一看也没啥用

深入Vite任意文件读取与分析复现

继续跟loadAndTransform

深入Vite任意文件读取与分析复现

这里id其实就是C:/Windows/win.ini?raw

深入Vite任意文件读取与分析复现

然后调用pluginContainer.load(id, { ssr }) 

深入Vite任意文件读取与分析复现
深入Vite任意文件读取与分析复现

最终锁定是这个load

深入Vite任意文件读取与分析复现

这里限制了payload必须要有raw

深入Vite任意文件读取与分析复现

然后这里有进行一次clean 清洗之后就是C:/Windows/win.ini了

深入Vite任意文件读取与分析复现

原文始发于微信公众号(Ting的安全笔记):深入Vite任意文件读取与分析复现

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

发表评论

匿名网友 填写信息