写点什么

mybatis 中如何防止 sql 注入和传参

  • 2021 年 12 月 24 日
  • 本文字数:8670 字

    阅读完需:约 28 分钟

环境

使用 mysql,数据库名为 test,含有 1 表名为 users,users 内数据如下



JDBC 下的 SQL 注入

在 JDBC 下有两种方法执行 SQL 语句,分别是 Statement 和 PrepareStatement,即其中,PrepareStatement 为预编译

Statement

SQL 语句


SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'
复制代码


当传入数据为


username = adminpassword = adminSELECT * FROM users WHERE username = 'admin' AND password = 'admin';
复制代码


即当存在 username=admin 和 password=admin 的数据时则返回此用户的数据


万能密码:admin' and 1=1#


【一>所有资源获取<一】1、200 份很多已经买不到的绝版电子书 2、30G 安全大厂内部的视频资料 3、100 份 src 文档 4、常见安全面试题 5、ctf 大赛经典题目解析 6、全套工具包 7、应急响应笔记 8、网络安全学习路线



最终的 sql 语句变为了


SELECT \* FROM users WHERE username = \'admin\' and 1=1#
复制代码


即返回用户名为 admin,同时 1=1 的所有数据,1=1 恒为真,所以始终返回所有数据


如果输入的时:admin' or 1=1#就会返回所有数据,因为 admin' or 1=1 恒为真


所以 JDBC 使用 Statement 是不安全的,需要程序员做好过滤,所以一般使用 JDBC 的程序员会更喜欢使用 PrepareStatement 做预编译,预编译不仅提高了程序执行的效率,还提高了安全性

PreParedStatement

与 Statement 的区别在于 PrepareStatement 会对 SQL 语句进行预编译,预编译的好处不仅在于在一定程度上防止了 sql 注入,还减少了 sql 语句的编译次数,提高了性能,其原理是先去编译 sql 语句,无论最后输入为何,预编译的语句只是作为字符串来执行,而 SQL 注入只对编译过程有破坏作用,执行阶段只是把输入串作为数据处理,不需要再对 SQL 语句进行解析,因此解决了注入问题


因为 SQL 语句编译阶段是进行词法分析、语法分析、语义分析等过程的,也就是说编译过程识别了关键字、执行逻辑之类的东西,编译结束了这条 SQL 语句能干什么就定了。而在编译之后加入注入的部分,就已经没办法改变执行逻辑了,这部分就只能是相当于输入字符串被处理


而 Statement 方法在每次执行时都需要编译,会增大系统开销。理论上 PrepareStatement 的效率和安全性会比 Statement 要好,但并不意味着使用 PrepareStatement 就绝对安全,不会产生 SQL 注入。


PrepareStatement 防御预编译的写法是使用?作为占位符然后将 SQL 语句进行预编译,由于?作为占位符已经告诉数据库整个 SQL 语句的结构,即?处传入的是参数,而不会是 sql 语句,所以即使攻击者传入 sql 语句也不会被数据库解析


String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
复制代码


//预编译 sql 语句


PreparedStatement pstt = connection.prepareStatement(sql);pstt.setString(1,username);pstt.setString(2, password);
复制代码


ResultSet resultSet = pstt.executeQuery();//返回结果集,封装了全部的产部的查询结果


首先先规定好 SQL 语句的结构,然后在对占位符进行数据的插入,这样就会对 sql 语句进行防御,攻击者构造的 paylaod 会被解释成普通的字符串,我们可以通过过输出查看最终会变成什么 sql 语句



可以发现还会对单引号进行转义,一般只能通过宽字节注入,下面将会在代码的层面展示为什么预编译能够防止 SQL 注入,同时解释为什么会多出一个转义符

不安全的预编译

拼接

总所周知,sql 注入之所以能被攻击者利用,主要原因在于攻击者可以构造 payload,虽然有的开发人员采用了预编译但是却由于缺乏安全思想或者是偷懒会直接采取拼接的方式构造 SQL 语句,此时进行预编译则无法阻止 SQL 注入的产生


代码(稍稍替换一下上面的代码):


//创建sql语句String sql = "SELECT * FROM users WHERE username = '" + req.getParameter("username") + "' AND password = '" + req.getParameter("password") + "'";System.out.println(sql);//预编译sql语句PreparedStatement pstt = connection.prepareStatement(sql);ResultSet resultSet = pstt.executeQuery(sql);//返回结果集,封装了全部的产部的查询结果
复制代码


