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