站在 java 的角度探讨 SQL 注入原理

在大二就接触过 sql 注入,之前一直在学习 windows 逆向研究方向,认为 web 安全以后不是自己的从业方向,当时也就没有深入研究,工作多年来,本人也一直从事安全开发相关工作,不过随着 java 的市场份额越来越重,在工作中接触 java 的机会也越来越多,也是机缘巧合的契机,自己开始走向了偏 java 开发的道路。

在最近工作中接触到一个项目,其代码风格极其不堪入目,更严重的是 DAO 部分存在大量 SQL 注入的隐患,所以趁这个机会,作者复习研究了一把 SQL 注入相关的知识,在这里与大家探讨一下。

1.靶场准备

首先我们来准备一个 web 接口服务,该服务可以提供管理员的信息查询,这里我们采用 springboot + jersey 来构建 web 服务框架,数据库则采用最常用的 mysql。

下面,我们来准备测试环境,首先建立一张用户表 jwtk_admin,创建表的 SQL 如下:

然后插入一条默认的管理员

这样我们就有了两位系统内置管理员了,管理员密码采用 MD5 进行 Hash,当然这是一个很简单的为了作为研究靶场的表,所以没有过多的字段。

接下来,我们创建 spring boot + jersey 构建的 RESTFul web 服务,这里我们提供了一个通过管理员用户名查询管理员具体信息的接口,如下:

2.注入测试

首先我们以开发者正向思维向 web 服务发送管理员查询请求,这里我们用 PostMan 工具发送一个 GET 请求,请求与结果如下图所示:

不出我们和开发者所料,Web 接口返回了我们想要的结果,用户名为 admin 的管理员信息。OK,现在开发任务完成,Git PUSHJira 任务点为待测试,那么这样的接口就真的没有问题了吗?现在我们发送一条 GET 请求,如下:

发送该请求后,我们发现 PostMan 没有接收到返回结果,而 Web 服务后台却开始抛 MySQLSyntaxErrorException 异常了,错误如下:

