并非所有攻击都是平等的:理解和防止Web应用程序中的DoS

2020-09-12 03:40:56

感谢克林特·吉布勒、格雷森·哈达维和r2c的巴勃罗·埃斯特拉达,感谢他们对这首曲子的贡献。感谢r2c签约让我写这篇文章!本文在Jacobian.org上交叉发布。

当我在Heroku管理安全团队时,我做了一个反复出现的噩梦:我的PagerDuty闹钟响了,提醒我发生了某种安全事件。在我的梦里,我会看着我的手机,意识到“哦,不,这是个大手机”--然后我就会醒来。

我仍然不确定我梦中的确切攻击是什么,但它很可能是拒绝服务(DoS)攻击。DoS攻击很简单,但可能是毁灭性的:攻击者精心编制流量,并将流量发送到您的应用程序,使您的服务器不堪重负。虽然这可以说没有远程代码执行或数据泄露那么糟糕,但仍然相当可怕。如果你的客户不能使用你的应用,你将失去他们的钱和信任。

“正常”拒绝服务(DoS)攻击,即一台机器就足以导致停机。这种攻击的经典老式版本是Zip炸弹:攻击者欺骗您的服务器扩展一个精心编制的ZIP文件,该文件压缩很小,但会扩展到完全填满您的磁盘空间。

分布式拒绝服务(DDoS)攻击。这些攻击依赖于攻击者从多台机器向您的站点发送大量流量(这是“分布式”部分)。通常,这些攻击来自僵尸网络-由攻击者控制的一队受危害的机器。这些僵尸网络可以在互联网的某些角落购买,使得DDoS攻击完全在任何有信用卡的人的触手可及之处。

从事Web应用程序工作的工程师经常遇到可用于DoS/DDoS攻击的漏洞。不幸的是,在如何处理这些漏洞的问题上,业界存在广泛的分歧。风险可能很难分析:我见过开发团队就如何处理DoS载体争论了数周。

本文试图驳斥这些争论。它为工程和应用安全团队提供了一个考虑拒绝服务风险的框架,将DoS漏洞细分为高风险、中等风险和低风险类别,并对每一层的缓解提出了建议。

这篇文章的主要关注点是大局,应该适用于任何类型的网络应用程序。但是为了使事情更具体,我添加了几个与Django相关的具体示例。(我帮助创建了它,所以它是我最熟悉的。)

在应用层评估DoS漏洞的风险可能很困难。安全专业人员之间存在着广泛的分歧:您经常会看到两个不同的AppSec团队以非常不同的方式处理类似的问题。

一些人认为:要完全缓解集中的DDoS几乎是不可能的--一个足够专业的攻击者可能会向你抛出超过你的应用程序所能处理的带宽。如果没有上游网络提供商(例如Cloudflare)提供的具有特定工具(例如Cloudflare)的认真支持来保护僵尸攻击,您永远无法完全缓解DDoS攻击。因此,追踪和修复假想的DoS漏洞似乎是在浪费开发人员的时间。这些团队将大多数潜在的DoS载体视为可接受的风险,并将精力集中在准备网络级别的缓解措施上。

其他团队指出,传统的风险模型有三个潜在的问题领域:机密性、完整性和可用性。我们早就明白正常运行时间是一个安全问题。攻击者关闭一项服务然后索要赎金以阻止攻击的做法越来越普遍。最近针对Garmin的攻击就是一个非常值得注意的例子;攻击者几乎关闭了Garmin的所有服务,据报道,他们要求100万美元来阻止攻击。(在本例中,攻击是勒索软件,但是很容易看出DoS攻击是如何产生类似效果的)。因此,DoS漏洞和任何其他漏洞一样都是风险,因此很容易理解应该全部减轻这些漏洞的论点。

重要的是要认识到这两种观点都是有效的!将DoS视为应用程序安全的范围之外是合理的;将其范围确定在范围内也是同样合理的。我经常看到安全团队在这两个职位之间争论不休。既然不是“对”也不是“错”,就不可能搞清楚如何前进。

我用来解决这一争论的模型是攻击者杠杆的概念。杠杆放大了力:施加在杠杆长端的少量力在短端倍增。在DoS攻击的上下文中,如果漏洞具有很高的影响力,这意味着攻击者可以用最少的资源消耗大量的服务器资源。

例如,如果您的Web应用程序中的一个bug允许单个GET请求消耗100%的CPU,这将是一个巨大的杠杆效应。只要一小部分攻击,您的Web服务器就会陷入停顿。另一方面,低杠杆漏洞需要大量攻击者资源才能导致较小的可用性降级。如果攻击者必须花费数千美元才能使一台服务器瘫痪,您可能会比他们扩展得更快。

