← Writing
CVE

Reverse-engineering CVE-2022-26318 (WatchGuard Firebox): from network trace to root cause

Dec 28, 2025 · 10 min read

Ethics / safety note: This post is written for defenders and researchers. I explain how to reverse-engineer and validate the bug, but I intentionally avoid weaponized exploit details (no offsets/ROP chains/payloads). Only test on devices and firmware you own and are authorized to assess.

0) What is CVE-2022-26318?

CVE-2022-26318 (WatchGuard advisory WGSA-2022-00002) is a critical issue in WatchGuard Firebox and XTM appliances where an unauthenticated remote attacker can potentially execute arbitrary code when management access is exposed. WatchGuard’s PSIRT advisory lists CVSS 9.8 and explicitly notes active exploitation in the wild, urging administrators to update and restrict management access.

NVD tracks this as FBX-22786, with affected Fireware OS ranges and fixed versions (e.g., 12.7.2_Update2 and others depending on branch).

Affected / fixed versions (high level)

Why defenders cared immediately

GreyNoise reported and later expanded on in-the-wild traffic, including requests to management port 4117 over TLS and suspicious POST /agent/login patterns (often gzip-encoded bodies and atypical lengths).


1) Exploit chain (high-level mental model)

You’ll see this phrased many ways (“null pointer deref”, “buffer overflow”, etc.). The practical RE story from public writeups (especially Assetnote) is:

Internet → management interface (TLS) → nginx → backend service → wgagent → libxml2 SAX parsing → unsafe string growth → memory corruption → control-flow opportunity

Assetnote describes the path through nginx and into the wgagent process, and how XML parsing plays a central role in triggering corruption. Rapid7’s module description (noting it’s exploiting a buffer overflow at the administration interface and that the endpoint /agent/login reaches wgagent) corroborates the key actors in the request path. GreyNoise’s defender view helps you anchor what to look for on the wire.


2) Your RE lab setup (firmware + tools)

Tools used

Firmware / binary acquisition (safe & legal)

  1. Obtain official Fireware OS firmware from WatchGuard support resources (use versions that match “vulnerable” and “fixed” branches as appropriate).
  2. Preserve hashes so your analysis is reproducible.
  3. Work on a non-production lab image/device.

Tip: Keep a timeline notebook: firmware version, build string, and whether it’s vulnerable/fixed per PSIRT + release notes.


3) Firmware extraction: finding the right binaries fast

Different vendors package firmware differently, but the goal is always:

  1. Extract the root filesystem
  2. Identify web stack components
  3. Identify native backend processes that parse attacker input

What you’re hunting for in this case

Practical extraction checklist:

GreyNoise highlights port 4117 as a key management port for observed exploit traffic.


4) Static RE in Ghidra: from /agent/login to the vulnerable callback

This section is written as a “follow-along checklist” you can apply to similar firmware bugs.

4.1 Identify the request handler entrypoint

Goal: locate code that processes something tied to /agent/login and funnels it toward XML parsing.

Practical anchors:

  1. Strings: search for "agent/login", "XML", "SAX", "libxml" strings in binaries.
  2. Imports: search for libxml2 parser APIs.
  3. Config: locate nginx routing and map it to backend binaries.

4.2 Import-centric pivot: find libxml2 usage

Open wgagent in Ghidra and look at Imports for libxml2 functions.

Common libxml2 parsing entrypoints include:

Ghidra steps

  1. Import wgagent.
  2. Run analysis with defaults.
  3. Open Symbol Tree → Imports.
  4. Filter imports by xml.
  5. Right-click xmlParseChunk → References to → follow callers that feed request bodies into the parser.

4.3 Prove it’s SAX and locate the callback table

A SAX handler is a struct of function pointers. In libxml2 this is struct _xmlSAXHandler; you’ll often see startElementNs used in SAX2.

In decompiler output you’ll typically see:

4.4 Find the unsafe primitive inside the callback

Open the suspected callback and look for classic unsafe patterns:

A common “tell” is building an XPath-like string by appending "/" + elementName repeatedly, leading to overflow.


5) Understanding the parser mechanics (SAX2) like an expert

If you want to be complete-expert level, you need to understand what libxml2 will call, when, and with what data.

5.1 SAX = “stream of events”

SAX parsers don’t build a full DOM by default. They call callbacks like startDocument, startElementNs, characters, endElementNs.

5.2 SAX2 magic: validating you found the real handler

libxml2 defines a constant often used in SAX2 blocks: XML_SAX2_MAGIC (0xDEEDBEAF). Searching for this value in the binary helps confirm you’re looking at SAX2 handler structures.


