所谓SQL注入,就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令,比如先前的很多影视网站泄露VIP会员密码大多就是通过WEB表单递交查询字符暴出的,这类表单特别容易受到SQL注入式攻击。当应用程序使用输入内容来构造动态sql语句以访问数据库时,会发生sql注入攻击。如果代码使用存储过程,而这些存储过程作为包含未筛选的用户输入的字符串来传递,也会发生sql注入。黑客通过SQL注入攻击可以拿到网站数据库的访问权限,之后他们就可以拿到网站数据库中所有的数据,恶意的黑客可以通过SQL注入功能篡改数据库中的数据甚至会把数据库中的数据毁坏掉。
1、sql注入原理
1.1 sql注入产生原因
sql注入攻击是指利用设计上的漏洞,在目标服务器上运行Sql语句以及进行其他方式的攻击,动态生成Sql语句时没有对用户输入的数据进行验证是Sql注入攻击得逞的主要原因。对于java数据库连接JDBC而言,SQL注入攻击只对Statement有效,对PreparedStatement是无效的,这是因为PreparedStatement不允许在不同的插入时间改变查询的逻辑结构。
如验证用户是否存在的SQL语句为:
username= '用户名' and password = '密码'
如果在用户名字段中输入: 'or 1=1
或是在密码字段中输入: 'or 1=1
,将绕过验证,但这种手段只对只对Statement有效,对PreparedStatement无效。PreparedStatement相对Statement有以下优点:
- 防注入攻击
- 多次运行速度快
- 防止数据库缓冲区溢出
- 代码的可读性可维护性好
这四点使得PreparedStatement成为访问数据库的语句对象的首选,缺点是灵活性不够好,有些场合还是必须使用Statement。
1.2 sql注入原理
SQL注入能使攻击者绕过认证机制,完全控制远程服务器上的数据库。SQL是结构化查询语言的简称,它是访问数据库的事实标准。目前,大多数Web应用都使用SQL数据库来存放应用程序的数据。几乎所有的Web应用在后台都使用某种SQL数据库。跟大多数语言一样,SQL语法允许数据库命令和用户数据混杂在一起的。如果开发人员不细心的话,用户数据就有可能被解释成命令,这样的话,远程用户就不仅能向Web应用输入数据,而且还可以在数据库上执行任意命令了。
- SQL注入式攻击的主要形式有两种。一是直接将代码插入到与SQL命令串联在一起并使得其以执行的用户输入变量。上例就是采用了这种方法。由于其直接与SQL语句捆绑,故也被称为直接注入式攻击法。二是一种间接的攻击方法,它将恶意代码注入要在表中存储或者作为原书据存储的字符串。在存储的字符串中会连接到一个动态的SQL命令中,以执行一些恶意的SQL代码。注入过程的工作方式是提前终止文本字符串,然后追加一个新的命令。如以直接注入式攻击为例。就是在用户输入变量的时候,先用一个分号结束当前的语句。然后再插入一个恶意SQL语句即可。由于插入的命令可能在执行前追加其他字符串,因此攻击者常常用注释标记“—”来终止注入的字符串。执行时,系统会认为此后语句位注释,故后续的文本将被忽略,不被编译与执行。
1.3 sql注入分类
1.数字型注入
当输入的参数为整型时,则有可能存在数字型注入漏洞。
假设存在一条 URL 为:HTTP://www.aaa.com/test.php?id=1
,可以对后台的 SQL 语句猜测为:SELECT * FROM table WHERE id=1
,这时候判断数字型漏洞的 SQL 注入点:
-
① 先在输入框中输入一个单引号 ',这时 SQL 语句就会变为:
SELECT * FROM table WHERE id=1'
,不符合语法,所以该语句肯定会出错,导致脚本程序无法从数据库获取数据,从而使原来的页面出现异常。 -
② 在输入框中输入
and 1 = 1
,SQL语句变为:SELECT * FROM table WHERE id=1 and 1 = 1
,语句正确,执行正常,返回的数据与原始请求无任何差异。 -
③ 在数据库中输入
and 1 = 2
,SQL 语句变为:SELECT * FROM table WHERE id=1 and 1 = 2
,虽然语法正确,语句执行正常,但是逻辑错误,因为 1 = 2 为永假,所以返回数据与原始请求有差异。
如果以上三个步骤全部满足,则程序就可能存在数字型 SQL 注入漏洞。
2.字符型注入
当输入参数为字符串时,则可能存在字符型注入漏洞。
- 数字型与字符型注入最大的区别在于:数字型不需要单引号闭合,而字符型一般需要使用单引号来闭合。
- 字符型注入最关键的是如何闭合 SQL 语句以及注释多余的代码。
假设后台的 SQL 语句如下:SELECT * FROM table WHERE username = 'admin'
,这时候判断字符型漏洞的 SQL 注入点:
-
① 还是先输入单引号 admin’ 来测试,这时 SQL 语句就会变为:
SELECT * FROM table WHERE username = 'admin''
,页面异常。 -
② 输入:
admin' and 1 = 1 --
,注意在 admin 后有一个单引号 ',用于字符串闭合,最后还有一个注释符 --(两条杠后面还有一个空格!!!)。SQL 语句变为:SELECT * FROM table WHERE username = 'admin' and 1 = 1 --
,页面显示正确。 -
③ 输入:
admin' and 1 = 2 --
,SQL 语句变为:SELECT * FROM table WHERE username = 'admin' and 1 = 2 --
,页面错误。
满足上面三个步骤则有可能存在字符型 SQL 注入。
3.其他类型注入
其实我觉得 SQL 注入只有两种类型:数字型与字符型。很多人可能会说还有如:Cookie 注入、POST 注入、延时注入等。的确如此,但这些类型的注入归根结底也是数字型和字符型注入的不同展现形式或者注入的位置不同罢了。以下是一些常见的注入叫法:
- POST注入:注入字段在 POST 数据中
- Cookie注入:注入字段在 Cookie 数据中
- 延时注入:使用数据库延时特性注入
- 搜索注入:注入处为搜索的地方
- base64注入:注入字符串需要经过 base64 加密
2、sql注入简单示例
下面以网站登陆页面为例,讲述一下注入的基本流程。
2.1 初步注入–绕过验证,直接登录
公司网站登陆框如下:
可以看到除了账号密码之外,还有一个公司名的输入框,根据输入框的形式可以推出SQL的写法如下:
SELECT * From Table WHERE Name='XX' and Password='YY' and Corp='ZZ';
通过尝试发现前两项都做了一些检查,而第三个输入框却疏忽了,漏洞就在这里!注入开始,我们可以在输入框中输入以下内容:
这里我们可以随便填写用户名,密码留空,公司名输入 ' or 1=1--
,这种情况下点击登录按钮后竟然成功登录了。我们看一下最终的SQL就会找到原因:
SELECT * From Table WHERE Name='SQL inject' and Password='' and Corp='' or 1=1--';
从sql脚本中可以看出,前一半单引号被闭合,后一半单引号被“–”给注释掉,中间多了一个永远成立的条件“1=1”,这就造成任何字符都能成功登录的结果。而Sql注入的危害却不仅仅是匿名登录。
2.2 中级注入–借助异常获取信息
我们在第三个输入框中写入:‘ or 1=(SELECT @@version) –
。如下:
生成的SQL类似这样:
SELECT * From Table WHERE Name='SQL inject' and Password='' and Corp='' or 1=(SELECT @@VERSION)--';
可以看到判断条件变成了 1=(SELECT @@VERSION),这个写法肯定会导致错误,但出错正是我们想要的。点击登录后,页面出现以下信息:
Conversion failed when converting the nvarchar value 'Microsoft SQL Server 2008 (SP3) - 10.0.5500.0 (X64) Sep 21 2011 22:45:45 Copyright (c) 1988-2008 Microsoft Corporation Developer Edition (64-bit) on Windows NT 6.1 <X64> (Build 7601: Service Pack 1) ' to data type int.
可怕的事情出现了,服务器的操作系统和SQL Server版本信息竟然通过错误显示出来。
2.3 高级注入–获取库表字段名
危害进一步扩大,通过修改注入语句我们可以获取服务器所有的库名、表名、字段名。
获取库名
在输入框中输入如下信息:t' or 1=(SELECT top 1 name FROM master..sysdatabases where name not in (SELECT top 0 name FROM master..sysdatabases))--
,此时发现第三个输入框有字数长度的限制,然而这种客户端的限制形同虚设,直接通过Google浏览器就能去除。
点击登录,返回的信息如下:
Conversion failed when converting the nvarchar value 'master' to data type int.
数据库名称“master”通过异常被显示出来!依次改变上面SQL语句中的序号,就能得到服务器上所有数据库的名称。
获取表名
接着,输入信息如下:b' or 1=(SELECT top 1 name FROM master..sysobjects where xtype='U' and name not in (SELECT top 1 name FROM master..sysobjects where xtype='U'))--
,得到返回信息如下:
Conversion failed when converting the nvarchar value 'www_phpernote_article' to data type int.
我们得到了master数据库中的第一张表名:www_phpernote_article
,同上,依次改变序号,可得到该库全部表名。
获取字段名
现在我们以"www_phpernote_article”表为例,尝试获取该表中所有的字段名。在输入框中输入以下代码:b' or 1=(SELECT top 1 master..syscolumns.name FROM master..syscolumns, master..sysobjects WHERE master..syscolumns.id=master..sysobjects.id AND master..sysobjects.name='www_phpernote_article');
,得到错误提示如下:
Conversion failed when converting the nvarchar value 'xserver_name' to data type int.
这样第一个字段名“xserver_name”就出来了,依次改变序号,就能遍历出所有的字段名。
2.4 终级注入–获取数据
我们已知通过SQL注入能获取全部的数据库,表,及其字段,为了防止本文完全沦为注入教程,获取数据的代码就不再描述,而这篇文章的目的也已达到,SQL注入意味着什么?意味着数据库中所有数据都能被盗取。
3、如何防御sql注入攻击
- 采用预编译语句集,它内置了处理SQL注入的能力,只要使用它的setXXX方法传值即可。
使用好处:
(1)代码的可读性和可维护性.
(2)PreparedStatement尽最大可能提高性能.
(3)最重要的一点是极大地提高了安全性.
String sql= "select * from users where username=? and password=?;
PreparedStatement preState = conn.prepareStatement(sql);
preState.setString(1, userName);
preState.setString(2, password);
ResultSet rs = preState.executeQuery();
原理:sql注入只对sql语句的准备(编译)过程有破坏作用,而PreparedStatement已经准备好了,执行阶段只是把输入串作为数据处理,而不再对sql语句进行解析,准备,因此也就避免了sql注入问题.
- 使用正则表达式过滤传入的参数
正则表达式:
private String CHECKSQL = “^(.+)\\sand\\s(.+)|(.+)\\sor(.+)\\s$”;
// 判断是否匹配
Pattern.matches(CHECKSQL,targerStr);
下面是具体的正则表达式:
检测SQL meta-characters的正则表达式 :/(\%27)|(\')|(\-\-)|(\%23)|(#)/ix
修正检测SQL meta-characters的正则表达式 :/((\%3D)|(=))[^\n]*((\%27)|(\')|(\-\-)|(\%3B)|(:))/i
典型的SQL 注入攻击的正则表达式 :/\w*((\%27)|(\'))((\%6F)|o|(\%4F))((\%72)|r|(\%52))/ix
检测SQL注入,UNION查询关键字的正则表达式 :/((\%27)|(\'))union/ix(\%27)|(\')
检测MS SQL Server SQL注入攻击的正则表达式:/exec(\s|\+)+(s|x)p\w+/ix
等等…..
其实也可以简单的使用replace方法实现上述功能:
public static String TransactSQLInjection(String str) {
return str.replaceAll(".*([';]+|(--)+).*", " ");
}
- 字符串过滤
比较通用的一个方法:(||之间的参数可以根据自己程序的需要添加)
public static Boolean sql_inj(String str) {
String inj_str = "'|and|exec|insert|select|delete|update|count|*|%|chr|mid|master|truncate|char|declare|;|or|-|+|,";
String inj_stra[] = split(inj_str,"|");
for (int i=0 ; i < inj_stra.length ; i++ ) {
if (str.indexOf(inj_stra[i])>=0) {
return true;
}
}
return false;
}
- 检查URL是否包函非法字符
防止SQL从URL注入,sql_inj.java代码:
import java.net.*;
import java.io.*;
import java.sql.*;
import java.text.*;
import java.lang.String;
public class sql_inj{
public static Boolean sql_inj(String str) {
String inj_str = "'|and|exec|insert|select|delete|update|count|*|%|chr|mid|master|truncate|char|declare|;|or|-|+|,";
//这里的东西还可以自己添加
String[] inj_stra=inj_str.split("\\|");
for (int i=0 ; i < inj_stra.length ; i++ ) {
if (str.indexOf(inj_stra[i])>=0) {
return true;
}
}
return false;
}
}
- JSP页面添加客户端判断代码
使用javascript在客户端进行不安全字符屏蔽。
- 功能介绍:检查是否含有”‘”,”\”,”/”
- 参数说明:要检查的字符串
- 返回值:0:是 1:不是
function check(a) {
return 1;
fibdn = new Array (”‘” ,”\\”,”/”);
i=fibdn.length;
j=a.length;
for (ii=0; ii<i; ii++) {
for (jj=0; jj<j; jj++) {
temp1=a.charAt(jj);
temp2=fibdn[ii];
if (tem'; p1==temp2) {
return 0;
}
}
}
return 1;
}
- 不要把服务器错误信息暴露给用户
当应用程序出现错误的时候,如果没有做错误页面处理的话,会直接输出一些敏感的信息出来(如系统信息,中间件信息,查询语句等),有时候甚至会直接将项目所在的物理路径给显示出来,严重危害了系统安全,并且错误种类繁多,页面风格不一,导致用户体验不好,我们可以通过定义统一的报错页面,出现错误和异常时统一跳转到该页面。
4、经验教训
关于安全性,本文总结出以下几点:
- 对用户输入的内容要时刻保持警惕。
- 只有客户端的验证等于没有验证。
- 永远不要把服务器错误信息暴露给用户。
- SQL注入不仅能通过输入框,还能通过Url达到目的。
- 除了服务器错误页面,还有其他办法获取到数据库信息。
- 可通过软件模拟注入行为,这种方式盗取信息的速度要比你想象中快的多。
- 漏洞跟语言平台无关。
评论区