最近项目交付,扫描出sql注入漏洞,我寻思spring开发框架底层应该解决了这些问题,就想研究一下是怎么解决注入的
学习mybatis时都知道 #{} 可以防注入,${}是可以注入,分别写下面两个方法
1 | xml复制代码<mapper namespace="com.example.ssm.mapper.UserMapper"> |
数据库连接字符串,注意别用ssl,否则抓包内容无法识别
1 | bash复制代码jdbc:mysql://localhost:3306/ssm?characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false |
两个 mapper 方法,根据用户名和密码查询表,如果有记录就返回 true,模拟简单的登录逻辑。前端通过 ' or '1 = 1
构造注入
login1 返回false,注入失败
login2 返回true, 注入成功
请求抓包
通过 Wireshark 来抓包,如果使用本机的数据库就选则回环网络,外网的就选联网的网卡
过滤器是 tcp.dstport == 3306 or tcp.srcport == 3306
login1 请求
select count(*) from user where username = 'admin' and password = 'dd'' or ''1 = 1 '
这个sql在or
两边增加了单引号,这样后面整体就是一个字符串,没有构成注入
login2 请求
select count(*) from user where username = 'admin' and password = 'dd' or '1 = 1 '
sql预编译
通过debug第一个请求,跟踪到
1 | java复制代码package org.apache.ibatis.executor.statement; |
login1 和 login2 执行过程中,在构造 StatementHandler
时选择的都是 PreparedStatementHandler
但 login1 的 boundSql 是带问号的,而 login2 的已经是拼接好参数的 sql。
所以login2对已经对注入成功的sql进行预编译,就达不到防注入的效果了。
看了这个类的方法 instantiateStatement
,里面有预编译的内容,打断点,可以看到会将带问号的sql进行预编译
查看 connection 的类型发现是 com.zaxxer.hikari.poolProxyConnection
,实际是调用
1 | java复制代码package com.mysql.cj.jdbc; |
跟踪到
1 | scala复制代码package com.mysql.cj.jdbc |
1 | arduino复制代码package com.mysql.cj |
ParseInfo 会根据问号把sql分成三段,存到 byte[][] staticSql
这个二维数组中,显示的都是对应的
ASCII的十进制
问号的index会添加到 endpointList
这个数组,
然后再循环这个数组将sql分成三段
1 | arduino复制代码this.staticSql = new byte[endpointList.size()][]; |
跟踪到最后调用底层socket发送数据
1 | scala复制代码package com.mysql.cj; |
在发送第二段sql时,bindValues[i]
中 or (111,114) 两边会加上单引号 (39)
下面查看这个引号是怎么加上的
参数处理
1 | scala复制代码package com.mysql.cj; |
有个判断 isEscapeNeededForString
这个方法是判读参数中是否有 \n \r \\ \' " \032
,如果有就会循环在这些符号的位置分别处理
在单引号出会额外添加一个单引号,这就是我们上面发现发送第二段sql,or 的两边都加了两个单引号
总结:
mybatis在向mysql发送执行sql前,会进行客户端的预编译(还有服务器端的预编译),使用#{}表达式会将其替换为问好进行预编译,而${}则是进行参数替换后的sql进行预编译,在发送请求拼接sql时,会将参数中产生注入的地方通过处理,使其在服务器端当作一个参数,消除注入风险。在看很多文章时都再说mybatis会对sql进行预编译,但是查看抓包都是一个完整sql的请求,一步一步查找,原来是通过对sql进行切割,然后拼接处理注入风险后的参数,达到预编译的效果。
参考:
本文转载自: 掘金