1. 项目概述从“黑盒”到“白盒”的审计视角转换上次我们聊了ofcms审计的入口和基础信息收集算是把“战场”的地形摸了一遍。今天这篇我们直接切入核心实战环节SQL注入漏洞的审计。很多刚入行的朋友一听到代码审计尤其是Java这种企业级应用总觉得门槛很高面对动辄几十上百个Controller和Service类无从下手。其实只要思路清晰SQL注入这种经典漏洞的审计路径是有章可循的。它不像逻辑漏洞那样需要天马行空的想象力更像是一场有明确线索的“寻宝游戏”——你的目标就是找到那些将用户输入未经充分处理就直接拼接进SQL语句的地方。ofcms作为一个内容管理系统其核心功能如文章发布、内容查询、用户管理都离不开数据库操作这自然就成了SQL注入的“重灾区”。我们今天的任务就是扮演一个“挑剔的开发者”用攻击者的思维去审视每一行与数据库交互的代码。我会带你走一遍我审计这类系统时的完整思路从危险函数/方法定位到数据流追踪再到漏洞确认与利用链构造。你会发现即便没有复杂的自动化工具仅凭IDE的搜索功能和一双“火眼金睛”也能挖出不少问题。当然过程中我会分享很多只有踩过坑才知道的“笨办法”和“小技巧”比如如何快速区分MyBatis的#{}和${}如何在Spring JDBC的代码里找到拼接的痕迹以及那些容易被忽略的“二次注入”场景。2. 审计环境与核心思路搭建工欲善其事必先利其器。在开始代码审计之前一个顺手的审计环境能极大提升效率。2.1 审计环境准备与工具链选择我个人的审计环境通常基于IntelliJ IDEA社区版即可它强大的代码索引和搜索功能是人工审计的“倍增器”。首先你需要将ofcms的源码以Maven或Gradle项目的形式导入IDEA确保所有依赖都能正确下载和索引。这一步看似简单却至关重要因为只有建立了完整的项目索引你才能进行高效的全局搜索和引用查找。关键工具与配置IDEA全局搜索CtrlShiftF这是最核心的武器。我们将用它搜索所有与SQL执行相关的关键词。Git Blame如果项目有Git历史有时查看某段问题代码的提交历史和作者信息能帮助你理解这段代码的上下文甚至发现一些因为历史原因遗留下来的“坏味道”。简单的HTTP调试代理如Burp Suite Community Edition用于验证我们发现的潜在漏洞点是否真的可利用。我们会在审计出疑似点后构造Payload进行测试。数据库监控可选如果你能在本地运行起ofcms可以开启MySQL的通用查询日志general log实时查看应用执行的所有SQL语句这对于验证SQL拼接情况有奇效。注意不要一开始就陷入复杂的代码审计工具。对于SQL注入这种模式相对固定的漏洞熟练使用IDE的搜索功能结合对框架的理解往往比依赖自动化工具更直接、更准确。工具可能会误报或漏报但你的眼睛和分析能力不会。2.2 SQL注入审计的核心思路拆解我的审计思路可以概括为“由面到点顺藤摸瓜”。第一步识别“危险源”面在Java Web应用中SQL注入的根源在于不可信的用户输入进入了SQL语句。因此我们首先要找到所有接收用户输入的地方。这通常包括HttpServletRequest的getParameter、getHeader、getCookie等方法。Spring MVC的RequestParam、PathVariable、RequestBody注解的参数。从Session、缓存中获取的但其原始来源是用户输入的数据。第二步定位“执行点”点找到输入后我们需要追踪这些数据流向哪里。最终的目标是定位到数据库操作层即真正执行SQL语句的地方。在Java中常见的执行点有原生JDBCStatement尤其是PreparedStatement用字符串拼接的情况。Spring JdbcTemplatequery、update等方法中拼接SQL字符串。MyBatis在XML映射文件中使用${}进行参数替换或者在注解中使用Select等拼接SQL。Hibernate/JPA使用createNativeQuery拼接SQL字符串或者错误使用HQL拼接。第三步分析“处理链”藤数据从“危险源”到“执行点”的路径就是我们需要分析的“处理链”。中间可能经过Controller、Service、多个工具类进行过滤、转换、拼接。我们需要仔细检查这条链上的每一个环节是否有全局或局部的过滤器Filter、拦截器Interceptor对输入进行了处理在Service层或工具类中是否对参数进行了安全的类型转换、过滤或转义参数是否被拼接进了更大的字符串如搜索条件、排序字段、表名然后再被传到执行层第四步验证“漏洞点”瓜通过以上分析锁定疑似漏洞点后就需要构造Payload进行验证。验证时要注意上下文比如参数是被单引号包裹字符串型还是直接使用数字型这决定了Payload的构造方式。3. ofcms SQL注入漏洞深度审计实战有了清晰的思路我们直接进入ofcms的源码。我会以几个典型的场景为例带你走完整个审计流程。3.1 审计入口从Controller层开始追踪我们首先在IDEA中全局搜索Controller或RestController注解快速浏览ofcms的所有控制器。对于一个CMS系统我们需要特别关注与“内容管理”、“用户交互”相关的控制器比如AdminArticleController、AdminCommentController、ApiController等。假设我们打开AdminContentController里面有一个用于获取内容列表的方法RequestMapping(value /list, method RequestMethod.GET) public String list(HttpServletRequest request, Model model) { String title request.getParameter(title); String categoryId request.getParameter(categoryId); String pageStr request.getParameter(page); // ... 后续处理 }这里title、categoryId、pageStr都直接来自用户请求。这就是我们找到的“危险源”。接下来我们需要看这些参数被传递到哪里去。3.2 关键代码追踪Service与DAO层分析通常Controller会调用Service层的方法。我们找到对应的ContentService看看有没有一个getContentList之类的方法。在ofcms中它可能会使用MyBatis作为持久层框架。场景一MyBatis XML中的${}陷阱我们追踪参数最终在MyBatis的Mapper XML文件如ContentMapper.xml中找到了执行SQL的地方select idselectContentList resultMapBaseResultMap SELECT * FROM of_cms_content WHERE 11 if testtitle ! null and title ! AND title LIKE %${title}% /if if testcategoryId ! null AND category_id #{categoryId} /if ORDER BY create_time DESC /select漏洞点分析categoryId参数使用了#{categoryId}这是MyBatis的预编译占位符会将参数安全地设置为PreparedStatement的参数能有效防止SQL注入。但是title参数使用了${title}。${}是字符串替换MyBatis会直接将title变量的值替换到SQL语句中。如果title的值是 OR 11那么拼接后的SQL就会变成SELECT * FROM of_cms_content WHERE 11 AND title LIKE % OR 11%这完全改变了查询逻辑是一个典型的SQL注入漏洞。实操心得在审计MyBatis项目时全局搜索\$\{是一个高效的方法。你需要逐一检查每个使用${}的地方判断其替换的内容是否用户可控。常见的危险场景包括排序字段order by、表名、列名、LIKE语句的匹配值部分。对于LIKE子句安全的做法是使用#{title}并在Service层给参数加上%通配符或者使用MyBatis的bind标签。场景二注解式SQL中的拼接有些开发者喜欢在Mapper接口中使用注解直接编写SQL这也可能出问题。Select(SELECT * FROM of_cms_user WHERE username #{username} AND status 1) // 错误这实际上变成了字符串拼接#{username}不会被正确解析。 User findByUsername(Param(username) String username);正确的注解写法应该是Select(SELECT * FROM of_cms_user WHERE username #{username} AND status 1) User findByUsername(Param(username) String username);在审计时看到注解中有使用号连接字符串和参数的都需要高度警惕。场景三动态SQL构建工具中的疏忽ofcms可能使用了类似QueryWrapperMyBatis-Plus或自定义的SQL构建工具。例如QueryWrapperContent wrapper new QueryWrapper(); wrapper.like(title, request.getParameter(title)); wrapper.eq(category_id, request.getParameter(categoryId));看起来使用了封装的方法似乎很安全。但是我们需要检查like和eq方法的内部实现。如果它们内部是直接将第二个参数用${}方式拼接或者使用了不安全的字符串格式化如String.format那么漏洞依然存在。更隐蔽的是**orderBy、groupBy等方法它们通常直接拼接列名**如果列名用户可控比如通过sort参数传入create_time; DROP TABLE xxx --就会导致注入。String sortField request.getParameter(sort); wrapper.orderBy(true, true, sortField); // 危险sortField直接拼接进SQL3.3 拓展审计不可忽略的“二次注入”与盲点除了上述直接注入点还有一些更隐蔽的场景。二次注入数据在存入数据库时经过了转义或过滤比如调用了HtmlUtils.htmlEscape被认为是“安全”的。但当这些数据被从数据库中取出并在不同的上下文比如拼接进新的SQL语句中被使用时就可能触发注入。例如用户注册时用户名包含转义后的单引号\存入数据库后就是\。后来在某个后台查询功能中这个用户名被直接拼接到SQL里转义符\在SQL解析时可能被忽略导致单引号逃逸。审计二次注入的关键是追踪用户可控数据从入库到出库再到被使用的完整生命周期。重点关注那些先insert/update再在后续逻辑中select出来用于拼接的功能点。存储过程/函数调用如果代码中调用了数据库的存储过程或函数并且参数是拼接而成的同样存在风险。搜索CallableStatement、{call等关键词。复杂业务逻辑中的拼接有时SQL拼接发生在复杂的业务代码深处可能是在循环中动态添加AND条件或者根据不同的业务分支选择不同的表名和列名。审计这类代码需要更多的耐心要理清整个业务逻辑的数据流。4. 漏洞验证与Payload构造技巧当我们通过代码分析锁定了一个疑似注入点后就需要通过实际请求来验证。这里我分享几个针对不同场景的验证技巧。4.1 验证环境搭建与测试方法本地运行最好能在本地IDE中启动ofcms项目并连接一个测试数据库如MySQL。这样你可以随时查看日志甚至调试代码。开启日志在application.yml或logback-spring.xml中将MyBatis的日志级别设置为DEBUG这样可以在控制台看到最终执行的SQL语句。开启MySQL的通用查询日志查看所有到达数据库的SQL。使用Burp Suite拦截浏览器请求将请求发送到Repeater模块方便我们修改参数反复测试。4.2 针对不同注入类型的Payload构造根据参数在SQL语句中的上下文构造不同的Payload。1. 数字型注入点假设找到的SQL是SELECT * FROM product WHERE id ${id}。验证Payloadid1 AND 11和id1 AND 12。观察页面返回是否不同。如果11时正常12时异常或无数据则基本确认存在注入。利用Payloadid1 UNION SELECT 1,user(),database(),4-- -2. 字符串型注入点单引号包裹假设SQL是SELECT * FROM user WHERE username ${username}。验证Payloadusernameadmin AND 11和usernameadmin AND 12。同样通过返回差异判断。闭合技巧你需要先闭合前面的单引号然后编写你的注入代码最后用注释--或#来注释掉后面的单引号。例如usernameadmin OR 11-- -最终SQL为...WHERE username admin OR 11-- ---后面的内容被注释。3. LIKE子句中的注入这是ofcms中非常常见的场景如LIKE %${keyword}%。验证Payloadkeywordtest。如果程序报错数据库语法错误说明存在注入。如果程序做了错误处理可以尝试keywordtest% AND 11观察结果。利用挑战由于被%包围且通常用在搜索功能利用起来比直接赋值更复杂。可能需要结合AND、OR以及子查询来提取数据。4. 排序字段Order By注入这是${}的高发区且通常无法使用UNION。注入类型为“盲注”。验证Payloadsortcreate_time正常sortcreate_time;尝试添加分号看是否报错sort(SELECT 1)看是否将1作为列名排序可能报错。利用Payload通常通过布尔盲注或时间盲注来利用。例如sortcreate_time, (IF(11, sleep(2), 0))观察响应是否延迟。在MySQL中可以尝试sort1 ASC, (SELECT 1 FROM DUAL WHERE 11)。注意事项在测试时间盲注时要注意应用和数据库之间可能有网络延迟或缓存需要设置一个明显的睡眠时间如5秒并对比正常请求的响应时间。同时Burp Suite的Repeater模块可以显示精确的响应时间非常有用。4.3 常见绕过技巧与WAF应对思路在实际审计中目标系统可能部署了简单的WAFWeb应用防火墙或代码层有简单的过滤。关键字过滤如过滤了SELECT、UNION、AND、OR等。大小写绕过SeLeCt双写绕过SELSELECTECT编码绕过URL编码、十六进制编码。SELECT-%53%45%4c%45%43%54或0x53454c454354注释符分割SEL/**/ECT空格过滤使用注释符SELECT/**/user()使用括号在MySQL中括号可以用于分隔。SELECT(user())FROM(users)使用Tab或换行符%09(Tab),%0a(换行)单引号过滤如果过滤了单引号但注入点是字符串型需要找到替代方案。使用十六进制将字符串转为十六进制。admin-0x61646d696e使用CHAR()函数CHAR(97, 100, 109, 105, 110)表示admin在审计代码时也要留意是否存在这类过滤函数如String.replaceAll(select, )并思考其是否可以被绕过。5. 修复建议与安全编码规范审计出漏洞不是终点给出靠谱的修复方案才是价值所在。针对ofcms中发现的SQL注入问题修复必须遵循“外部过滤内部转义”的原则但最根本的是使用预编译PreparedStatement。5.1 针对发现漏洞的修复方案MyBatis XML中的${}修复对于LIKE语句改用#{}并在Java代码中拼接%。!-- 修复前 -- AND title LIKE %${title}% !-- 修复后 -- AND title LIKE CONCAT(%, #{title}, %)或者使用bind标签bind nametitleLike value% title %/ AND title LIKE #{titleLike}对于排序字段、表名等动态内容如果必须动态应建立白名单机制。例如对于排序字段// Service层代码 private static final SetString ALLOWED_SORT_FIELDS new HashSet(Arrays.asList(create_time, update_time, view_count)); public String validateSortField(String input) { if (ALLOWED_SORT_FIELDS.contains(input)) { return input; } return create_time; // 默认值 }然后在Mapper中使用${validatedSortField}。注解SQL中的拼接修复严格检查所有Select、Update等注解确保参数全部使用#{}杜绝字符串连接。SQL工具类/Wrapper修复审查QueryWrapper等工具类的使用确保传入like、eq等方法的值是使用预编译方式传递的MyBatis-Plus默认是安全的。对于orderBy、groupBy等接收列名的方法必须对输入进行白名单校验。5.2 建立长效的安全编码规范修复具体漏洞的同时应该在团队内推行安全编码规范从源头减少问题强制使用预编译规定所有数据库操作必须使用预编译MyBatis的#{}JPA的Query配合参数绑定JdbcTemplate的?占位符禁止在业务代码中进行字符串拼接SQL。代码审查重点在代码审查Code Review环节将SQL语句编写作为审查重点。任何出现的${}、连接SQL字符串、String.format拼接SQL都必须给出合理解释。使用安全的ORM框架特性鼓励使用框架提供的安全查询方式。例如在JPA中使用CriteriaQuery进行类型安全的动态查询在MyBatis-Plus中使用LambdaQueryWrapper避免手写列名字符串。输入验证与过滤在Controller层或专门的校验层对输入进行严格的类型、格式、长度、范围校验。对于无法使用预编译的场景如动态表名必须实施白名单策略。定期安全扫描与审计将静态代码安全扫描SAST工具集成到CI/CD流程中定期对代码库进行自动化扫描。同时像我们今天做的这样定期对核心业务模块进行人工代码审计。5.3 漏洞挖掘后的思考与记录每挖到一个漏洞尤其是那种需要绕好几道弯才发现的二次注入或逻辑复杂的注入点我都会做一个简单的记录漏洞文件与行号精确定位。触发路径从用户请求到漏洞执行的完整调用链。漏洞原理一两句话说明为什么这里不安全。Payload示例证明漏洞可用的有效Payload。修复方案具体的代码修改建议。这份记录不仅是审计报告更是你个人经验积累的宝贵财富。下次再审计类似系统或功能时你会更快地定位到同类问题。审计工作就像侦探破案需要耐心、细心和对“犯罪模式”的熟悉。SQL注入虽然是一种古老的漏洞但在复杂的业务代码和快速迭代的开发节奏下它依然会以各种新的面貌出现。掌握从源码中系统性寻找SQL注入的方法是每一个安全研究员和开发者的基本功。希望这篇基于ofcms的实战讲解能帮你建立起清晰的审计思路。在实际操作中最大的技巧往往就是“耐心”和“多问一句为什么这个参数可以这样传”。
Java Web应用SQL注入漏洞审计实战:从MyBatis到二次注入的深度挖掘
1. 项目概述从“黑盒”到“白盒”的审计视角转换上次我们聊了ofcms审计的入口和基础信息收集算是把“战场”的地形摸了一遍。今天这篇我们直接切入核心实战环节SQL注入漏洞的审计。很多刚入行的朋友一听到代码审计尤其是Java这种企业级应用总觉得门槛很高面对动辄几十上百个Controller和Service类无从下手。其实只要思路清晰SQL注入这种经典漏洞的审计路径是有章可循的。它不像逻辑漏洞那样需要天马行空的想象力更像是一场有明确线索的“寻宝游戏”——你的目标就是找到那些将用户输入未经充分处理就直接拼接进SQL语句的地方。ofcms作为一个内容管理系统其核心功能如文章发布、内容查询、用户管理都离不开数据库操作这自然就成了SQL注入的“重灾区”。我们今天的任务就是扮演一个“挑剔的开发者”用攻击者的思维去审视每一行与数据库交互的代码。我会带你走一遍我审计这类系统时的完整思路从危险函数/方法定位到数据流追踪再到漏洞确认与利用链构造。你会发现即便没有复杂的自动化工具仅凭IDE的搜索功能和一双“火眼金睛”也能挖出不少问题。当然过程中我会分享很多只有踩过坑才知道的“笨办法”和“小技巧”比如如何快速区分MyBatis的#{}和${}如何在Spring JDBC的代码里找到拼接的痕迹以及那些容易被忽略的“二次注入”场景。2. 审计环境与核心思路搭建工欲善其事必先利其器。在开始代码审计之前一个顺手的审计环境能极大提升效率。2.1 审计环境准备与工具链选择我个人的审计环境通常基于IntelliJ IDEA社区版即可它强大的代码索引和搜索功能是人工审计的“倍增器”。首先你需要将ofcms的源码以Maven或Gradle项目的形式导入IDEA确保所有依赖都能正确下载和索引。这一步看似简单却至关重要因为只有建立了完整的项目索引你才能进行高效的全局搜索和引用查找。关键工具与配置IDEA全局搜索CtrlShiftF这是最核心的武器。我们将用它搜索所有与SQL执行相关的关键词。Git Blame如果项目有Git历史有时查看某段问题代码的提交历史和作者信息能帮助你理解这段代码的上下文甚至发现一些因为历史原因遗留下来的“坏味道”。简单的HTTP调试代理如Burp Suite Community Edition用于验证我们发现的潜在漏洞点是否真的可利用。我们会在审计出疑似点后构造Payload进行测试。数据库监控可选如果你能在本地运行起ofcms可以开启MySQL的通用查询日志general log实时查看应用执行的所有SQL语句这对于验证SQL拼接情况有奇效。注意不要一开始就陷入复杂的代码审计工具。对于SQL注入这种模式相对固定的漏洞熟练使用IDE的搜索功能结合对框架的理解往往比依赖自动化工具更直接、更准确。工具可能会误报或漏报但你的眼睛和分析能力不会。2.2 SQL注入审计的核心思路拆解我的审计思路可以概括为“由面到点顺藤摸瓜”。第一步识别“危险源”面在Java Web应用中SQL注入的根源在于不可信的用户输入进入了SQL语句。因此我们首先要找到所有接收用户输入的地方。这通常包括HttpServletRequest的getParameter、getHeader、getCookie等方法。Spring MVC的RequestParam、PathVariable、RequestBody注解的参数。从Session、缓存中获取的但其原始来源是用户输入的数据。第二步定位“执行点”点找到输入后我们需要追踪这些数据流向哪里。最终的目标是定位到数据库操作层即真正执行SQL语句的地方。在Java中常见的执行点有原生JDBCStatement尤其是PreparedStatement用字符串拼接的情况。Spring JdbcTemplatequery、update等方法中拼接SQL字符串。MyBatis在XML映射文件中使用${}进行参数替换或者在注解中使用Select等拼接SQL。Hibernate/JPA使用createNativeQuery拼接SQL字符串或者错误使用HQL拼接。第三步分析“处理链”藤数据从“危险源”到“执行点”的路径就是我们需要分析的“处理链”。中间可能经过Controller、Service、多个工具类进行过滤、转换、拼接。我们需要仔细检查这条链上的每一个环节是否有全局或局部的过滤器Filter、拦截器Interceptor对输入进行了处理在Service层或工具类中是否对参数进行了安全的类型转换、过滤或转义参数是否被拼接进了更大的字符串如搜索条件、排序字段、表名然后再被传到执行层第四步验证“漏洞点”瓜通过以上分析锁定疑似漏洞点后就需要构造Payload进行验证。验证时要注意上下文比如参数是被单引号包裹字符串型还是直接使用数字型这决定了Payload的构造方式。3. ofcms SQL注入漏洞深度审计实战有了清晰的思路我们直接进入ofcms的源码。我会以几个典型的场景为例带你走完整个审计流程。3.1 审计入口从Controller层开始追踪我们首先在IDEA中全局搜索Controller或RestController注解快速浏览ofcms的所有控制器。对于一个CMS系统我们需要特别关注与“内容管理”、“用户交互”相关的控制器比如AdminArticleController、AdminCommentController、ApiController等。假设我们打开AdminContentController里面有一个用于获取内容列表的方法RequestMapping(value /list, method RequestMethod.GET) public String list(HttpServletRequest request, Model model) { String title request.getParameter(title); String categoryId request.getParameter(categoryId); String pageStr request.getParameter(page); // ... 后续处理 }这里title、categoryId、pageStr都直接来自用户请求。这就是我们找到的“危险源”。接下来我们需要看这些参数被传递到哪里去。3.2 关键代码追踪Service与DAO层分析通常Controller会调用Service层的方法。我们找到对应的ContentService看看有没有一个getContentList之类的方法。在ofcms中它可能会使用MyBatis作为持久层框架。场景一MyBatis XML中的${}陷阱我们追踪参数最终在MyBatis的Mapper XML文件如ContentMapper.xml中找到了执行SQL的地方select idselectContentList resultMapBaseResultMap SELECT * FROM of_cms_content WHERE 11 if testtitle ! null and title ! AND title LIKE %${title}% /if if testcategoryId ! null AND category_id #{categoryId} /if ORDER BY create_time DESC /select漏洞点分析categoryId参数使用了#{categoryId}这是MyBatis的预编译占位符会将参数安全地设置为PreparedStatement的参数能有效防止SQL注入。但是title参数使用了${title}。${}是字符串替换MyBatis会直接将title变量的值替换到SQL语句中。如果title的值是 OR 11那么拼接后的SQL就会变成SELECT * FROM of_cms_content WHERE 11 AND title LIKE % OR 11%这完全改变了查询逻辑是一个典型的SQL注入漏洞。实操心得在审计MyBatis项目时全局搜索\$\{是一个高效的方法。你需要逐一检查每个使用${}的地方判断其替换的内容是否用户可控。常见的危险场景包括排序字段order by、表名、列名、LIKE语句的匹配值部分。对于LIKE子句安全的做法是使用#{title}并在Service层给参数加上%通配符或者使用MyBatis的bind标签。场景二注解式SQL中的拼接有些开发者喜欢在Mapper接口中使用注解直接编写SQL这也可能出问题。Select(SELECT * FROM of_cms_user WHERE username #{username} AND status 1) // 错误这实际上变成了字符串拼接#{username}不会被正确解析。 User findByUsername(Param(username) String username);正确的注解写法应该是Select(SELECT * FROM of_cms_user WHERE username #{username} AND status 1) User findByUsername(Param(username) String username);在审计时看到注解中有使用号连接字符串和参数的都需要高度警惕。场景三动态SQL构建工具中的疏忽ofcms可能使用了类似QueryWrapperMyBatis-Plus或自定义的SQL构建工具。例如QueryWrapperContent wrapper new QueryWrapper(); wrapper.like(title, request.getParameter(title)); wrapper.eq(category_id, request.getParameter(categoryId));看起来使用了封装的方法似乎很安全。但是我们需要检查like和eq方法的内部实现。如果它们内部是直接将第二个参数用${}方式拼接或者使用了不安全的字符串格式化如String.format那么漏洞依然存在。更隐蔽的是**orderBy、groupBy等方法它们通常直接拼接列名**如果列名用户可控比如通过sort参数传入create_time; DROP TABLE xxx --就会导致注入。String sortField request.getParameter(sort); wrapper.orderBy(true, true, sortField); // 危险sortField直接拼接进SQL3.3 拓展审计不可忽略的“二次注入”与盲点除了上述直接注入点还有一些更隐蔽的场景。二次注入数据在存入数据库时经过了转义或过滤比如调用了HtmlUtils.htmlEscape被认为是“安全”的。但当这些数据被从数据库中取出并在不同的上下文比如拼接进新的SQL语句中被使用时就可能触发注入。例如用户注册时用户名包含转义后的单引号\存入数据库后就是\。后来在某个后台查询功能中这个用户名被直接拼接到SQL里转义符\在SQL解析时可能被忽略导致单引号逃逸。审计二次注入的关键是追踪用户可控数据从入库到出库再到被使用的完整生命周期。重点关注那些先insert/update再在后续逻辑中select出来用于拼接的功能点。存储过程/函数调用如果代码中调用了数据库的存储过程或函数并且参数是拼接而成的同样存在风险。搜索CallableStatement、{call等关键词。复杂业务逻辑中的拼接有时SQL拼接发生在复杂的业务代码深处可能是在循环中动态添加AND条件或者根据不同的业务分支选择不同的表名和列名。审计这类代码需要更多的耐心要理清整个业务逻辑的数据流。4. 漏洞验证与Payload构造技巧当我们通过代码分析锁定了一个疑似注入点后就需要通过实际请求来验证。这里我分享几个针对不同场景的验证技巧。4.1 验证环境搭建与测试方法本地运行最好能在本地IDE中启动ofcms项目并连接一个测试数据库如MySQL。这样你可以随时查看日志甚至调试代码。开启日志在application.yml或logback-spring.xml中将MyBatis的日志级别设置为DEBUG这样可以在控制台看到最终执行的SQL语句。开启MySQL的通用查询日志查看所有到达数据库的SQL。使用Burp Suite拦截浏览器请求将请求发送到Repeater模块方便我们修改参数反复测试。4.2 针对不同注入类型的Payload构造根据参数在SQL语句中的上下文构造不同的Payload。1. 数字型注入点假设找到的SQL是SELECT * FROM product WHERE id ${id}。验证Payloadid1 AND 11和id1 AND 12。观察页面返回是否不同。如果11时正常12时异常或无数据则基本确认存在注入。利用Payloadid1 UNION SELECT 1,user(),database(),4-- -2. 字符串型注入点单引号包裹假设SQL是SELECT * FROM user WHERE username ${username}。验证Payloadusernameadmin AND 11和usernameadmin AND 12。同样通过返回差异判断。闭合技巧你需要先闭合前面的单引号然后编写你的注入代码最后用注释--或#来注释掉后面的单引号。例如usernameadmin OR 11-- -最终SQL为...WHERE username admin OR 11-- ---后面的内容被注释。3. LIKE子句中的注入这是ofcms中非常常见的场景如LIKE %${keyword}%。验证Payloadkeywordtest。如果程序报错数据库语法错误说明存在注入。如果程序做了错误处理可以尝试keywordtest% AND 11观察结果。利用挑战由于被%包围且通常用在搜索功能利用起来比直接赋值更复杂。可能需要结合AND、OR以及子查询来提取数据。4. 排序字段Order By注入这是${}的高发区且通常无法使用UNION。注入类型为“盲注”。验证Payloadsortcreate_time正常sortcreate_time;尝试添加分号看是否报错sort(SELECT 1)看是否将1作为列名排序可能报错。利用Payload通常通过布尔盲注或时间盲注来利用。例如sortcreate_time, (IF(11, sleep(2), 0))观察响应是否延迟。在MySQL中可以尝试sort1 ASC, (SELECT 1 FROM DUAL WHERE 11)。注意事项在测试时间盲注时要注意应用和数据库之间可能有网络延迟或缓存需要设置一个明显的睡眠时间如5秒并对比正常请求的响应时间。同时Burp Suite的Repeater模块可以显示精确的响应时间非常有用。4.3 常见绕过技巧与WAF应对思路在实际审计中目标系统可能部署了简单的WAFWeb应用防火墙或代码层有简单的过滤。关键字过滤如过滤了SELECT、UNION、AND、OR等。大小写绕过SeLeCt双写绕过SELSELECTECT编码绕过URL编码、十六进制编码。SELECT-%53%45%4c%45%43%54或0x53454c454354注释符分割SEL/**/ECT空格过滤使用注释符SELECT/**/user()使用括号在MySQL中括号可以用于分隔。SELECT(user())FROM(users)使用Tab或换行符%09(Tab),%0a(换行)单引号过滤如果过滤了单引号但注入点是字符串型需要找到替代方案。使用十六进制将字符串转为十六进制。admin-0x61646d696e使用CHAR()函数CHAR(97, 100, 109, 105, 110)表示admin在审计代码时也要留意是否存在这类过滤函数如String.replaceAll(select, )并思考其是否可以被绕过。5. 修复建议与安全编码规范审计出漏洞不是终点给出靠谱的修复方案才是价值所在。针对ofcms中发现的SQL注入问题修复必须遵循“外部过滤内部转义”的原则但最根本的是使用预编译PreparedStatement。5.1 针对发现漏洞的修复方案MyBatis XML中的${}修复对于LIKE语句改用#{}并在Java代码中拼接%。!-- 修复前 -- AND title LIKE %${title}% !-- 修复后 -- AND title LIKE CONCAT(%, #{title}, %)或者使用bind标签bind nametitleLike value% title %/ AND title LIKE #{titleLike}对于排序字段、表名等动态内容如果必须动态应建立白名单机制。例如对于排序字段// Service层代码 private static final SetString ALLOWED_SORT_FIELDS new HashSet(Arrays.asList(create_time, update_time, view_count)); public String validateSortField(String input) { if (ALLOWED_SORT_FIELDS.contains(input)) { return input; } return create_time; // 默认值 }然后在Mapper中使用${validatedSortField}。注解SQL中的拼接修复严格检查所有Select、Update等注解确保参数全部使用#{}杜绝字符串连接。SQL工具类/Wrapper修复审查QueryWrapper等工具类的使用确保传入like、eq等方法的值是使用预编译方式传递的MyBatis-Plus默认是安全的。对于orderBy、groupBy等接收列名的方法必须对输入进行白名单校验。5.2 建立长效的安全编码规范修复具体漏洞的同时应该在团队内推行安全编码规范从源头减少问题强制使用预编译规定所有数据库操作必须使用预编译MyBatis的#{}JPA的Query配合参数绑定JdbcTemplate的?占位符禁止在业务代码中进行字符串拼接SQL。代码审查重点在代码审查Code Review环节将SQL语句编写作为审查重点。任何出现的${}、连接SQL字符串、String.format拼接SQL都必须给出合理解释。使用安全的ORM框架特性鼓励使用框架提供的安全查询方式。例如在JPA中使用CriteriaQuery进行类型安全的动态查询在MyBatis-Plus中使用LambdaQueryWrapper避免手写列名字符串。输入验证与过滤在Controller层或专门的校验层对输入进行严格的类型、格式、长度、范围校验。对于无法使用预编译的场景如动态表名必须实施白名单策略。定期安全扫描与审计将静态代码安全扫描SAST工具集成到CI/CD流程中定期对代码库进行自动化扫描。同时像我们今天做的这样定期对核心业务模块进行人工代码审计。5.3 漏洞挖掘后的思考与记录每挖到一个漏洞尤其是那种需要绕好几道弯才发现的二次注入或逻辑复杂的注入点我都会做一个简单的记录漏洞文件与行号精确定位。触发路径从用户请求到漏洞执行的完整调用链。漏洞原理一两句话说明为什么这里不安全。Payload示例证明漏洞可用的有效Payload。修复方案具体的代码修改建议。这份记录不仅是审计报告更是你个人经验积累的宝贵财富。下次再审计类似系统或功能时你会更快地定位到同类问题。审计工作就像侦探破案需要耐心、细心和对“犯罪模式”的熟悉。SQL注入虽然是一种古老的漏洞但在复杂的业务代码和快速迭代的开发节奏下它依然会以各种新的面貌出现。掌握从源码中系统性寻找SQL注入的方法是每一个安全研究员和开发者的基本功。希望这篇基于ofcms的实战讲解能帮你建立起清晰的审计思路。在实际操作中最大的技巧往往就是“耐心”和“多问一句为什么这个参数可以这样传”。