这样即使使用了预编译,但是预编译的语句已经是被攻击者构造好的语句,所以无法防御 SQL 注入



又或者是前面使用?占位符后,又对语句进行拼接,也会导致 SQL 注入


想要做到阻止 sql 注入,首先要做到使用?作为占位符,规定好 sql 语句的结构,然后在后面不破坏结构

使用 in 语句

String sql = \"delete from users where id in(\"+delIds+\");


此删除语句大多用在复选框内,在 in 当中使用拼接而不使用占位符做预编译的原因是因为很多时候无法确定 deIds 里含有多少个对象


输入:1,2


正常只会输出 id 为 1 和 2 的值


如果此时输入:1,2) or 1=1#


就会形成 SQL 注入,输出苦库里所有的值



正确写法:


还是要用到预编译,所以我们要对传入的对象进行处理,首先确定对象的个数,然后增加同量的占位符?以便预编译


public int gradeDelete(Connection con, String delIds) throws Exception{    String num = "";    //将对象分割开来,分割的点以实际而定    String[] spl = delIds.split(".");
//根据对象的个数添加同量的占位符?,用来预编译 for(int i = 0; i< spl.length; i++){ if(i == 0){ num += "?"; } else { num += ".?"; } } String sql = "delete from users where id in("+num+")"; prepareStatement pstmt = con.prepareStatement(sql); try { for(int j = 0; j < spl.length; j++){ pstmt.setInt(j+1, integer.parseint(spl[j])); } return pstmt.executeUpdate(); } catch(Exception e){ // }
return 0;}
复制代码


以 bilibili 的删除视频为例,当我取消收藏夹复数个视频的收藏时抓到的包为



892223071%3A2%2C542789708%3A2%2C507228005%3A2%2C422244777%3A2%2C549672309%3A2%2C719381183%3A2%2C976919238%3A2%2C722053417%3A2解码后为892223071:2,542789708:2,507228005:2,422244777:2,549672309:2,719381183:2,976919238:2,722053417:2
复制代码


可以发现其以:2,分割,那我们只需在 split 中填写