You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''xxxx''' at line 1

原因是在我们查询的 xxxx' 处 sql 语句语法不正确导致。这里我们先不讨论 SQL 语法问题,我们继续实验,再次构造一条 GET 查询请求如下:

此时我们可以惊讶的发现,查询接口非但没有报错,反而将我们数据库 jwti_admin 表中的所有管理员信息都查询出来了:

这是什么鬼,难道管理员表中还有叫 name=xxxx’ or ‘a’=‘a 的用户?这就是 SQL Injection。

3.注入原理分析

我们的接口接受了一个 String 类型的 name 参数,并且在接口中通过字符串拼接的方式构建了查询语句。在正常情况下,用户会传入合法的 name 进行查询,但是黑客却会传入精心构造的参数,只要参数通过字符串拼接后依然是一句合法的 SQL 查询,此时 SQL 注入就发生了。

正如我们上文输入的 name=xxxx’ or ‘a’=‘a 与我们接口中的查询语句进行拼接后构成如下 SQL 语句:

当接口执行此句 SQL 后,系统后台也就拱手送给黑客了,黑客一看到管理员密码这个 hash,都不用去查 Cmd5 了,直接用 123456 密码去登录你的后台系统了。

Why?因为 123456 的 md5 哈希太常见了,别笑,这就是很多中小网站的现实,弱口令横行,不见棺材不落泪!

好了,现在我们应该明白了,SQL Injection 原因就是由于传入的参数与系统的 SQL 拼接成了合法的 SQL 而导致的,而其本质还是将用户输入的数据当做了代码执行。在系统中只要有一个 SQL 注入点被黑客发现,那么黑客基本上可以执行任意想执行的 SQL 语句了,例如添加一个管理员,查询所有表,甚至“脱裤” 等等,当然本文不是讲解 SQL 注入技巧的文章,这里我们只探讨 SQL 注入发生的原因与防范方法。

4.jdbc 下的预处理

在上文的接口中,DAO 使用了比较基础的 JDBC 的方式进行数据库操作,JDBC 的直接使用在比较老的系统中还是比较常见的,但这并不意味着使用 JDBC 就一定不安全,如果我将传入的参数 xxxx'or'a'='a 整体作为参数进行 name 查询,那就不会产生 SQL 注入。

在 JDBC 中,提供了 PreparedStatement (预处理执行语句)的方式,进行查询参数化,使用预处理后的代码如下:

同样,我们使用上文的注入方式注入,此时我们发现,SQL 注入失败了。

现在,我们来打印一下,被看看被预处理后的 SQL 有什么变化:

看到了吗?所有的 都被转义掉了,这样就防止了 SQL 注入。

5.MyBatis 下的传参

MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。

MyBatis 可以对配置和原生 Map 使用简单的 XML 或注解,将接口和 Java 的 POJOs(Plain Old Java Objects, 普通的 Java 对象)映射成数据库中的记录,因此 mybatis现在在市场中采用率也非常高。

这里我们定义如下一个 mapper,来实现通过用户名查询管理员的接口:

同样提供 Web 访问接口:

接下来,我们尝试 SQL 注入 name 字段,可以发现注入没有成功,可以看到 mybatis 框架对参数进行了预处理处理,所以没有发生注入:

那是否只要使用了 Mybatis 就一定可以避免 SQL 注入的危险?

我们把 mapper 修改为如下所示,将参数 #{name} 修改为 ${name},此时使用 name='xxxx'or'a'='a' 作为接口参数就可以发现 SQL 注入还是发生了:

那这是为什么,mybatis ${}#{} 的差别在哪里?

原来在 mybatis 中如果以 ${} 形式声明为 SQL 传递参数,mybatis 将不会进行参数预处理,会直接动态拼接 SQL 语句,此时就会存在被注入的风险,所以在使用 mybatis 作为持久框架时应尽量避免使用 ${} 的形式进行参数传递,如果无法避免,那就对传入参数自行进行转义过滤(有些 SQL 如 like、in、order by 等,程序员可能依旧会选择 ${} 的方式传参)。

6.JPA 下的注入

JPA 是 Sun 公司未来整合 ORM 技术,实现天下归一的 ORM 标准而定义的 Java Persistence API(java 持久层 API)。

JPA 只是一套接口,目前引入 JPA 的项目都会采用 Hibernate 作为具体实现,随着无配置 Spring Boot 框架的流行,JPA 越来越具有作为持久化首选的技术,因为其能让程序员写更少的代码,就能完成现有的功能,例如强大的 JpaRepository,常规的 SQL 查询只需按照命名规则定义接口,便可以不写SQL(JPQL/SQL)就可以实现数据的查询操作,从 SQL 注入防范的角度来说,安全责任抛给框架远比依靠程序员保险,因此如果项目使用 JPA 基本上很大程度的消除了 SQL 注入的风险。

但是话不能说的太死,在我见过的一个项目中,虽然项目采用了 JPA 作为持久框架,但是由于一位老程序员不习惯于使用 JPQL 来构建查询接口,依旧使用字符串拼接的方式来实现业务,为项目安全埋下了隐患。

做安全就是 100 - 1 = 0,即使你防御了 99% 的攻击,那不算胜利,只要有一次被入侵了,那就有可能给公司带来灭顶之灾。关于 JPA 的 SQL 注入,我们就不详细讨论了,只要遵循 JPA 的开发规范,程序员就无需担心注入问题,框架都为你做好幕后工作了。

7.SQL 注入的其他防范方式

很多公司都会存在老系统中有大量 SQL 注入风险代码的问题,但是由于其已稳定支持公司业务很久,不宜采用大面积代码更新的方式来消除注入隐患,所以需要考虑其他方式来防范 SQL 注入。除了在 SQL 执行方式上防范 SQL 注入,很多时候还可以通过架构上,或者通过其他过滤方式来达到防止 SQL 注入的效果。

1、对于接口的调用参数,要进行格式匹配,例如 admin 的通过 name 查询的接口,与之匹配的 Path 应该使用正则匹配(因为用户名中不应该存在特殊字符),从而确保传入参数是程序控制范围之内的参数数。

2、前端尽量静态化,尽量少的暴露可以访问到 DAO 层的接口到公网环境中,如果现有项目,很难修改存在注入的代码,可以考虑在 web 服务之前增加 WAF 进行流量过滤,当然代码上就不给 hacker 留有攻击的漏洞才最好的方案。也可以在拥有 nginx 的架构下,采用 OpenRestry 做流量过滤,将一些特殊字符进行转义处理。

8.小结

其实随着 ORM 技术的发展,Java web 开发在大趋势上已经越来越远离 SQL 注入的问题了,有 Entity Framework 框架支持的 ASP.NET MVC 从来都是高冷范。

现在互联网中,使用 PHP 和 Python 构建的 web 应用是目前 SQL 注入的重灾区。本文虽然是从 JAVA 的角度来研究 SQL 注入的问题,但原理上同样适用于其他开发语言,希望读者可以通过此文,触类旁通。最后说一句,珍爱数据,远离拼接!

package com.tarena.dingdang.filter; 02 03 import java.io.IOException; 04 import java.util.Enumeration; 05 06 import javax.servlet.Filter; 07 import javax.servlet.FilterChain; 08 import javax.servlet.FilterConfig; 09 import javax.servlet.ServletException; 10 import javax.servlet.ServletRequest; 11 import javax.servlet.ServletResponse; 12 import javax.servlet.http.HttpServletRequest; 13 14 public class AntiSqlInjectionfilter implements Filter { 15 16 public void destroy() { 17 // TODO Auto-generated method stub 18 } 19 20 public void init(FilterConfig arg0) throws ServletException { 21 // TODO Auto-generated method stub 22 } 23 24 public void doFilter(ServletRequest args0, ServletResponse args1, 25 FilterChain chain) throws IOException, ServletException { 26 HttpServletRequest req=(HttpServletRequest)args0; 27 HttpServletRequest res=(HttpServletRequest)args1; 28 //获得所有请求参数名 29 Enumeration params = req.getParameterNames(); 30 String sql = ""; 31 while (params.hasMoreElements()) { 32 //得到参数名 33 String name = params.nextElement().toString(); 34 //System.out.println("name===========================" + name + "--"); 35 //得到参数对应值 36 String[] value = req.getParameterValues(name); 37 for (int i = 0; i < value.length; i++) { 38 sql = sql + value[i]; 39 } 40 } 41 //System.out.println("============================SQL"+sql); 42 //有sql关键字,跳转到error.html 43 if (sqlValidate(sql)) { 44 throw new IOException("您发送请求中的参数中含有非法字符"); 45 //String ip = req.getRemoteAddr(); 46 } else { 47 chain.doFilter(args0,args1); 48 } 49 } 50 51 //效验
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值