Skip to content
Rapid7 Analysis: CVE-2025

Rapid7 Analysis: CVE-2025

Rapid7 June 17, 2026

On April 3, 2025, Ivanti published an advisory for CVE-2025-22457, an unauthenticated remote code execution vulnerability due to a stack based buffer overflow. This vulnerability affects Ivanti Connect Secure, Pulse Connect Secure, Ivanti Policy Secure, and ZTA Gateways. Ivanti, in conjunction with the incident response firm Mandiant, also disclosed that this vulnerability was exploited in the wild by a suspected China-nexus threat actor.

Interestingly, the disclosure of CVE-2025-22457 on April 3, postdates the actual discovery and patching of the vulnerability by several months. Ivanti states that they discovered this vulnerability and patched it circa February 2025; however, due to Ivanti’s understanding of the issue at the time, it was not patched as a security vulnerability, but rather as a product bug. According to the vendor, that decision stemmed from the perceived complexity in leveraging the vulnerability for exploitation.

It has transpired that a China-nexus threat actor was able to reverse engineer the February 2025 patch, discover the vulnerability, and then proceed to build a successful exploit in spite of the complexity in leveraging the vulnerability for remote code execution. This is a salient reminder that state- threat actors are actively reverse engineering vendor patches for high-profile software targets, and are able to identify silently patched (or otherwise not publicly disclosed) vulnerabilities. Additionally, state- threat actors have both significant time and expertise to develop nuanced and complex exploits against high-profile targets. This highlights what is arguably an asymmetry between threat actor resources and capabilities, and technology producer resources and capabilities when making impact judgments potential security issues.

This analysis details the Rapid7 vulnerability research team’s work in building a remote code execution exploit for CVE-2025-22457, targeting Ivanti Connect Secure version 22.7r2.4. For reference, it took us approximately 4 business days of work to go from an initial crash to RCE.

Security firm watchTowr published a blog on April 4, 2025 that detailed the root cause of the vulnerability. For completeness, we will also outline the vulnerability below.

The vulnerability lies in the HTTP(S) web server, located in the / /bin/web binary. The function WebRequest::dispatchRequest will process an incoming HTTP request, and iterate over every HTTP header in the request. The following block of pseudo code (simplified from the original decompilation) shows how an X-Forwarded-For header value is processed.

We can see from the above at [1] that if an HTTP request supplies an X-Forwarded-For header, the function strspn is used to count the number of leading characters in the header value that are either ASCII numbers, of a period character (e.g. any character within the character set 0123456789.). Then at [2], this count value sz is used during a call to strlcpy to copy the leading ASCII numbers or period characters from the HTTP header value to a fixed 50 character buffer on the stack, before null terminating the destination string, buff50.

As no length check is performed on sz, an attacker may supply an X-Forwarded-For header value with a length greater than 50 characters and overflow the buff50 buffer on the stack. However due to the use of strspn to count the number of characters to copy, the attacker can only overflow the buff50 buffer with characters from the character set of 0123456789..

Note [3] above, where a value is written to ctx->in_addrD8. The ctx variable is a parameter to WebRequest::dispatchRequest, and holds the state of the current request. As we will see, the ctx variable will become a crucial component for exploiting this vulnerability.

The limitation in the potential usable characters during the overflow makes exploitation challenging; typically an attacker would want to place arbitrary bytes in the range 0x00 - 0xFF within the overflow buffer, in order to overwrite a return address with an arbitrary value, or build out a Return Oriented Programming (ROP) chain that will contain pointers and other arbitrary values.

Using GDB, we can quickly see the issue with only being able to place characters from the character set 0123456789. in the overflow buffer. While Address Space Layout Randomization (ASLR) is in place, we know from our work on Ivanti Connect Secure that the target web binary is a 32-bit x86 process, and that only 9 bits of entropy are used. This makes brute forcing a single address possible in around ~512 attempts (2 to the power of 9). However, inspecting the memory layout below shows us that, notwithstanding ASLR, there is nothing actually mapped in an addressable range that is comprised of characters we can actually construct a valid pointer for (e.g. 0x39393939 or 0x2e2e2e2e). The main process binary, heap, and shared objects are all mapped from around 0x56000000 and above.

It will not be possible to overflow a return address, as if we do we will not be able to address anything useful. To continue, we need to understand what other things we can target during the overflow that are not the saved return address on the stack.

To explore this, we can use the Ruby snippet below to construct a simple HTTP request to trigger the overflow.

The resulting 646 character value of 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111122223333444455556666 will overflow the stack, and we can see in GDB that a segmentation fault (SIGSEGV) has occurred.

We can see above that we have crashed just after the overflow (location [2] in our pseudo code snippet earlier), during the statement ctx->in_addrD8.s_addr = -1; (location [3] earlier). Of note is the register edx, which is now fully controlled by our overflow (0x36363636, or 6666 in ASCII). We can see from the disassembly in GDB that the value of edx was read from the stack’s base pointer at [ebp+8]. As the function WebRequest::dispatchRequest uses a base pointer-based stack frame (the function’s prologue stores esp in ebp), and the ctx variable is stored on the stack, we can overwrite this variable during the overflow and the overflowed value is the used throughout the function, referenced via [ebp+8]. We can leverage this capability to control the ctx variable, rather than targeting a return address on the stack.