String\[\] spl = delIds.split(\":2,\");


即可,结果为:



然后再使用预编译

使用 like 语句

boolean jud = true;String sql = "select * from users ";System.out.println("请输入要查询的内容:");String con = sc.nextLine();for (int i = 0; i < con.length(); i++){    if(!Character.isDigit(con.charAt(i))){        jud = false;        break;    }}if(jud){    sql += "where password like '%" + con + "%'";}else{    sql += "where username like '%" + con + "%'";}
复制代码


当用户输入的为字符串则查询用户名和密码含有输入内容的用户信息,当用户输入的为纯数字则单查询密码,用拼接地方式会造成 SQL 注入


正常执行:




SQL 注入



正确写法


首先我们要将拼接的地方全部改为?做占位符,但是使用占位符后要使用 setString 来把传入的参数替换占位符,所以我们要先进行判断,判断需要插入替换多少个占位符


boolean jud = true;int v = 0;String sql = "select * from users ";System.out.println("请输入要查询的内容:");String con = sc.nextLine();for (int i = 0; i < con.length(); i++){    if(!Character.isDigit(con.charAt(i))){        jud = false;        break;    }}if(jud){    sql += "where password like ?";    v = 1;}else{    sql += "where username like ? and password like ?";    v = 2;}
//预编译sql语句PreparedStatement pstt = connection.prepareStatement(sql);if(v == 1){ pstt.setString(1, "%"+con+"%");}else if (v == 2){ pstt.setString(1, "%"+con+"%"); pstt.setString(2, "%"+con+"%");}
复制代码



尝试进行 SQL 注入



发现被转义了

使用 order by 语句

通过上面对使用 in 关键字和 like 关键字发现,只需要对要传参的位置使用占位符进行预编译时似乎就可以完全防止 SQL 注入,然而事实并非如此,当使用 order by 语句时是无法使用预编译的,原因是 order by 子句后面需要加字段名或者字段位置,而字段名是不能带引号的,否则就会被认为是一个字符串而不是字段名,然而使用 PreapareStatement 将会强制给参数加上',我在下面会在代码层面分析为什么会这样处理参数


所以,在使用 order by 语句时就必须得使用拼接的 Statement,所以就会造成 SQL 注入,所以还要在过滤上做好防御的准备

调试分析 PrepareStatement 防止 SQL 注入的原理

进入调试,深度查看 PrepareStatement 预编译是怎么防止 sql 注入的


用户名输入 admin,密码输入 admin',目的是查看预编译如何对一个合理的字符串以及一个不合理的字符串进行处理



由于我们输入的 username 和 password 分别是 admin 和 admin',而其中 admin'属于非法值,所以我们只在


pstt.setString(2, password);


打上断点,然后步入 setString 方法



步过到 2275 行,这里有一个名为 needsQuoted 的布尔变量,默认为 true



然后进入 if 判断,其中有一个方法为 isEscapeNeededForString



步入后发现有一个布尔的 needsHexEscape,默认为 false,然后将字符串,也就是传入的参数 admin'进行逐字解析,判断是否有非法字符,如果有则置 needsHexEscape 为 true 且 break 循环,然后返回 needsHexEscape


由于我们传入的是 admin',带有'单引号,所以在 switch 的过程中会捕捉到,置 needsHexEscape = true 后直接 break 掉循环,然后直接返回 needsHexEscape



向上返回到了 setString 方法,经过 if 判断后运行 if 体里面的代码,首先创建了一个 StringBuilder,长度为传入参数即 admin+2,然后分别在参数的开头和结尾添加'



简单来说,此 switch 体的作用就是对正常的字符不做处理,直接向 StringBuilder 添加同样的字符,如果非法字符,则添加转移后的非法字符,由于不是直接的替换,而是以添加的方式,简单来说就是完全没有使用到用户传入的的参数,自然就做到了防护



我们传入的为 admin',被 switch 捕捉到后'后会在 StringBuilder 添加\和',最终我们的 admin'会变为'admin\',也就是'admin',同样防止了 SQL 注入最重要的一环--闭合语句


然后根据要插入占位符的位置进行插入


Mybatis 下的 SQL 注入

Mybatis 的两种传参方式

首先要了解在 Mybatis 下有两种传参方式,分别是{}以及#{},其区别是,使用{}的方式传参,mybatis 是将传入的参数直接拼接到 SQL 语句上,二使用 #{}传参则是和 JDBC 一样转换为占位符来进行预编译


在 #{}下运行的结果:


select * from users where username = #{username} and password = #{password}



在 ${}下运行的结果:


select * from users where username = "${username}" and password = "${password}"


SQL 注入

${}

PeopleMapper 设置


<select id="getPeopleList1" resultType="com.g1ts.People">    select * from users where username = #{username} and password = #{password}</select>
复制代码


正常运行:


username:adminpassword:admin
复制代码



sql 注入:


username:admin" and 1=1#password:sef
复制代码



成功 sql 注入

#{}

Mapper 设置


<select id="getPeopleList2" resultType="People">    select * from users where username = #{username} and password = #{password}</select>
复制代码


正常运行


username:adminpassword:admin
复制代码



尝试 SQL 注入


username:admin" and 1=1#password:sef
复制代码



SQL 注入失败

使用 like 语句

正确写法


mysql:    select * from users where username like concat('%',#{username},'%')oracle:    select * from users where username like '%'||#{username}||'%'sqlserver:    select * from users where username like '%'+#{username}+'%'
复制代码

使用 in 语句

正确写法


mysql:    select * from users where username like concat('%',#{username},'%')oracle:    select * from users where username like '%'||#{username}||'%'sqlserver:    select * from users where username like '%'+#{username}+'%'
复制代码

使用 order by 语句

和 JDBC 同理,使用 #{}方式传参会导致 order by 语句失效,所以使用 order by 语句的时候还是需要做好过滤

调试分析 Mybatis 防止 SQL 注入的原理

本人学艺不精,一直定位定位不到 XMLScriptBuilder 上,所以只好看一下别人写的 mybatis 解析过程,通过解析过程来定位方法位置


先说结论,首先 Mybatis 会先对 mapper 里面的 SQL 语句进行判断,判断内容为是以 ${}传参还是以 #{}传参,如果以 #{}传参则使用?作为占位符进行预编译,Mybatis 只会对 SQL 语句的占位符做一定的处理,处理传入参数最后的步骤还是调用会 JDBC 的预编译


完整调用流程:


${}解析执行过程


首先在 XMLScriptBuilder 中的 parseDynamicNode()



在这里进行了一次判断,先说结论,这个 isDynamic 的判断其实就是判断 mapper.xml 中的 sql 语句是使用 #{}预编译还是使{}则进入 DynamicSqlSource,否则进入 RawSqlSource


进入 parseDynamicTags 方法,可以发现有两种情况会使 isDynamic 为 true,而其中 isDynamic()就是用来判定的



进入 isDynamic()



可以看到运行了两个方法,分别是 DynamicCheckerTokenParser()以及 createParser(),主要出在 createParser()上,查看方法体



发现调用了 GenericTokenParser 方法,传入了 openToken、closeToken 以及 handler 三个参数,其中 openToken 的值为{}方式传参,进入到 GenericTokenParser 方法



然而只是单纯的设置变量的值,一直向上返回到 isDynamic(),进入下一条语句 parser.parse(this.text);


在调试使就可清楚看到传入的值了,${}和 sql 语句同时出现,猜测就是在这里进行了匹配



进入到 parse 方法,此方法对 sql 语句进行解析,当遇到 ${}的字段则将此位置空(null),从返回的 StringBuilder 值可以看出



执行完后返回到 isDynamic()方法下,在 return 值递归,其实就是返回 isDynamic 的值,然后向上返回到一直返回到了 parseScriptNode()方法



最终结果就会创建一个 DynamicSqlSource 对象


至此,对 SQL 语句的解析告一段落,直到运行到 peopleMapper.getPeopleList1(people1),步入到 invoke 方法



前面的方法大致就是获取传入的参数和获取 sql 语句,步进到 execute 方法,此方法作用是判断 SQL 语句的类型



由于我们的 SQL 语句使 select,所以会落在 witch 体的 select 内,步入 case select 的 excuteForMany 方法



继续步入 selectList 方法,后面这里我不知道准确流程是怎么样的,反正经过我一番调试最终到了 query 方法这里,然后步入 getBoundSql 方法



步入 getBoundSql 方法后可以看一下参数,发现 sqlSource 的类型正是前面设置的 DynamicSqlSource



继续步入 getBoundSql 方法,然后步进到 rootSqlNode.apply 方法



这里有有个坑点啊,可能是因为我技术不够,由于这个 apply 方法有很多个实现,直接步进会跑到 MixerSqlNode 里面,但我查阅了资料发现实际上是在 TextSqlNode 里面


步入 createParser 方法,发现调用了 GenericTokenParser,这在上面解析的过程也是一样的



从 parse 方法中返回的 StringBuider 可以发现,已经成功将参数和 SQL 语句拼接在一起了


#{}解析执行过程

在前面分析{}方式传参还是使用 #{}方式传参,如果是 #{}方式则最终会调用 RawSqlSource 方法



步入 RawSqlSource 方法



继续运行,步入到 sqlSourceParser.parse 方法



可以发现出现了解析 ${}时用到的函数


GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);


进入方法体后发现目的是设置 openToken 和 closeToken 的值分别为 #{和}



真正对 SQL 语句进行了操作的是


String sql = parser.parse(originalSql);


步入 parser.parse 方法,运行到结尾后查看 StringBuilder 的值,发现函数把 #{}用?替换了




到此,解析过程结束,一直运行到 peopleMapper.getPeopleList1(people1),步入到 invoke 方法,然后前面的流程大致和 ${}解析一致,进入 mapperMethod.execute 方法,然后会判断执行的 sql 语句类型,然后进入 executeForMany 方法,一直运行到 selectList 方法,最后进入 query 方法



query 方法会调用自身作为返回值



在此方法的返回值又会调用 delegate.query 方法,而这个方法就是我执行 #{}的方法,进入后一直运行到


else {    list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}
复制代码


后进入



进入 queryFromDatabase 方法后运行到


try {    list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);}
复制代码


进入 doQuery 方法,进入 prepareStatement()方法




其中


Connection connection = this.getConnection(statementLog);


是与数据库建立连接的对象


步入 parameterize()方法



继续步入,到 setParameters 方法



setParameters 方法的作用,是将 SQL 语句和传入的参数进行拼接



List<ParameterMapping> parameterMappings = this.boundSql.getParameterMappings();中,获取了 boundSql,即获取到了设置的 sql 语句



ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);


