SQL注入介绍
SQL注入是代审中最容易找的漏洞之一,一般都在固定的模块存放SQL语句,只需在这些SQL语句中搜寻是否拼接参数即可。
案例项目使用Mybatis作为数据持久层框架,进行数据库的各种操作。MyBatis的主要思想是将程序中的大量SQL语句剥离出来,配置在配置文件当中,实现SQL的灵活配置。配置文件常存放在src/main/resources/mapper中,配置文件命名为ExampleMapper.xml
在项目的第一个issue中看到已经有人提出SQL注入漏洞,并给出poc: http://127.0.0.1:28089/search?goodsCategoryId=&keyword=\%')) UNION ALL SELECT NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,CONCAT(0x7176627871,IFNULL(CAST(CURRENT_USER() AS CHAR),0x20),0x7162786b71),NULL,NULL#&orderBy=default   
随后作者对该SQL注入点进行修复,commit中将like模糊查询处的${keyword}改为#{keyword}。
SQL注入产生原理
为什么将${}改为#{}就能防止SQL注入呢?
MyBatis官方文档中有如下叙述:
#{}告诉 MyBatis 创建一个预编译语句(PreparedStatement)参数,在 JDBC 中,这样的一个参数在 SQL 中会由一个“?”来标识,并被传递到一个新的预处理语句中,就像这样:
1  | // 近似的 JDBC 代码,非 MyBatis 代码...  | 

${} 仅仅是纯粹的 string 替换,在动态 SQL 解析阶段将会进行变量替换,类似于直接替换字符串,会导致SQL注入产生。
官方也给出警示:
用这种方式接受用户的输入,并将其用于语句中的参数是不安全的,会导致潜在的 SQL 注入攻击,因此要么不允许用户输入这些字段,要么自行转义并检验。
开发的原则是能使用#{}的地方,一定使用#{}。但是SQL语句中存在无法使用#{}的场景,因为使用#{}会在原本的字段加上引号'',导致SQL语句报错。不能使用#{}的场景我们需要特别注意,此处极易产生SQL注入。
不能使用#{}的场景有:
- 表名/字段名
 - order by/group by
 - like模糊查询
 - in
 
以表名为例,使用#{}替换字符串时会带上单引号 '',这会导致 sql 语法错误,例如:
1  | select * from #{tableName} where name = #{name};  | 
预编译之后的sql 变为:
1  | select * from ? where name = ?;  | 
假设我们传入的参数为 tableName = “user” , name = “username”,那么在占位符进行变量替换后,sql 语句变为
1  | select * from 'user' where name='username';  | 
上述 sql 语句是存在语法错误的,表名不能加单引号 ''(不过反引号 ``是可以的)。
不过表名一般不会通过用户传入,即使是用户传入,由于Mybatis的查询机制,并不会产生SQL注入。
避免使用${}的方法
- 表名/字段名
 
尽量直接使用表名和字段名,如果有动态查询的需求时,将表名和字段名限定在指定字符。
1  | tableName = tableName.replaceAll("[^a-zA-Z0-9+]", "");  | 
- order by/group by
 
order by后
修复方法是推荐开发在Java层面做映射,设置一个字段/表名数组,仅允许用户传入索引值。这样保证传入的字段或者表名都在白名单里面。
1  | query.append(" ORDER BY ");  | 
或者将传入的值限定在指定字符,如
1  | orderByField = orderByField.replaceAll("[^a-zA-Z0-9+]", "");  | 
- like模糊查询
 
1  | mysql:  | 
- in
 