6) Heap layout + function pointer overwrite (how researchers prove control-relevant corruption)

This is the “Aha” section: buffer overflow is only interesting when it reaches something that changes control-flow, like a function pointer table.

6.1 Why callback tables are high-value overwrite targets

A SAX handler is data fields plus function pointers. If you corrupt a callback pointer, the next SAX event dispatch can call attacker-influenced memory.

6.2 The single best technique: watchpoints

A single hardware watchpoint turns “I think it overwrites X” into “here is the exact instruction that did it.”


7) Dynamic RE: proving the overwrite with GDB (non-weaponized workflow)

You may run this either inside a lab VM/container with extracted binaries, or via QEMU user-mode for convenience (architecture permitting).

7.1 Pick a stop point before parsing

Set a breakpoint at the function that feeds bytes into the parser (often the caller of xmlParseChunk).

7.2 Find the handler pointer in memory

Once you identify where the handler struct lives (stack or heap), print its address and locate key fields like startElementNs. Then set a watchpoint on that pointer.

watch *(void**)startElementNs_ptr_address
continue

When the overflow corrupts the callback, GDB breaks at the exact write site. That is the cleanest proof that corruption reaches control-flow.


8) Safe code snippets: understand the bug pattern without weaponizing

8.1 Toy example: “XPath-like string growth + strcat overflow”

This is not WatchGuard code; it just mirrors the bug shape in a harmless harness.

// toy_xpath_overflow.c (educational pattern)
// Shows how repeated strcat without bounds checks can overflow a path buffer.

#include <stdio.h>
#include <string.h>

int main(void) {
  char path[64];
  memset(path, 0, sizeof(path));
  strcpy(path, "/root");

  // Simulate deep XML nesting with long tag names.
  const char *tags[] = {
    "aaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbb", "cccccccccccccccc", "dddddddddddddddd"
  };

  for (int i = 0; i < 4; i++) {
    strcat(path, "/");          // unsafe
    strcat(path, tags[i]);      // unsafe
  }

  printf("path=%s\n", path);
  return 0;
}

How to learn from it defensively

8.2 Toy example: “overflow → function pointer corruption” + watchpoint

// watchpoint_lab.c (educational toy)
// Overflows a buffer adjacent to a function pointer.

#include <stdio.h>
#include <string.h>

typedef void (*cb_t)(void);

typedef struct {
  char buf[32];
  cb_t cb;
} Handler;

static void legit(void) { puts("[+] legit callback"); }

int main(void) {
  Handler h;
  memset(&h, 0, sizeof(h));
  h.cb = legit;

  // Overflow buf into cb (educational only)
  strcpy(h.buf, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");

  // May crash or behave unexpectedly if cb is corrupted
  h.cb();
  return 0;
}

In GDB:

b main
run
next     # step until after h.cb is assigned
p &h.cb
watch *(void**)&h.cb
c

You’ll break at the exact write that changes h.cb. That’s the same method used to prove control-relevant corruption in real targets.


9) Patch verification: how to confirm the fix (without source code)

Once you have both a vulnerable build (e.g., <= 12.7.2_Update1) and a fixed build (12.7.2_Update2), you can validate remediation as a reverse engineer.

9.1 Behavior-based confirmation

9.2 Binary-diff confirmation (what to look for)

Release notes explicitly list the vulnerability as resolved in Update 2; the PSIRT advisory lists resolved versions across branches.


10) Detection & mitigation (what you do as a defender)

10.1 Patch

Apply WatchGuard’s fixed versions for your branch (see PSIRT advisory).

10.2 Reduce attack surface

10.3 Look for suspicious traffic patterns

10.4 Why this belongs in “KEV thinking”

NVD includes CISA’s “Known Exploited Vulnerabilities” metadata (date added and due date) for CVE-2022-26318.


11) Lessons learned (the “expert takeaways”)

  1. Don’t build unbounded strings from attacker-controlled structure. SAX parsing creates a natural growth vector (deep nesting) that attackers can exploit.
  2. Callback tables are control-flow gold. If you store function pointers near variable-length buffers, you’ve created a corruption-to-control bridge.
  3. Watchpoints beat guesswork. A single watchpoint can turn “I think it overwrites X” into “here is the exact instruction that did it.”
  4. Vendor advisories + wire intel + RE = complete picture.

Reverse-engineering this bug responsibly gives defenders a blueprint to harden management planes, validate firmware fixes, and detect exploit traffic early.

← All writing