网络安全Java代码审计实战
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.1.3 常见Java SQL注入

1.SQL语句参数直接动态拼接

在常见的场景下SQL注入是由SQL语句参数直接动态拼接的,典型漏洞的示例代码如下:

上述代码首先加载数据库驱动,然后进行数据库的连接,通过“request.getParameter("id")”获取了传入id的值,并通过“"SELECT*FROM\"IWEBSEC\".\"user\" WHERE\"id\"="+id”直接进行了SQL语句的拼接,然后通过st.executeQuery执行SQL语句。此代码id参数可控并且进行SQL语句的拼接,存在明显SQL注入漏洞。输入“http://www.any.com/index.jsp?id=1”,返回id=1的数据信息,如图2-5所示。

图2-5 返回id=1的数据信息

输入“http://www.any.com/index.jsp?id=1 union select null,null,SYS_CONTEXT('USERENV','CURRENT_USER')from dual”,返回联合查询后的数据信息,如图2-6所示。

图2-6 返回联合查询后的数据信息

2.预编译有误

上面我们讲述了使用Statement执行SQL语句和使用PrepareStatement执行SQL语句的方法,使用PrepareStatement执行SQL语句是因为预编译参数化查询能够有效地防止SQL注入。那么是否能将使用Statement执行SQL语句的方式丢弃掉,使用PrepareStatement执行SQL语句防止SQL注入?

答案是否定的,很多开发者因为个人开发习惯的原因,没有按照PrepareStatement正确的开发方式进行数据库连接查询,在预编译语句中使用错误编程方式,那么即使使用了SQL语句拼接的方式,同样也会产生SQL注入漏洞。

典型漏洞代码如下:

上述代码就是典型的预编译错误编程方式,虽然id参数使用了PrepareStatement进行SQL查询,但是后面的username使用了SQL语句拼接的方式“sql+="and username like'%"+username+"%'";”,将username参数进行了拼接,这样导致了SQL注入漏洞的产生。传入的username值为“"user%'or'1'='1'#"”,如图2-7所示。执行完此代码后,会造成SQL注入,将user表中所有的数据输出。

3.order by注入

是否在预编译语句中按照规范正确编程就能防止SQL注入?答案是否定的,因为在有些特殊情况下不能使用PrepareStatement,比较典型的就是使用order by子句进行排序。order by子句后面需要加字段名或者字段位置,而字段名是不能带引号的,否则就会被认为是一个字符串而不是字段名。PrepareStatement是使用占位符传入参数的,传递的字符都会有单引号包裹,“ps.setString(1,id)”会自动给值加上引号,这样就会导致order by子句失效。例如:正常的order by子句为“SELECT*FROM user order by id;”,如图2-8所示执行SQL语句后会返回按照id排序后的结果。

图2-7 将user表中所有的数据输出

PrepareStatement预编译后的子句“SELECT*FROM user order by'id';”,如图2-9所示,因为id被单引号包裹,order by子句失效,执行完成后并没有按照id列进行排序。

图2-8 返回按照id排序后的结果

图2-9 order by子句失效

综上所述,当使用order by子句进行查询时,需要使用字符拼接的方式,在这种情况下就有可能存在SQL注入。要防御SQL注入,就要进行关键字符串过滤。典型的漏洞代码如下:

因为order by只能使用字符串拼接的方式,当使用“String sql="SELECT*FROM user"+"order by"+id”进行id参数拼接时,就出现了SQL注入漏洞。id参数传入的值为“String id="2 or 1=1"”,因为存在SQL注入漏洞,故当执行完成后会将所有的user表中信息输出,如图2-10所示。

图2-10 将所有的user表中信息输出

4.%和_模糊查询

在java预编译查询中不会对%和_进行转义处理,而%和_刚好是like查询的通配符,如果没有做好相关的过滤,就有可能导致恶意模糊查询,占用服务器性能,甚至可能耗尽资源,造成服务器宕机。如图2-11所示,当传入的username为“"%user%"”时,通过动态调试发现数据库在执行时并没有将%进行转义处理,而是作为通配符进行查询的。

图2-11%作为通配符进行查询

对于此攻击方式最好的防范措施就是进行过滤,此类攻击场景大多出现在查询的功能接口中,直接将%进行过滤就是最简单和有效的方式。

5.MyBatis中#{}和${}的区别

#{}在底层实现上使用“?”作为占位符来生成PreparedStatement,也是参数化查询预编译的机制,这样既快又安全。

${}将传入的数据直接显示生成在SQL语句中,类似于字符串拼接,可能会出现SQL注入的风险。