杠杆率越高,风险就越高,我就越有可能直接解决这个问题。杠杆越低,我就越有可能接受风险和/或依赖网络级别的缓解措施。

让我们说得更具体些。我已经根据杠杆率将DoS风险细分为高、中、低风险类别。对于每个类,我将研究如何识别属于此类的漏洞,讨论几个示例,并提供一些缓解建议。

典型的高风险DoS漏洞是攻击者自己使用很少的资源就可以导致资源匮乏的漏洞。这可能意味着耗尽任意数量的资源类型,包括:

磁盘空间-例如,放大上传的数据并填满磁盘的漏洞,就像典型的Zip炸弹的情况一样。

网络带宽-例如,放大输入流量的漏洞,其中单个传入请求会消耗大量带宽,从而导致网络饥饿。我在微服务系统中的一个bug中看到过这种情况,其中一个传入请求触发了数百万个内部API请求(包括在网络中移动一些相当大的文件),并阻塞了内部网络带宽。

CPU利用率-例如,触发意外二次算法,导致Web服务器停止的利用漏洞。

并发限制-大多数服务器都有最大并发限制(例如,最大线程或进程数,或数据库的最大连接数);导致进程运行非常缓慢(或永远不退出)的利用漏洞攻击可能会导致服务器达到这些限制并开始拒绝请求。

在所有这些情况下,统一的因素是应用程序中的错误将允许显著放大。

在考虑资源放大DoS媒介的风险时,一个重要因素是触发漏洞所需的身份验证级别。如果完全匿名的用户可以轻松触发资源匮乏攻击,则攻击者极易使您屈服。未经验证的DoS向量应被视为非常高的风险。另一方面,如果只有使用您的公司单点登录服务器进行身份验证的用户才能触发该漏洞,则风险要低得多。大多数攻击者都不是内部人员(不过,也有一些是内部人员!)。而且,如果确实发生了攻击,则很容易将其归类和阻止。在许多情况下,“我们可以确定并阻止此攻击”即使不是完全的,也是一种合理的缓解策略。许多漏洞介于这两个极端之间:大多数服务使创建新帐户变得相当简单(例如,您只需要一个电子邮件地址)。这确实为属性和阻塞提供了最低限度的能力,但通常还不够。

通常,我建议将此类DoS漏洞(特别是未经身份验证的漏洞)视为高风险,并予以消除。如果被利用,这些漏洞可能是毁灭性的;它们允许单个攻击者完全淹没您的应用程序。我会像查找和消除其他高风险安全漏洞(如XSS和CSRF)一样,投入相同级别的精力来查找和消除这些类型的错误。

最后一种类型的资源匮乏(并发限制)的一个常见示例是正则表达式拒绝服务(又名redos)。当某些类型的字符串可能导致构造不当的正则表达式性能极差时,就会出现重做错误。不幸的是,这类漏洞在Python中相对常见;内置的正则表达式模块(Re)没有针对它们的固有保护(不像像Re2这样的库,它是Go的内置正则表达式模块,因此使该语言或多或少地不受此类攻击的影响)。(django本身在过去几年中也有几个这样的漏洞;例如,cve-2019-14232和cve-2019-14233都是redos漏洞)。在django中,这些漏洞最常出现在两个地方:regex。幸运的是,这类漏洞相当容易找到;请参阅以下r2c文章:

如果您使用的是Python,那么可以使用Semgrep轻松地扫描应用程序中的redos,它具有从Dlint移植的redos检测功能。检测需要使用Semgrep强大的Pattern-WHERE-Python子句编写的一些额外逻辑,该子句使规则能够充分利用Python的全部功能,因此您必须使用--dangerously-allow-arbitrary-code-execution-from-rules标志。

再往下看,我们发现了一种不同的资源匮乏:您的应用程序中固有速度较慢或资源密集型较高的区域。例如:

复杂的报表,需要读取和计算相当多的数据。考虑一下长期汇总指标的临时报告,或者汇总数百万交易的季度财务报告。

需要昂贵的重新索引的数据库或搜索引擎写入。典型的Web应用程序针对快速读取进行了调整,但代价是写入速度较慢。对分布式数据库的一致写入尤其如此(感谢CAP定理!)。

像GraphQL这样的API可以生成任意深度的数据库连接。这是一个比这里讨论的更深层次的主题;要获得一个好的介绍,请参阅保护您的GraphQL API免受Apollo团队的恶意查询。

发现比正常速度慢得多的区域的攻击者可以向该端点发送垃圾邮件,从而导致与上面类似的资源耗尽。但通常这些不是bug;它们是应用程序的功能。有些特性总是较慢或更耗费资源;很少有“修复”只需要一些时间的东西。有时,性能优化可以降低风险,但通常需要认真的投资或不可接受的权衡,如放弃一致的写入。但是,有几个缓解因素可以降低此类问题的风险:

