阅读 364

mybatis 是怎样防止sql注入的

最近项目交付,扫描出sql注入漏洞,我寻思spring开发框架底层应该解决了这些问题,就想研究一下是怎么解决注入的

学习mybatis时都知道 #{} 可以防注入,${}是可以注入,分别写下面两个方法

<mapper namespace="com.example.ssm.mapper.UserMapper">     <select id="login1" resultType="integer">         select count(*) from user where username = #{username} and password = #{password}     </select>     <select id="login2" resultType="integer">         select count(*) from user where username = '${username}' and password = '${password}'     </select> </mapper> 复制代码

数据库连接字符串,注意别用ssl,否则抓包内容无法识别

jdbc:mysql://localhost:3306/ssm?characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false 复制代码

两个 mapper 方法,根据用户名和密码查询表,如果有记录就返回 true,模拟简单的登录逻辑。前端通过 ' or '1 = 1 构造注入

image.png login1 返回false,注入失败

image.png login2 返回true, 注入成功

通过 Wireshark 来抓包,如果使用本机的数据库就选则回环网络,外网的就选联网的网卡

image.png

过滤器是 tcp.dstport == 3306 or tcp.srcport == 3306

login1 请求

8885f160c7ea5987bc9c570eee6e585.png select count(*) from user where username = 'admin' and password = 'dd'' or ''1 = 1 ' 这个sql在or两边增加了单引号,这样后面整体就是一个字符串,没有构成注入

请求抓包

login2 请求

f0524322a8e8d1ed027c42065b7556d.png select count(*) from user where username = 'admin' and password = 'dd' or '1 = 1 '

sql预编译

通过debug第一个请求,跟踪到

package org.apache.ibatis.executor.statement;     public class RoutingStatementHandler implements StatementHandler 复制代码

login1 和 login2 执行过程中,在构造 StatementHandler 时选择的都是 PreparedStatementHandler 但 login1 的 boundSql 是带问号的,而 login2 的已经是拼接好参数的 sql。

7677d593df3c44d1fab3a7e9784257e.png 所以login2对已经对注入成功的sql进行预编译,就达不到防注入的效果了。 image.png

看了这个类的方法 instantiateStatement,里面有预编译的内容,打断点,可以看到会将带问号的sql进行预编译

image.png

查看 connection 的类型发现是 com.zaxxer.hikari.poolProxyConnection,实际是调用

package com.mysql.cj.jdbc;        public class ConnectionImpl implements JdbcConnection, SessionEventListener, Serializable             public java.sql.PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException  复制代码

image.png

跟踪到

package com.mysql.cj.jdbc     public class ClientPreparedStatement extends com.mysql.cj.jdbc.StatementImpl implements JdbcPreparedStatement         public ClientPreparedStatement(JdbcConnection conn, String sql, String db, ParseInfo cachedParseInfo) throws SQLException  复制代码

image.png

package com.mysql.cj     public class ParseInfo         public ParseInfo(String sql, Session session, String encoding, boolean buildRewriteInfo) 复制代码

ParseInfo 会根据问号把sql分成三段,存到 byte[][] staticSql 这个二维数组中,显示的都是对应的 ASCII的十进制

问号的index会添加到 endpointList 这个数组, image.png 然后再循环这个数组将sql分成三段

this.staticSql = new byte[endpointList.size()][]; 复制代码

4d42241dd38e33fc65ce53cf8e07229.png 跟踪到最后调用底层socket发送数据

package com.mysql.cj;     public abstract class AbstractPreparedQuery<T extends QueryBindings<?>> extends AbstractQuery implements PreparedQuery<T>         public <M extends Message> M fillSendPacket(QueryBindings<?> bindings)  复制代码

在发送第二段sql时,bindValues[i] 中 or (111,114) 两边会加上单引号 (39) image.png

下面查看这个引号是怎么加上的

参数处理

package com.mysql.cj;     public class ClientPreparedQueryBindings extends AbstractQueryBindings<ClientPreparedQueryBindValue>         public void setString(int parameterIndex, String x)  复制代码

有个判断 isEscapeNeededForString

image.png

这个方法是判读参数中是否有 \n \r \\ \' " \032,如果有就会循环在这些符号的位置分别处理 07de7a08b4368c5a769eb15113d4757.png 在单引号

 伪原创工具 SEO网站优化  https://www.237it.com/ 

出会额外添加一个单引号,这就是我们上面发现发送第二段sql,or 的两边都加了两个单引号 image.png

总结: mybatis在向mysql发送执行sql前,会进行客户端的预编译(还有服务器端的预编译),使用#{}表达式会将其替换为问好进行预编译,而${}则是进行参数替换后的sql进行预编译,在发送请求拼接sql时,会将参数中产生注入的地方通过处理,使其在服务器端当作一个参数,消除注入风险。在看很多文章时都再说mybatis会对sql进行预编译,但是查看抓包都是一个完整sql的请求,一步一步查找,原来是通过对sql进行切割,然后拼接处理注入风险后的参数,达到预编译的效果。


作者:weigram
链接:https://juejin.cn/post/7035248301083459621

文章分类
代码人生
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