Understanding HTTP/2 Rapid Reset: CVE-2023-44487 – Full Guide

HTTP/2 is a protocol that has significantly transformed the way our web applications communicate. It has brought speed, efficiency, and security to the table.

However, like every technology, HTTP/2 is not without its vulnerabilities. One such vulnerability that has recently been identified is the HTTP/2 Rapid Reset (CVE-2023-44487).

In this guide, we will delve into what this vulnerability is, how it can be exploited, and crucially, how you can protect your systems against it.

What is HTTP/2 Rapid Reset (CVE-2023-44487)?

CVE-2023-44487, also known as the HTTP/2 Rapid Reset, is a vulnerability that was recently discovered in the HTTP/2 protocol. This vulnerability allows an attacker to cause a denial of service (DoS) by sending a stream of reset (RST_STREAM) frames to a target server.

The RST_STREAM frame in HTTP/2 is designed to allow either the client or the server to abruptly terminate a stream. However, if an attacker sends a rapid series of these frames, it can overwhelm the server, causing it to become unresponsive. This is where the term “Rapid Reset” comes from.

How is the Vulnerability Exploited?

The HTTP/2 Rapid Reset vulnerability, also known as CVE-2023-44487, has indeed been exploited in the wild. The vulnerability was actively exploited from August 2023 to October 2023, causing significant disruptions in service and resource consumption.

This flaw lies in how the HTTP/2 protocol handles multiplexed streams. To exploit this vulnerability, an attacker would repeatedly make a request for a new multiplex stream and immediately cancel it. This process of rapid requests and cancellations is what gives the vulnerability its “Rapid Reset” moniker.

The exploitation of this vulnerability does not require any special privileges or specific information about the server. It merely necessitates the ability to send HTTP/2 requests to the server. The HTTP/2 protocol allows the client to cancel streams without needing the server’s agreement, which is exploited in this attack.

The impact of this vulnerability was felt across all HTTP/2 servers. At its peak, the largest recorded attack exploiting this vulnerability reached a staggering 398 million requests per second, making it one of the largest DDoS attacks ever recorded.

This vulnerability was also exploited to mount high-volume DDoS attacks. These attacks were so significant that they were even larger than the biggest attack ever recorded at CloudFlare before the exploit of the HTTP/2 Rapid Reset Zero-Day.

Therefore, the HTTP/2 Rapid Reset (CVE-2023-44487) is a severe vulnerability that can be exploited by sending a barrage of requests and immediate cancellations to overwhelm a server’s resources, leading to a denial of service.

Proof-of-Concept (PoC) for CVE-2023-44487

A proof-of-concept (PoC) for the CVE-2023-44487 vulnerability, also known as the HTTP/2 Rapid Reset vulnerability, has been developed and made available on GitHub. This PoC is a basic vulnerability scanning tool designed to assess if web servers may be vulnerable to this specific exploit.

The tool operates by checking if a web server accepts HTTP/2 requests without downgrading them. If the web server does accept these requests and does not downgrade them, the tool attempts to open a connection stream and subsequently reset it.

If the web server accepts the creation and resetting of a connection stream then the server is flagged as definitely vulnerable. However, if it only accepts HTTP/2 requests but the stream connection fails, it may still be vulnerable if server-side capabilities are enabled.

The script outputs a CSV file with information including a timestamp of the request, the internal and external IP addresses of the host sending the HTTP requests, the URL being scanned, the vulnerability status (categorized as “VULNERABLE”, “LIKELY”, “POSSIBLE”, “SAFE”, or “ERROR”), and the error or the version the HTTP server downgrades the request to.

The PoC tool can also be run through an HTTP proxy by specifying the –proxy flag. This makes it a versatile tool for assessing potential vulnerabilities in a variety of network configurations.

#!/usr/bin/env python3

import ssl
import sys
import csv
import socket
import argparse

from datetime import datetime
from urllib.parse import urlparse
from http.client import HTTPConnection, HTTPSConnection

from h2.connection import H2Connection
from h2.config import H2Configuration

import httpx
import requests

def get_source_ips(proxies):
    """
    Retrieve the internal and external IP addresses of the machine.
    
    Accepts:
        proxies (dict): A dictionary of proxies to use for the requests.
    
    Returns:
        tuple: (internal_ip, external_ip)
    """
    try:
        response = requests.get('http://ifconfig.me', timeout=5, proxies=proxies)
        external_ip = response.text.strip()

        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.settimeout(2)
        try:
            s.connect(('8.8.8.8', 1))
            internal_ip = s.getsockname()[0]
        except socket.timeout:
            internal_ip = '127.0.0.1'
        except Exception as e:
            internal_ip = '127.0.0.1'
        finally:
            s.close()
        
        return internal_ip, external_ip
    except requests.exceptions.Timeout:
        print("External IP request timed out.")
        return None, None
    except Exception as e:
        print(f"Error: {e}")
        return None, None
    
