Java Web应用SQL注入漏洞审计实战:从MyBatis到二次注入的深度挖掘

📅 2026/7/2 22:30:02 👁️ 阅读次数 📝 编程学习
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,确保所有依赖都能正确下载和索引。这一步看似简单,却至关重要,因为只有建立了完整的项目索引,你才能进行高效的全局搜索和引用查找。

关键工具与配置:

  1. IDEA全局搜索(Ctrl+Shift+F):这是最核心的武器。我们将用它搜索所有与SQL执行相关的关键词。
  2. Git Blame(如果项目有Git历史):有时,查看某段问题代码的提交历史和作者信息,能帮助你理解这段代码的上下文,甚至发现一些因为历史原因遗留下来的“坏味道”。
  3. 简单的HTTP调试代理(如Burp Suite Community Edition):用于验证我们发现的潜在漏洞点是否真的可利用。我们会在审计出疑似点后,构造Payload进行测试。
  4. 数据库监控(可选):如果你能在本地运行起ofcms,可以开启MySQL的通用查询日志(general log),实时查看应用执行的所有SQL语句,这对于验证SQL拼接情况有奇效。

注意:不要一开始就陷入复杂的代码审计工具。对于SQL注入这种模式相对固定的漏洞,熟练使用IDE的搜索功能,结合对框架的理解,往往比依赖自动化工具更直接、更准确。工具可能会误报或漏报,但你的眼睛和分析能力不会。

2.2 SQL注入审计的核心思路拆解

我的审计思路可以概括为“由面到点,顺藤摸瓜”

第一步:识别“危险源”(面)在Java Web应用中,SQL注入的根源在于不可信的用户输入进入了SQL语句。因此,我们首先要找到所有接收用户输入的地方。这通常包括:

  • HttpServletRequestgetParametergetHeadergetCookie等方法。
  • Spring MVC的@RequestParam@PathVariable@RequestBody注解的参数。
  • 从Session、缓存中获取的,但其原始来源是用户输入的数据。

第二步:定位“执行点”(点)找到输入后,我们需要追踪这些数据流向哪里。最终的目标是定位到数据库操作层,即真正执行SQL语句的地方。在Java中,常见的执行点有:

  • 原生JDBCStatement(尤其是PreparedStatement用字符串拼接的情况)。
  • Spring JdbcTemplatequeryupdate等方法中拼接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系统,我们需要特别关注与“内容管理”、“用户交互”相关的控制器,比如AdminArticleControllerAdminCommentControllerApiController等。

假设我们打开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"); // ... 后续处理 }

这里,titlecategoryIdpageStr都直接来自用户请求。这就是我们找到的“危险源”。接下来,我们需要看这些参数被传递到哪里去。

3.2 关键代码追踪:Service与DAO层分析

通常,Controller会调用Service层的方法。我们找到对应的ContentService,看看有没有一个getContentList之类的方法。在ofcms中,它可能会使用MyBatis作为持久层框架。

场景一:MyBatis XML中的${}陷阱我们追踪参数,最终在MyBatis的Mapper XML文件(如ContentMapper.xml)中找到了执行SQL的地方:

<select id="selectContentList" resultMap="BaseResultMap"> SELECT * FROM of_cms_content WHERE 1=1 <if test="title != null and title != ''"> AND title LIKE '%${title}%' </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> ORDER BY create_time DESC </select>

漏洞点分析:

  • categoryId参数使用了#{categoryId},这是MyBatis的预编译占位符,会将参数安全地设置为PreparedStatement的参数,能有效防止SQL注入。
  • 但是title参数使用了${title}${}字符串替换,MyBatis会直接将title变量的值替换到SQL语句中。如果title的值是' OR '1'='1,那么拼接后的SQL就会变成:
    SELECT * FROM of_cms_content WHERE 1=1 AND title LIKE '%' OR '1'='1%'
    这完全改变了查询逻辑,是一个典型的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可能使用了类似QueryWrapper(MyBatis-Plus)或自定义的SQL构建工具。例如:

