SQL 注入是一种攻击技术,攻击者利用应用程序代码中构建动态 SQL 查询的缺陷。攻击者可以访问应用程序的特权部分,从数据库检索所有信息,篡改现有数据,甚至在数据库主机上执行危险的系统级命令。当开发人员在他们的 SQL 语句中连接或插入任意输入时,这种漏洞就会发生。
示例 #1 将结果集切割成页面……并创建超级用户(PostgreSQL)。
在下面的示例中,用户输入直接插入到 SQL 查询中,使得攻击者能够在数据库中获得超级用户账户。
<?php
$offset = $_GET['offset']; // 注意,没有输入验证!
$query = "SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;";
$result = pg_query($conn, $query);
?>
0; insert into pg_shadow(usename,usesysid,usesuper,usecatupd,passwd) select 'crack', usesysid, 't','t','crack' from pg_shadow where usename='postgres'; --
0;
是为了向原始查询提供有效的偏移量并终止。
注意:
这是常见的技术,使用 SQL 中的注释符号
--
,强制 SQL 解析器忽略开发者编写的查询的其余部分。
获取密码的一种可行方式是欺骗搜索结果页面。攻击者只需查看是否有已提交的未经适当处理变量在 SQL
语句中使用。这些过滤器通常可以在先前的表单中设置,以定制 SELECT
语句中的
WHERE、ORDER BY、LIMIT
和 OFFSET
子句。如果数据库支持
UNION
构造,攻击者可能会尝试将整个查询附加到原始查询中,以从任意表中列出密码。强烈建议仅存储密码的安全散列值,而不是密码本身。
示例 #2 列出文章……以及一些密码(任何数据库服务器)
<?php
$query = "SELECT id, name, inserted, size FROM products
WHERE size = '$size'";
$result = odbc_exec($conn, $query);
?>
SELECT
语句组合来显示所有密码:
' union select '1', concat(uname||'-'||passwd) as name, '1971-01-01', '0' from usertable; --
UPDATE
和 INSERT
语句也容易受到这种攻击的影响。
示例 #3 从重置密码……到获得更多权限(任何数据库服务器)
<?php
$query = "UPDATE usertable SET pwd='$pwd' WHERE uid='$uid';";
?>
' or uid like'%admin%
给 $uid 来改变 admin 的密码,或者简单设置
$pwd 为 hehehe', trusted=100, admin='yes
去获得更多权限,然后查询语句实际上就变成了:
<?php
// $uid: ' or uid like '%admin%
$query = "UPDATE usertable SET pwd='...' WHERE uid='' or uid like '%admin%';";
// $pwd: hehehe', trusted=100, admin='yes
$query = "UPDATE usertable SET pwd='hehehe', trusted=100, admin='yes' WHERE
...;";
?>
虽然攻击者必须具备至少一些关于数据库架构的知识才能进行成功的攻击,但获取这些信息通常非常简单。例如代码可以是开源软件的一部分并且公开可用。这些信息也可能通过闭源代码泄露——即使它经过了编码、混淆或编译——甚至通过自己的代码显示错误消息来泄露。其他方法包括使用典型的 table 和列名。例如,使用“users” table 和列名“id”、“username”和“password”的登录表单。
示例 #4 攻击数据库主机操作系统(MSSQL Server)
一种可怕的示例是一些数据库主机上可以访问操作系统级别的命令。
<?php
$query = "SELECT * FROM products WHERE id LIKE '%$prod%'";
$result = mssql_query($query);
?>
a%' exec master..xp_cmdshell 'net user test testpass /ADD' --
作为变量 $prod 的值,那么 $query 将会变成
<?php
$query = "SELECT * FROM products
WHERE id LIKE '%a%'
exec master..xp_cmdshell 'net user test testpass /ADD' --%'";
$result = mssql_query($query);
?>
sa
身份运行,并且 MSSQLSERVER 服务以足够的权限运行,则攻击者现在将拥有一个账户,可以用此账户访问这台机器。
注意:
以上的一些示例与特定的数据库服务器相关联,这并不意味着不能对其他产品进行类似的攻击。用户的数据库服务器可能以其他方式同样存在漏洞。
避免 SQL 注入的推荐方法是通过使用预处理语句绑定所有数据。仅仅使用参数化查询并不能完全避免 SQL 注入,但它是提供输入给
SQL 语句的最简单和最安全的方式。在 WHERE
、SET
和 VALUES
子句中,所有动态数据常量都必须替换为占位符。实际数据将在执行过程中进行绑定,并与 SQL 命令分开发送。
参数绑定只能用于数据。SQL 查询的其他动态部分必须根据已知的允许值列表进行筛选。
示例 #5 通过使用 PDO 预处理语句来避免 SQL 注入
<?php
// The dynamic SQL part is validated against expected values
$sortingOrder = $_GET['sortingOrder'] === 'DESC' ? 'DESC' : 'ASC';
$productId = $_GET['productId'];
// The SQL is prepared with a placeholder
$stmt = $pdo->prepare("SELECT * FROM products WHERE id LIKE ? ORDER BY price {$sortingOrder}");
// The value is provided with LIKE wildcards
$stmt->execute(["%{$productId}%"]);
?>
SQL 注入攻击主要是基于利用代码在编写时没有考虑安全性。永远不要相信任何输入,特别是来自客户端的输入,即使它来自于选择框、隐藏的输入字段或 cookie。第一个示例表明,即使是如此简单的查询也可能带来灾难。
深度防御策略涉及几种良好的编程实践:
除此之外,如果数据库支持日志记录,还可以从脚本里或通过数据库自身记录查询语句。显然,日志记录无法阻止任何有害尝试,但它可以帮助追踪绕过了哪个应用程序。日志本身并没有用处,但通过其中包含的信息可以得到帮助。通常情况下,更详细的信息比较少的信息更好。