def check_http2_support(url, proxies):
    """
    Check if the given URL supports HTTP/2.
    
    Parameters:
        url (str): The URL to check.
        proxies (dict): A dictionary of proxies to use for the requests.
        
    Returns:
        tuple: (status, error/version)
        status: 1 if HTTP/2 is supported, 0 otherwise, -1 on error.
        error/version: Error message or HTTP version if not HTTP/2.
    """
    try:
        # Update the proxies dictionary locally within this function
        local_proxies = {}
        if proxies:
            local_proxies = {
                'http://': proxies['http'],
                'https://': proxies['https'],
            }
        
        # Use the proxy if set, otherwise don't
        client_options = {'http2': True, 'verify': False}  # Ignore SSL verification
        if local_proxies:
            client_options['proxies'] = local_proxies
        
        with httpx.Client(**client_options) as client:
            response = client.get(url)
        
        if response.http_version == 'HTTP/2':
            return (1, "")
        else:
            return (0, f"{response.http_version}")
    except Exception as e:
        return (-1, f"check_http2_support - {e}")

def send_rst_stream_h2(host, port, stream_id, uri_path='/', timeout=5, proxy=None):
    """
    Send an RST_STREAM frame to the given host and port.
    
    Parameters:
        host (str): The hostname.
        port (int): The port number.
        stream_id (int): The stream ID to reset.
        uri_path (str): The URI path for the GET request.
        timeout (int): The timeout in seconds for the socket connection.
        proxy (str): The proxy URL, if any.
        
    Returns:
        tuple: (status, message)
        status: 1 if successful, 0 if no response, -1 otherwise.
        message: Additional information or error message.
    """
    try:
        # Create an SSL context to ignore SSL certificate verification
        ssl_context = ssl.create_default_context()
        ssl_context.check_hostname = False
        ssl_context.verify_mode = ssl.CERT_NONE

        # Create a connection based on whether a proxy is used
        if proxy and proxy != "":
            proxy_parts = urlparse(proxy)
            if port == 443:
                conn = HTTPSConnection(proxy_parts.hostname, proxy_parts.port, timeout=timeout, context=ssl_context)
                conn.set_tunnel(host, port)
            else:
                conn = HTTPConnection(proxy_parts.hostname, proxy_parts.port, timeout=timeout)
                conn.set_tunnel(host, port)
        else:
            if port == 443:
                conn = HTTPSConnection(host, port, timeout=timeout, context=ssl_context)
            else:
                conn = HTTPConnection(host, port, timeout=timeout)

        conn.connect()

        # Initiate HTTP/2 connection
        config = H2Configuration(client_side=True)
        h2_conn = H2Connection(config=config)
        h2_conn.initiate_connection()
        conn.send(h2_conn.data_to_send())

        # Send GET request headers
        headers = [(':method', 'GET'), (':authority', host), (':scheme', 'https'), (':path', uri_path)]
        h2_conn.send_headers(stream_id, headers)
        conn.send(h2_conn.data_to_send())

        # Listen for frames and send RST_STREAM when appropriate
        while True:
            data = conn.sock.recv(65535)
            if not data:
                break

            events = h2_conn.receive_data(data)
            has_sent = False
            for event in events:
                if hasattr(event, 'stream_id'):
                    if event.stream_id == stream_id:
                        h2_conn.reset_stream(event.stream_id)
                        conn.send(h2_conn.data_to_send())
                        has_sent = True
                        break # if we send the reset once we don't need to send it again because we at least know it worked

            if has_sent: # if we've already sent the reset, we can just break out of the loop
                return (1, "")
            else:
                # if we haven't sent the reset because we never found a stream_id matching the one we're looking for, we can just try to send to stream 1
                
                available_id = h2_conn.get_next_available_stream_id()
                if available_id == 0:
                    # if we can't get a new stream id, we can just send to stream 1
                    h2_conn.reset_stream(1)
                    conn.send(h2_conn.data_to_send())
                    return (0, "Able to send RST_STREAM to stream 1 but could not find any available stream ids")
                else:
                    # if we can get a new stream id, we can just send to that
                    h2_conn.reset_stream(available_id)
                    conn.send(h2_conn.data_to_send())
                    return (1, "")
                    
        conn.close()
        return (0, "No response")
    except Exception as e:
        return (-1, f"send_rst_stream_h2 - {e}")

def extract_hostname_port_uri(url):
    """
    Extract the hostname, port, and URI from a URL.
    
    Parameters:
        url (str): The URL to extract from.
        
    Returns:
        tuple: (hostname, port, uri)
    """
    try:
        parsed_url = urlparse(url)
        hostname = parsed_url.hostname
        port = parsed_url.port
        scheme = parsed_url.scheme
        uri = parsed_url.path  # Extracting the URI
        if uri == "":
            uri = "/"

        if not hostname:
            return -1, -1, ""

        if port:
            return hostname, port, uri

        if scheme == 'http':
            return hostname, 80, uri

        if scheme == 'https':
            return hostname, 443, uri

        return hostname, (80, 443), uri
    except Exception as e:
        return -1, -1, ""

