ComoDoS - Exploiting a Remote Kernel Vulnerability in Comodo Internet Security

Note: at the time of publishing this article (2026-06-03), the vulnerability is still a zero-day. On 2026-06-07, it was assigned as CVE-2026-49494 but is still yet to be patched.

I’ve submitted a full report, root-cause analysis, along with patch suggestions, and a proof-of-concept to Comodo’s security team. In spite of this, I’ve gotten no response. I followed up twice, my most recently email simply asking for a confirmation that they received my report, but only radio silence.

Given the company has no bug-bounty program, there’s only so much free effort I’m willing to expend trying to chase a vendor who doesn’t want to be reached. If they wish to fix the issue, they can, regardless of whether I post about it or not. Furthermore, a post from ZDI claims that they spent nearly 2 years trying to get the vendor to patch a vulnerability to no avail.

Although the vulnerability can be used to remotely trigger both an out-of-bounds (OOB) read & out-of-bounds write in the Windows kernel, the limitations on both primitives lead me to believe it’s unlikely this bug could be weaponized into RCE.

The bug does, however, enable you to remotely crash the target system with a single TCP/IP packet, even if the firewall is configured to block all ports.

From BYOVD research to remote kernel exploitation

Two months ago I began building an autonomous system for finding local privilege escalation vulnerabilities in 3rd-party Windows kernel drivers. Since LLMs are very good at finding the type of low-hanging fruit that tends to get weaponized by threat actors for BYOVD attacks, I figure this would be the perfect use case. The goal was to build a system that preemptively blocks zero-day exploits for my day job at Expel, where I do MDR work.

Despite my AI system only being good at finding fairly obvious vulnerabilities, I was surprised by how often it’d flag cybersecurity vendor’s drivers, especially those belonging to antivirus/firewall vendors. It’s a little ironic that you could potentially bypass a security products by using its own driver for privilege escalation.

Anyway, I figured that if an AI can shake a random cybersecurity vendor’s driver and have a pile of local privilege escalation zero-days fall out, there’s a good chance I can find something more interesting with manual analysis.

Enter, Inspect.sys - Comodo Internet Security’s firewall driver

The reason I ended up looking at Comodo’s firewall driver specifically, was happenstance. A flaw in my AI analysis pipeline led to it ingesting a very outdated version of Inspect.sys, which is Comodo’s firewall driver. The system was intended to analyze only the latest version of each driver, so a driver from 2014 making it into the mix resulted in an exploitability score much higher than the other candidates.

While reviewing the long-since-patched vulnerabilities, I noticed a pattern of poor design decisions, which gave me hope that I could manually find a zero-day in the latest version of the driver.

Finding bugs is super easy, finding useful ones, not so much

The first bug I found was in the first place I looked: the IP header parser. IP vulnerabilities are great, because the firewall needs the information from the TCP/IP packet in order to decide if to allow or block data. This means that the parsing happens before enforcement of any firewall rules; therefore, TCP/IP vulnerabilities can often be exploited even if the firewall is configured to block every port.

The bug I found in the IPv4 parser is so simple it’s hard to believe it was missed. But… it’s also not super useful due to pesky RFCs, so we won’t spend too much time on it. The TL;DR is an IPv4 packet has two length fields: the length of the IP header, and the length of the IP header + any additional headers/data. One field should obviously be larger than the other, but the firewall driver doesn’t validate this.

The parser just blindly subtracts the header size field (Internet Header Length AKA IHL) from total size field (Total Length). By setting the IHL field to a value larger than the total length field, an integer underflow occurs, causing the firewall to calculate the packet’s payload size a 4 billion bytes (0xFFFFFFFC).

Unfortunately, the RFC requires that routers validate that the IHL value doesn’t exceed the header’s total length. So, while the firewall won’t drop the packet for being invalid, most routers should (at least in theory). Consequently, the exploit packet should be dropped long before it reaches the target’s network.

The bug can still be exploited locally, and from within the same LAN (assuming the packet doesn’t transit a Layer 3 switch or a router), but that’s boring. The people yearn for remote exploits.

IPv6 is never the answer, unless the question is ‘where can I find better vulnerabilities?’

IPv6 is a little more interesting for two reasons: 1) IPv6 parsing is more complex than IPv4, often leading to buggy code. Additionally, the attack surface doesn’t get audited as much because everyone who has had to deal with IPv6 is doing so against their will. 2) The RFC is a little more flexible on the amount of shenanigans you can get away with without having your packet dropped. This is important because internet packets have to transit through many routers before reaching the target.