1)安全示例代码

与前面MyBatis执行SQL语句中的示例代码一样,在“userMapper.xml”定义SQL映射文件中设置的是“#{id}”安全写法。

定义主体测试代码文件mybaitstest.java,设置传入的id的值为“1 and 1=2 union select 1,database(),3”。

运行完成后,因为使用的是“#{id}”安全写法,所以进行参数化查询不会造成注入,运行完成后只输出了id为1的数据信息,如图2-12所示。

图2-12 只输出了id为1的数据信息

2)不安全示例代码示例

与上面完全相同的代码案例,如果在“userMapper.xml”定义SQL映射文件中设置的是“${id}”,不安全的写法如下。

前面讲到了${id}不会进行SQL参数化查询,如果传入的数据没有经过过滤就有可能出现SQL注入,运行完成后,因为主体测试代码文件mybaitstest.java设置传入的id的值为“1 and 1=2 union select 1,database(),3”,所以输出了SQL注入后数据库的数据信息test,如图2-13所示。

图2-13 输出了SQL注入后数据库的数据信息test

6.MyBatis常见SQL注入漏洞

1)order by查询

在前面order by注入中已经讲到,order by子句不能使用参数化查询的方式,只能使用字符拼接的方式,而在MyBatis中#{}是进行参数化查询的,如果在MyBatis的order by子句中使用#{},则order by子句会失效,例如:“SELECT*FROM user order by#{id};”;要使用order by子句只能使用${},例如:“SELECT*FROM user order by${id};”。但${}可能会存在SQL注入漏洞,要避免SQL注入漏洞就要进行过滤。MyBatis框架order by子句使用#{}:

运行完成后,输出的结果并没有根据age进行排序,因为使用#{}参数化查询后order by子句失效,如图2-14所示。

图2-14 使用#{}参数化查询后order by子句失效

这是由于通过MyBatis的日志插件“MyBatis Log Plugin”查看运行的SQL语句是“SELECT*FROM user order by'age';”,使用#{}参数化查询会将age进行单引号包裹而导致order by失效,如图2-15所示。

图2-15 将age进行单引号包裹而导致order by子句失效

在MyBatis框架中order by子句使用${}:

运行完成后,输出的结果根据age进行排序,说明使用${}查询后order by子句正常,如图2-16所示。

图2-16 用${}查询后order by子句正常

通过MyBatis的日志插件“MyBatis Log Plugin”查看运行的SQL语句是“SELECT*FROM users order by age;”,这样就可以正常运行了,但是${}使用的是字符串拼接的方式,很有可能会存在SQL注入漏洞,如图2-17所示。

图2-17 字符串拼接的方式很有可能会存在SQL注入漏洞

2)like查询

MyBatis的like子句中使用#{}程序会报错,例如:“select*from users where name like'%#{user}%'”;为了避免报错只能使用${},例如:“select*from users where name like'%${user}%'”;但${}可能会存在SQL注入漏洞,要避免SQL注入漏洞就要进行过滤。

MyBatis框架like子句使用#{}:

运行后,程序报错并无法正常运行,如图2-18所示。

图2-18 程序报错并无法正常运行

在MyBatis框架中like子句使用${}:

运行后,程序正常运行,输出查询后的结果信息,如图2-19所示。

但是${}使用的是字符串拼接的方式,很有可能会存在SQL注入漏洞,如果user传入的是“user'union select 1,database(),3#”就会造成SQL注入漏洞。

图2-19 输出查询后的结果信息

3)in参数

MyBatis框架的in子句中使用#{}与${},参数类似于“'user1','user2','user3','user4'”,多个参数时结果也会有不同。在MyBatis的in子句中使用#{}会将多个参数当作一个整体。

MyBatis的in子句中使用#{}参数化查询,会将“select*from users where name in(#{user})”转变为“select*from users where name like(''user1','user2','user3','user4'')”,这样把“'user1','user2','user3','user4'”当作一个整体,偏离了原来的程序设计逻辑,无法查到数据,如图2-20所示。

图2-20 偏离原来的程序设计逻辑

为了避免这个问题,只能使用${}。

Mybatis的in子句中使用${}参数化查询,会将“select*from users where name in(#{user})”转变为“select*from users where name like('user1','user2','user3','user4')”,这是正常的程序设计逻辑,输出查询数据,但是${}使用的是字符串拼接的方式很有可能会存在SQL注入漏洞,如图2-21所示。

图2-21 ${}使用的是字符串拼接的方式很有可能会存在SQL注入漏洞