You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

205 lines
14 KiB
Markdown

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 07 | SQL注入明明设置了强密码为什么还会被别人登录
你好,我是何为舟。
在上一讲中我们介绍了XSS攻击。今天我们来介绍另外一种常见的Web攻击SQL注入。
在讲正文之前,让我们先来看一个案例。某天,当你在查看应用的管理后台时,发现有很多异常的操作。接着,你很快反应过来了,这应该是黑客成功登录了管理员账户。于是,你立刻找到管理员,责问他是不是设置了弱密码。管理员很无辜地表示,自己的密码非常复杂,不可能泄露,但是为了安全起见,他还是立即修改了当前的密码。奇怪的是,第二天,黑客还是能够继续登录管理员账号。问题来了,黑客究竟是怎么做到的呢?你觉得这里面的问题究竟出在哪里呢?你可以先自己思考一下,然后跟着我开始今天的学习!
## SQL注入攻击是如何产生的
在上一讲中我们讲了XSS是黑客通过篡改HTML代码来插入并执行恶意脚本的一种攻击。其实SQL注入和XSS攻击很类似都是黑客通过篡改代码逻辑发起的攻击。那么不同的点是什么SQL注入到底是什么呢
通常来说我们会将应用的用户信息存储在数据库中。每次用户登录时都会执行一个相应的SQL语句。这时黑客会通过构造一些恶意的输入参数在应用拼接SQL语句的时候去篡改正常的SQL语意从而执行黑客所控制的SQL查询功能。这个过程就相当于黑客“注入”了一段SQL代码到应用中。这就是我们常说的**SQL注入**。
这么说可能还是有点理论不够具体。接下来我就以几个简单而又经典的示例来给你介绍两种主要的SQL注入方式。
### 1.修改WHERE语句
我们先来看一个例子。现在有一个简单的登录页面需要用户输入Username和Password这两个变量来完成登录。具体的Web后台代码如下所示
```
uName = getRequestString("username");
uPass = getRequestString("password");
sql = 'SELECT * FROM Users WHERE Username ="' + uName + '" AND Password ="' + uPass + '"'
```
当用户提交一个表单假设Username为adminPassword为123456Web将执行下面这行代码
```
SELECT * FROM Users WHERE Username ="admin" AND Password ="123456"
```
用户名密码如果正确的话这句SQL就能够返回对应的用户信息如果错误的话不会返回任何信息。因此只要返回的行数≥1就说明验证通过用户可以成功登录。
所以,当用户正常地输入自己的用户名和密码时,自然就可以成功登录应用。那黑客想要在不知道密码的情况下登录应用,他又会输入什么呢?他会输入 **`" or ""="`**。这时,应用的数据库就会执行下面这行代码:
```
SELECT * FROM Users WHERE Username ="" AND Password ="" or ""=""
```
我们可以看到WHERE语句后面的判断是通过or进行拼接的其中""=""的结果是true。那么当有一个or是true的时候最终结果就一定是true了。因此这个WHERE语句是恒为真的所以数据库将返回全部的数据。
这样一来,我们就能解答文章开头的问题了,也就是说,黑客只需要在登录页面中输入 **`" or ""="`**就可以在不知道密码的情况下成功登录后台了。而这也就是所谓的“万能密码”。而这个“万能密码”其实就是通过修改WHERE语句改变数据库的返回结果实现无密码登录。
### 2.执行任意语句
除此之外大部分的数据库都支持多语句执行。因此黑客除了修改原本的WHERE语句之外也可以在原语句的后面插入额外的SQL语句来实现任意的增删改查操作。在实际工作中MySQL是最常用的数据库我们就以它为例来介绍一下任意语句是如何执行的。
在MySQL中实现任意语句执行最简单的方法就是利用分号将原本的SQL语句进行分割。这样我们就可以一次执行多个语句了。比如下面这个语句在执行的时候会先插入一个行然后再返回Users表中全部的数据。
```
INSERT INTO Users (Username, Password) VALUES("test","000000"); SELECT * FROM Users;
```
接下来我们来看一个具体的例子。在用户完成登录后应用通常会通过userId来获取对应的用户信息。其Web后台的代码如下所示
```
uid = getRequestString("userId");
sql = "SELECT * FROM Users WHERE UserId = " + uid;
```
在这种情况下黑客只要在传入的userId参数中加入一个分号就可以执行任意的SQL语句了。比如黑客想“删库跑路”的话就令userId为 **`1;DROP TABLE Users`**那么后台实际执行的SQL就会变成下面这行代码而数据库中所有的用户信息就都会被删除。
```
SELECT * FROM Users WHERE UserId = 1DROP TABLE Users
```
SQL注入的“姿势”还有很多比如[没有回显的盲注](https://www.freebuf.com/articles/web/175049.html)、[基于INSERT语句的注入](https://www.jianshu.com/p/1f82582452df?utm_campaign)等等它们的原理都是一样的都是通过更改SQL的语义来执行黑客设定的SQL语句。如果你有兴趣可以通过我前面给出的链接去进一步了解。
## 通过SQL注入攻击黑客能做什么
通过上面对SQL注入的简单介绍我们已经知道SQL注入会令Web后台执行非常规的SQL语句从而导致各种各样的问题。那么通过SQL注入攻击黑客究竟能够干些什么呢下面我们就一一来看。
### 1.绕过验证
在上面的内容中,我们已经介绍过,**`" or ""="`** 作为万能密码可以让黑客在不知道密码的情况下通过登录认证。因此SQL注入最直接的利用方式就是绕过验证也就相当于身份认证被破解了。
### 2.任意篡改数据
除了绕过验证我们在任意语句执行的部分中讲到SQL注入漏洞导致黑客可以执行任意的SQL语句。因此通过插入DML类的SQL语句INSERT、UPDATE、DELETE、TRUNCATE、DROP等黑客就可以对表数据甚至表结构进行更改这样数据的完整性就会受到损害。比如上面例子中黑客通过插入DROP TABLE Users删除数据库中全部的用户。
### 3.窃取数据
在XSS漏洞中黑客可以通过窃取Cookie和“钓鱼”获得用户的隐私数据。那么在SQL注入中黑客会怎么来获取这些隐私数据呢
在各类安全事件中我们经常听到“拖库”这个词。所谓“拖库”就是指黑客通过类似SQL注入的手段获取到数据库中的全部数据如用户名、密码、手机号等隐私数据。最简单的黑客利用UNION关键词将SQL语句拼接成下面这行代码之后就可以直接获取全部的用户信息了。
```
SELECT * FROM Users WHERE UserId = 1 UNION SELECT * FROM Users
```
### 4.消耗资源
通过[第1讲](https://time.geekbang.org/column/article/176567)对CIA三元组的学习我们知道除了获取数据之外影响服务可用性也是黑客的目标之一。
SQL注入破坏可用性十分简单可以通过完全消耗服务器的资源来实现。比如在Web后台中黑客可以利用WHILE打造死循环操作或者定义存储过程触发一个无限迭代等等。在这些情况下数据库服务器因为CPU被迅速打满持续100%,而无法及时响应其他请求。
总结来说通过SQL注入攻击黑客可以绕过验证登录后台非法篡改数据库中的数据还能执行任意的SQL语句盗取用户的隐私数据影响公司业务等等。所以我认为SQL注入相当于让黑客直接和服务端的数据库进行了交互。正如我们一直所说的应用的本质是数据黑客控制了数据库也就相当于控制了整个应用。
## 如何进行SQL注入防护
在认识到SQL注入的危害之后我们知道一个简单的SQL查询逻辑能够带来巨大的安全隐患。因此我们应该做到在开发过程中就避免出现SQL注入漏洞。那具体应该怎么做呢接下来我会为你介绍3种常见的防护方法它们分别是使用PreparedStatement、使用存储过程和验证输入。接下来我们一一来看。
### 1.使用PreparedStatement
通过**合理地**使用PreparedStatement我们就能够避免99.99%的SQL注入问题。你肯定很好奇我为什么会这么说。接下来让我们一起看一下它的实现过程。
当数据库在处理一个SQL命令的时候大致可以分为两个步骤
* 将SQL语句解析成数据库可使用的指令集。我们在使用EXPLAIN关键字分析SQL语句就是干的这个事情
* 将变量代入指令集开始实际执行。之所以在批量处理SQL的时候能够提升性能就是因为这样做避免了重复解析SQL的过程。
那么PreparedStatement为什么能够避免SQL注入的问题呢
这是因为SQL注入是在解析的过程中生效的用户的输入会影响SQL解析的结果。因此我们可以通过使用PreparedStatement将SQL语句的解析和实际执行过程分开只在执行的过程中代入用户的操作。这样一来无论黑客提交的参数怎么变化数据库都不会去执行额外的逻辑也就避免了SQL注入的发生。
在Java中我们可以通过执行下面的代码将解析和执行分开
```
String sql = "SELECT * FROM Users WHERE UserId = ?";
PreparedStatement statement = connection.prepareStatement(sql);
statement.setInt(1, userId);
ResultSet results = statement.executeQuery();
```
为了实现相似的效果在PHP中我们可以使用PDOPHP Data Objects在C#中我们可以使用OleDbCommand等等。
这里有一点需要你注意前面我们说了通过合理地使用PreparedStatement就能解决99.99%的SQL注入问题那到底怎么做才算“合理地”使用呢
PreparedStatement为SQL语句的解析和执行提供了不同的“方法”你需要分开来调用。但是如果你在使用PreparedStatement的时候还是通过字符串拼接来构造SQL语句那仍然是将解析和执行放在了一块也就不会产生相应的防护效果了。我这里给你展示了一个错误案例你可以和上面的代码进行对比。
```
String sql = "SELECT * FROM Users WHERE UserId = " + userId;
PreparedStatement statement = connection.prepareStatement(sql);
ResultSet results = statement.executeQuery();
```
### 2.使用存储过程
接下来,我们说一说,如何使用[存储过程](https://baike.baidu.com/item/%E5%AD%98%E5%82%A8%E8%BF%87%E7%A8%8B/1240317?fr=aladdin)来防止SQL注入。实际上它的原理和使用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);
```
### 3.验证输入
在上一节课中,我们讲过,**防护的核心原则是,一切用户输入皆不可信**。因此SQL注入的防护手段和XSS其实也是相通的主要的不同在于
* SQL注入的攻击发生在输入的时候因此我们只能在输入的时候去进行防护和验证
* 大部分数据库不提供针对SQL的编码因为那会改变原有的语意所以SQL注入没有编码的保护方案。
因此对所有输入进行验证或者过滤操作能够很大程度上避免SQL注入的出现。比如在通过userId获取Users相关信息的示例中我们可以确认userId必然是一个整数。因此我们只需要对userId参数进行一个整型转化比如Java中的Integer.parseIntPHP的intval就可以实现防护了。
当然部分场景下用户输入的参数会比较复杂。我们以用户发出的评论为例其内容完全由用户定义应用无法预判它的格式。这种情况下应用只能通过对部分关键字符进行过滤来避免SQL注入的发生。比如在MySQL中需要注意的关键词有" % \\ \_。
这里我简单地总结一下在实际使用这些防护方法时的注意点。对于验证输入来说尤其是在复杂场景下的验证输入措施其防护效果是最弱的。因此避免SQL注入的防护方法首要选择仍然是PreparedStatement或者存储过程。
## 总结
好了,这一节内容差不多了,下面我来带你总结回顾一下,你要掌握的重点内容。
SQL注入就是黑客通过相关漏洞篡改SQL语句的攻击。通过SQL注入黑客既可以影响正常的SQL执行结果从而绕过验证也可以执行额外的SQL语句对数据的机密性、完整性和可用性都产生影响。
为了避免SQL注入的出现我们需要正确地使用PreparedStatement方法或者存储过程尽量避免在SQL语句中出现字符串拼接的操作。除此之外SQL注入的防护也可以和XSS一样对用户的输入进行验证、检测并过滤SQL中的关键词从而避免原有语句被篡改。
今天的内容比较多,为了方便你记忆,我总结了一个知识脑图,你可以通过它来对今天的重点内容进行复习巩固。
![](https://static001.geekbang.org/resource/image/1b/36/1b898391a3a04a764d0442d8481c4236.jpg)
## 思考题
好了,今天的内容差不多了,我们来看一道思考题。
假设有下面这样一个语句:
```
SELECT Username FROM Users WHERE UserId = 1
```
你现在已经知道WHERE语句中存在了SQL注入的点。那么我们怎么才能获取到除了Username之外的其他字段呢这里我给你一个小提示你可以先了解一下“[盲注](https://www.freebuf.com/articles/web/175049.html)”这个概念,之后再来思考这个问题。
欢迎留言和我分享你的思考和疑惑,也欢迎你把文章分享给你的朋友。我们下一讲再见!