1  | select * from goods where id in  | 
SQL注入代码审计
知道了SQL注入产生的原理,那么找漏洞就非常简单了。SQL注入是代码审计中最好找的漏洞之一,只需分析SQL语句发现拼接,再逆向追踪拼接参数用户是否可控。不用被代码的多层调用所干扰。
SQL注入审计过程:
- 在Dao层(Mybaits在Mapper中,Mybatis也有注解写SQL的方式,但很少用),查看SQL语句是否使用拼接,关注
${} 
1  | <select id="getUID" parameterType="string" reusltType="User">  | 
其他SQL拼接:
1  | # JDBC中的拼接,关注+:  | 
- 若存在拼接参数,则逆向追踪拼接的参数传入过程,逆向追踪参数的路径大致为
Mapper -> Dao -> ServiceImpl -> Controller 
⚠️ 并不是全部的${}拼接都会产生漏洞的,有以下几种情况是不存在SQL注入的:
- param不是用户传参进来的
 - param不是字符类型,比如说parameter为int类型,只能传入数字,就没法产生SQL注入
 - param在过程中已经转义或过滤字符,但是在Mybaits的SQL语句中看不出来,需考虑是否能绕过
 
参数追踪
以文章开头的SQL注入为例,来进行keyword参数逆向追踪过程
/src/main/resources/mapper/NewBeeMallGoodsMapper.xml:70,94存在${}拼接

可以看到在like后面使用了concat拼接,这是因为mapper.xml是使用mybatis-generator自动生成的,产生的like语句和in语句默认使用#{},但这里使用${}拼接字符可能是由于作者修改功能时更改,导致SQL注入漏洞产生。
同文件第三行,可以看到namespace为ltd.newbee.mall.dao.NewBeeMallGoodsMapper
/src/main/resources/mapper/NewBeeMallGoodsMapper.xml:3

找到ltd.newbee.mall.dao.NewBeeMallGoodsMapper,根据xml中的select id找到findNewBeeMallGoodsList和findNewBeeMallGoodsListBySearch这两个方法。
/src/main/java/ltd/newbee/mall/dao/NewBeeMallGoodsMapper.java

查看findNewBeeMallGoodsListBySearch方法的引用,追踪该处引用(Sublime将鼠标放在方法上可直接查看引用,IntelliJ IDEA可右键“Find Usages”或Option/Alt+F7查看引用)

追踪到searchNewBeeMallGoods方法,这里是Service层,主要负责业务模块逻辑处理。Service层中有两种类,一是Service,用来声明接口;二是ServiceImpl,作为实现类实现接口中的方法。当前类NewBeeMallGoodsServiceImpl中的Impl就是implement(实现)中的impl。
由于Service中都是接口,审计时一般直接查看ServiceImpl,忽视Service。
/src/main/java/ltd/newbee/mall/service/impl/NewBeeMallGoodsServiceImpl.java:73

再追踪searchNewBeeMallGoods方法引用,来到searchPage方法。这里是Controller层,负责业务模块流程的控制,获取用户传来的参数后调用Service层的接口来控制业务流程。
SpringBoot使用注解来控制URL路径,searchNewBeeMallGoods使用@GetMapping({"/search", "/search.html"})表明接收来自/search或/search.html的get请求。
/src/main/java/ltd/newbee/mall/controller/mall/GoodsController.java:57

从48行可以看到,keyword为字符串类型,可以传入任意字符。51行将params.get("keyword")中的值赋给keyword变量,仅做了非空判断。
其中if判断注释写着“去掉空格”,并不是将keyword参数中的空格去掉,而是去掉空格之后进行非空判断,不用考虑SQL注入绕过空格的方法。
1  | //对keyword做过滤 去掉空格  | 
整个参数追踪就到这,还有一个需要注意的地方就是看看应用中是否存在过滤器,过滤器是否会将特殊字符拦截。该应用没有针对SQL注入的过滤器,所以追踪完参数,可以确定该处SQL拼接存在注入漏洞。
总结
对于SQL注入的审计,在Mapper中搜寻是否存在${}拼接的情况,尤其注意order by、group by、like、in。找到拼接后再逆向追踪参数,判断参数是否可控,是否是字符类型,检查是否存在过滤器过滤SQL字符。
参考链接
https://mybatis.org/mybatis-3/zh/sqlmap-xml.html#select