QueryWrapper<Content> wrapper = new QueryWrapper<>(); wrapper.like("title", request.getParameter("title")); wrapper.eq("category_id", request.getParameter("categoryId"));

看起来使用了封装的方法,似乎很安全。但是,我们需要检查likeeq方法的内部实现。如果它们内部是直接将第二个参数用${}方式拼接,或者使用了不安全的字符串格式化(如String.format),那么漏洞依然存在。更隐蔽的是**orderBygroupBy等方法,它们通常直接拼接列名**,如果列名用户可控(比如通过sort参数传入create_time; DROP TABLE xxx --),就会导致注入。

String sortField = request.getParameter("sort"); wrapper.orderBy(true, true, sortField); // 危险!sortField直接拼接进SQL

3.3 拓展审计:不可忽略的“二次注入”与盲点

除了上述直接注入点,还有一些更隐蔽的场景。

二次注入:数据在存入数据库时经过了转义或过滤(比如调用了HtmlUtils.htmlEscape),被认为是“安全”的。但当这些数据被从数据库中取出,并在不同的上下文(比如拼接进新的SQL语句)中被使用时,就可能触发注入。例如,用户注册时用户名包含转义后的单引号\',存入数据库后就是\'。后来在某个后台查询功能中,这个用户名被直接拼接到SQL里,转义符\在SQL解析时可能被忽略,导致单引号逃逸。

审计二次注入的关键是:追踪用户可控数据从入库到出库再到被使用的完整生命周期。重点关注那些先insert/update,再在后续逻辑中select出来用于拼接的功能点。

