ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

CVE-2023-2130漏洞剖析:MyBatis中${}误用引发的SQL注入实战与修复

2026/8/13 1:09:47 拓冰建站 浏览量
CVE-2023-2130漏洞剖析:MyBatis中${}误用引发的SQL注入实战与修复 1. 从一次内部渗透测试说起CVE-2023-2130的发现与定位那天下午我正在对一个客户的内网资产进行常规的渗透测试。目标是一个基于Spring Boot开发的内部管理系统看起来平平无奇。在信息收集阶段我习惯性地用工具扫了一下接口发现了一个/api/user/list的端点参数是deptId。随手丢了个单引号‘过去页面返回了一个500错误但错误信息被全局异常处理吞掉了只显示“系统内部错误”。这种反应太常见了很多开发团队为了“安全”和“用户体验”会把详细的SQL错误信息隐藏起来。但经验告诉我这恰恰可能是注入点的一个信号——系统在处理异常而不是直接返回“参数错误”。我换了个思路尝试布尔盲注的经典探测方式deptId1 and 11和deptId1 and 12。前者返回了正常的用户列表JSON后者返回了一个空数组。心跳瞬间加速这个差异太明显了这意味着后端SQL语句的逻辑被我们的输入改变了并且应用程序根据查询结果的不同返回了不同的响应内容这是典型的基于布尔逻辑的SQL注入漏洞的特征。我立刻意识到这可能不是一个简单的、孤立的编码错误。因为Spring生态尤其是结合MyBatis这类ORM框架时如果开发不规范很容易产生一类特定的注入问题。我马上联想到了近期安全社区在讨论的一个Spring相关CVE经过核对确认我触发的正是CVE-2023-2130。这个漏洞本质上不是一个Spring框架本身的“零日”漏洞而是一个在特定使用场景下由于开发者未能正确遵循安全编码规范导致MyBatis的${}占位符被误用从而引发的SQL注入风险。它之所以被赋予CVE编号并引起广泛关注是因为这种模式在大量基于Spring Boot和MyBatis的项目中非常普遍危害面极大。接下来我就结合这次实战经历为你彻底拆解CVE-2023-2130让你不仅知道它是什么更明白它为什么会产生如何精准地发现它以及从根本上修复和防御它。2. 漏洞根因深度剖析MyBatis动态SQL的“安全”与“危险”边界要理解CVE-2023-2130你必须先理解MyBatis中两种参数占位符的天壤之别#{}和${}。这是很多Java开发者在面试时能倒背如流但在实际编码中却会混淆的关键概念。2.1#{}与${}预编译与字符串拼接的本质区别#{}安全占位符的工作原理当你使用#{}时例如在XML映射文件中写作SELECT * FROM user WHERE id #{userId}MyBatis在底层会创建一个PreparedStatement。程序运行时userId参数的值会被单独发送给数据库服务器而不是直接拼接到SQL语句字符串中。数据库引擎会先对SQL语句的结构即SELECT * FROM user WHERE id ?进行编译和优化生成一个执行计划。然后再将传入的userId值作为“数据”绑定到那个预编译好的“模板”的占位符?上。因为SQL结构是固定的无论userId传入的是1、1 OR 11还是任何复杂字符串数据库都只会将其视为一个普通的查询值而不会将其解释为SQL指令的一部分。这就从根本上防止了SQL注入。${}危险占位符的工作原理相反${}实现的是纯粹的字符串替换。比如SELECT * FROM user WHERE id ${userId}MyBatis在解析时会直接将userId变量的值以字符串形式拼接到最终的SQL语句中。如果userId是数字1生成的SQL是SELECT * FROM user WHERE id 1但如果userId被恶意传入1 OR 11生成的SQL就会变成SELECT * FROM user WHERE id 1 OR 11。这条语句被完整地发送到数据库执行OR 11这个条件就被数据库引擎当作合法的SQL逻辑进行解析从而导致注入成功。关键理解你可以把#{}想象成“填空题”试卷SQL结构是固定的你填的答案参数值不会改变题目本身。而${}则是“让你自己写题目”你写什么数据库就执行什么。2.2 CVE-2023-2130的典型触发场景${}的误用CVE-2023-2130指出的高风险场景正是开发者在本应使用#{}的地方错误地使用了${}尤其是在处理用户可控的输入时。常见的误用场景包括动态排序Order By这是最经典的场景。开发者想要实现动态排序比如前端传create_time DESC。!-- 危险写法直接拼接排序字段和方式 -- SELECT * FROM t_order ORDER BY ${sortField} ${sortOrder}攻击者可以将sortField设置为(SELECT CASE WHEN (11) THEN id ELSE (SELECT 1 UNION SELECT 2) END)这类子查询通过数据库执行结果的时间差或错误信息来进行盲注。动态表名/列名某些分库分表或动态查询场景下表名可能需要动态传入。!-- 危险写法拼接表名 -- SELECT * FROM ${tableName} WHERE status 1如果tableName可控攻击者可以将其设置为user; DROP TABLE user; --或其他恶意语句尽管MyBatis通常一次执行一条语句但结合其他技巧仍可能造成危害。IN查询参数拼接错误地使用${}来拼接一个由逗号分隔的ID字符串。!-- 危险写法直接拼接ID列表 -- SELECT * FROM product WHERE id IN (${ids})如果ids来自用户输入1,2,3看似正常。但攻击者可以输入1) OR 11 --最终语句变为... IN (1) OR 11 --)注释掉后面的括号成功注入。那么为什么开发者会明知危险还用${}呢因为#{}在处理某些特定场景时会产生“不符合预期”的语法。比如在ORDER BY后面使用#{sortField}数据库会将其解析为ORDER BY create_time字段名被加上了引号这是一个语法错误。这种“不好用”的体验迫使一些开发者或者从网上拷贝代码的人转向了“好用”但危险的${}。而CVE-2023-2130正是将这类广泛存在的、不安全的编码实践作为一个明确的公共安全风险进行了通告。3. 实战漏洞挖掘如何系统性地发现此类注入点知道了原理我们如何在黑盒或灰盒测试中高效地挖掘这类漏洞呢不能只靠运气丢一个单引号。下面是我总结的一套组合拳。3.1 信息收集与目标锁定首先你需要识别目标技术栈。指纹识别查看HTTP响应头如X-Powered-By: Spring Boot、静态资源路径如/js/common.js中的特定代码、默认错误页面Whitelabel Error Page等判断是否为Spring Boot应用。接口探测使用爬虫工具如Burp Suite的Scanner、AWVS或主动扫描器收集所有API端点。特别关注带有查询参数的端点尤其是那些看起来像是用于搜索、筛选、排序、分页的接口。参数名如sort、order、field、group、table、column等是高风险关键词。3.2 手工探测与Payload构造自动化工具对这类注入的检测效果有限手工测试至关重要。初步探测单引号/双引号在参数值后添加或观察响应是否出现500错误、异常回显、或者响应时间有明显差异。逻辑测试使用参数原始值 AND 11和参数原始值 AND 12。观察页面内容、返回的JSON数据量、状态码是否不同。如果11返回正常数据12返回空或无数据强烈暗示存在布尔盲注。时间延迟测试使用数据库特有的延时函数。对于MySQL可以尝试参数1 AND SLEEP(5)。如果响应时间大约延迟了5秒说明注入存在且可被用于时间盲注。判断注入类型与数据库如果错误信息回显尽管Spring Boot通常不直接回显可以根据信息判断数据库类型。通过时间盲注的差异函数判断SLEEP(5)MySQL/MariaDB、PG_SLEEP(5)PostgreSQL、WAITFOR DELAY 0:0:5SQL Server。通过字符串连接函数判断CONCAT(a,b)MySQL、a||bPostgreSQL/Oracle。针对${}误用场景的特殊探测排序参数如果参数是sortid尝试改为sort(CASE WHEN (11) THEN id ELSE name END)。如果排序结果发生了变化按id排说明CASE语句被执行了证明参数被直接拼接进ORDER BY子句存在注入。数值参数对于像deptId这样的参数除了常规注入测试可以尝试deptId11。如果返回的结果与deptId2一致说明参数被直接当作表达式计算很可能使用了${}。因为#{}会将11作为字符串“11”传入查询结果会不同。3.3 利用漏洞进行数据提取以布尔盲注为例假设我们确认了/api/user/list?deptId${injectableParam}存在基于布尔的注入且后端是MySQL数据库。我们的目标是获取管理员用户的密码哈希。步骤一判断当前数据库名长度deptId1 AND LENGTH(DATABASE())1 deptId1 AND LENGTH(DATABASE())2 ...当假设长度等于真实长度时页面会返回正常数据真否则返回空假。通过遍历我们可以得知数据库名长度例如是8。步骤二逐字符猜解数据库名利用SUBSTRING()或MID()函数结合ASCII()函数将字符转换为ASCII码进行二分法比较效率远高于暴力枚举。deptId1 AND ASCII(SUBSTRING(DATABASE(),1,1))97 deptId1 AND ASCII(SUBSTRING(DATABASE(),1,1))100 ...通过反复询问“第一个字符的ASCII码是否大于X”“是否等于Y”最终确定第一个字符是‘m’。重复此过程8次得到数据库名‘myapp_db’。步骤三猜解表名、列名、数据流程类似但需要利用information_schema数据库。猜解表名SELECT table_name FROM information_schema.tables WHERE table_schemaDATABASE() LIMIT 0,1猜解列名SELECT column_name FROM information_schema.columns WHERE table_nameusers LIMIT 0,1提取数据SELECT username,password_hash FROM users LIMIT 0,1整个过程完全基于“页面返回有数据”真和“页面返回无数据”假的差异自动化工具如sqlmap可以高效完成但理解其原理对于手动验证和编写复杂Payload至关重要。4. 修复方案不仅仅是替换${}为#{}找到漏洞只是第一步更重要的是如何修复。直接说“把${}改成#{}”过于简单因为在ORDER BY等场景下行不通。下面提供几个层次的解决方案。4.1 方案一入参白名单校验推荐这是最安全、最根本的解决方案。在Java代码的业务逻辑层Controller或Service层对传入的动态参数进行严格校验。适用于动态排序GetMapping(/api/user/list) public Result listUsers(RequestParam String sortField, RequestParam String sortOrder) { // 定义允许的排序字段白名单 ListString allowedFields Arrays.asList(id, create_time, username); // 定义允许的排序方式白名单 ListString allowedOrders Arrays.asList(ASC, DESC); // 校验排序字段 if (!allowedFields.contains(sortField)) { sortField id; // 或抛出参数异常 } // 校验排序方式 if (!allowedOrders.contains(sortOrder.toUpperCase())) { sortOrder ASC; } // 将校验后的安全参数传入Service层 return userService.listUsers(sortField, sortOrder); }在MyBatis XML中可以继续使用${}但此时传入的sortField和sortOrder已经是经过白名单过滤的安全值从根源上杜绝了注入。select idselectUserList resultMapuserMap SELECT * FROM user ORDER BY ${safeSortField} ${safeSortOrder} /select适用于动态表名如按月份分表// 假设表名格式为 order_202401, order_202402 public ListOrder getOrdersByMonth(String yearMonth) { // 正则校验年份月份格式 if (!yearMonth.matches(^202[4-9](0[1-9]|1[0-2])$)) { throw new IllegalArgumentException(Invalid table suffix); } String tableName order_ yearMonth; // 拼接出的表名是安全的 return orderMapper.selectFromTable(tableName); }4.2 方案二使用MyBatis的script标签与choose、if动态SQL对于简单的非此即彼的动态排序可以利用MyBatis的动态SQL在XML层面实现安全拼接。select idselectUserList resultMapuserMap SELECT * FROM user ORDER BY choose when testsortField createTime create_time /when when testsortField username username /when otherwise id /otherwise /choose choose when testsortOrder DESC DESC /when otherwise ASC /otherwise /choose /select这种方法将逻辑判断放在XML里避免了在SQL中直接拼接用户输入。但缺点是当排序字段很多时XML会显得冗长。4.3 方案三极端情况下的安全映射慎用如果动态需求非常复杂白名单无法穷举理论上应该避免这种设计且必须使用${}可以考虑建立一个“安全映射表”。// 在Service层建立一个安全的映射 private static final MapString, String SAFE_FIELD_MAP new HashMap(); static { SAFE_FIELD_MAP.put(userName, u.username); SAFE_FIELD_MAP.put(deptName, d.name); // ... 其他映射 } public String getSafeOrderField(String clientField) { String dbField SAFE_FIELD_MAP.get(clientField); return dbField ! null ? dbField : u.id; // 默认值 }在Mapper接口中接收这个安全的数据库字段名。select idselectComplexList resultMapcomplexMap SELECT u.*, d.name as deptName FROM user u LEFT JOIN department d ON u.dept_id d.id ORDER BY ${safeDbField} /select4.4 全局防御与最佳实践代码审计与组件升级在项目初期和定期审计中使用grep或SAST静态应用安全测试工具全局搜索\$\{.*?\}审查所有使用${}的地方确认其必要性。同时保持MyBatis、Spring Boot等依赖库为最新稳定版以获取安全更新。最小权限原则连接数据库的账号应遵循最小权限原则只授予应用必要的SELECT、INSERT、UPDATE、DELETE权限避免使用DROP、CREATE、ALTER等高危权限。这样即使发生注入攻击者能造成的破坏也有限。启用SQL日志在开发测试环境开启MyBatis的SQL日志配置mybatis.configuration.log-impl为STDOUT_LOGGING。观察最终执行的SQL语句可以直观地看到参数是如何被拼接的有助于提前发现风险。使用ORM框架的安全特性考虑使用JPAHibernate等更“重量级”的ORM它们通常对动态查询提供了更类型安全的方式如Criteria API、QueryDSL从设计上减少了字符串拼接SQL的机会。5. 防御者视角构建代码审计与自动化检测流程作为安全工程师或团队负责人不能只依赖渗透测试的事后发现更应该将安全左移在开发阶段就堵住漏洞。5.1 静态代码分析SAST集成将SAST工具集成到CI/CD流水线中是现代DevSecOps的标配。工具选择可以使用SonarQube、Fortify SCA、Checkmarx等商业工具也可以使用开源的SpotBugs配合find-sec-bugs插件或Semgrep。规则定制针对MyBatis编写或启用检测${}使用的规则。但更聪明的规则是检测“在ORDER BY、GROUP BY、表名、列名等位置使用了非白名单参数的${}”。流程卡点在CI流水线中配置如果SAST扫描出高危的SQL注入漏洞如CVE-2023-2130这类则自动失败Fail the Build阻止不安全的代码合并到主分支。5.2 动态应用安全测试DAST与IASTDAST使用OWASP ZAP、Burp Suite Enterprise等工具对已部署的应用进行定期或触发式扫描。配置扫描策略重点测试包含sort、order、field等参数的接口。IAST交互式应用安全测试工具如Contrast Security、Hdiv Detection在应用运行时通过插桩技术监控数据流能更准确地发现从用户输入到SQL语句执行这条路径上的漏洞误报率低非常适合检测此类注入。5.3 人工代码审计清单在每次迭代的代码评审Code Review中安全人员或资深开发者应关注以下方面Mapper XML文件重点审查所有SELECT、UPDATE、INSERT、DELETE语句中的${}使用。询问开发者“这个参数用户可控吗”“这里为什么不能用#{}”Java Service/Controller层查看接收前端参数的RequestParam、PathVariable、RequestBody对象追踪这些参数最终是否被传递到了Mapper层以及传递前是否有校验。搜索常见的危险方法全局搜索String.format()、StringBuilder.append()拼接SQL字符串的代码虽然MyBatis中不常见但在一些老项目或原生JDBC代码中可能存在。5.4 安全培训与意识提升最终所有技术手段都需要人来执行。对开发团队进行定期的安全编码培训至关重要。培训内容应讲清原理用生动的例子演示#{}和${}的区别以及SQL注入的实际危害数据泄露、篡改、删库。提供安全代码样例给出动态排序、动态表名等场景下的正确代码示例并放入公司内部的知识库或代码模板库中。建立奖惩机制将安全漏洞数量与团队或个人绩效适当挂钩同时对于主动发现并上报安全问题的行为给予奖励。CVE-2023-2130与其说是一个需要紧急打补丁的“漏洞”不如说是一记响亮的警钟它提醒我们框架的便利性不能以牺牲安全为代价。安全是一个持续的过程而非一劳永逸的状态。从理解漏洞原理到掌握探测手法再到实施多层次修复与防御每一步都需要我们保持警惕将安全思维融入到软件开发生命周期的每一个环节。在实战中我修复这个漏洞的方式就是在对应的Service方法里加上了不到十行的白名单校验代码并同步更新了团队的代码规范文档。有时候最有效的安全措施恰恰是那些最简单、最基础的实践。