We still have an issue in that we cannot actually address anything useful. As we have already seen, nothing is loaded at an address we can construct a valid pointer for, using the limited character set available to us. While we cannot influence where any shared libraries are loaded, we can influence where data is stored. With this in mind, and given the target process is a 32-bit process, we can leverage a heap spray to place attacker-controlled data at a location we can actually address. This is complicated somewhat by the heap being loaded after the main binary (/ /bin/web) somewhere after the address 0x56000000, so any heap spray will need to place data at an address we can construct a valid pointer for, e.g. 0x39393939 or 0x2e2e2e2e, all of which will have to occur before the main binary. This forces us to perform a large heap spray and consume the entire address space, in order to force the heap allocator to wrap around and start serving allocations at lower addresses, and ultimately at addresses we can construct valid pointers for.

Generating a heap spray requires the attacker to force large heap allocations to be made that are both long-lived (i.e., they must not be freed before the exploit completes) and contain attacker-controlled data. By repeating this, the attacker can consume huge amounts of heap memory, and by writing a repeatable pattern of bytes in these allocations, the attacker can guess an address. Due to the heap spray, the location of this address will be mapped and contain the pattern the attacker expects, thus defeating ASLR (for the purpose of a pointer to attacker-controlled data, not for a pointer to a shared object).

We identified the ability to force large long-lived heap allocations via the web server’s IF-T/TLS transport mechanism. Using this mechanism, we can spray around 2.3 GB of data into the target process. After the heap spray has completed, we end up with an attacker-controlled pattern addressable at a low address such as 0x39393939. This lets us overwrite the ctx variable and then control the contents of this data structure. Doing so will let us influence the control flow of the function WebRequest::dispatchRequest, and ultimately let us execute arbitrary code.

We can explore this further: By spraying the following pattern, we can see in GDB how we can construct a valid pointer to address the contents of the spray pattern.

After our heap spray has completed, our pattern will always be addressable at 0x39393918. As we cannot write the byte 0x18 during our overflow due to the character restrictions, we will use the address 0x39393930 to address our pattern, referencing 24 (0x18) bytes into the pattern (0x39393918 + 0x18 == 0x39393930). We therefore trigger the overflow as before, but this time adjusting the overwritten ctx variable to be 0x39393930.

In GDB, we can inspect the memory at 0x39393930 and confirm it contains the expected data.

We know we can overwrite the ctx variable pointer, and we can leverage a heap spray to point the ctx variable to a spray pattern we control. We also know this pattern can contain arbitrary bytes (i.e. bytes not subject to any character restrictions). However, to execute arbitrary code we will need to gain control of the instruction pointer (eip) using a value from our spray pattern.

Looking at the decompilation of WebRequest::dispatchRequest, we can see that all header values are processed inside a while loop, and a value ctx->max_headers is used to break out of the loop when all the header values are processed. The pseudo code of this is shown below at [4].

The ctx->max_headers member variable is at offset 0x64 in the ctx structure. If we spray a value of 0x00000000 at location 0x39393930+0x64 (0x39393994) then we can break out of the while loop after the overflow has occurred when processing the X-Forwarded-For header, and before any other headers are processed. If we do not break out of the loop early, it results in a segmentation fault that we cannot avoid.

Further down the function (we have omitted a large portion of irrelevant code from the above pseudo code), we can see a call to processCookieHeader occurs if ctx->cookie_str is not null. The cookie_str member variable is at offset 0xC0 and will be located in the adjacent spray pattern at 0x393939f0 and will not be null, so this check passes and processCookieHeader is called (at [5] above).

Looking at the pseudo code for processCookieHeader below, we can see that a virtual function call happens based on a series of three pointer dereferences originating from the ctx structure.

First a pointer is read from offset 0x2C ([6] above). We can control this value via our spray pattern at 0x39393930 + 0x2c (0x3939395C). This pointer is then dereferenced and the resulting value adjusted by +16 (0x10) before itself being dereferenced. The dereferenced value is then used as a function pointer at the call site for [7] above. For clarity, a breakdown of these three dereferences are shown in the below pseudo code.

Understanding the above, we can adjust our spray pattern as shown below. We set the ctx->max_headers member variable to be null, and set the ctx->dword2C member variable, which when dereferenced three times, as per [7] above, will give us arbitrary eip control.

Spraying the above pattern and then triggering the overflow results in a segmentation fault in GDB as follows.

We now have arbitrary eip control at the location 0x41414141, and this pointer value originated from our spray pattern. We are no longer subject to the character restrictions of the original stack-based buffer overflow and can proceed to leverage a ROP chain to achieve RCE.

We can note that the ebp register, as previously mentioned, will point into our spray pattern at 0x39393930. We will be able to leverage this for the purpose of a stack pivot gadget later.

We can also note that the edi register is 0xcafef14d which originates from our spray pattern. We will be able to leverage this during the ROP chain.

