CVE-2025-0282 复现与分析

CVE-2025-0282 复现与分析

henry Lv4

CVE-2025-0282 复现与分析

CVE-2025-0282 是发生在 ivanti 服务器中的一个栈溢出漏洞,CVSS 评分为 9.0,一如既往地,环境搭建还是那么的抽象,在搭建好调试环境之后,漏洞利用就很快了,同时感谢 @blonet 师傅(References[5])提供的帮助。

1. References

2. Set env

测试环境为:Ivanti Connect Secure 22.7R2.3

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

环境配置参考如下两篇文章,均有较详细介绍,同时在该节中也给出了所有可能用到的脚本。

https://mp.weixin.qq.com/s/e6X7GcKq1DaipmfsRqNq2w

https://mp.weixin.qq.com/s/59TrRqK-Znk-CBTzL8db9Q

在搭建好环境之后,可以直接访问如下url,看到登陆界面

nipaste_2025-05-18_18-55-3

2.1 文件互传脚本

下面两个脚本不能实现往ivanti传文件,原因是ivanti的端口被禁了。

receiver.py

不适用于ivanti服务器

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
cat > receiver.py << 'EOF'
#!/usr/bin/env python2
import socket
import sys

if len(sys.argv) != 3:
print("Usage: python receiver.py <port> <output_file>")
sys.exit(1)

PORT = int(sys.argv[1])
OUTPUT_FILE = sys.argv[2]

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', PORT))
s.listen(1)
print("Listening on port %d..." % PORT)

conn, addr = s.accept()
print("Connection from", addr)

with open(OUTPUT_FILE, 'wb') as f:
while True:
data = conn.recv(4096)
if not data:
break
f.write(data)

conn.close()
print("File saved as '%s'" % OUTPUT_FILE)

sender.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
cat > sender.py << 'EOF'
#!/usr/bin/env python2
import socket
import sys
import os

if len(sys.argv) != 4:
print("Usage: python sender.py <target_ip> <port> <file_to_send>")
sys.exit(1)

TARGET_IP = sys.argv[1]
PORT = int(sys.argv[2])
FILE_TO_SEND = sys.argv[3]

if not os.path.isfile(FILE_TO_SEND):
print("File not found:", FILE_TO_SEND)
sys.exit(1)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((TARGET_IP, PORT))

with open(FILE_TO_SEND, 'rb') as f:
while True:
data = f.read(4096)
if not data:
break
s.sendall(data)

s.close()
print("File '%s' sent successfully." % FILE_TO_SEND)

2.2 发送文件到ivanti服务器

在ubuntu上需要发送文件的目录下,运行该脚本

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env python2
import SimpleHTTPServer
import SocketServer

PORT = 1234

Handler = SimpleHTTPServer.SimpleHTTPRequestHandler
httpd = SocketServer.TCPServer(("", PORT), Handler)

print "Serving at port", PORT
httpd.serve_forever()

ivanti server下载文件

在下载文件之前,首先需要获取命令行shell,我们需要做的是挂起虚拟机(这一步需等待虚拟机出现Press <Enter> to view or update your appliance settings 时在挂起),然后到虚拟机对应的目录,找到如下图所示的内存文件,替换所有字符串/home/bin/dsconfig.pl///////////////bin/sh

nipaste_2025-05-10_18-24-0

替换如下所示,之后我们打开虚拟机,回车就可以正常拿到shell。

nipaste_2025-05-10_18-26-1

在ivanti服务器上执行如下脚本即可

1
2
import urllib
urllib.urlretrieve("http://192.168.132.128:1234/filename", "local_filename")

这里我们上传一个 busybox 到 ivanti 服务器(为方便后面调试可以同时上传一个gdbserver),然后执行下面命令,开启telnet服务

1
./busybox telnetd -l /bin/sh -b 0.0.0.0 -p 8009

然后ubuntu执行下面的命令

nipaste_2025-05-17_22-29-4

2.3 将ivanti服务器文件从端口上传

1
2
3
4
5
6
7
8
9
cat > filehttp2.py << 'EOF'
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()

具体如下图所示

nipaste_2025-05-17_22-23-5

然后执行该脚本,从浏览器访问如下所示,这样就可以直接从ubuntu下载ivanti中想要的文件了

nipaste_2025-05-17_22-20-1

2.4 搭建调试环境

在 ivanti 的shell中执行如下命令

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

nipaste_2025-05-17_23-49-3

然后就可以尝试正常调试了

nipaste_2025-05-17_23-50-1

3. Vuln

3.1 static analysis

先来看看漏洞触发原因(文件位置位于/home/bin/web):

nipaste_2025-05-10_21-31-1

对面上面的函数的部分关键内容解释如下:

1
EPMessage::EPMessage((EPMessage *)v54, (DSUtilMemPool *)v56);

这是 构造 EPMessage 类对象,构造函数的含义是:

  • v54 是新建的消息对象;
  • v56 是一个内存池对象(DSUtilMemPool)——这个机制可能用于减少内存分配带来的性能损耗,多个字段可能会使用同一个内存池来避免碎片化。
1
2
3
sub_11D6B8((int)v54, "clientIp", *(DSUtilMemPool **)(a1 + 108));
sub_11D6B8((int)v54, "clientHostName", *(DSUtilMemPool **)(a1 + 124));
sub_11D6B8((int)v54, "clientCapabilities", *(DSUtilMemPool **)(a1 + 140));

