SQL注入

SQL注入:明明设置了强密码,为什么还会被别人登录?

XSS 是黑客通过篡改 HTML 代码,来插入并执行恶意脚本的一种攻击。SQL 注入和 XSS 攻击很类似,都是黑客通过篡改代码逻辑发起的攻击。

我们会将应用的用户信息存储在数据库中。每次用户登录时,都会执行一个相应的 SQL 语句。这时,黑客会通过构造一些恶意的输入参数,在应用拼接 SQL 语句的时候,去篡改正常的 SQL 语意,从而执行黑客所控制的 SQL 查询功能。这个过程,就相当于黑客“注入”了一段 SQL 代码到应用中。

  1. 修改 WHERE 语句
uName = getRequestString("username");
uPass = getRequestString("password");

sql = 'SELECT * FROM Users WHERE Username ="' + uName + '" AND Password ="' + uPass + '"'

当用户提交一个表单(假设 Username 为 admin,Password 为 123456)时,Web 将执行下面这行代码:

SELECT * FROM Users WHERE Username ="admin" AND Password ="123456"

当用户正常地输入自己的用户名和密码时,自然就可以成功登录应用。那黑客想要在不知道密码的情况下登录应用,他会输入 " or ""="。这时,应用的数据库就会执行下面这行代码:

SELECT * FROM Users WHERE Username ="" AND Password ="" or ""=""

WHERE 语句后面的判断是通过 or 进行拼接的,其中""=""的结果是 true。那么,当有一个 or 是 true 的时候,最终结果就一定是 true 了。因此,这个 WHERE 语句是恒为真的,所以,数据库将返回全部的数据。

  1. 执行任意语句

在原语句的后面,插入额外的 SQL 语句,来实现任意的增删改查操作。

在 MySQL 中,实现任意语句执行最简单的方法,就是利用分号将原本的 SQL 语句进行分割。这样,我们就可以一次执行多个语句了。比如,下面这个语句在执行的时候会先插入一个行,然后再返回 Users 表中全部的数据。

INSERT INTO Users (Username, Password) VALUES("test","000000");
 SELECT * FROM Users;

在用户完成登录后,应用通常会通过 userId 来获取对应的用户信息。

uid = getRequestString("userId");
sql = "SELECT * FROM Users WHERE UserId = " + uid;

在这种情况下,黑客只要在传入的 userId 参数中加入一个分号,就可以执行任意的 SQL 语句了。比如,黑客想“删库跑路”的话,就令 userId 为 1;DROP TABLE Users,那么,后台实际执行的 SQL 就会变成下面这行代码,而数据库中所有的用户信息就都会被删除。

SELECT * FROM Users WHERE UserId = 1;DROP TABLE Users
SELECT * FROM Users WHERE UserId = 1;DROP TABLE Users

通过 SQL 注入攻击,黑客能做什么?

  1. 绕过验证

" or ""=" 作为万能密码,可以让黑客在不知道密码的情况下,通过登录认证。因此,SQL 注入最直接的利用方式,就是绕过验证,也就相当于身份认证被破解了。

  1. 任意篡改数据

通过插入 DML 类的 SQL 语句(INSERT、UPDATE、DELETE、TRUNCATE、DROP 等),黑客就可以对表数据甚至表结构进行更改,这样数据的完整性就会受到损害。黑客通过插入 DROP TABLE Users,删除数据库中全部的用户。

  1. 窃取数据

黑客通过类似 SQL 注入的手段,获取到数据库中的全部数据(如用户名、密码、手机号等隐私数据)。最简单的,黑客利用 UNION 关键词,将 SQL 语句拼接成下面这行代码之后,就可以直接获取全部的用户信息了。

SELECT * FROM Users WHERE UserId = 1 UNION SELECT * FROM Users
  1. 消耗资源

在 Web 后台中,黑客可以利用 WHILE 打造死循环操作,或者定义存储过程,触发一个无限迭代等等。在这些情况下,数据库服务器因为 CPU 被迅速打满,持续 100%,而无法及时响应其他请求。

总结来说,通过 SQL 注入攻击,黑客可以绕过验证登录后台,非法篡改数据库中的数据;还能执行任意的 SQL 语句,盗取用户的隐私数据影响公司业务等等。所以,SQL 注入相当于让黑客直接和服务端的数据库进行了交互。应用的本质是数据,黑客控制了数据库,也就相当于控制了整个应用。

如何进行 SQL 注入防护 ?

使用 PreparedStatement、使用存储过程和验证输入。

  1. 使用 PreparedStatement

当数据库在处理一个 SQL 命令的时候,大致可以分为两个步骤:

  • 将 SQL 语句解析成数据库可使用的指令集。我们在使用 EXPLAIN 关键字分析 SQL 语句,就是干的这个事情;
  • 将变量代入指令集,开始实际执行。之所以在批量处理 SQL 的时候能够提升性能,就是因为这样做避免了重复解析 SQL 的过程。

SQL 注入是在解析的过程中生效的,用户的输入会影响 SQL 解析的结果。因此,我们可以通过使用 PreparedStatement,将 SQL 语句的解析和实际执行过程分开,只在执行的过程中代入用户的操作。这样一来,无论黑客提交的参数怎么变化,数据库都不会去执行额外的逻辑,也就避免了 SQL 注入的发生。

String sql = "SELECT * FROM Users WHERE UserId = ?";
PreparedStatement statement = connection.prepareStatement(sql);
statement.setInt(1, userId); 
ResultSet results = statement.executeQuery();

  1. 使用存储过程

它的原理和使用 PreparedStatement 类似,都是通过将 SQL 语句的解析和执行过程分开,来实现防护。区别在于,存储过程防注入是将解析 SQL 的过程,由数据库驱动转移到了数据库本身。

delimiter $$  #将语句的结束符号从分号;临时改为两个$$(可以是自定义)
CREATE PROCEDURE select_user(IN p_id INTEGER)
BEGIN
  SELECT * FROM Users WHERE UserId = p_id;
END$$ 
delimiter;  #将语句的结束符号恢复为分号

call select_user(1);
  1. 验证输入

防护的核心原则是,一切用户输入皆不可信。

对所有输入进行验证或者过滤操作,能够很大程度上避免 SQL 注入的出现。在通过 userId 获取 Users 相关信息的示例中,我们可以确认 userId 必然是一个整数。因此,我们只需要对 userId 参数,进行一个整型转化(比如,Java 中的 Integer.parseInt,PHP 的 intval),就可以实现防护了。

总结

SQL 注入就是黑客通过相关漏洞,篡改 SQL 语句的攻击。通过 SQL 注入,黑客既可以影响正常的 SQL 执行结果,从而绕过验证,也可以执行额外的 SQL 语句,对数据的机密性、完整性和可用性都产生影响。

为了避免 SQL 注入的出现,我们需要正确地使用 PreparedStatement 方法或者存储过程,尽量避免在 SQL 语句中出现字符串拼接的操作。除此之外,SQL 注入的防护也可以和 XSS 一样,对用户的输入进行验证、检测并过滤 SQL 中的关键词,从而避免原有语句被篡改。

posted @ 2020-04-13 09:41  insist钢  阅读(239)  评论(0编辑  收藏  举报