获取到了 SQL 语句中所需要的参数,我的 SQL 语句为 select * from users whereusername = #{username} and password = #{password},所以需要 username 和 password 两个参数



运行到



步入 setParameter 方法



在图示处打上断点,步入 setNonNullParameter 方法



继续在图示处打上断点,步入 setParameter 方法



继续在图示处打上断点,步入 setNonNullParameter 方法



虽然方法名是一样的,但是并不是同一个方法,步入 setString 方法



这里用到了动态代理,最终还是调用回了 jdbc 的 preperStatement,在图示处打上断点并步入



发现这个 setString 和上文所讲的 JDBC 的预编译使用一个函数,后面的编译方式与 JDBC 相同

Hibernate

Hibernate 执行语句的两种方法

Hibernate 可以使用 hql 来执行 SQL 语句,也可以直接执行 SQL 语句,无论是哪种方式都有可能导致 SQL 注入

Hibernate 下的 SQL 注入

HQL

hql 语句:


String hql = "from People where username = '" + username + "' and password = '" + password + "'";
复制代码


首先先观察一下正常登录和错误登陆下的的情况


正常登录:


Hibernate:     /* from    People where    username = 'admin'     and password = 'admin' */ select        people0_.id as id1_0_,        people0_.username as username2_0_,        people0_.password as password3_0_     from        users people0_     where        people0_.username='admin'         and people0_.password='admin'admin
复制代码