Now we have arbitrary eip control, we will build a ROP chain that will achieve RCE. We have a constraint here in that we do not know the base address of any shared object in the process’s address space. In lieu of a suitable info leak, we will aim to brute force this address. As previously mentioned, ASLR will employ 9 bits of entropy, so we expect to guess correctly in around 512 attempts or less. With this in mind we will build our ROP chain consisting of gadgets located in a common shared object. We choose the shared object binary / /lib/libdsplibs.so to pick gadgets from as this is a large binary so should offer plenty of suitable gadgets. By choosing gadgets from a single shared object, we only need to guess one address correctly (the base address of libdsplibs), as all gadgets within this shared object will be offsets from this common base address, and these offsets can be known in advance.

Working backwards through the chain, we want to end up being able to execute an arbitrary OS command string in order to deliver a payload. We for function gadgets that may let us call the system function. The function DSSys::isInterfaceEnabled, as shown below, will be suitable for our purpose.

If we can call DSSys::isInterfaceEnabled and pass a pointer to an attacker-controlled string as the first parameter, we can perform a command injection in this function and execute an arbitrary OS command. Given we are injecting our payload into the command string /sbin/ifconfig %s > /dev/null 2>&1, we will pass our payload as the string a; OUR_PAYLOAD_STRING # . This will satisfy the call to /sbin/ifconfig before executing our payload string, and finally commenting out the trailing > /dev/null 2>&1.

Searching for calls to the function DSSys::isInterfaceEnabled, we find this gadget.

This will use the edi register as the first parameter to DSSys::isInterfaceEnabled. As we noted in the section above, we can control the edi register value as it originates from our spray pattern. We have seen previously that the edi register is set to 0xcafef14d, this is originating from location 0x39393968 in our spray pattern. To point edi to an arbitrary payload command string, we will set the value at 0x39393968 to point to the end of our spray pattern. We will extend our spray pattern from 128 bytes, to 256 bytes. This allows for an additional 128 contiguous bytes to be used for our payload command. If we do not extend the spray pattern, then we have to smuggle the payload command into the existing 128 byte spray pattern, which is unnecessarily awkward.

As the shared object is compiled with -fPIC, we will need to set the ebx register to the location of the objects .got.plt section (i.e,. the end of the binary’s Global Offset Table). This is required for the call to system to work correctly. The following gadget will pop a value we control (i.e., the address of the .got.plt section) into ebx.

Finally, to kickstart the whole chain, we will need a stack pivot gadget to point esp into our spray pattern. As we know the ebp register will point into our spray pattern, we can use the following stack pivot gadget.

Armed with our three gadgets, we can modify the spray pattern to interleave the ROP chain into the ctx structures data, as shown below.

So long as we guess the base address of libdsplibs correctly, performing the heap spray and then triggering the stack-based buffer overflow will achieve unauthenticated RCE on the target.

To brute force ASLR and guess the correct address of libdsplibs we have two potential strategies.

The first is to pick a static and potentially valid base address and repeatedly try this same address over and over. This will work if the / /bin/web process does not fork for child connections. Every failed attempt will crash the / /bin/web process, and the process will automatically be restarted. Every new instance of the / /bin/web process will be subject to ASLR, and the base address of libdsplibs will change each time. Eventually the static address we have chosen will be correct.

The second strategy is to increment the base address upon each attempt, and effectively over a range of 512 possible addresses. This strategy is better suited if the / /bin/web process forks, as the forked child will have the same memory layout as the parent. We cannot rely on repeatedly trying a single static base address as we will need to for the correct address.

In our test setup, the Ivanti Connect Secure virtual appliance did not fork the / /bin/web process for child requests, so we developed our PoC using the first strategy.

The following PoC exploit script implements the exploitation strategy described in this analysis. The PoC has been tested against a vulnerable Ivanti Connect Secure version 22.7r2.4, running as a virtual appliance on a local hypervisor.

An example of the PoC running in our test setup can be seen below. In this example the base address of libdsplibs is being brute forced, and was successful on the 35th attempt.

The exploit described in this analysis can be improved in the following ways.

The following vendor-supplied patches will remediate CVE-2025-22457, per Ivanti’s advisory:

Notably, while the patch for Ivanti Connect Secure has been available since February 2025, Ivanti has not yet made patches available for Ivanti Policy Secure, or ZTA Gateways. These are due to receive a fix on April 21 and 19 respectively. Additionally, Pulse Connect Secure is no longer supported, so users must migrate to the latest version of Ivanti Connect Secure.

For more information on patching, please referer to the vendor advisory .

To help identify a compromised appliance, the vendor advisory states the following:

Customers should monitor their external ICT and look for web server crashes

Based on our understanding of exploitation, examining an appliance for web server crashes is a useful indication of attempted exploitation. This is due to how the exploit, in lieu of a suitable info leak to break ASLR, must rely upon brute forcing an address of a shared object library in the web server process. Every failed attempt to guess the correct address will result in the web server process crashing, and subsequently restarting. We can see evidence of this as event SYS10306 in the event logs, as shown below. We can note the numerous events of the web server starting in quick succession (around 1 minute intervals in the example below, although this interval will vary depending on how efficiently the attacker is able to perform the heap spray).

Extracted Entities