引言
目前值得高兴的是,开发者在建立网站时,已经开始关注安全问题了——几乎每个开发者都知道SQL注入漏洞了。在本文中,我将为读者介绍另一种与SQL数据库相关的漏洞,虽然它的危害性与SQL注入不相上下,但目前却很少为人所知。接下来,我将为读者详细展示这种攻击手法,以及相应的防御策略。
背景知识
最近,我遇到了一段有趣的代码,它尝试尽一切可能来保护数据库的访问安全,例如每当新用户进行注册时,将运行以下代码:
[size=1em]1
[size=1em]2
[size=1em]3
[size=1em]4
[size=1em]5
[size=1em]6
[size=1em]7
[size=1em]8
[size=1em]9
[size=1em]10
[size=1em]11
[size=1em]12
[size=1em]13
[size=1em]14
[size=1em]15
[size=1em]16
[size=1em]17
[size=1em]18
[size=1em]19
[size=1em]20
[size=1em]21
[size=1em]22
| [size=1em][size=1em]<?php
[size=1em]// Checking whether a user with the same username exists
[size=1em]$username = mysql_real_escape_string($_GET['username']);
[size=1em]$password = mysql_real_escape_string($_GET['password']);
[size=1em]$query = "SELECT *
[size=1em] FROM users
[size=1em] WHERE username='$username'";
[size=1em]$res = mysql_query($query, $database);
[size=1em]if($res) {
[size=1em] if(mysql_num_rows($res) > 0) {
[size=1em] // User exists, exit gracefully
[size=1em] .
[size=1em] .
[size=1em] }
[size=1em] else {
[size=1em] // If not, only then insert a new entry
[size=1em] $query = "INSERT INTO users(username, password)
[size=1em] VALUES ('$username','$password')";
[size=1em] .
[size=1em] .
[size=1em] }
[size=1em]}
|
为了验证登录信息,将用到下列代码:
[size=1em]1
[size=1em]2
[size=1em]3
[size=1em]4
[size=1em]5
[size=1em]6
[size=1em]7
[size=1em]8
[size=1em]9
[size=1em]10
[size=1em]11
[size=1em]12
[size=1em]13
[size=1em]14
| [size=1em][size=1em]<?php
[size=1em]$username = mysql_real_escape_string($_GET['username']);
[size=1em]$password = mysql_real_escape_string($_GET['password']);
[size=1em]$query = "SELECT username FROM users
[size=1em] WHERE username='$username'
[size=1em] AND password='$password' ";
[size=1em]$res = mysql_query($query, $database);
[size=1em]if($res) {
[size=1em] if(mysql_num_rows($res) > 0){
[size=1em] $row = mysql_fetch_assoc($res);
[size=1em] return $row['username'];
[size=1em] }
[size=1em]}
[size=1em]return Null;
|
安全注意事项周全吗?
过滤用户输入参数了吗? - 检查了
使用单引号(')来增加安全性了吗? - 检查了
很好,还有什么可能出错的地方吗?
是的,攻击者依然能够以任意用户身份进行登录!
攻击手法
在谈论这种攻击手法之前,首先需要介绍几个至关重要的知识点。
1. 在处理SQL中的字符串时,字符串末尾的空格字符都会被删除。换句话说,“vampire”与“vampire ”几乎是等效的,这在大多数情况下是正确的,例如WHERE子句中的字符串或INSERT语句中的字符串。例如,以下语句的查询结果,与使用用户名“vampire”进行查询时的结果是一样的。
[size=1em]1
| [size=1em][size=1em] SELECT * FROM users WHERE username='vampire ';
|
但是,除此之外也确实存在例外情况,例如LIKE子句。注意,对尾部空白字符的这种修剪操作,主要是在“字符串比较”期间进行的。这是因为,SQL会在内部使用空格来填充字符串,以便在比较之前使其它们的长度保持一致。
2. 在任意INSERT查询中,SQL会根据varchar(n)来限制字符串的最大长度,也就是说,如果字符串的长度大于“n”个字符的话,那么仅使用字符串的前“n”个字符。例如,如果特定列的长度约束为“5”个字符,那么在插入字符串“vampire”时,实际上只能插入字符串的前5个字符,即“vampi”。
现在,让我们建立一个测试数据库来演示具体攻击过程。
[size=1em]1
[size=1em]2
[size=1em]3
[size=1em]4
[size=1em]5
| [size=1em][size=1em]vampire@linux:~$ mysql -u root -p
[size=1em]mysql> CREATE DATABASE testing;
[size=1em]Query OK, 1 row affected (0.03 sec)
[size=1em]mysql> USE testing;
[size=1em]Database changed
|
我将创建一个数据表users,它有两列,即username和password。并且,这两个字段的最大长度为25个字符。接下来,我将插入一行记录,其中以“vampire”作为用户名,以“my_password”作为密码。
[size=1em]1
[size=1em]2
[size=1em]3
[size=1em]4
[size=1em]5
[size=1em]6
[size=1em]7
[size=1em]8
[size=1em]9
[size=1em]10
[size=1em]11
[size=1em]12
[size=1em]13
[size=1em]14
[size=1em]15
| [size=1em][size=1em]mysql> CREATE TABLE users (
[size=1em] -> username varchar(25),
[size=1em] -> password varchar(25)
[size=1em] -> );
[size=1em]Query OK, 0 rows affected (0.09 sec)
[size=1em]mysql> INSERT INTO users
[size=1em] -> VALUES('vampire', 'my_password');
[size=1em]Query OK, 1 row affected (0.11 sec)
[size=1em]mysql> SELECT * FROM users;
[size=1em]+----------+-------------+
[size=1em]| username | password |
[size=1em]+----------+-------------+
[size=1em]| vampire | my_password |
[size=1em]+----------+-------------+
[size=1em]1 row in set (0.00 sec)
|
为了展示尾部空白字符的修剪情况,我们可以输入下列命令:
[size=1em]1
[size=1em]2
[size=1em]3
[size=1em]4
[size=1em]5
[size=1em]6
[size=1em]7
[size=1em]8
| [size=1em][size=1em]mysql> SELECT * FROM users
[size=1em] -> WHERE username='vampire ';
[size=1em]+----------+-------------+
[size=1em]| username | password |
[size=1em]+----------+-------------+
[size=1em]| vampire | my_password |
[size=1em]+----------+-------------+
[size=1em]1 row in set (0.00 sec)
|
现在,假设一个易受攻击的网站使用了前面提到的PHP代码来处理用户的注册和登录。为了入侵任意用户的帐户(就本例来说,用户名为“vampire”),只需使用用户名“vampire[一些空白字符]1”和一个随机密码进行注册即可。对于选择的用户名,前25个字符应该只包含vampire和空白字符。这样做的好处是,将有助于绕过检查特定用户名是否已存在的查询。
[size=1em]1
[size=1em]2
[size=1em]3
| [size=1em][size=1em]mysql> SELECT * FROM users
[size=1em] -> WHERE username='vampire 1';
[size=1em]Empty set (0.00 sec)
|
需要注意的是,在执行SELECT查询语句时,SQL是不会将字符串缩短为25个字符的。因此,这里将使用完整的字符串进行搜索,所以不会找到匹配的结果。接下来,当运行INSERT查询语句时,它只会插入前25个字符。
[size=1em]1
[size=1em]2
[size=1em]3
[size=1em]4
[size=1em]5
[size=1em]6
[size=1em]7
[size=1em]8
[size=1em]9
[size=1em]10
[size=1em]11
[size=1em]12
| [size=1em][size=1em]mysql> INSERT INTO users(username, password)
[size=1em] -> VALUES ('vampire 1', 'random_pass');
[size=1em]Query OK, 1 row affected, 1 warning (0.05 sec)
[size=1em]mysql> SELECT * FROM users
[size=1em] -> WHERE username='vampire';
[size=1em]+---------------------------+-------------+
[size=1em]| username | password |
[size=1em]+---------------------------+-------------+
[size=1em]| vampire | my_password |
[size=1em]| vampire | random_pass |
[size=1em]+---------------------------+-------------+
[size=1em]2 rows in set (0.00 sec)
|
很好,如果现在搜索“vampire”的话,将返回两个用户。注意,第二个用户名实际上是“vampire”加上尾部的18个空格。现在,如果使用用户名“vampire”和密码“random_pass”登录的话,则所有搜索该用户名的SELECT查询都将返回第一个数据记录,也就是原始的数据记录。这样的话,攻击者就能够以原始用户身份登录。
这个攻击已经在MySQL和SQLite上成功通过测试。我相信它同样适用于其他数据库下。
防御措施
显然,要想开发安全的软件,必须对这种安全漏洞严加防范。下面是我们可采取的几项防御措施:
1. 应该为要求/预期具有唯一性的那些列添加UNIQUE约束。这实际上是一个非常重要的软件开发规则。即使您的代码已经提供了完整性检查,也要正确定义您的数据。由于'username'列具有UNIQUE约束,所以插入另一个记录将是不可能的。这两个字符串将被视为等同的,并且INSERT查询将失败。
2. 最好使用'id'作为数据库表的主键。此外,数据应该通过程序中的id进行跟踪。
3. 为了增加安全性,您还可以手动方式将输入参数修剪为特定长度(具体长度可以视数据库的中设置而定)。 |