
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.
libc stdin FILE structure, letting us overwrite its read callback with the address of system().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 webfontsFigure 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.
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.cgiFigure 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.cgiFigure 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 0x3fb5c48cFigure 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.
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.
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 + 0x4afd4Figure 9: Key addresses as fixed offsets from the libc base
After establishing stdin_FILE as the target, the exploit was five moves.
Content-Disposition line overran the accumulator and clobbered the adjacent free chunk’s {size, fd, bk} metadata fields.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.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.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.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.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.
libgcgi builds. System > Services) and manage devices exclusively via SSH or local console until patched.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:
/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.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.
We reported the vulnerability to Opengear through coordinated disclosure, and Opengear has since released fixed firmware. The issue is tracked as CVE-2026-102212.
Special thanks to the Armadin Defensive Services (ADS) team for their contributions to the “Defensive Considerations” section.
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.