if __name__ == "__main__":

    parser = argparse.ArgumentParser()
    parser.add_argument('-i', '--input', required=True)
    parser.add_argument('-o', '--output', default='/dev/stdout')
    parser.add_argument('--proxy', help='HTTP/HTTPS proxy URL', default=None)
    parser.add_argument('-v', '--verbose', action='store_true')
    args = parser.parse_args()

    proxies = {}
    if args.proxy:
        proxies = {
            'http': args.proxy,
            'https': args.proxy,
        }

    internal_ip, external_ip = get_source_ips(proxies)

    with open(args.input) as infile, open(args.output, 'w', newline='') as outfile:
        csv_writer = csv.writer(outfile)
        csv_writer.writerow(['Timestamp', 'Source Internal IP', 'Source External IP', 'URL', 'Vulnerability Status', 'Error/Downgrade Version'])
        
        for line in infile:
            addr = line.strip()
            if addr != "":
                now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                
                if args.verbose:
                    print(f"Checking {addr}...", file=sys.stderr)
                
                http2support, err = check_http2_support(addr, proxies)
                
                hostname, port, uri = extract_hostname_port_uri(addr)
                
                if http2support == 1:
                    resp, err2 = send_rst_stream_h2(hostname, port, 1, uri, proxy=args.proxy)
                    if resp == 1:
                        csv_writer.writerow([now, internal_ip, external_ip, addr, 'VULNERABLE', ''])
                    elif resp == -1:
                        csv_writer.writerow([now, internal_ip, external_ip, addr, 'POSSIBLE', f'Failed to send RST_STREAM: {err2}'])
                    elif resp == 0:
                        csv_writer.writerow([now, internal_ip, external_ip, addr, 'LIKELY', 'Got empty response to RST_STREAM request'])
                else:
                    if http2support == 0:
                        csv_writer.writerow([now, internal_ip, external_ip, addr, 'SAFE', f"Downgraded to {err}"])
                    else:
                        csv_writer.writerow([now, internal_ip, external_ip, addr, 'ERROR', err])

Instructions for running PoC

Perform a quick vulnerability scan on web servers to identify potential vulnerabilities to CVE-2023-44487.

To begin, install the necessary requirements with the following command:

$ python3 -m pip install -r requirements.txt

Next, run the vulnerability scan using the provided input_urls.txt file and save the results to output_results.csv:

$ python3 cve202344487.py -i input_urls.txt -o output_results.csv

If desired, you can also specify an HTTP proxy to route all requests through using the –proxy flag:

$ python3 cve202344487.py -i input_urls.txt -o output_results.csv --proxy http://proxysite.com:1234

For a Dockerized approach, follow these steps:

Build the Docker image:

$ docker build -t py-cve-2023-44487 .

Run the vulnerability scan:

$ docker run --rm -v /path/to/urls:/shared py-cve-2023-44487 -i /shared/input_urls.txt -o /shared/output_results.csv

Please note that this tool is non-invasive and only checks for potential vulnerabilities. It does not exploit them. This makes it a valuable resource for system administrators and cybersecurity professionals looking to secure their systems against potential HTTP/2 Rapid Reset attacks.

How Can You Protect Your System?

Now that we understand the vulnerability, let’s discuss how you can protect your systems from it.

  1. Update Your Software: The first and most crucial step is to ensure that your servers are running the latest versions of their software. Many software vendors have released patches to mitigate this vulnerability.
  2. Limit Rate of RST_STREAM Frames: Another effective countermeasure is to implement rate limiting for RST_STREAM frames. By limiting the number of these frames that a server can receive in a given period, you can prevent an attacker from overwhelming the server.
  3. Monitor Your Servers: Regularly monitor your servers for unusual activity. If you notice a spike in RST_STREAM frames, it could be an indication of an attempted attack.
  4. Use a Web Application Firewall (WAF): A WAF can provide an additional layer of protection by filtering out malicious HTTP/2 traffic.

While the HTTP/2 Rapid Reset vulnerability poses a significant threat, there are effective measures you can take to protect your systems.

The exploitation of this vulnerability involves the attacker sending a barrage of RST_STREAM frames to the target server. Because these frames are part of the normal operation of HTTP/2, the server will attempt to process them. However, the sheer volume of frames can cause the server to exhaust its resources, leading to a DoS condition.

It’s important to note that exploiting this vulnerability does not require any special privileges or information about the server. All an attacker needs is the ability to send HTTP/2 requests to the server.

By staying informed about the latest vulnerabilities and implementing robust security practices, you can help ensure the integrity of your web applications.