通常,这些类型的端点位于某种身份验证或登录之后。例如,GraphQL API需要API密钥;财务报告仅对特权用户可用;对数据库的写入只能由登录的客户触发。如上所述,这降低了风险。

通常需要更多的攻击流量才能压倒这些类型的功能,而不是高杠杆级别。例如,虽然在典型的应用程序中,写入比读取慢,但也不是那么慢;一个调优良好的数据库仍然可以每秒处理数千次写入。因此,攻击者将不得不更加努力地工作,投入更多的资源来造成资源匮乏。

综上所述,我认为这意味着将这一类别中的潜在漏洞视为可接受的风险要合理得多。“我们只会阻止一个试图让我们不知所措的API密钥”似乎是一个合理的决定。

也就是说,有一个共同的架构缓解措施值得考虑:速率限制。速率限制对某个短时间窗口内发送到特定端点的请求数量设置阈值。速率限制很容易设置和应用,而且通常只是一种积极的工程实践。只要您将限制设置得足够高,不会阻碍正常使用,它们就可以帮助防止一系列问题,包括DoS。

在Django中,django-ratlimit提供了一个简单的基于修饰器的API,这使得向视图添加速率限制变得非常容易:

或者,如果您使用的是Django REST框架,它具有内置的速率限制和一系列选项。对于某些应用程序,广泛应用速率限制是有意义的--即使是在每个视图上都是如此。在这些情况下,您可以使用Semgrep查找未装饰的视图并发出警告。下面是一个Semgrep配置示例,它可以在没有@ratlimit装饰符的情况下查找视图:

规则:-id:my_pattern_id Patterns:-Pattern-One:-Pattern:|def$FUNC(...,request,...):...-Pattern-Not:|@ratlimit.decators.ratlimit(...)。定义$FUNC(...,请求,...):...。消息:|此视图似乎没有应用速率限制。考虑使用@ratlimit装饰符应用一个。FIX:|严重性:警告。

您可能希望为您的特定应用程序修改此规则集;这只是一个起点。迭代开发适合您的自定义规则集的一个好方法是从交互式Semgrep操场中的此规则集开始。

最后,我们来看最后一类DoS攻击:真正的分布式拒绝服务攻击,即攻击者指挥一大批计算机(通常是僵尸网络)向您的应用程序发送大量流量。此流量并不总是特定于应用程序的;它通常是大量无意义的TCP或UDP数据包,旨在使网络本身不堪重负。DDoS攻击的规模通常只受攻击者预算的限制,这类攻击会让应用程序安全工程师举手投降--包括我自己!您可以做的并不是很多,当然不是在应用程序级别。我倾向于同意真正的DDoS超出了应用程序安全的范围。

也就是说,有一些工作可以在网络层面上完成,主要是在准备方面:

您应该考虑将您的应用程序放在像Cloudflare这样可以防御DDoS的服务后面。您还将从像CloudFlare这样的CDN中获得一些实质性的性能优势,所以这通常是非常值得的。

您应该了解网络层以及可以应用网络规则的位置。可以识别许多DDoS攻击(通过IP、源端口、通信量类型或某种组合)。了解如何快速应用网络规则来丢弃或限制恶意流量有助于确保您能够快速响应攻击。

除了您自己控制的系统之外,您还应该知道您的网络提供商是谁,以及他们可以采取哪些缓解措施。通常,您的网络提供商可以比您更有效地阻止这些攻击。例如,如果您托管在AWS上,您可以作为AWS Shield Advanced的一部分全天候访问AWS DDoS响应团队。起价为每年3.6万美元,但这取决于你的业务,可能看起来贵得离谱,也可能看起来便宜得离谱。

如果您想阅读更多关于准备和减轻DDoS攻击的内容,Google的“构建安全可靠的系统”第10章是一个很好的起点。

拒绝服务漏洞可以通过多种不同的方式表现出来。其中一些应该优先处理并立即修复,但另一些则被合理地认为是“可接受的风险”。没有放之四海而皆准的方法;在找到适当的响应之前,您需要考虑漏洞的相对风险。

我发现用于评估此风险的最佳框架是放大:考虑需要多少攻击者流量才能触发某种级别的服务降级。如果几个琐碎的请求会使您的服务器瘫痪,这是非常高的风险,应该得到适当的处理。另一方面,如果大量的交通可能会导致适度的减速,那么将你的时间优先安排在其他地方是合理的。

下次您遇到关于DoS向量的不确定性时,请尝试使用此框架。我希望它能避免那种令人沮丧的争论!

获取有关Semgrep的最新功能和我们的安全研究的最新新闻。我们保证永远不会给您发垃圾邮件。