# Exploitation Walkthrough and Techniques – Ivanti Connect Secure RCE (CVE-2025-0282)
As we saw in our previous blogpost, we fully analyzed Ivanti’s most recent unauthenticated Remote Code Execution vulnerability in their Connect Secure (VPN) appliance. Specifically, we analyzed CVE-2025-0282.
Today, we’re going to walk through exploitation. Once again, however, stopping short of providing the world with a Detection Artifact Generator (also known as a proof of concept, apparently) – as previously mentioned, release and sharing of our PoC (in a to-be-decided form) will be held back until the 16th Jan ‘25.
> Note from the editor: Before that – an observation. We’ve seen a number of security executives and leaders tell the world how unfair it is that people could possibly criticize Ivanti and their continuous spate of mission critical vulnerabilities in mission critical appliances – because “attackers always use new [in 1999] techniques” and zero-day can happen to anyone :^)
>
> We believe this is a real-world example of stockholm syndrome – get help – or perhaps these individuals don’t understand the vulnerabilities? **Modern** security engineering, is hard – but none of this is modern. We are discussing vulnerability classes – with no sophisticated trigger mechanisms that fuzzing couldnt find – **discovered in the 1990** s, that can be trivially discovered via basic fuzzing, SAST (the things product security teams do with real code access).
>
> As an industry, should we really be communicating that these vulnerability classes are simply too complex for a multi-billion dollar technology company that builds **enterprise-grade, enterprise-priced** network security solutions to proactively resolve?
We’ve enjoyed walking through the exploitation of this vulnerability, and we’re excited to share our work with everyone.
So without further ado, onto the fun part…
### Previously On ‘Reproducing CVE-2025-0282’
In our previous post, we discussed the root cause of CVE-2025-0282 – a stack-based buffer overflow in code designed to handle IF-T connections.
The following analysis is based on decompiled code from an Ivanti Connect Secure appliance running 22.7R2.3. The vulnerability specifically is found in the binary /home/bin/web, which handles all incoming HTTP requests and VPN protocols – including IFT TLS – for an Ivanti Connect Secure appliance.
Previously, we identified that if an attacker sends a `clientCapabilities` block with more than 256 bytes, they’ll spill over into other stack variables, and eventually into the return address, where RCE awaits.
As you may recall, and repeated here if not, we identified that Ivanti developers used `strncpy`, instead of the unsafe `strcpy` when handling clientCapabilities, but had mistakenly passed in the size of the input string – _not_ the size of the destination buffer – as the size limit.
As we can see in the output of decompiled below, the dest buffer is defined with a 256 byte size. Subsequently at line 22, we can see that the value of the clientCapabilities parameter is extracted, with the length of this value calculated at line 25 and copied into the dest buffer at line 31.
This ultimately provides the smoking gun for the vulnerability that we’re hunting, and allows for an out-of-bound write.
“`
2: int __cdecl ift_handle_1(int a1, IftTlsHeader *a2, char *a3) 3: { 4: 5: int v18; 6: int v19; 7: char dest[256]; // [esp+120h] [ebp-8ECh] BYREF 8: char object_to_be_freed[4]; // [esp+220h] [ebp-7ECh] BYREF 9: void *ptr; // [esp+224h] [ebp-7E8h] 10: int v20; // [esp+228h] [ebp-7E4h] 11: int v21; // [esp+22Ch] [ebp-7E0h] 12: int v22; // [esp+230h] [ebp-7DCh] 13: char v23; // [esp+234h] [ebp-7D8h] 14: char v24; // [esp+235h] [ebp-7D7h] 15: void *v25; // [esp+23Ch] [ebp-7D0h] 16: _DWORD v26[499]; // [esp+240h] [ebp-7CCh] BYREF 17: 18: 19: [..SNIP..] 20: 21: 22: clientCapabilities = getKey(req, “clientCapabilities”); 23: if ( clientCapabilities != NULL ) 24: { 25: clientCapabilitiesLength = strlen(clientCapabilities); 26: if ( clientCapabilitiesLength != 0 ) 27: connInfo->clientCapabilities = clientCapabilities; 28: } 29: } 30: memset(dest, 0, sizeof(dest)); 31: strncpy(dest, connInfo->clientCapabilities, clientCapabilitiesLength); 32: 33: v24 = 46; 34: v25 = &v57; 35: if ( ((unsigned __int8)&v57 & 2) != 0 ) 36: { 37: LOBYTE(v24) = 44; 38: v57 = 0; 39: v25 = (__int16 *)&v58; 40: } 41: memset(v25, 0, 4 * (v24 >> 2)); 42: v26 = &v25[2 * (v24 >> 2)]; 43: if ( (v24 & 2) != 0 ) 44: *v26 = 0; 45: na = 46; 46: 47: 48: (*(void (__cdecl **)(int, __int16 *))(*(_DWORD *)a1 + 0x48))(a1, &v22); 49: 50: 51: isValid = 1; 52: EPMessage::~EPMessage((EPMessage *)v18); 53: DSUtilMemPool::~DSUtilMemPool((DSUtilMemPool *)object_to_be_freed); 54: return isValid; 55: 56: 57: }
“`
Now that we’ve gone through this section, we can hear you already: “Great, but what does the stack layout look like?”
### Stack It Up
Well, we’ve laid this out for you below. We have our dest buffer and a number of other variables, and as you can see – we have our stored return address.
Now, in normal and easier situations, you’d write enough data via the out-of-bounds write flaw to overwrite this return address – made easier because stack canaries aren’t in use – to gain control of our instruction pointer.
However, life is not so simple, and we have an issue:
“`
+———————+ | v18 (int) | +———————+ | v19 (int) | +———————+ | dest[256] | x/10wx $esp 0xff9fa6e0: 0xff9fa800 0x56d7fe10 0x00000d35 0x56767c7f 0xff9fa6f0: 0x00000032 0x5677d44c 0x00000000 0x00000000 0xff9fa700: 0x00000000 0x00000000
“`
None of these values are in our control, and so the `ret` that we longed for will go somewhere that is useless to us.
### Pivot Duck Slide Around The Stack
Although we were dismayed by this stack given it did not look at all hopeful, looking further down the stack shows something promising.
The bytes we have sprayed as part of our initial payload show up at exactly `$esp+0x120`.
Therefore, if we can perform a stack pivot and shift `$esp` to point to our controlled buffer, we can gain an early `eip` control instead of relying on the original IF-T parser epilogue.
“`
gdb> x/100wx $esp 0xff9fa6e0: 0xff9fa800 0x56d7fe10 0x00000d35 0x56767c7f 0xff9fa6f0: 0x00000032 0x5677d44c 0x00000000 0x00000000 0xff9fa700: 0x00000000 0x00000000 0x00000d34 0xff9fa752 0xff9fa710: 0x00001547 0xff9fa728 0x00000000 0xff9fa900 0xff9fa720: 0x00000000 0x00000000 0xff9fa900 0xf7a68490 0xff9fa730: 0xff9fa900 0x00000003 0x00000010 0xff9fa948 0xff9fa740: 0x00000000 0x00000000 0x00000000 0x00000000 0xff9fa750: 0x00000000 0x00000000 0x00000000 0x00000000 0xff9fa760: 0x00000000 0x00000000 0x00000000 0x00000000 0xff9fa770: 0x00000000 0x00000000 0x00000000 0x00000000 0xff9fa780: 0x00000000 0x00000000 0x00000000 0x00000000 0xff9fa790: 0x00000000 0x00000000 0x00000000 0x00000000 0xff9fa7a0: 0x00000000 0x00000000 0x00000000 0x00000000 0xff9fa7b0: 0x00000000 0x00000000 0x00000000 0x00000000 0xff9fa7c0: 0x00000000 0x00000000 0x00000000 0x00000000 0xff9fa7d0: 0x00000000 0x00000000 0x00000000 0x00000000 0xff9fa7e0: 0x00000000 0x00000000 0x00000000 0x00000000 0xff9fa7f0: 0x00000000 0x00000000 0x00000000 0x00000000 0xff9fa800: 0x61616161 0x61616162 0x61616163 0x61616164 0xff9fa810: 0x61616165 0x61616166 0x61616167 0x61616168 0xff9fa820: 0x61616169 0x6161616a 0x6161616b 0x6161616c 0xff9fa830: 0x6161616d 0x6161616e 0x6161616f 0x61616170 0xff9fa840: 0x61616171 0x61616172 0x61616173 0x61616174 0xff9fa850: 0x61616175 0x61616176 0x61616177 0x61616178 0xff9fa860: 0x61616179 0x6261617a 0x62616162 0x62616163
“`
As discussed though, we can’t just use any address for our initial gadget – we need to go through the whole process again and find a valid pointer that can be faked as a `this` pointer which points to a `vtable`, where a member at offset+0x48 performs both a stack pivot _and_ an early `ret`.
It’s a good thing that the Ivanti `web` binary contains so many libraries.
After spending a while looking around and discarding gadgets that would throw a segfault during execution by dereferencing various registers, **we found something magical.**
### A Gadget From The Gods
Not only does our newly found shiny and magical gadget perform a stack pivot, but it also does an early `ret` and has no instruction that causes an early segfault.
Perfect!
“`
Memory Layout: +————————–+ | *fake_this Pointer | +————————–+ | v +————————–+ | fake_vtable Address |