Compromising the Out-of-Band Management Plane: Pre-Auth RCE via Heap Overflow in Opengear Console Servers

03 Nov 2026
WRITTEN BY
NICK CERNE
Compromising the Out-of-Band Management Plane: Pre-Auth RCE via Heap Overflow in Opengear Console Servers
Contents

Opengear CM7100 devices are console servers, typically used for out-of-band access to network equipment. For example, if a misconfiguration or an outage cuts off normal network access (SSH, VPN, or WAN) to a device, an operator can still reach that equipment out-of-band through the Opengear console server. 

We discovered a heap buffer overflow in the HTTP multipart request parser of the embedded web server. It was reachable without authentication and led to remote code execution (RCE). Opengear has released a fix and the issue is tracked as CVE-2026-102212.

We proved the full chain end to end on our local test device running the 4.8.0 firmware. The exploit script brute-forced the base address of libc and echoed the command output of id into /tmp/pwned.txt. 

Key Takeaways

  • Armadin leveraged the heap overflow to go from a remote unauthenticated position to dropping an SSH key onto the Opengear device. From there, Armadin authenticated as the root user and was capable of accessing every device serially connected to the Opengear console.
  • Exploitation required corrupting heap metadata so the allocator returned a pointer into the libc stdin FILE structure, letting us overwrite its read callback with the address of system().
  • The vulnerable parser executed before any authentication check and was shared by every CGI on the device, so anyone who could reach the web interface could trigger it.

Initial Discovery

After recovering the firmware, we discovered it contained a squashfs filesystem, as shown in the figure below.

$ unsquashfs -s -o 8 ./cm71xx-4.8.0.flash
Found a valid SQUASHFS 4:0 superblock on ./cm71xx-4.8.0.flash.
Creation or last append time Fri May  8 05:11:31 2020
Filesystem size 14022088 bytes (13693.45 Kbytes / 13.37 Mbytes)
...omitted for brevity...

Figure 1: unsquashfs fingerprinting the firmware flash file

After using unsquashfs to recover the filesystem, we built a Software Bill of Materials (SBOM), identifying targets reachable from an unauthenticated network position. 

Each Opengear console manager shipped with an embedded web application for administering and configuring the device remotely, which made it a natural research target. The web application ran on the Cherokee web server, and the web root lived under the /home/httpd directory, as shown in the figure below.

$ pwd && ls -la
/opengear/rootfs/home/httpd
total 44
drwxrwxr-x 10 user user 4096 Aug 18 17:18 .
drwxrwxr-x  3 user user 4096 Aug 18 17:18 ..
drwxrwxr-x  3 user user 4096 Aug 18 17:18 cgi-bin
drwxrwxr-x  2 user user 4096 Aug 18 17:18 css
drwxrwxr-x  2 user user 4096 Aug 18 17:18 forms
drwxrwxr-x  2 user user 4096 Aug 18 17:18 js
-rw-rw-r--  1 user user 3240 Aug 18 17:18 nav.xml
drwxrwxr-x  2 user user 4096 Aug 18 17:18 pixmaps
drwxrwxr-x  2 user user 4096 Aug 18 17:18 public
drwxrwxr-x  2 user user 4096 Aug 18 17:18 scripts
drwxrwxr-x  2 user user 4096 Aug 18 17:18 webfonts

Figure 2: Reviewing the contents of the Cherokee webroot

The web application presented a few interesting attack surfaces to unauthenticated users. Specifically, the application shipped with several CGI binaries that handled incoming requests matching /cgi-bin routes. The CGIs were Opengear’s own code, far less exercised than Cherokee itself, so that was where we focused.

The CGI binary disassemblies identified several unbounded string-copy callsites (e.g., strcat, strcpy) whose safety depends entirely on the calling code. Therefore, we prioritized fuzzing the endpoints that reached these callsites with American Fuzzing Library (AFL) in a virtualized environment. After instructing an LLM to fuzz the binaries with malformed HTTP requests, a vulnerable primitive was discovered in a matter of minutes.

The Vulnerable Parser

Our fuzzing harness initially pointed straight at index.cgi, the binary behind the login page and the main console UI. Every CGI binary on the device shared a single HTTP request parsing library, and that parser ran before any session or credential check. One routine inside it accumulated header lines into a single heap buffer, which was where the overflow lived.

Ghidra recovered that routine as mimeReturnHeader. In 5.5.2 it lived in the shared library libgcgi.so.0. In 4.8.0 the identical logic was statically linked into index.cgi and eight other CGI binaries. Below is the decompiled function, with generic variables renamed and key lines annotated for readability.