it’s worth noting that despite the IPv6 RFC being more flexible, some providers go way beyond what the RFC mandates when it comes to validation. Cloud providers often block certain IPv6 headers outright, or perform extended validation and drop packets that pass the RFC’s validation requirements but are otherwise still invalid. This information is bough to you at the cost of me wasting an entire day trying to test my exploit on an Azure VM because my ISP doesn’t actually support IPv6 at all (which, fair).

Interestingly, the last bug I analyzed on my blog was also an IPv6 vulnerability. Albeit, that one was in the Windows kernel, and much cooler. That said, I didn’t find that bug, I just reverse engineered the patch, so it’s nice to have something to call your own.

A quick introduction to IPv6

An IPv6 header with an example optional header attached.

The grey portion of the packet is what’s referred to as the “fixed header”, which is the mandatory portion of an IPv6 packet. This header is always exactly 40 bytes in size. But additionally, IPv6 supports ‘optional’ headers, which can be used to extend the fixed IPv6 header, which is the blue portion.

The next header field of the fixed header does what it says on the tin. It specifies the type of header that comes next. In a TCP packet, this would be set to 6 (the protocol number for TCP).

We can also set it to several IPv6 extension header types, which are listed below:
0 - Hob-by-Hop options
43 - Routing options
44 - Fragment header
50 - Encapsulating security payload
51 - Authentication header
60 - Destination options
135 - Mobility
139 - Host Identity Protocol
140 - Shim6 Protocol
253 - vibes
254 - also vibes

For the most part, barely anyone knows half these header exist, or what they’re used for, but since they sit between the fixed IPv6 header and the packet’s upper-layer header (TCP, UDP, ICMP, etc), parsing them is necessary. They can also be chained, so it’s not just a case of parsing a single extension header.

There’s two main approaches to handling IPv6 packets with extension headers.

  1. Throw the entire IPv6 packet in the trash if its size is anything other than 40 bytes. Extension headers are for weenies. IMO, this is the correct approach.
  2. Walk the entire IPv6 extension header chain, totaling up the size of each extension header’s extension length field.

Since IPv6 extension headers can be chained, mixed & matched, repeated, and nested, things can get really messy. This is unsurprisingly the source of a lot of TCP/IP vulnerabilities.

The ‘at least you tried’ of IPv6 parsing

Now that we understand the basics of IPv6, let’s take a look at the IPv6 parser in Inspect.sys. The parser’s job is to find the offset of the upper-layer header, which contains the upper-layer protocol (ULP).


two consecutive snippets of code illustrating the entire parse function.

The IPv6 parser maintain an object describing the current packet, which I’ve named packet_desc.

Towards the bottom of snippet one, the parser sets packet_desc->payload_length to ext_hdr_len, which comes from the payload length field of the fixed IPv6 header. This field should (normally) contain the total size of everything that follows the fixed IPv6 header (extension headers, ULP, etc.).

In order to obtain the offset of the ULP header, the parser iterates through each extension header and extracts its length. The code for the length extraction is at address 0x14000ee06. The code reads the extension header’s extension length field and converts the value to bytes.

Note: The extension length field specifies lengths in multiples of 8 bytes, so the << 3 is just multiplying the value by 8 to get the actual size in bytes. Extension headers also include a mandatory 8 byte header which isn’t included in the size, hence the +8 at the end of the line.

The vulnerability exists at 0x1400ee21 (packet_desc->payload_length -= ext_hdr_len). Each time the loop iterates over an extension header, it subtracts the extension header’s length from packet_desc->payload_length.

The problem is, packet_desc->payload_length is set by us, via the IPv6 fixed header’s payload length field. The payload length should be the total size of the packet (minus the fixed IPv6 header). But at no point does the code ever validate this.

There is no sanity check on the payload length field at all. There’s plenty of sanity checks to prevent buffer overflows, as well as potential integer overflows that shouldn’t even theoretically be possible, but nothing validating packet_desc->payload_length.

If we set the fixed IPv6 header’s payload length field to a value smaller than the sum of all the extension header lengths, packet_desc->payload_length will continue being decremented by ext_hdr_len, until it drops below zero.

Since packet_desc->payload_length is an unsigned 64-bit integer, rather than becoming negative, it wraps around to the largest possible 64-bit value. By setting the payload length field to 8for a packet with an extension header length of 16, we get a packet_desc->payload_length value of 0xFFFFFFFFFFFFFFF8 or 18,446,744,073,709,551,616 in decimal (18 quintillion).