三行都调用了同一个函数 sub_11D6B8(...),作用是:

EPMessage 消息添加一个 字符串类型字段,键为 "clientIp""clientHostName""clientCapabilities",值是从对象 a1 中获取的指针。

字段含义如下:

字段名 含义说明
clientIp 客户端的 IP 地址(字符串)
clientHostName 客户端的主机名
clientCapabilities 客户端支持的功能列表(如支持哪些安全特性、模块)

这些字段的值在结构体偏移:

  • a1 + 108: 指向 IP 字符串
  • a1 + 124: 指向主机名字符串
  • a1 + 140: 指向支持能力字符串

需要注意的是 clientCapabilities 字段的大小是用户可控的,这里表示的是所拷贝的字符串的长度大小。

漏洞触发函数所在位置如下:

nipaste_2025-05-10_21-29-4

加上注释之后如下图所示

nipaste_2025-05-10_21-41-0

通过红框中的内容可以发现,v22 表示我们前面发送过去的clientCapabilities 字符串的长度,然后经过值的传递,最后v23 = *(_DWORD *)(a1 + 144) + 1 = v22 + 1,紧接着下面的 strncpy 会进行拷贝,因此这里会触发栈溢出,由于是 32 位系统地址的特殊性(很少会发生0截断),使得我们可以几乎随意地设置 gadget。

3.1 debug

这里即是发生溢出的地方,从 src 可以看到拷贝的内容是由我们控制的

nipaste_2025-05-18_13-42-5

部分 payload 内容如下所示:

nipaste_2025-05-18_19-17-2

有了溢出自然想到的是要思考如何劫持控制流,一般的思路就是劫持函数指针或者函数返回地址,在该漏洞中就是通过劫持函数指针来实现控制流劫持,从下图可以关注到,在发生越界拷贝之后,紧接着下面存在一处函数指针调用

nipaste_2025-05-18_19-20-0

变量a1实际就是该函数的 this 指针,该指针通常保存在栈帧的前面,因此我们可以考虑覆盖该指针,此时栈空间分布大致如下,其中a1Return Address 的后面,

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
+---------------------+
|  v18 (int)          |
+---------------------+
| v19 (int)           |
+---------------------+
| dest[256]           | <- 256 bytes begin to Overflow
+---------------------+
| 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 pointer where we need to overwrite it
+---------------------+
| IftTlsHeader *a2    |
+---------------------+

对应的汇编表示如下

nipaste_2025-05-18_19-41-0

动态调试如下所示:nipaste_2025-05-18_02-02-4

这里的思路就是我们需要传递一个fake vtable的地址给到eax,使其可以执行我们的 ROP。

首先是gadget的选择,不难想到的是,由于此时esp指向的区域为用户不可控区域,我们需要找到一个gadget,使其能够将esp指向用户可控的部分(这个比较好找),但同时如果 gadget 还要能够设置ebx寄存器就使得任务比较艰巨了(ebx 在后面call system时会起作用),针对gadget的查找,Swing 的博客中也进行了详细说明(References[3]),这里就直接给出对应libdsplibs.so0x093849C 偏移处的gadget内容如下:

注意整个过程中是没有泄露地址的,但由于目标系统为 32位,导致其地址爆破并不复杂,且每次程序打崩重启其地址不会被重新随机化,成功概率大概为 1/4096。

nipaste_2025-05-18_19-50-4

比较奇怪的是上面的gadget我通过ROPgadget没有找到,貌似只有采用 objdump 的方式才可以(swing 的做法)

1
2
objdump --x86-asm-syntax=intel -D  $(find . -name "libdsplibs.so") 2>&1 > libdsplibs.so.txt
cat libdsplibs.txt|grep -e "add\tesp, 0x204c"

既然找到了 gadget,我们还需要找到一个引用该地址的虚表指针,才能完成正常跳转,原理如下:

025-01-15-7ebb63e11446ebcb90d9700b46299a8b-cf5a7

比较一个简单的搜索办法就是直接在 ida 中 alt+b,输入要查找的十六进制数据,进行搜索

nipaste_2025-05-18_20-00-5

最后搜到的结果如下:

nipaste_2025-05-18_16-55-0

下图为动态调试验证,可以看到与上面 ida 中的结果是一致的,只不过由于随机化的原因,下面的地址都已经加好了程序的基地址。

nipaste_2025-05-18_16-34-0

nipaste_2025-05-18_16-34-2

之后我们只需要思考如何布置剩余的 gadget 即可

nipaste_2025-05-18_20-04-3

由于是 32 位系统,所以后面 ROP 的大致思路是,使得esp中保存指向反弹shell的指针,这里采用的 ROP 如下:

nipaste_2025-05-18_20-07-2

最后效果如下图所示:

nipaste_2025-05-18_20-08-1

可以看到最终成功调用 system 函数,并让其参数指向执行的 shell 命令。

4. Final

nipaste_2025-05-18_18-42-4

  • Title: CVE-2025-0282 复现与分析
  • Author: henry
  • Created at : 2025-05-19 10:39:41
  • Updated at : 2025-05-19 11:03:02
  • Link: https://henrymartin262.github.io/2025/05/19/CVE-2025-0282/
  • License: This work is licensed under CC BY-NC-SA 4.0.
 Comments