JeecgBoot高危漏洞CVE-2023-41544:表达式注入导致RCE的深度剖析与修复
1. 项目概述一次对低代码平台高危漏洞的深度剖析最近在梳理一些开源低代码平台的安全风险时JeecgBoot这个国内开发者圈子里相当流行的框架进入了我的视野。它主打快速开发通过可视化配置生成代码确实能极大提升中小型项目的交付效率。但“效率”和“安全”往往像天平的两端这次要聊的CVE-2023-41544就是一个典型的因功能强大但安全边界模糊而导致的高危远程代码执行漏洞。这个漏洞的核心出在报表模块jmreport的一个接口loadTableData上攻击者能够通过精心构造的请求最终在服务器上执行任意系统命令。对于任何使用了受影响版本JeecgBoot的系统来说这无异于将服务器的最高控制权拱手让人。今天我就从一个安全研究兼开发者的角度带大家完整地走一遍这个漏洞的复现与分析过程不仅要知道怎么“打”更要明白它为什么会产生以及在实际开发中如何规避此类问题。2. 漏洞背景与影响范围解析2.1 JeecgBoot与jmreport模块简介JeecgBoot是一个基于Spring Boot的快速开发平台它的一大亮点就是强大的在线报表设计器jmreport。这个模块允许开发者甚至业务人员通过拖拽的方式配置复杂的SQL查询、API数据源并生成各种图表和表格无需编写后端代码。loadTableData接口正是这个报表引擎的核心接口之一负责根据前端的查询条件动态加载和返回报表所需的数据。其设计初衷是为了灵活性能够解析前端传递的复杂JSON参数来动态拼接查询逻辑。2.2 CVE-2023-41544漏洞本质这个漏洞的根源在于表达式注入。为了实现高度的动态性jmreport在解析loadTableData接口传入的某些参数时使用了类似SpEL的表达式引擎进行处理而这个过程没有对用户输入进行充分的过滤和沙箱隔离。攻击者可以将恶意的Java代码片段嵌入到请求参数中当服务器解析这些参数时恶意代码会被当作表达式执行从而导致远程代码执行。注意这与常见的SQL注入有本质区别。SQL注入是让数据库执行非法SQL而表达式注入是让后端的Java应用服务器本身执行Java代码危害等级通常更高因为它直接威胁应用服务器主机。2.3 受影响版本与严重性根据公开的漏洞公告受影响的JeecgBoot版本主要集中在某个版本范围之内。由于低代码平台常被用于开发内部管理系统、甚至一些对外的运营后台一旦被利用攻击者可以读取数据库敏感数据、植入webshell、窃取服务器文件甚至利用服务器作为跳板进行内网横向移动。在CVSS 3.x评分体系中此类漏洞的评分通常高达9.0以上属于严重级别。对于企业而言这意味着必须立即进行版本升级或漏洞修复。3. 漏洞原理深度拆解与代码溯源要真正理解一个漏洞光知道利用方法是不够的必须深入到代码层面看看安全防线是在哪一环被突破的。下面我们结合漏洞原理进行推演和拆解。3.1 动态查询参数的处理流程在JeecgBoot的jmreport模块中loadTableData接口会接收一个包含查询条件、排序、分页等信息的复杂JSON对象。为了将前端的过滤条件转换为后端的SQL查询框架需要解析这些条件。例如前端可能传递一个过滤条件“name”: “like ‘%张三%’”。后端需要将这个条件解析为SQL的WHERE子句片段。问题就出在解析机制上。为了实现类似“通过字段名动态调用查询方法”这类高级功能开发人员可能会引入表达式引擎来解析参数值。一段伪代码可能如下所示// 危险示例说明性伪代码 public Object loadTableData(QueryParams params) { for (Condition condition : params.getConditions()) { String field condition.getField(); // 字段名如“userName” String operator condition.getOperator(); // 操作符如“eq” Object value condition.getValue(); // 值可能来自用户输入 // 为了动态调用可能会使用表达式来构造方法调用或值处理 if (value instanceof String ((String) value).startsWith(“$”)) { // 假设以$开头的字符串是需要解析的表达式 String expression ((String) value).substring(1); // 危险操作直接将用户输入传入表达式解析器 Object evaluatedValue expressionParser.parseExpression(expression).getValue(); condition.setValue(evaluatedValue); } // ... 后续使用condition拼接查询 } }上面这段伪代码清晰地展示了风险点如果condition.getValue()的内容用户可控并且系统以某种标识如$认定其为需要解析的表达式那么用户传入${T(java.lang.Runtime).getRuntime().exec(‘calc’)}这样的字符串就会被表达式引擎执行。3.2 表达式引擎的安全沙箱缺失Spring框架的SpEL表达式功能强大但在默认情况下它能够访问任意类、调用任意方法。安全的做法是配置一个受限的EvaluationContext例如使用SimpleEvaluationContext来限制可访问的属性。然而在追求功能灵活性的快速开发框架中开发者有时会直接使用功能更强大但也更危险的StandardEvaluationContext或者自定义的解析器没有做任何限制这就为表达式注入敞开了大门。在实际的CVE-2023-41544漏洞中攻击路径可能更迂回一些。攻击者可能并非直接传入一个显式的表达式而是通过控制某个参数该参数在后续的流程中被拼接到了某个即将被解析的表达式字符串里。例如参数a的值被拼接到了“${” a “}”这个模板中然后整个字符串被送去解析。如果用户控制a为T(Runtime).getRuntime().exec(“touch /tmp/poc”)那么最终执行的表达式就是灾难性的。3.3 漏洞触发的必要条件总结起来这个漏洞触发需要几个关键条件存在一个用户输入可控的接口参数在loadTableData的请求体中至少有一个参数值能被攻击者完全控制。输入被传入表达式解析器框架内部的处理逻辑将该参数值或其衍生字符串传递给了表达式解析引擎如SpEL、OGNL、MVEL等。表达式解析器处于高权限模式解析器没有运行在安全沙箱中允许执行任意Java代码。4. 漏洞复现环境搭建与实操理解了原理我们动手搭建环境进行复现。请注意所有操作请在授权的测试环境进行。4.1 环境准备与漏洞版本部署首先我们需要一个存在漏洞的JeecgBoot版本。你可以从官方的GitHub仓库的Release历史中找到对应的漏洞版本进行下载。这里假设我们使用一个基于Spring Boot的JeecgBoot war包进行部署。基础环境准备一台Linux测试服务器或本地虚拟机安装JDK 8或11Maven以及Tomcat 9。获取漏洞版本代码从版本控制历史中checkout出漏洞版本的代码或者直接下载对应版本的发布包。编译与部署# 进入项目根目录 cd jeecg-boot # 使用Maven打包。如果网络问题可能需要配置国内镜像。 mvn clean package -DskipTests # 打包后在jeecg-module-system/jeecg-module-system-start/target目录下找到生成的war包如jeecg-system-start-3.5.0.war # 将war包复制到Tomcat的webapps目录下 cp jeecg-system-start-*.war /opt/tomcat/webapps/jeecgboot.war # 启动Tomcat /opt/tomcat/bin/startup.sh初始化与访问启动后访问http://your-server-ip:8080/jeecgboot根据引导完成数据库初始化通常需要创建一个空的MySQL数据库并在安装页面配置数据源。完成初始化后使用默认账号如admin/123456登录。实操心得部署老版本开源项目时经常遇到依赖库无法下载的问题。一个实用的技巧是检查项目根目录的pom.xml将其中的Maven仓库地址临时替换为阿里云镜像可以极大提升下载速度。另外数据库版本也要注意JeecgBoot老版本可能对MySQL 8.x兼容性不好建议使用MySQL 5.7。4.2 构造漏洞验证请求成功登录系统后我们需要找到报表设计模块并创建一个最简单的报表来触发loadTableData接口。但作为漏洞复现我们更直接的方式是分析网络请求直接构造攻击Payload。定位接口通过浏览器开发者工具F12进入“网络”标签页。在报表列表页面点击预览某个报表观察发出的请求。通常会找到一个指向/jmreport/loadTableData的POST请求。分析请求结构查看该请求的请求体Request Payload它通常是一个复杂的JSON结构包含了sql、params、queryParams等字段。我们需要找到那个可以被注入的“参数点”。构造恶意Payload根据漏洞披露的细节攻击载荷可能隐藏在queryParams或某个条件值中。一个典型的攻击请求可能如下所示以下为示例实际参数名需根据版本调整POST /jeecgboot/jmreport/loadTableData HTTP/1.1 Host: your-target-ip:8080 Content-Type: application/json;charsetUTF-8 Cookie: JSESSIONIDxxxxxx { “apiId”: “test_report”, “queryParams”: { “id”: 1, “filter”: “${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(‘whoami’).getInputStream())}” } }关键点解释T(org.apache.commons.io.IOUtils).toString(...)这是利用Spring SpEL表达式调用静态方法。T()操作符用于指定类。T(java.lang.Runtime).getRuntime().exec(‘whoami’)执行系统命令whoami。.getInputStream()获取命令执行的输出流。整个表达式的目的是执行whoami命令并将输出结果转换为字符串这个字符串可能会被框架当作查询参数值从而在响应中返回给攻击者。4.3 执行复现与结果确认使用Burp Suite、Postman或Curl工具发送上述构造的恶意请求。发送请求将构造好的JSON替换到实际的请求体中发送POST请求。观察响应漏洞存在如果服务器存在漏洞你可能会在HTTP响应中看到命令执行的结果例如当前服务器进程的用户名root或tomcat。也可能服务器会返回一个500错误但错误信息中包含了命令执行的输出。更隐蔽的情况下命令可能已执行但无回显此时可以尝试执行一个带延时的命令如ping -c 4 127.0.0.1或写文件的命令如touch /tmp/success然后通过其他方式验证。漏洞不存在或已修复服务器可能返回一个正常的业务错误如“参数解析失败”或者直接忽略该参数响应中不包含命令执行结果。注意事项在实际测试中命令执行可能受到目标服务器环境的影响。例如Java的Runtime.exec()处理带空格的参数或管道符|、重定向符时行为可能与直接在shell中执行不同。通常更可靠的方式是编码后执行或者使用bash -c {command}来执行复杂命令。另外绝对不要在生产环境或未授权的系统上进行测试。5. 漏洞修复方案与安全加固建议复现漏洞是为了更好地防御。对于使用JeecgBoot的开发者或运维人员应立即采取以下措施。5.1 官方修复方案最直接有效的方法是升级JeecgBoot到已修复该漏洞的最新版本。官方在后续版本中针对jmreport模块的表达式解析逻辑进行了加固主要措施包括禁用或严格限制表达式解析移除了对参数值进行动态表达式解析的功能或者将其限制在一个极小的、预定义的安全白名单内。使用安全的EvaluationContext如果确实需要表达式功能则改用SimpleEvaluationContext替代StandardEvaluationContext从根本上限制可访问的类和方法。输入过滤与校验在参数进入核心处理逻辑前增加强力的过滤对包含特殊字符如$,{,},T(),#等SpEL元字符的参数进行拦截或转义。5.2 临时缓解措施如果因种种原因无法立即升级可以考虑以下临时缓解方案WAF防护在应用前端部署Web应用防火墙配置规则拦截包含T(java.lang.Runtime),getRuntime(),exec(等关键字的请求。但这种方法可能被绕过。代码层临时修补找到项目中处理loadTableData或更通用的参数解析的类文件手动添加输入过滤逻辑。例如在参数解析入口处对字符串类型的参数值进行扫描和拒绝。// 临时补丁示例在参数处理入口处添加过滤 public Object sanitizeParameter(Object value) { if (value instanceof String) { String strValue (String) value; // 简单的黑名单过滤实际需要更完善的规则 if (strValue.contains(“T(”) strValue.contains(“getRuntime”) strValue.contains(“exec”)) { throw new IllegalArgumentException(“检测到非法参数内容”); } // 更严格的做法禁止所有SpEL表达式特征字符 if (strValue.matches(“.*[${}T()#].*”)) { // 进行转义或直接拒绝 return strValue.replaceAll(“[$”, “\\$”).replaceAll(“{“, “\\{“); // 简单转义需谨慎评估 } } return value; }提示临时补丁的过滤规则需要精心设计避免影响正常业务比如正常业务参数里可能包含花括号同时也要防止被各种编码方式绕过。这只是一个应急方案治本之道仍是升级。5.3 安全开发规范启示这个漏洞给所有开发者尤其是低代码平台和快速开发框架的开发者敲响了警钟永远不要信任用户输入这是安全的第一原则。任何来自客户端前端、接口调用方的数据都必须视为不可信的必须经过严格的校验、过滤和转义。谨慎使用动态代码/表达式执行eval()、SpEL、OGNL、ScriptEngine等功能极其危险。如非必要避免使用。如果必须使用务必配置在严格的白名单沙箱环境中。依赖组件安全扫描将安全左移在CI/CD流程中集成依赖项漏洞扫描工具如OWASP Dependency-Check及时更新存在已知漏洞的第三方库。最小权限原则运行Java应用的服务器账户如tomcat用户应遵循最小权限原则避免使用root权限运行以限制漏洞成功利用后造成的破坏范围。6. 拓展思考漏洞挖掘与防御的博弈通过CVE-2023-41544的复现我们可以窥见现代Web应用安全攻防的一个缩影。攻击者的入口往往就是这些为了“灵活性”和“用户体验”而设计的强大功能点。对于安全研究人员来说挖掘此类漏洞的思路可以归纳为功能点定位寻找应用中可以接受复杂参数、尤其是能够影响后端逻辑执行流的接口。报表引擎、规则引擎、工作流引擎、动态查询接口都是高危区。参数追踪对每一个用户可控的参数进行追踪看它最终“流”向了哪里。是否参与了字符串拼接是否被当作代码或表达式的一部分进行解析利用链构造当发现一个注入点后需要构造有效的利用链。在Java中除了直接的Runtime执行命令还可以考虑利用反序列化、反射链等更隐蔽的方式。对于防御方而言除了及时打补丁更重要的是建立一套安全开发生命周期流程。在需求设计阶段就考虑安全在代码审查时重点关注危险函数的使用在测试阶段进行充分的安全测试。安全不是产品上线前的一个附加动作而应贯穿于整个软件开发的始终。每一次漏洞的复现和分析都是一次对自身系统安全状况的审视和加固机会。