ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RuoYi 组合拳 RCE 的代码审计与防御加固

RuoYi 组合拳 RCE 的代码审计与防御加固 RuoYi 这套后台框架在国内中小型项目里的普及程度基本上做 Java 后端的都碰过。但正因为用得多、改得多围绕它的安全问题也一直没消停过。最近圈子里讨论比较多的一个词叫组合拳 RCE说的不是某一个孤立的漏洞编号而是一类把多个看似不致命的小毛病串起来、最终拿到命令执行权限的攻击路径。这篇文章我不打算写成漏洞公告而是站在代码审计和防御加固的视角把这类链条的成因、入口、串联逻辑和封堵手段拆开讲清楚——因为对绝大多数还在跑 RuoYi 系列项目的团队来说知道哪里容易被串起来比记住某个 CVE 编号有用得多。关键词里出现频率最高的是 RuoYi、RCE 这两个词其余大多是围绕框架本身的功能模块定时任务、代码生成、Sa-Token 鉴权、MQTT、进销存等延伸出来的搜索。这说明很多人其实已经意识到风险点就藏在框架自带的那些便利功能里只是不清楚它们是怎么被组合起来的。下面进入正题。1. 为什么组合拳这个词在 RuoYi 场景里特别值得警惕1.1 单点小瑕疵不等于安全串起来就可能致命很多同学对漏洞的理解停留在一个朴素模型上一个接口有没有做权限校验、一个参数有没有做过滤。这个模型没错但它有个致命前提——假设攻击者只会用单个漏洞打单个点。真实的攻击不是这样运作的。攻击者手里有一堆看起来危害有限的东西一个未授权的信息接口、一个能读取配置的调试端点、一个对方法名缺少白名单的调度模块、一个允许上传任意后缀的文件处理逻辑。单独拿任何一个出来评级可能都只是中危甚至低危因为它们单独用的时候要么拿不到关键数据要么入口够不着。组合拳的精髓就在于把这些低危片段拼成一条完整的链用信息泄露接口摸清框架版本和路由结构用鉴权绕过拿到后台入口再用调度或模板模块实现代码执行。每一步都不是新闻但连起来就是实打实的高危。这也是为什么很多团队明明打了补丁、升级了依赖还是会在溯源时发现被拿下了——他们修的是单点攻击者走的是链。RuoYi 这类快速开发框架天生容易形成这种局面因为它为了开箱即用内置了大量动态能力动态调用、动态生成代码、动态执行任务。动态能力越强攻击面就越大而这些能力往往是分散在不同模块里的安全团队很难一次性看全。1.2 RuoYi 生态的攻击面天然分散在哪些模块要把组合拳讲明白先得知道素材在哪。RuoYi 及其衍生版本前后端分离版、微服务版以及各种二次开发的办公、进销存、CRM 分支常见的风险入口大致有这么几类模块功能定位潜在风险性质定时任务调度动态创建/修改任务反射调用、脚本执行代码生成器按模板生成代码模板注入、路径穿越参数解析/表达式动态计算、规则匹配表达式注入鉴权与签名Sa-Token、API 签名白名单绕过、越权文件上传/导入附件、Excel 导入任意文件、反序列化系统监控缓存、服务状态信息泄露、调试入口这张表不是为了吓人而是说明一个事实任何一个模块单独看都能找到合理用途的解释但它们的权限边界如果没设计好就会互相借力。调度模块本来只该调用业务 Bean可它如果能调用到 JDK 里的任意类就变成了命令执行代码生成本来只该读写项目目录可它如果能穿出根路径就变成了文件落盘。这些都不是设计者原本想要的但实现上的宽松让它们成为了事实上的危险入口。我个人的判断是评估 RuoYi 类项目的风险不要只盯着版本号和已知 CVE要盯着哪些功能允许用户输入间接控制到方法名、类名、表达式或文件路径。这四个控制点是组合拳能不能打起来的关键。2. 定时任务调度模块RuoYi 绕不开的高危第一现场2.1 为什么调度模块天生就容易出问题RuoYi 自带的定时任务模块设计目标是让运维能在后台不重启应用的情况下动态添加任务。为了实现调用任意业务方法它的底层普遍使用 Java 反射机制后台把任务配置成目标字符串 调用目标字符串调度器解析这个字符串反射出对应的类和类方法去执行。这个设计思路本身在业界很常见Quartz 生态里也有类似做法。问题在于调用目标字符串如果没有任何白名单约束用户的输入就等价于让 JVM 去反射调用任意一个类的方法。而 Java 反射能调用到的东西远超业务 Bean——它可以触达 JDK 类库里的许多类这些类里存在能够间接执行命令、加载类、写文件的静态方法。攻击者要做的就是把配置字段填成能触发这些行为的组合。看到这里你应该明白了这是设计上允许动态 实现上缺少白名单共同造成的问题不是某一行代码写错了。这也是为什么单纯升级框架版本往往解决不了这类问题——如果你的二次开发还在沿用旧的调度实现或者团队自己又加了一个自定义脚本任务功能那攻击面是原生的、跟着代码走的。2.2 从代码审计视角看这类逻辑的通病如果你要审计自己项目里的调度模块我建议按这么几条线索去翻输入到反射之间的路径有没有过滤从 Controller 接收调用目标到最终Class.forName(...)或getMethod(...)之间是否只做了非空校验而没有做类名/方法名白名单白名单是黑名单还是白名单很多项目写的是禁止某些关键字这属于黑名单天然容易被绕过大小写混合、编码、字符串拼接。安全做法应该是只允许调用指定包下的、已注册的方法。有没有引入脚本引擎如果任务支持 Groovy、JavaScriptNashorn/GraalJS之类的动态脚本那等于把任意代码执行的口子直接开在了后台——这类功能的生产环境使用要极其谨慎。权限校验覆盖到哪一层新增任务和修改任务的接口是否只有超级管理员能访问有没有可能被越权用户或匿名用户触达这里分享一个我审计时常踩的观察点很多人以为定时任务必须登录后台才能改但实际上有些项目的任务接口虽然挂了注解却因为白名单配置把整个任务路径放行了还有些项目的鉴权是前端路由级的后端接口并未真正校验。这两点一结合调度模块就从前台后台之间的隔离墙变成了敞开的大门。注意判断一个动态任务功能是否危险核心不是看它能不能立即执行而是看用户输入能否间接决定调用哪个类、哪个方法。只要这个决定权在用户手里风险就已经存在了。2.3 关于立即执行与下次执行的常见误判一个很实际的细节有些任务配置保存后不会马上跑要等触发器时间到。很多运维因此觉得我只要盯着调度结果就安全了。这个判断不可靠。原因有两点一是 Quartz 支持startNow()之类的立即触发任务保存的同时就可能执行二是真正危险的不是任务本身跑没跑而是任务里配置的目标方法一旦被调用副作用可能在任何后续时间点发生。换句话说验收入口的安全不能依赖我还没看到执行结果。我在实际排查中遇到过更隐蔽的一种情况攻击者并不直接让任务执行命令而是让目标方法做一次看起来无副作用的操作比如加载一个远程资源、写入一个标记文件之后再通过别的入口接续利用。这让单看调度日志很难立刻察觉异常。所以对调度模块的监控不能只看任务是否成功还要看任务调用了哪些类。3. 鉴权与签名层的缝隙Sa-Token 配置疏忽带来的连锁反应3.1 接口鉴权白名单与权限校验的错位RuoYi 的权限体系普遍基于 Sa-Token 或 Spring Security。无论是哪一套鉴权逻辑的核心都是哪些路径免登录、哪些路径需要权限。白名单配置是重灾区。为了放行登录接口、验证码接口、静态资源、回调接口开发者往往会写一堆excludePathPatterns或SaIgnore。这些放行规则如果写得太宽——比如用了通配符把它覆盖到了本该受保护的接口上——就会出现看似给登录页放行实际把系统管理接口也放行了的情况。这类问题之所以成为组合拳的第一跳是因为它让攻击者不需要任何凭据就能摸到后台功能接口。一旦能匿名访问调度、上传、配置读取等接口后面的事情就顺理成章了。我见过不少项目的白名单是从网上直接抄来的抄的时候没验证过路径匹配规则/**一写整个应用就裸奔了。3.2 API 签名机制被形式化之后的隐患有些衍生版本引入了 API 签名本意是让外部调用方按签名规则访问防止参数被篡改。但如果签名校验只验有没有带签名参数或者签名密钥硬编码在可访问的配置里那这层防护就是形式主义。更麻烦的是签名机制一旦成为鉴权绕过的旁路——比如带签名的请求可以跳过常规权限校验——攻击者会优先盯上这条路径因为它天然是信任通道。审计签名逻辑时我通常会关注三点密钥从哪来是否硬编码、签名算法是否可被降级能否指定为none或弱算法、时间戳与随机数校验是否真正生效能不能重放。这三点任何一点缺失签名层就形同虚设。而一旦攻击者拿到了合法签名的通行能力原本因为鉴权挡住的后台接口就会全部暴露组合拳的第一环就接上了。4. 一条典型攻击链的防御视角推演前面讲了素材和缝隙现在把链条串起来看。需要强调的是下面描述的是从防御和溯源角度理解的逻辑顺序目的是帮助运维和安全同学识别自己在链条的哪一环被突破不是操作指引。4.1 侦察阶段暴露了什么信息链条的第一段通常是信息收集。攻击者会先确认目标是不是 RuoYi 系框架——这很容易因为默认响应头、错误页、静态资源路径、/prod-api之类的接口前缀都带有明显的框架特征。接着他会尝试访问一些不需要权限的接口比如获取验证码、系统配置、版本信息等用来判断框架版本和二次开发程度。这一段为什么难以完全杜绝因为框架指纹暴露在开源项目里几乎是必然的。我们能做的不是彻底隐藏收益有限而是减少匿名可达的信息接口数量让攻击者拿不到足以判断该用哪条链的细节。一个常见误区是开发者把注意力全放在堵住执行入口上却忽视了暴露的版本信息本身就是导航仪。4.2 从入口到执行的逻辑串联中段是链条真正咬合的地方。通常有两种走向一种是调度模块直连执行。攻击者通过一个鉴权有缝隙的任务接口提交一个经过精心构造的调用目标让框架反射调用到能执行命令的方法。这里的关键节点有两个入口是否可达、反射是否有约束。只要其中一个没守住链条就走通了。另一种是文件落点转执行。攻击者利用代码生成、模板渲染或文件导入功能把内容写到服务器上的某个目录再通过另一个入口触发它。这种组合更隐蔽因为写文件和执行文件是两个不同接口单看任何一个都不足以定级为严重。排查时如果只按接口维度各自为战很容易漏掉它们之间的关联。无论哪种走向你都会发现一个共同点真正让危害升级的往往不是执行这一步而是前面权限被绕过或输入被信任的几步。也就是说防守方如果把资源压在最后一步是防不住的必须往前压。4.3 为什么单点修补挡不住组合拳这一点我必须展开说因为它是最容易被忽视的。假设一个团队发现某接口存在未授权访问于是给这个接口加了鉴权补丁上线觉得万事大吉。但攻击者手里还有别的未授权接口或者换了一条完全不同的利用路径——比如从文件上传绕到模板执行。单点修补的思维是哪个洞漏了堵哪个而组合拳的思维是我不用你堵的那个洞。有效的做法是按能力而不是按接口来加固不是这个接口要鉴权而是所有能决定方法名、类名、表达式、文件路径的入口都必须受强约束。把能力收敛了攻击者换多少个接口都是白搭因为底层的能力阀门被关小了。这个思路的转变是防御组合拳的核心。5. 代码层加固把危险入口关进笼子5.1 调度模块必须做白名单收敛针对动态调度我推荐的加固方向是把调用目标从自由字符串改造成受控枚举或注册表。具体可以这样落地——维护一个允许被调度的 Bean 与方法注册表后台配置时只能从这个注册表里选而不是手填类名和方法名。这样即便接口被人触达攻击者也只能在你划定的小圈子里挑挑不出执行命令的方法。如果业务实在需要灵活性退一步的做法是对调用目标做严格的正则白名单限定包名前缀、限定方法名集合、禁止出现Runtime、ProcessBuilder、Class、getClass这类敏感符号并且对参数类型做严格校验。但请注意黑名单式拦截永远只是缓解不是根治能上白名单就上白名单。5.2 表达式与模板引擎的沙箱化代码生成、规则匹配、动态计算这些功能背后往往用到了 SpEL、OGNL、FreeMarker、Velocity 之类的表达式或模板引擎。这些组件的默认能力非常强能调用方法和访问对象属性。加固思路有三层输入层模板内容、表达式字符串如果来自用户必须经过严格校验理想情况是完全不允许用户自定义。引擎层使用引擎提供的沙箱或受限上下文关闭危险特性如禁止调用任意方法、禁止访问类加载器等限制可用变量集合。输出层对生成的文件路径做归一化校验确保最终落点始终在你允许的目录之内防止路径穿越。我特别想提醒的是模板引擎的路径问题。很多团队防住了模板内容的注入却忘了模板文件本身的路径也可能被控制导致读任意文件或写任意文件。这两个能力中写任意文件是可以直接接上 RCE 的所以目录约束这一环千万别跳过。5.3 上传与导入功能的边界控制文件上传和 Excel 导入是另一个高频入口。加固重点在于后缀白名单 内容类型校验 存储目录隔离 文件名随机化。后缀白名单要从允许列表出发而不是禁止列表存储目录要放在 Web 根目录之外并禁止该目录下文件的执行权限文件名要随机生成避免用户可控的路径拼接。导入功能还要额外注意解析器的安全性。某些解析组件在处理恶意构造的文件时可能触发反序列化或资源耗尽所以及时升级解析库版本、限制文件大小和解析深度都是必要的。6. 运行时可观测在被拿下之前发现异常6.1 关键日志与告警点代码加固是一方面运行时能不能发现异常是另一方面。我建议至少盯住这几类日志和事件定时任务的新增/修改记录谁在什么时候改了调用目标、鉴权失败的集中出现可能是在试探白名单、异常堆栈中出现的反射相关类名、以及文件写入目录的非预期变化。这些信号单独出现可能都是正常业务但短时间内集中出现就值得警惕。日志的价值在于它能把散落的单点关联起来。前面说了组合拳的危害在于串联那么防守的突破口也在于串联——通过统一的时间线把不同接口的异常访问拼起来看往往能提前发现链条的痕迹。6.2 进程与网络行为基线更进一步的是主机层面的可观测性。正常业务进程不应该频繁地创建子进程、不应该连接到陌生的外部地址、不应该在非预期目录下读写文件。建立这些基线之后任何偏离基线的行为都能第一时间触发告警。这一步的意义在于它不依赖你事先知道攻击者用哪条链。无论攻击走的是调度反射还是模板落盘最终要执行命令、要外联、要落地文件都会在主机行为上留下痕迹。行为基线是应对未知组合的兜底手段尤其适合那些二次开发较多、无法完全依赖框架自身安全性的项目。7. 排查与恢复怀疑中招后的处置顺序7.1 快速自检清单如果怀疑自己的 RuoYi 项目已经被打了组合拳我建议按这个顺序快速过一遍检查项关注点异常信号调度任务列表是否有非业务方创建的任务调用目标含敏感类名或方法名鉴权白名单是否存在过宽通配管理接口可匿名访问上传目录是否有脚本类文件非预期后缀、非预期时间进程树应用是否派生子进程出现 shell、解释器类进程网络连接是否有外联陌生 IP、异常端口账号与日志是否有新账号、清除日志权限变更、日志短缺这张表不是什么高深工具就是让你有一条固定的排查路径避免慌乱中到处乱翻。实际处置时顺序比工具重要得多。7.2 清除与回归验证确认问题后处置要果断隔离受影响主机、下线可疑任务、重置相关凭据、清除落地的可疑文件。但清理完不是结束还要做回归验证——在修复后的环境里重新尝试原先的入口确认它们已经不可达或已被约束。很多团队清理后就上线了结果攻击者一开始就是靠多个入口你只堵了发现的这一个剩下的还在过几天又被绕进去。我在实际处置中总结的一点经验一次性把所有决定方法名、类名、表达式、文件路径的入口都过一遍哪怕某些入口暂时没有发现被利用的痕迹。因为组合拳的特点就是你永远不知道攻击者顺手记下了几个备用入口。宁可多查几个也别留下后手。最后分享一个我自己的体会。RuoYi 这类框架的安全问题本质上不是哪个版本有漏洞的问题而是动态能力与权限边界没设计清楚的问题。只要项目里还存在让用户输入能间接决定调用什么、执行什么、写到哪里的功能组合拳的土壤就一直在。与其追着漏洞编号打补丁不如回到代码里把这四类控制点一个个收进白名单和沙箱——这件事做扎实了比任何一次应急都管用。
返回列表