The POC to exploit this bug is so small and simple that it could fit in a Tweet.

ext = IPv6ExtHdrDestOpt(nh=6, options=[PadN(optdata=b"\x00" * 8)])
tcp = TCP(sport=1337, dport=80, flags="S", seq=0, ack=1, window=0x2000)
ipv6 = IPv6(dst=dst_ip, nh=60, hlim=64, plen=8)
pkt = ipv6 / ext / tcp
send(packet)

Note: I went with the ‘Destination Options’ extension header, as this one is the least likely to be validated by a router, since it’s designated for the destination host.

The exploit, which I’ve named ComoDoS, allows you to remotely crash (BSOD) a target system with just a single IPv6 packet. And because the bug is in the firewall driver, it doesn’t matter whether the target port is open or closed, unless the system has a firewall for its firewall.

For testing purposes, if your system doesn’t support IPv6, you can still trigger the crash by checking the Filter IPv6 traffic option in Comodo’s firewall, then sending an IPv6 packet directly to the target NIC’s MAC address.

When a single variable turns your potential RCE into DoS

While there is a reachable control path which does allow for an OOB-write, it’s constrained in a way that makes me think the bug is not viable for RCE (or at least not by someone of my skill level).

The OOB-read occurs in a function that appears to scan the incoming packet for several WebDAV related artifacts. A snippet from part of the scanner function looking for WebDAV headers.

The function which calls the WebDAV scanner looks like so:

The parent function which calls the HTTP scanner function.

The payload_len passed to the scanner (which I’ve named ScanForHTTPArtifacts) is the same value that gets underflowed in the IP parser. The value is truncated to 16-bit, bringing it down from 16 exabytes to a more reasonable 64 KB.

However, If the full 65 KB read occurs, there is still a medium-high chance that the driver will read past the end of the kernel pool into unallocated memory, resulting in a page fault. Since the driver is running at DISPATCH_LEVEL, any page faults will immediately crash the entire system.

However, if we look at the actual scanner code, OOB-read can be controlled, or even prevented.

A snippet from ScanForHTTPArtifacts.

The scanner searches the packet’s TCP payload until it encounters the value 0xD or 0xA (\r or \n). It then checks for the presence of an HTTP/1.x header starting 8 bytes prior. Therefore, a minimal HTTP GET header (GET / HTTP/1.0\r\n\r\n) will satisfy the condition, ending the scan without triggering an OOB-read.

Since this portion of the code appears not to do any connection tracking, it’s not necessary to perform a full TCP handshake. We can just send a standalone TCP packet containing our malicious IPv6 header and a TCP packet containing the HTTP string.

The OOB-read itself isn’t useful from a remote perspective, since the data is only ever used locally. However, avoiding the crash by providing a matching HTTP header does lead to an OOB-write primitive. Though, I’m almost certain it’s unexploitable due to the nature of the bug.

To understand why, we’ll have to dig a little bit deeper into the code.

The only code I could find where our underflowed size field is used in a write operation was this memcpy.

A snippet of the memcpy which uses our corrupted size value.

While the scanner function doesn’t do any connection tracking, in order to reach this function, we need to perform a full TCP handshake, which means the system must have at least one open TCP port on the host. It may be possible we can bypass this by just sending a SYN, SYN/ACK, and ACK packet, without them actually being accepted, but I haven’t tested this.

The size parameter (arg3) is our underflowed size variable, and the source address is the start of our packets TCP data. Since the destination buffer is large enough to accommodate the maximum possible TCP packet, the memcpy would copy whatever is after our packet in memory to whatever is after the destination buffer in memory. But that’s fixable with heap grooming, and the least of our problems.

The killer issue is that unlike in the scanner function, where our size value was truncated to 16-bits, this one is truncated to 32-bit. Which means the size of the memcpy operation is 4 billion bytes, or 4 GB and is guaranteed to crash the system.

If we could get the underflow down to a smaller size, this bug might make for a viable RCE. But because standard network packets are a maximum of 65 KB in size, the absolute maximum amount we can decrement the underflowed size variable by is 65 KB. Which isn’t anywhere near enough to turn a 4 GB kernel pool overflow into something that won’t crash the system.

I totally accept defeat, but at least the journey was fun.

Proof Of Concept

Below is a video of the PoC in action as well as a link to the full code.

Full Proof-of-Concept: https://github.com/MalwareTech/ComoDoS