During a recent assessment, I came upon a VPN management portal I had never seen before that turned out to be a FatPipe MPVPN appliance. After a little research I discovered the backend for several of the web handlers were native applications, ideal for pointing Claude/GPT 5.6 sol at to hopefully find some vulnerabilities and land an old-fashioned native memory-corruption exploit. The initial target was the login function located in /usr/sbin/auth_user_pass, and the first thing that stood out was an unchecked strcpy() into a fixed-size stack buffer…. what are the chances???

The plan was pretty straightforward. I needed to reach the strcpy from an unauthenticated HTTPS request, control the saved return address, execute a ROP chain, and return the results of any command I executed back through the web application. What made things challenging was not the overwrite itself. The payload had to survive form encoding, Tomcat string handling, an XSS filter, protobuf serialization, a native daemon, a bounded command buffer, a POSIX shell, argument parsing, and finally a sequence of repeated copies into the same stack frame.

While tracing the login request route, I found that one of the layers was not merely transporting the exploit. The xtremed daemon was rebuilding attacker-controlled authentication data as a shell command and passing it to popen(). The path to the memory-corruption RCE also contained a much simpler root command injection.

Those became two separate vulnerabilities: CVE-2026-90822, an OS command injection in xtremed, and CVE-2026-90823, a stack-based buffer overflow in auth_user_pass. Both are reachable before authentication when the affected management interface has been enabled and is accessible to the attacker. Both ultimately execute with uid=0(root).

This is the story of beginning with the harder bug, finding the easier bug in the middle, and then returning to make the native exploit work anyway.

Starting with the crash

The vulnerable native program accepts authentication-related values as command-line options. One option handles a group string by copying the complete argument into a fixed-size local buffer without checking its length:

Copy to Clipboard

In the tested build, the saved return address is 5,176 bytes from the start of the destination buffer. The executable is x86-64, non-PIE, protected by NX, and compiled without a stack canary. NX prevents simply placing code on the stack, but the fixed executable layout provides stable return-oriented-programming gadgets.

A sufficiently long -g argument overwrote control data and reached the saved instruction pointer. Remotely, however, there was no direct HTTP parameter named groups. Reaching that option meant first understanding every component between the login form and the native process.

Following the authentication path

The live login servlet did not execute auth_user_pass directly. It collected userName, password, and ipAddress, placed them into a protobuf WebAuthRequest, and sent that message to a native service on the appliance.

Copy to Clipboard

Tracing the live path produced the complete chain:

Copy to Clipboard

Each transition changed the representation of the data. The HTTP layer percent-decodes it. Java stores it as Unicode. Protobuf serializes the string as UTF-8. Native code places the values into a C command buffer. The shell interprets metacharacters and performs expansions. Only after all of that does getopt() expose an optarg pointer to the vulnerable copy.

The shell hiding in the middle

The xtremed handler reconstructed the protobuf values as a command string and executed that string with popen(). In simplified form, the dangerous operation looked like this:

Copy to Clipboard

popen() does not treat that string as a safe argument vector. It invokes a shell. Consequently, shell syntax embedded in an authentication field is evaluated before auth_user_pass starts.

Copy to Clipboard

The existing XSS filter did not change that result. It was built to recognize HTML and JavaScript-oriented patterns, not POSIX shell metacharacters. The characters needed for command substitution passed through the HTTP, Java, protobuf, and native-string layers intact.

At that point the complicated exploit was temporarily unnecessary. A short crafted login request caused the shell to execute an attacker-selected command as root. No memory corruption, race condition, address disclosure, or ASLR bypass was required. That direct path became CVE-2026-90822.

Going back for the overflow

After landing the command injection exploit I decided to give the memory corruption bug another look. The challenge was fitting a useful payload into xtremed‘s 4,097-byte command buffer while creating an argument more than 5,176 bytes long after shell expansion. A literal overflow string could not fit through the native command buffer. The request needed a compact description that the shell would expand into the much larger argument required by auth_user_pass.

Quoted printf command substitutions was the solution. The outer command remained a few kilobytes long, but each substitution produced thousands of padding bytes as a single argument after the shell parsed it.

The direct command-injection CVE executes the requested command during shell expansion. The memory-corruption exploit uses shell expansion only as a transport mechanism to construct the oversized -g arguments. The payload is written into memory and invoked by the ROP chain after control flow has been hijacked.

Making addresses survive UTF-8

The next problem was not finding gadgets. It was representing them. A 64-bit code address contains NULL bytes, and strcpy() stops at the first zero. Raw bytes above 0x7f risk changing when Java and protobuf serialize the string as UTF-8. To make things harder, Quotes, dollar signs, backslashes, and backticks have meaning to the shell.

The solution was to restrict the HTTP-visible payload to printable ASCII and let repeated copies reconstruct the binary values. Every -g option invokes strcpy() on the same destination. By sending the longest layer first and progressively shorter layers afterward, each terminating NULL lands at a chosen offset while preserving bytes already written farther into the buffer.

The exploit treats strcpy()‘s terminator as a write primitive. Instead of trying to transmit a NULL byte through HTTP and protobuf, it asks the vulnerable function to create that byte locally.

Copy to Clipboard

The first reliable chain called an existing process-execution function with a fixed command string already present in the executable. That was enough to prove instruction-pointer control and return uid=0(root) through the original HTTP response.

The extended chain used a pop gadget and a stosd write gadget to copy four command bytes at a time into writable static memory. It then placed the resulting string in the first argument register and called system(). The compact form supports printable-ASCII commands of up to 27 bytes while keeping both the generated xtremed command and every expanded stack layer within their respective limits.

Two CVEs in one trust chain

The two vulnerabilities share an entry point and privilege level, but they have different root causes and different fixes:

Issue CVE Root cause Execution point
OS command injection CVE-2026-90822 Untrusted protobuf fields concatenated into a shell command The shell evaluates attacker syntax before auth_user_pass starts
Stack-based buffer overflow CVE-2026-90823 Unchecked strcpy() into a fixed-size stack buffer A ROP chain executes after overwriting the saved return address

The published CVE records currently assign both issues CVSS 3.1 scores of 9.8 Critical: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H.

The fix

The confirmed affected release, 10.1.2r60p100, is end-of-life. FatPipe customers should contact FatPipe Support to confirm their firmware and move to a current supported release containing the fixes.

Until an upgrade is complete, the management interface should remain disabled where it is not required. If it must be enabled, access should be restricted to a dedicated administrative network and constrained with WAN ACLs.

At the implementation level, both root causes need independent correction:

  1. Replace the unbounded group copy with an explicit length check and guaranteed termination.

  2. Remove the shell from the authentication path. Invoke the helper with a fixed executable and an argument array rather than constructing a command string.

  3. Validate authentication fields at the HTTP, protobuf, and native boundaries instead of relying on an XSS-oriented filter.

  4. Run the broker and helper with the minimum privileges required for authentication.

  5. Rebuild privileged native components with stack canaries, PIE, full RELRO, and fortified libc checks.

The overflow was the bug I went looking for. The shell was the bug I found on the way.