int mimeReturnHeader(void *stream, char **bufp, size_t *capp)
{
  char   *line;
  char   *oldbuf;
  size_t  len;
  int     total;

  // capacity starts at 256 bytes
  *capp  = 0x100;  
  total  = 0;
  // the header accumulator buffer
  *bufp  = xmalloc(0x100);   
  memset(*bufp, 0, 0x100);

  do {
    // next CRLF header line
    line   = (char *)crlfsbuf_read(stream);
    len    = strlen(line);
    total += len;   // total header bytes so far
	// the only size check
    if (total < (int)*capp) {   
      strcat(*bufp, line);
	// else branch taken when header > buffer
    } else {    
      oldbuf = *bufp;
	// double buffer capacity to 512 bytes
      *capp  = *capp << 1;   
      *bufp  = xrealloc(oldbuf);
    // overflow when header exceeds 512 bytes
      strcat(*bufp, line);   
    }

/* loop logic to continue until the end of the headers */
  } while (line != NULL);

  return -1;
}

Figure 3: mimeReturnHeader decompilation that contained the vulnerable sink

The header accumulator started at 256 bytes and doubled its capacity only once per line to 512 bytes when the running total of header bytes reached it. A single header line longer than 512 bytes therefore ran straight past the end of the buffer in the final strcat, and overflow bytes were attacker-controlled. To demonstrate where the overflow occurs, we issued a baseline request to our local test device to fill the buffer to exactly 512 bytes, as shown below.

NAME=$(python3 -c 'print("A"*473, end="")')
printf -- '--X\r\nContent-Disposition: form-data; name="%s"\r\n\r\nx\r\n--X--\r\n' "$NAME" \
| curl -sik -H 'Content-Type: multipart/form-data; boundary=X' \
    --data-binary @- https://192.168.0.1/cgi-bin/index.cgi

Figure 4: curl request that filled the buffer to 512 bytes

With the 512-byte buffer, the server processed the request normally, as shown below.

HTTP/1.1 200 OK
Transfer-Encoding: chunked
Date: Sun, 25 Oct 2026 13:46:58 GMT
Server: Cherokee/1.2.101 (UNIX)
…omitted for brevity…

Figure 5: HTTP 200 OK response indicating the 512-byte buffer was processed

However, a header that exceeded the allocated buffer size elicited an HTTP 500 Internal Server error, as shown in the figures below.

NAME=$(python3 -c 'print("A"*475, end="")')
printf -- '--X\r\nContent-Disposition: form-data; name="%s"\r\n\r\nx\r\n--X--\r\n' "$NAME" \
| curl -sik -H 'Content-Type: multipart/form-data; boundary=X' \
    --data-binary @- https://192.168.0.1/cgi-bin/index.cgi

Figure 6: curl request with payload exceeding the 512-byte buffer

HTTP/1.1 500 Internal Server Error
Server: Cherokee/1.2.101 (UNIX)
…omitted for brevity…

Figure 7: HTTP 500 Internal Server Error elicited from the application

Next, we replayed the request and captured the heap layout with a GDB script that showed exactly where the segmentation fault occurred.

Temporary breakpoint 2, 0x001ba9e8 in ?? ()
---- adjacent chunk header, AFTER strcat ----
  prev_size = 0x41414141
  size      = 0x41414141  (len=1094795584, prev_inuse=1)
  fd        = 0x41414141
  bk        = 0x41414141
  raw words @ 0x272900:
0x272900:       0x41414141      0x41414141      0x41414141      0x41414141
0x272910:       0x41414141      0x41414141      0x41414141      0x41414141
(continuing until the allocator touches the corrupted chunk...)
Program received signal SIGSEGV, Segmentation fault.
0x3fb5c46c in ?? ()
==== FAULT ====
pc             0x3fb5c46c          0x3fb5c46c
lr             0x3fb5c3d4          1068876756
sp             0x40800048          0x40800048
r0             0x26                38
r1             0xc                 12
r2             0x24                36
r3             0x41414141          1094795585
faulting instruction:
=> 0x3fb5c46c:  str     r8, [r3, #8]
   0x3fb5c470:  bne     0x3fb5c48c

Figure 8: GDB script output showing the faulting instruction

The faulting instruction occurred inside the dynamic allocator’s unlink functionality, which was responsible for updating chunk metadata pointers when removing the adjacent free chunk. Because the bk pointer value (r3) was clobbered with an invalid address (0x41414141), the dynamic allocator crashed when attempting to write to that unmapped address.

Because header lines were measured with strlen() and appended with strcat(), our payload needed to be a NUL-free C string. Specifically, a few values were off limits: 0x00 (NUL), 0xd and 0xa (CRLF), and 0x22 (double quote). This immediately complicated things as it eliminated quick-win paths like overwriting the Global Offset Table (GOT), because GOT addresses carried a NUL in the high byte that we couldn’t include in our payload. 

From Overflow to Write Primitive

The accumulator buffer bordered a free chunk sitting in the allocator’s unsorted bin, directly after it in memory. The overflow therefore landed directly on that neighbouring chunk’s metadata, the {size, fd, bk} header that the uClibc allocator (a dlmalloc-2.7 derivative) trusted the next time it recycled the chunk. Once we controlled those metadata fields, we controlled where the allocator would write and what it handed back to the next malloc.

The stdin FILE Hijack

Rather than forging fake chunks, targeting GOT, or spraying the heap, we aimed the corrupted chunk’s back pointer back into libc, at the stdin FILE object. A FILE is just a struct, but it is a struct full of function pointers. One of them is the FILE’s read() callback, the routine the library calls to pull in more input. That mattered because the parser was still draining our POST body through getc(stdin), and a buffered getc only goes back for more input when its buffer runs dry, at which point it calls that read() callback. Overwriting the read() callback with system() and the neighboring cookie() field with a pointer to a command string turned the parser’s next getc(stdin) into system("id").

The address we targeted was V = stdin_FILE - 4. In our test run we disabled ASLR, so libc sat at a fixed 0x40c7f000 and V resolved to 0x40cd71dc. Two properties made V ideal. None of its bytes were NUL, so it survived inside the overflowing header text where NULs were forbidden. Additionally, the four bytes at V+4 were the struct’s modeflags field, 0xa1, which the allocator used as the “chunk’s” size field. Masking off the low-status bits (the 0x1 is PREV_INUSE) leaves 0xa0, which dropped V neatly into a small, easy-to-hit small bin.

The key addresses were shown relative to one concrete libc base, 0x40c7f000, from our test run. ASLR randomized that base on every fork, so the absolute values differ each time. However, the offsets from the base stayed constant, so the whole payload shifted with it as shown below.

V            0x40cd71dc   stdin_FILE - 4  (the chunk malloc returns)
stdin FILE   0x40cd71e0   gcs.read @ +0x28, cookie @ +0x24, modeflags @ +0
system       0x40cc9fd4   libc base + 0x4afd4

Figure 9: Key addresses as fixed offsets from the libc base

After establishing stdin_FILE as the target, the exploit was five moves.

  1. Overflow. An over-long Content-Disposition line overran the accumulator and clobbered the adjacent free chunk’s {size, fd, bk} metadata fields.
  2. Aim and disarm. We set the chunk’s bk to V (stdin_FILE - 4), so the allocator would later hand back the FILE struct as the next free “chunk.” The overflow also overwrote the chunk’s size field, and since it couldn’t contain NULs, it became a large value. We chose the value carefully so the allocator’s bookkeeping stayed within mapped memory and didn’t crash before our real target was reached.
  3. Steer. On the next malloc, the unsorted-bin scan followed the bk pointer to V, interpreted stdin_FILE’s modeflags (0xa1) as a chunk of size 0xa0, and filed V into smallbin[20]. After the allocator filed our fake chunk into smallbin[20], its bin-walk continued into the stdin buffer, which held our POST body verbatim. The walk stopped only when it read a pointer equal to the bin’s head, which was how dlmalloc knew it reached the end of the bin. Since the body included the multipart boundary we chose, we placed the bin-head address at exactly the bytes the walk read next, so it terminated cleanly.
  4. Install. The last piece was getting the allocator to hand V (stdin_FILE) back as the next chunk to store our data. The CGI copied each Content-Disposition parameter with malloc(strlen + 1), so we sized the parameter precisely, at 148 bytes, to make that request round up to 0xa0, the exact size class where our fake chunk was waiting. The malloc(0xa0) pulled V back out of smallbin[20] on an exact-fit path and returned (V+8). The parameter’s strncpy then wrote our bytes straight over the FILE struct: the read callback became system, the cookie became a pointer to the string “id”, and the buffer cursors were set equal so the stream read as empty.
  5. Fire. Immediately after the header line, the parser ran its next getc(stdin). The buffer looked empty, so getc took the refill path, which called gcs.read(cookie). That was now system("id"). There was no shellcode, ROP, or smashed return address. We simply redirected an indirect call the program was going to make anyway.

Reaching a Live Device

index.cgi was not a Position-Independent Executable (PIE), so the binary itself never moved, but libc, the stack, and the heap were mapped and randomized on real hardware. As noted earlier, every address in the payload was expressed as libc_base + constant, so the whole exploit hinged on a single correct guess of the libc base. The ARM mmap base carried only about eight bits of entropy, approximately 256 possible locations for libc. A wrong guess cost nothing: Cherokee forked a fresh process per request, so a crash killed only that child and the next request started clean.

The result was unauthenticated command execution as the web server user, reachable by anyone who could talk to the console’s web interface, on a device whose entire purpose was break-glass access to everything else in the rack. The same parser was statically linked into nine CGIs in 4.8.0 and shipped as the shared library libgcgi.so.0 in 5.5.2, so the defect was not limited to the 2020 build we started from.

Defensive Considerations

Near-Term Remediation

  • Upgrade Immediately: Apply firmware version 5.5.3 (or later) to remediate CVE-2026-102212 across all CM7100 and related models running vulnerable libgcgi builds.
    • Considerations: Upgrading console server firmware typically reboots the unit, terminating active serial sessions and background port-logging daemons. Coordinate updates during scheduled maintenance windows to prevent monitoring blind spots.
    • Considerations: Older appliances that cannot support firmware 5.5.3 due to memory or hardware constraints must have the web daemon permanently disabled, relying entirely on hardened SSH/CLI access.
  • Disable HTTP/HTTPS Management: If firmware cannot be updated immediately, disable the embedded web interface entirely (System > Services) and manage devices exclusively via SSH or local console until patched.

Long-Term Posture and Hardening

  • Dedicated OOB network isolation: Isolate out-of-band management planes into dedicated, non-routable VRFs or physically separate management networks. Console servers should never share routing domains or transit networks with user traffic.
  • ZTNA/authenticated bastion architecture: Place all administrative web interfaces behind an authenticated Zero-Trust Network Access (ZTNA) or VPN gateway. The unauthenticated multipart parser must never be reachable by untrusted hosts.
    • Considerations: Restricting OOB access behind VPNs and ZTNA gateways creates a circular dependency. If the primary network or identity provider goes down, administrators may lose break-glass access. Ensure isolated, out-of-band emergency jump paths (e.g., dedicated dial-in/cellular with hardware MFA) are pre-configured and tested.
  • Restricted inbound access via ACLs: Enforce strict Layer 3/4 access control lists (ACLs) on upstream management switches and local firewall rules, permitting web traffic only from designated administrative jump boxes.

Detection and Alerting

Forward logs for the web interface to a SIEM, from the device or from a proxy in front of it where one exists, and build detections around the traffic this chain produces:

  • HTTP 500 responses to requests for /cgi-bin/index.cgi and other /cgi-bin paths, particularly in bursts from one source; an over-long header produced a 500 in testing, and the exploit has to guess a randomized memory address from about 256 possibilities, so repeated failed attempts are expected before one succeeds.
  • SSH logins as the root user from addresses outside the management network, and any new authorized SSH key on a console server.

Enable file integrity monitoring on the authorized SSH keys of each unit where the platform allows it, or compare them against a known-good list on a schedule.

Disclosure Timeline and Remediation

We reported the vulnerability to Opengear through coordinated disclosure, and Opengear has since released fixed firmware. The issue is tracked as CVE-2026-102212.

  • September 2026: Armadin tested and notified affected customers
  • September 2026: Responsible disclosure to Digi International (Digi)
  • September 2026: Digi confirmed the findings
  • October 2026: Digi released a patch and informed customers
  • October 2026: CVE-2026-102212 released and Armadin blog post published

Acknowledgements

Special thanks to the Armadin Defensive Services (ADS) team for their contributions to the “Defensive Considerations” section.

Stay Ahead of Whatever Threats May Come

AI-driven threats require a new approach and the right platform to keep environments secure. To pressure-test your own environment with the Armadin attacker, request a demo.

Continue reading
Opportunity Intelligence Research to Reduce Risk
Blog
11.2.26
Opportunity Intelligence Research to Reduce Risk
Solving for Opportunity: The Role of Threat Intelligence in the Age of AI
Blog
11.2.26
Solving for Opportunity: The Role of Threat Intelligence in the Age of AI
Weaponizing Cryptography Mistakes to Achieve RCE and Administrative Access: Kill Chains and Coffee
Blog
Video
10.29.26
Weaponizing Cryptography Mistakes to Achieve RCE and Administrative Access: Kill Chains and Coffee