MyBatis中${}与#{}的SQL注入风险剖析与防御实践
1. 项目概述一次典型的企业CMS前台SQL注入漏洞深度剖析最近在分析历史漏洞案例库时一个编号为CNVD-2024-06148的漏洞引起了我的注意。这个漏洞涉及Mingsoft MCMS v5.2.9版本一个在国内中小企业中曾经有一定使用量的内容管理系统。漏洞类型是前台查询文章列表接口的SQL注入这意味着攻击者无需登录直接通过构造特定的HTTP请求参数就能对数据库进行非法操作。这类漏洞的危害性极高因为它直接暴露了核心数据可能导致数据泄露、篡改甚至服务器被完全控制。我决定对这个漏洞进行一次彻底的逆向分析和复现不仅是为了理解其成因更重要的是梳理出一套针对此类Java Web应用SQL注入漏洞的通用分析、验证与防御思路。无论你是安全研究人员、开发工程师还是运维人员通过这次深度拆解你都能掌握从漏洞公告到实操复现再到代码审计和修复建议的完整闭环。2. 漏洞背景与MCMS架构初探在深入漏洞细节之前有必要先了解一下Mingsoft MCMS。这是一款基于Java开发的开源内容管理系统采用了比较经典的Spring MVC MyBatis技术栈。在v5.2.9及更早的版本中它被用于快速搭建企业官网、资讯门户等。其架构通常分为前台面向用户和后台管理端。本次漏洞发生在前台即普通访客可以访问的页面接口上这大大降低了攻击门槛。漏洞编号CNVD-2024-06148是国家信息安全漏洞共享平台收录的说明其具备一定的普遍性和危害性。根据公开信息漏洞点位于文章列表查询的相关接口。在内容管理系统中文章列表查询是最基础、调用最频繁的功能之一通常涉及分类筛选、排序、分页等复杂参数处理。如果开发人员对用户输入的参数过滤不严就极易将参数直接拼接进SQL语句从而埋下注入隐患。我搭建了MCMS v5.2.9的环境准备从黑盒测试入手逐步定位到白盒代码分析。3. 黑盒测试与漏洞点定位我的分析从黑盒模糊测试开始。目标是找到那个存在注入漏洞的前台接口。根据经验这类接口的URL往往包含/list、/search、/content等关键词并且会接收诸如categoryId、orderBy、keyword等查询参数。3.1 接口探测与参数发现首先我使用浏览器开发者工具观察网站前台点击文章分类或搜索时的网络请求。很快我锁定了一个疑似接口/ms/article/list。通过Burp Suite拦截并重放该请求发现其GET参数中包含categoryId和orderBy。初步测试在categoryId参数后添加一个单引号‘页面返回了数据库错误信息而不是友好的“参数错误”提示。这是一个非常强烈的SQL注入信号。注意在实际渗透测试中直接使应用报错是发现注入点的重要手段之一。但需要谨慎操作避免对生产环境造成破坏。我是在本地隔离的测试环境中进行的。3.2 手工注入验证与信息获取确认可能存在注入后我开始进行经典的手工注入测试以判断注入类型和可利用性。判断注入类型我尝试了categoryId1‘ and ‘1’’1和categoryId1‘ and ‘1’’2。前者返回了正常的文章列表后者返回了空列表或错误。这基本确认了这是一个字符型注入并且参数值被单引号包裹。判断字段数使用order by语句。我发送请求categoryId1‘ order by 10 --逐渐增加数字直到页面报错。测试发现order by 8正常order by 9报错说明当前查询结果的字段数为8。联合查询探测确定了字段数后就可以使用union select来获取数据。我构造了如下PayloadcategoryId-1‘ union select 1,2,3,4,5,6,7,8 --。这里将categoryId设置为一个不存在的值如-1目的是让原查询结果为空从而使得页面直接显示我们union select的结果。回显页面中数字2和3的位置显示了实际内容说明这两个位置的回显点可以利用。至此黑盒层面已经可以100%确认该接口存在可被利用的SQL注入漏洞。攻击者可以通过这个漏洞逐步查询数据库名、表名、字段名最终获取管理员账号密码等敏感信息。4. 白盒代码审计漏洞根源追踪黑盒测试确认了漏洞的存在但作为一名开发者或深度安全分析者必须找到代码层面的根源。我下载了MCMS v5.2.9的源代码开始进行白盒审计。4.1 控制器层Controller分析根据URL/ms/article/list我定位到对应的控制器类ArticleController中的list方法。代码如下为简洁起见已做简化RequestMapping(value /list) public String list(HttpServletRequest request, ModelMap model) { // 获取请求参数 String categoryId request.getParameter(categoryId); String orderBy request.getParameter(orderBy); // 构建查询条件 Article article new Article(); if (StringUtils.isNotBlank(categoryId)) { article.setCategoryId(categoryId); // 直接set未过滤 } if (StringUtils.isNotBlank(orderBy)) { article.setOrderBy(orderBy); // 直接set未过滤 } // 调用服务层查询 Page page articleService.list(article); model.addAttribute(page, page); return /article/list; }问题一目了然控制器直接从HttpServletRequest中获取参数categoryId和orderBy未经过任何校验、转义或过滤就直接设置到了实体对象Article中。这是SQL注入漏洞产生的第一个环节——不可信数据直接进入系统业务对象。4.2 服务层与数据访问层分析接着我跟踪articleService.list(article)方法。服务层通常负责业务逻辑但在这里它可能只是将对象传递给数据访问层。最终我定位到了MyBatis的Mapper XML文件中的SQL语句。在ArticleMapper.xml中我找到了类似下面的动态SQL片段select idselectArticleList parameterTypeArticle resultMapArticleResult SELECT * FROM ms_article where if testcategoryId ! null and categoryId ! ‘’ AND category_id #{categoryId} /if if testorderBy ! null and orderBy ! ‘’ ORDER BY ${orderBy} !-- 这里是关键 -- /if /where /select漏洞根源在此这里暴露了两个关键点但只有一个是真正的“罪魁祸首”。#{categoryId}的使用对于category_id的查询条件MyBatis使用了#{}语法。这是一个预编译参数占位符。MyBatis底层通过JDBC会将其转换为?然后对传入的categoryId值进行类型处理和安全转义最后再交给数据库执行。即使前端传入恶意SQL片段在这里也会被当作一个普通的字符串值因此categoryId参数在条件下是安全的。${orderBy}的使用对于ORDER BY子句开发者错误地使用了${}语法。这是一个字符串替换占位符。MyBatis会直接将orderBy变量的值以字符串拼接的方式替换到SQL语句中。如果orderBy的值是update_time DESC那么SQL就是ORDER BY update_time DESC但如果orderBy的值是恶意构造的update_time DESC; DROP TABLE ms_article --那么拼接后的SQL将变成ORDER BY update_time DESC; DROP TABLE ms_article --从而执行额外的破坏性命令。实操心得这是MyBatis使用中最经典的SQL注入陷阱。#{}和${}的区别必须刻在脑子里。#{}用于传递值Where条件中的值Insert的值${}一般用于传递SQL语句本身的不变部分如动态表名、动态列名但即使如此也需要非常严格的过滤。绝对禁止将用户输入直接用${}拼接4.3 漏洞利用链完整还原现在我们可以完整还原攻击链攻击者访问前台页面触发文章列表查询。攻击者篡改请求参数例如将orderBy设置为1 AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)。该恶意参数未经任何过滤从ArticleController传至ArticleService再传至MyBatis Mapper。MyBatis将${orderBy}直接拼接到SQL语句中形成SELECT * FROM ms_article ORDER BY 1 AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)。数据库执行该SQL语句由于SLEEP(5)函数页面响应会延迟5秒攻击者由此可判定注入成功时间盲注。进而攻击者可以使用更复杂的Payload来窃取数据。5. 漏洞修复方案与安全编码实践找到根源后修复方案就清晰了。核心原则是避免将用户可控数据直接拼接进SQL语句。5.1 紧急修复方案治标对于ORDER BY、GROUP BY这类子句由于不能使用预编译的#{}它会给列名加上引号导致语法错误必须采用白名单过滤的方式。在Controller或Service层增加过滤逻辑public String safeOrderBy(String input) { // 定义允许的排序字段白名单 ListString allowedColumns Arrays.asList(update_time, create_time, click); // 定义允许的排序方式 ListString allowedOrders Arrays.asList(ASC, DESC); if (input null) return null; String[] parts input.split(\\s); if (parts.length 0 || parts.length 2) return update_time DESC; // 默认值 String column parts[0]; String order (parts.length 2) ? parts[1].toUpperCase() : DESC; // 白名单校验 if (!allowedColumns.contains(column)) { column update_time; } if (!allowedOrders.contains(order)) { order DESC; } return column order; }在接收到orderBy参数后调用此方法进行清洗再将清洗后的安全字符串传递给Mapper。5.2 根本性修复与安全开发规范治本强制使用#{}在团队内建立代码规范明确规定所有传入SQL的值必须使用#{}。在Code Review时重点检查。严格限制${}的使用场景仅在动态表名、动态列名等必要时使用并且必须与白名单机制结合。禁止使用${}拼接任何来自用户输入或外部接口的数据。使用安全的ORM框架特性例如MyBatis-Plus提供了QueryWrapper等条件构造器它内部对排序、分组等操作进行了安全封装能有效防止拼接。QueryWrapperArticle wrapper new QueryWrapper(); wrapper.orderBy(true, false, update_time); // 安全的方式实施纵深防御输入校验在参数进入系统的最外层如Controller进行格式、类型、长度校验。Web应用防火墙WAF部署WAF可以拦截常见的SQL注入攻击Payload作为一道有效的边界防护。最小权限原则连接数据库的应用程序账号只授予其必要的最小权限如SELECT,INSERT,UPDATE切勿使用root或sa等超级管理员账号。6. 漏洞复现的实操记录与思考为了加深理解我在本地虚拟机中完整复现了漏洞利用过程。环境为Windows 10 JDK 8 Tomcat 8.5 MySQL 5.7 MCMS v5.2.9。6.1 复现步骤环境搭建从官网下载历史版本MCMS v5.2.9导入数据库脚本配置application.properties中的数据库连接部署至Tomcat。漏洞验证使用浏览器访问首页。通过Burp Suite拦截对文章列表页的请求。将拦截到的GET请求中的orderBy参数修改为update_time AND SLEEP(5)。转发请求观察到服务器响应时间明显超过5秒证明时间盲注存在。数据获取利用sqlmap进行自动化利用验证漏洞危害的严重性。sqlmap.py -u http://192.168.1.100:8080/ms/article/list?categoryId1orderByupdate_time --batch --dbs很快sqlmap便成功列出了所有数据库名。进一步可以导出指定数据库的表、字段和数据。6.2 复现过程中的难点与技巧难点一Payload构造。由于是ORDER BY后的注入一些常见的UNION SELECTPayload可能因为原SQL语句的语法限制而失败。需要根据报错信息灵活调整。时间盲注SLEEP()和布尔盲注IF(11, 1, (SELECT 1 UNION SELECT 2))在这种场景下往往更可靠。技巧二使用sqlmap的--technique参数。当自动检测不准确时可以指定注入技术。例如--techniqueT指定使用时间盲注。难点三Java应用报错信息配置。生产环境通常会将详细的数据库错误信息隐藏只返回通用错误页这会给黑盒测试中的漏洞确认带来困难。此时更需要依赖盲注技术。7. 从MCMS漏洞延伸的通用防御策略CNVD-2024-06148漏洞虽然发生在特定的MCMS版本中但其反映的问题是普遍的。总结下来对于Java Web应用尤其是使用MyBatis/iBATIS框架的团队以下几点必须成为开发纪律框架特性认知是基础每个开发者必须深刻理解所用ORM框架的安全机制。就像MyBatis的#{}和${}Hibernate的HQL/JPQL与原生SQL的区别。代码审计流程化将SQL注入检查纳入常规的代码审计Code Review清单。重点审计Mapper XML文件、任何包含StringBuilder或StringBuffer拼接SQL的DAO层代码。依赖组件安全扫描使用Maven插件如OWASP Dependency-Check或GitLab/GitHub的依赖扫描功能定期检查项目引入的第三方库是否存在已知安全漏洞。MCMS也可能使用了存在漏洞的旧版本组件。自动化安全测试左移在CI/CD流水线中集成静态应用安全测试SAST工具如SonarQube配合FindSecBugs插件、Fortify等在代码提交阶段就发现潜在的安全漏洞。安全意识常态化定期对开发团队进行安全编码培训通过内部靶场演练如搭建一个存在类似漏洞的Demo应用让开发者亲身感受漏洞的危害和利用过程远比枯燥的条文规定更有效。这次对CNVD-2024-06148漏洞的深度分析再次印证了一个道理大多数安全漏洞并非源于高深的技术攻防而是源于最基本的安全编码规范的缺失。${orderBy}这样一个简单的写法背后是整个安全开发体系的松懈。作为技术人员我们每一次${}的随意使用都可能为系统打开一扇危险的后门。