错误登陆:


Hibernate:     /* from    People where    username = 'admin'     and password = 'adadawd' */ select        people0_.id as id1_0_,        people0_.username as username2_0_,        people0_.password as password3_0_     from        users people0_     where        people0_.username='admin'         and people0_.password='adadawd'
复制代码


可以发现之间的区别在于成功登录后最后面返回了用户名


尝试进行 SQL 注入:


输入:


请输入用户名:admin' or '1'='1请输入密码qwer
复制代码


返回:


Hibernate:     /* from    People where    username = 'admin'     or '1'='1'     and password = 'qwer' */ select        people0_.id as id1_0_,        people0_.username as username2_0_,        people0_.password as password3_0_     from        users people0_     where        people0_.username='admin'         or '1'='1'         and people0_.password='qwer'admin
复制代码


可以发现,经过拼接后,SQL 语句变为了


from People where username = 'admin' or '1'='1' and password = 'qwer'


说明了使用这种拼接的方式和 jdbc 以及 mybatis 是一样会产生 sql 注入的


正确写法:


正确使用以下几种 HQL 参数绑定的方式可以有效避免注入的产生


位置参数(Positional parameter)


String parameter = "g1ts";Query<User> query = session.createQuery("from users name = ?1", User.class);query.setParameter(1, parameter);
复制代码


命名参数(named parameter)


Query<User> query = session.createQuery("from users name = ?1", User.class);String parameter = "g1ts";Query<User> query = session.createQuery("from users name = :name", User.class);query.setParameter("name", parameter);
复制代码


命名参数列表(named parameter list)


List<String> names = Arrays.asList("g1ts", "g2ts");Query<User> query = session.createQuery("from users where name in (:names)", User.class);query.setParameter("names", names);
复制代码


类实例(JavaBean)


user1.setName("g1ts");Query<User> query = session.createQuery("from users where name =:name", User.class);query.setProperties(user1);
复制代码

SQL

Hibernate 支持使用原生 SQL 语句执行,所以其风险和 JDBC 是一致的,直接使用拼接的方法时会导致 SQL 注入


语句如下:Query<People> query = session.createNativeQuery("select * from user where username = '" + username + "' and password = '" + password + "'");


正确写法


String parameter = "g1ts";Query<User> query = session.createNativeQuery("select * from user where name = :name");query.setParameter("name",parameter);
复制代码

调试分析 Hibernate 预防 SQL 注入原理

Hibernate 框架最终还是使用了 JDBC 中预编译防止 SQL 注入的方法


完整过程


查看一下 hibernate 预编译的过程


首先在


List\<People> list = query.list();


打下断点,步入



步入 list 方法






继续步入 list 方法




步入 doList 方法








步入 bind 方法



步入 nullSafeSet 方法




步入 getBinder 方法



最后调用的 st.setString 就是 jdbc 的 setString 方法

用户头像

我是一名网络安全渗透师 2021.06.18 加入

关注我,后续将会带来更多精选作品,需要资料+wx:mengmengji08

评论

发布
暂无评论
mybatis中如何防止sql注入和传参