安全攻防
morning
被高估的 RCE 与被低估的物理控制:JimuReport 攻防复盘与源码证伪
摘要
在最近的一次 SRC 项目里,笔者遇到了一个基于若依(RuoYi)二次开发的高校能源管控平台。通过 JimuReport 组件的未授权配置漏洞,我们不仅获取了核心业务数据,顺藤摸瓜获得了数据库访问权限,还发现了一个能直接下发物理设备指令的未授权接口。 在复盘该漏洞时,笔者遇到了一个技术分歧:目前业界有较多文章声称该模块存在参数注入漏洞,可导致远程代码执行(RCE)。然而,在实战评估中,准确界定漏洞...
drag
RCE
prod
api
JimuReport
anon
edit
Shiro
filterChainDefinitionMap
put
2026-11-10
1 阅读
约9分钟阅读
先知社区
字号:
在最近的一次 SRC 项目里,笔者遇到了一个基于若依(RuoYi)二次开发的高校能源管控平台。通过 JimuReport 组件的未授权配置漏洞,我们不仅获取了核心业务数据,顺藤摸瓜获得了数据库访问权限,还发现了一个能直接下发物理设备指令的未授权接口。 在复盘该漏洞时,笔者遇到了一个技术分歧:目前业界有较多文章声称该模块存在参数注入漏洞,可导致远程代码执行(RCE)。然而,在实战评估中,准确界定漏洞的真实危害边界比盲目相信网传 PoC 更为重要。本文将分为两部分:第一部分记录真实靶标上的业务级降维打击;第二部分则是笔者针对底层字节码的逆向分析,还原那个被高估的“RCE 幻象”被拦截的真实原因。 第一部分:真实靶标的降维打击(未授权与业务沦陷) 一、突破:被 Shiro 放行的报表引擎 目标系统的 /prod-api/ 路径下暴露了大量积木报表(JimuReport)的接口。按若依常见的配置, ShiroConfig 中为积木报表的路径配置了匿名白名单: filterChainDefinitionMap.put("/jmreport/**", "anon"); filterChainDefinitionMap.put("/drag/**", "anon"); 在 Shiro 的安全模型中, anon 的语义是将匹配该路径的请求直接短路,跳过后续的认证与授权过滤器链(如 FormAuthenticationFilter 和 AuthorizationFilter )。这意味着访问 /drag/ 或 /jmreport/ 下的接口时,认证链路直接被绕过。 源码: 我们对 /prod-api/drag/onlDragDatasetHead/edit 发起了修改数据集名称的请求: POST /prod-api/drag/onlDragDatasetHead/edit HTTP/1.1 Content-Type: application/json {"id":"970251620641243136","name":"test"} 响应包如下: {"success":true,"message":"更新成功!","code":200} 请求未携带任何 Cookie 或 Token,报表配置写操作即告成功。攻击者可进一步利用 /drag/page/edit 向报表页面注入恶意脚本或钓鱼链接,实施水坑攻击。 二、窃取:从配置文件到数据源的沦陷 顺藤摸瓜,我们在 DataSource 相关接口中发现了 /dragonlDragDataSource/list ,该接口未授权返回了数据源配置信息: { "success": true, "result": { "records": [{ "dbUrl": "jdbc:mysql://:3306/jimureport?...", "dbUsername": "root", "dbPassword": "[REDACTED]" }] } } JimuReport 内置的对称加密密钥硬编码在依赖包中。获取密文后,通过提取本地 jar 包中的密钥即可还原明文密码。获取该数据库凭据后,若该数据库与业务系统同库,攻击者可绕过应用层逻辑,直接修改 sys_user 表接管系统。不过这次的目标没有条件,呜呜 三、深水区:致命的设备控制指令未授权 真正将威胁等级推向高危的,是我们在审计 OpenAPI 模块时发现的 /openapi/device/{sn}/cmd 接口: POST /prod-api/openapi/device/941260100259/cmd HTTP/1.1 Content-Type: application/json {} 返回: {"msg":"functionCode不能为空","code":400} 该接口未进行任何身份认证,仅因缺少业务参数 functionCode 而拒绝执行。攻击者补齐该参数后,即可通过该接口向全校电表下发控制指令。物理世界的断电或过载等破坏性操作,是此漏洞最核心的威胁。之后可以在找到具体的功能码,这里不遍展现。保护自己也保护你们哟 第二部分:源码拆解——为什么“参数注入 RCE”打不通? 在真实靶标上,通过未授权访问已获取了足够的信息。但为彻底评估风险,笔者对 jimureport-2.5.0 及其底层依赖 aviator-5.2.6 进行了逆向分析,以验证网传的参数注入 RCE 是否成立。 四、参数流转的完整调用链 导出接口挂载了 @JimuNoLoginRequired 注解( @Retention(RUNTIME) ),通过反射机制绕过鉴权。参数从 JimuReportAutoExportVO 的 reportParams 流入报表引擎的调用链如下: getBaseSql(JmReportDb, JSONObject, Map, String) ↓ ExpressUtil.a(String, Map) → String ↓ AviatorEvaluatorInstance.compile() / Expression.execute() 从字节码角度, getBaseSql 内部对 params 的处理并不是简单的字符串拼接。在 desreport/service/a/g.class 里,参数传入后会先经过 ExpressUtil.a(String, Integer, Map) 做表达式解析预处理,再交给 AviatorEvaluatorInstance.compile() 。这个过程里,如果参数值以 = 开头,会被优先识别为表达式,而不是普通数据。这也是为什么攻击者能通过 reportParams 注入表达式的根本原因。 需要说明的是, compile() 方法在编译时会检查是否触发了 feature 限制。但由于 JimuReport 初始化时只关掉了 NewInstance ,理论上 StaticMethods 是允许的。但问题在于,编译阶段不报错不代表执行阶段能通过,语法解析器的限制是在执行阶段生效的。 参数最终确实被传递至 compile 和 execute 方法。 五、字节码层面的限制分析 查看 ExpressUtil 的静态初始化块( ): i = AviatorEvaluator.newInstance(); i.useLRUExpressionCache(1000); i.setCachedExpressionByDefault(true); i.setOption(Options.TRACE_EVAL, false); i.setOption(Options.ALWAYS_PARSE_FLOATING_POINT_NUMBER_INTO_DECIMAL, true); i.disableFeature(Feature.NewInstance); // 全文唯一一处 feature 限制 Aviator 5.2.6 的 Feature 枚举包含 17 个值,涵盖 Use 、 StaticMethods 等能力。JimuReport 仅禁用了 NewInstan
这篇文章对您有帮助吗?
订阅66必读
每日精选科技资讯,直达你的邮箱