Windows Unprivileged - WupService
In a previous article sudo I described a complicated way to emulate the Unix sudo command in Windows using JEA and scheduled tasks.
Since then I have been thinking, there must be an even more complicated way to do this.
I thought about it and found a perhaps better way to implement the same idea. This solution does not use a local admin account but the calling user’s account and it’s not based on a scheduled task but on a service.
Note that absolutely none of this is authoritative or directly based on relevant documentation. It’s mostly what I found and figured out and guessed and (in some cases) made up. Some of it may be wrong or dangerous or lead to disaster or confusion. I am not taking responsibility here for anything. Especially not for granting SeTcbPrivilege to a user account! Read and act on it at your own peril!
You can download the required Windows service here:
WupService x64 for 64 bit x86 and WupService arm64 for 64 bit ARM
These are in development phase and not finished.
How to use
The service runs using the target user’s account and a service SID that has all the desired group memberships and privileged required by the program to be launched plus SeTcbPrivilege and SeAssignPrimaryTokenPrivilege. (SeTcbPrivilege should not generally be held by any account except SYSTEM and will not be assigned to the privileged program ultimately started.)
For each control from 128 to 255 (see ControlService) one program can be assigned. (The assignment can be done by a function in a JEA configuration, but this is left as an exercise to the reader.)
Create a registry key HKLM:\SOFTWARE\AB\WUPService\ with a subkey for each program to be started. The default value of the subkey is the path to the program image.
In this example, cmd.exe is the program to be started.
PS C:\> Get-ItemProperty HKLM:\SOFTWARE\AB\WUPService\128
(default) : c:\windows\system32\cmd.exe
PSPath : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\AB\WUPService\128
PSParentPath : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\AB\WUPService
PSChildName : 128
PSDrive : HKLM
PSProvider : Microsoft.PowerShell.Core\Registry
PS C:\>
Then, install the service with the target user’s account and a service SID and assign the service SID all wanted group membersips (Administrators) and privileges. Also add SeTcbPrivilege and SeAssignPrimaryTokenPrivilege to the service SID and SeServiceLogonRight to the user account. The groups and privileged assigned to the service SID (minus SeTcbPrivilege) will be the groups and privileges the running program will have.
Also use eventcreate.exe to create the proper event source.
PS C:\WupService> dir
Directory: C:\WupService
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 4/19/2025 2:25 PM 142336 WUPS.exe
PS C:\WupService> sc.exe create WUPS_Benoit binpath=C:\WupService\WUPS.exe obj=.\benoit password=BenoitSecretPassword
[SC] CreateService SUCCESS
PS C:\WupService> sc.exe sidtype WUPS_Benoit Unrestricted
[SC] ChangeServiceConfig2 SUCCESS
PS C:\WupService> net localgroup Administrators /add "NT Service\WUPS_Benoit"
The command completed successfully.
PS C:\WupService> & 'C:\Program Files\ABTokenTools\AccountRights.exe' "NT Service\WUPS_Benoit" SeTcbPrivilege
0
SeTcbPrivilege
1
PS C:\WupService> & 'C:\Program Files\ABTokenTools\AccountRights.exe' "NT Service\WUPS_Benoit" SeAssignPrimaryTokenPrivilege
SeTcbPrivilege
1
SeTcbPrivilege
SeAssignPrimaryTokenPrivilege
2
PS C:\WupService> & 'C:\Program Files\ABTokenTools\AccountRights.exe' benoit SeServiceLogonRight
SeServiceLogonRight
SeBatchLogonRight
2
SeServiceLogonRight
SeBatchLogonRight
2
PS C:\WupService> eventcreate.exe /l application /so WupService /t information /id 99 /d foo
SUCCESS: An event of type 'information' was created in the 'application' log with 'WupService' as the source.
PS C:\WupService> Start-Service WUPS_Benoit
PS C:\WupService> Get-Service WUPS_Benoit
Status Name DisplayName
------ ---- -----------
Running WUPS_Benoit WUPS_Benoit
PS C:\WupService>
Now that the service is installed and cmd.exe is configured for control 128, user benoit can start cmd.exe from the service using sc.exe. (This can be done by a wrapper script, of course.)
If you are using cmd.exe you first need to grant permissions to your window station (session) to yourself:
PS C:\> & 'C:\Program Files\ABTokenTools\RunJob.exe' /WindowStationPermission /user benoit
PS C:\>
And then you can try it out:
Note that the cmd.exe is running in an unthemed window and has all of user benoit’s group memberships plus Administrators and NT Service\WUPS_Benoit.
It has the latter group because it is a daughter process of the WUPS_Benoit service and it has the former group because NT Service\WUPS_Benoit is a member of Administrators. (It also has SeAssignPrimaryToken from NT Service\WUPS_Benoit but I will remove this in a final version.)
If the privileged program requires any other group memberships and privileges, just assign them to NT Service\WUPS_Benoit and the privileged program will have those groups and privileges.
How it works
The service reacts to controls.
When a control is received (should be between 128 and 255 for this to work) it will read the appropriate registry key’s default value.
If it finds a path there it will
1) get the current process token (which will be user benoit’s token)
HANDLE hProcess = GetCurrentProcess();
HANDLE hPrimToken = NULL;
ok = OpenProcessToken(hProcess, TOKEN_ALL_ACCESS, &hPrimToken);
2) create a duplicate of the token, or rather create a restricted version of the token without SeTcbPrivilege
HANDLE hDupToken = NULL;
TOKEN_PRIVILEGES privs;
LUID luid;
ok = LookupPrivilegeValue(NULL, L"SeTcbPrivilege", &luid);
privs.PrivilegeCount = 1;
privs.Privileges[0].Luid = luid;
ok = CreateRestrictedToken(hPrimToken, 0, 0, NULL, privs.PrivilegeCount, privs.Privileges, 0, NULL, &hDupToken);
3) find the calling user’s session (actually finds the first session using the same user name, works with local users so far, not tested with domain users)
DWORD cchCurrentUserName = UNLEN + 1;
LPWSTR sCurrentUserName = GlobalAlloc(0, cchCurrentUserName * sizeof(WCHAR));
ok = GetUserName(sCurrentUserName, &cchCurrentUserName);
DWORD sessionid = 0;
PWTS_SESSION_INFO aSessionInfo = NULL;
DWORD cSessionInfo = 0;
ok = WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, &aSessionInfo, &cSessionInfo);
for (DWORD i = 0; i < cSessionInfo; i++) {
LPWSTR sSessionUserName = NULL;
DWORD cbSessionUserName = 0;
ok = WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, aSessionInfo[i].SessionId, WTSUserName, &sSessionUserName, &cbSessionUserName);
if (0 == wcscmp(sSessionUserName, sCurrentUserName)) {
sessionid = aSessionInfo[i].SessionId;
break;
}//if
}//for
4) change the token’s session id to the calling user’s session (this requires SeTcbPrivilege)
EnablePrivilege(L"SeTcbPrivilege");
ok = SetTokenInformation(hDupToken, TokenSessionId, &sessionid, sizeof(DWORD));
5) create a suspended process using this duplicated/restricted token (this requires SeAssignPrimaryTokenPrivilege)
EnablePrivilege(L"SeAssignPrimaryTokenPrivilege");
PROCESS_INFORMATION pi;
STARTUPINFOW si;
si.cb = sizeof(STARTUPINFOW);
ZeroMemory(&si, si.cb);
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
ok = CreateProcessAsUserW(hDupToken, pathImage, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
6) configure a job object with a process limit and add the process to it
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pi.dwProcessId);
HANDLE hJob = CreateJobObject(NULL, L"WupsJob");
JOBOBJECT_BASIC_LIMIT_INFORMATION limits;
limits.LimitFlags = JOB_OBJECT_LIMIT_ACTIVE_PROCESS;
limits.ActiveProcessLimit = processlimit;
ok = SetInformationJobObject(hJob, JobObjectBasicLimitInformation, &limits, sizeof(JOBOBJECT_BASIC_LIMIT_INFORMATION));
ok = AssignProcessToJobObject(hJob, hProcess);
7) and resume the process’ main thread.
status = ResumeThread(pi.hThread);
The new process appears in the user’s session (or rather, in one of them) and will have the group NT Service\ServiceName as well as all groups and privileges of that group
(Note that the code examples lack even the most basic error checking and probably feature lots of type and indirection errors. I would not recommend anyone to use it like this. If you find bugs, let me know. I am sure there are a few many of those. Find the code for EnablePrivilege() somewhere in my github repositories.)
Registry configuration values
Location | Value | Type | Description |
Image subkey | processlimit | DWORD | If present, will allow the privileged process to launch daughter processes. Set to 0 to allow any number of daughter processes or any number to allow that number of daughter processes. The default value is 1 and won’t allow any daughter processes. |
Program key | OverrideUserName | string | If present, will override the user name. This is useful if the service runs as LocalSystem and should launch programs running under LocalSystem into a specific user’s session. |
Program key | OverrideSession | DWORD | If present, will override the session id. This is useful for testing, I suppose. |
How this worked in the past
In OpenVMS an image can be “installed” which allowed certain programs to run with extra privileged assigned to them rather than to the calling user. This works for interactive programs, not just for services (detached programs). The privileged program will then run with the extra privileged but otherwise in the context of the calling user.
Next: PostgreSQL