存储过程/函数调用:如果代码中调用了数据库的存储过程或函数,并且参数是拼接而成的,同样存在风险。搜索CallableStatement{call等关键词。

复杂业务逻辑中的拼接:有时SQL拼接发生在复杂的业务代码深处,可能是在循环中动态添加AND条件,或者根据不同的业务分支选择不同的表名和列名。审计这类代码需要更多的耐心,要理清整个业务逻辑的数据流。

4. 漏洞验证与Payload构造技巧

当我们通过代码分析锁定了一个疑似注入点后,就需要通过实际请求来验证。这里我分享几个针对不同场景的验证技巧。

4.1 验证环境搭建与测试方法

  1. 本地运行:最好能在本地IDE中启动ofcms项目,并连接一个测试数据库(如MySQL)。这样你可以随时查看日志,甚至调试代码。
  2. 开启日志
    • application.ymllogback-spring.xml中,将MyBatis的日志级别设置为DEBUG,这样可以在控制台看到最终执行的SQL语句。
    • 开启MySQL的通用查询日志,查看所有到达数据库的SQL。
  3. 使用Burp Suite:拦截浏览器请求,将请求发送到Repeater模块,方便我们修改参数,反复测试。

4.2 针对不同注入类型的Payload构造

根据参数在SQL语句中的上下文,构造不同的Payload。

1. 数字型注入点:假设找到的SQL是SELECT * FROM product WHERE id = ${id}

  • 验证Payloadid=1 AND 1=1id=1 AND 1=2。观察页面返回是否不同。如果1=1时正常,1=2时异常或无数据,则基本确认存在注入。
  • 利用Payloadid=1 UNION SELECT 1,user(),database(),4-- -

2. 字符串型注入点(单引号包裹):假设SQL是SELECT * FROM user WHERE username = '${username}'

  • 验证Payloadusername=admin' AND '1'='1username=admin' AND '1'='2。同样通过返回差异判断。
  • 闭合技巧:你需要先闭合前面的单引号,然后编写你的注入代码,最后用注释--#来注释掉后面的单引号。例如:username=admin' OR '1'='1'-- -,最终SQL为...WHERE username = 'admin' OR '1'='1'-- -'--后面的内容被注释。

3. LIKE子句中的注入:这是ofcms中非常常见的场景,如LIKE '%${keyword}%'

  • 验证Payloadkeyword=test'。如果程序报错(数据库语法错误),说明存在注入。如果程序做了错误处理,可以尝试keyword=test%' AND '1'='1,观察结果。
  • 利用挑战:由于被%包围,且通常用在搜索功能,利用起来比直接赋值更复杂。可能需要结合ANDOR以及子查询来提取数据。

4. 排序字段(Order By)注入:这是${}的高发区,且通常无法使用UNION。注入类型为“盲注”。

  • 验证Payloadsort=create_time(正常),sort=create_time;(尝试添加分号,看是否报错),sort=(SELECT 1)(看是否将1作为列名排序,可能报错)。
  • 利用Payload:通常通过布尔盲注或时间盲注来利用。例如:sort=create_time, (IF(1=1, sleep(2), 0)),观察响应是否延迟。在MySQL中,可以尝试sort=1 ASC, (SELECT 1 FROM DUAL WHERE 1=1)

注意事项:在测试时间盲注时,要注意应用和数据库之间可能有网络延迟或缓存,需要设置一个明显的睡眠时间(如5秒),并对比正常请求的响应时间。同时,Burp Suite的Repeater模块可以显示精确的响应时间,非常有用。

4.3 常见绕过技巧与WAF应对思路

在实际审计中,目标系统可能部署了简单的WAF(Web应用防火墙)或代码层有简单的过滤。

  • 关键字过滤:如过滤了SELECTUNIONANDOR等。
    • 大小写绕过SeLeCt
    • 双写绕过SELSELECTECT
    • 编码绕过:URL编码、十六进制编码。SELECT->%53%45%4c%45%43%540x53454c454354
    • 注释符分割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 针对发现漏洞的修复方案

  1. MyBatis XML中的${}修复

    • 对于LIKE语句:改用#{},并在Java代码中拼接%
      <!-- 修复前 --> AND title LIKE '%${title}%' <!-- 修复后 --> AND title LIKE CONCAT('%', #{title}, '%')
      或者使用bind标签:
      <bind name="titleLike" value="'%' + title + '%'"/> AND title LIKE #{titleLike}
    • 对于排序字段、表名等动态内容:如果必须动态,应建立白名单机制。例如,对于排序字段:
      // Service层代码 private static final Set<String> 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}
  2. 注解SQL中的拼接修复:严格检查所有@Select@Update等注解,确保参数全部使用#{},杜绝字符串连接。

  3. SQL工具类/Wrapper修复:审查QueryWrapper等工具类的使用,确保传入likeeq等方法的是使用预编译方式传递的(MyBatis-Plus默认是安全的)。对于orderBygroupBy等接收列名的方法,必须对输入进行白名单校验。

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 漏洞挖掘后的思考与记录

每挖到一个漏洞,尤其是那种需要绕好几道弯才发现的二次注入或逻辑复杂的注入点,我都会做一个简单的记录:

  1. 漏洞文件与行号:精确定位。
  2. 触发路径:从用户请求到漏洞执行的完整调用链。
  3. 漏洞原理:一两句话说明为什么这里不安全。
  4. Payload示例:证明漏洞可用的有效Payload。
  5. 修复方案:具体的代码修改建议。

这份记录不仅是审计报告,更是你个人经验积累的宝贵财富。下次再审计类似系统或功能时,你会更快地定位到同类问题。

审计工作就像侦探破案,需要耐心、细心和对“犯罪模式”的熟悉。SQL注入虽然是一种古老的漏洞,但在复杂的业务代码和快速迭代的开发节奏下,它依然会以各种新的面貌出现。掌握从源码中系统性寻找SQL注入的方法,是每一个安全研究员和开发者的基本功。希望这篇基于ofcms的实战讲解,能帮你建立起清晰的审计思路。在实际操作中,最大的技巧往往就是“耐心”和“多问一句为什么这个参数可以这样传”。