Get-Process Cmdlet -IncludeUserName Requires Elevation
Prerequisites
- [X] Write a descriptive title.
- [X] Make sure you are able to repro it on the latest released version
- [X] Search the existing issues.
- [X] Refer to the FAQ.
- [X] Refer to Differences between Windows PowerShell 5.1 and PowerShell.
Steps to reproduce
Hi,
Just wondering if specifying the -includeUsername switch is supposed to prompt for elevation. Not sure why it needs it when task manager or the tasklist.exe can pull the username without admin priv
If its intended behaviour, is there any way to get the username of a process without elevation. Currently bulding a script to grab the processes of a specific user but this is a stumbling block i cant seem to avoid
Expected behavior
Be able to use the includeusername switch without elevation
Actual behavior
Prompts for elevation
Error details
No response
Environment data
Name Value
---- -----
PSVersion 7.3.10
PSEdition Core
GitCommitId 7.3.10
OS Microsoft Windows 10.0.22631
Platform Win32NT
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0
Visuals
No response
You are right, looks like they added a check for it here: https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Management/commands/management/Process.cs#L553 and if we read the documentation for the API they use: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocesstoken#remarks it says:
To get a handle to an elevated process from within a non-elevated process, both processes must be started from the same account. If the process being checked was started by a different account, the checking process needs to have the SE_DEBUG_NAME privilege enabled.
Maybe whoever authored that code misunderstood and thought you needed to be elevated to use it at all?
I tried removing the check on my local machine and it seems to work perfectly fine so someone (not me) just needs to create a PR that does this + add a test (I guess).
While you can certainly get the username of processes running under the same account in reality to get usernames of other processes, like services, other logons, runas processes you are going to need the SeDebugPrivilege which is a powerful privilege only granted to admins. The reason why task manager can do this without elevation is that it is actually elevated. It is specially signed by Microsoft to do this without prompting you to elevate through UAC.
We could potentially remove this check and just set the UserName property to $null for processes the user cannot retrieve but keep in mind this will be all processes that aren't running as the current user.
#Requires -Module Ctypes
Function Get-ProcessUserName {
[OutputType([System.Security.Principal.NTAccount])]
[CmdletBinding()]
param (
[Parameter(Mandatory, ValueFromPipeline)]
[Alias("Id")]
[int[]]
$ProcessId
)
begin {
$PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
$TOKEN_QUERY = 0x0008
$TokenUser = 1
$a32 = New-CtypesLib Advapi32.dll
$k32 = New-CtypesLib Kernel32.dll
ctypes_struct SID_AND_ATTRIBUTES {
[IntPtr]$PSid
[int]$Attributes
}
ctypes_struct TOKEN_USER {
[SID_AND_ATTRIBUTES]$User
}
}
process {
foreach ($procId in $ProcessId) {
$procHandle = $accessToken = $userRaw = [IntPtr]::Zero
try {
$procHandle = $k32.SetLastError($true).OpenProcess[IntPtr](
$PROCESS_QUERY_LIMITED_INFORMATION,
$false,
$procId)
if ($procHandle -eq [IntPtr]::Zero) {
$exp = [System.ComponentModel.Win32Exception]::new($k32.LastError)
$PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new(
$exp,
"OpenProcessError",
"NotSpecified",
$procId))
continue
}
$res = $a32.SetLastError($true).OpenProcessToken[bool]($procHandle, $TOKEN_QUERY, [ref]$accessToken)
if (-not $res) {
$exp = [System.ComponentModel.Win32Exception]::new($a32.LastError)
$PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new(
$exp,
"OpenProcessTokenError",
"NotSpecified",
$procId))
continue
}
$returnLength = 0
$res = $a32.SetLastError($true).GetTokenInformation[bool](
$accessToken,
$TokenUser,
$null,
0,
[ref]$returnLength)
$userRaw = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($returnLength)
$res = $a32.SetLastError($true).GetTokenInformation[bool](
$accessToken,
$TokenUser,
$userRaw,
$returnLength,
[ref]$returnLength)
if (-not $res) {
$exp = [System.ComponentModel.Win32Exception]::new($a32.LastError)
$PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new(
$exp,
"GetTokenInformationError",
"NotSpecified",
$procId))
continue
}
$tokenUser = [System.Runtime.InteropServices.Marshal]::PtrTostructure[TOKEN_USER]($userRaw)
$userSid = [System.Security.Principal.SecurityIdentifier]::new($tokenUser.User.PSid)
$userSid.Translate([System.Security.Principal.NTAccount])
}
finally {
if ($userRaw -ne [IntPtr]::Zero) {
[System.Runtime.InteropServices.Marshal]::FreeHGlobal($userRaw)
}
if ($accessToken -ne [IntPtr]::Zero) {
$k32.CloseHandle[void]($accessToken)
}
if ($procHandle -ne [IntPtr]::Zero) {
$k32.CloseHandle[void]($procHandle)
}
}
}
}
}
Taskmanager silently elevates (to test, log on as regular user, then check taskmanager again: usernames will be blank for any process that was started by someone else).
That said, there are plenty of sensitive/protected process properties, i.e. CPU, that require elevation to read. Get-Process simply returns NULL for those. Why should an exception be raised specifically for the username property?
Rather for consistency, my personal feeling is the cmdlet should return NULL for any property that cannot be read due to insufficient privileges.
The WG discussed this and agree with the statement made by @TobiasPSP above:
@JamesWTruher Maybe I misunderstand the "Resolution by design" tag, but it seems contradictory to the WG conclusion. Shouldn't it be marked as "Up for grabs" so someone can go in and remove the admin check and make it return null for the usernames that can't be retrieved?
This issue has been marked as by-design and has not had any activity for 1 day. It has been closed for housekeeping purposes.
📣 Hey @blackops786187, how did we do? We would love to hear your feedback with the link below! 🗣️
🔗 https://aka.ms/PSRepoFeedback
Pragmatically speaking, given that running non-elevated would effectively limit you to querying the username of your own processes, we could alternatively implement a new switch such as -CurrentUser that limits retrieval to the current user's processes:
- #21301
When that switch is used, all processes returned are by definition those whose username is the current user's.
It would be the (possibly -Name-filtered) equivalent of the following, which on Windows can currently only be run with elevation:
#requires -RunAsAdministrator
Get-Process -IncludeUserName |
Where UserName -eq ($IsWindows ? "$env:USERDOMAIN\$env:USERNAME" : $env:USER)
Btw, @jborean93: your Ctypes module is great - makes P/Invoke so much easier.
I've opened https://github.com/PowerShell/PowerShell/pull/21302 to enact @TobiasPSP statement at the end which I think it implied by the WG agreement on it.
Rather for consistency, my personal feeling is the cmdlet should return NULL for any property that cannot be read due to insufficient privileges.
Btw, @jborean93: your https://github.com/PowerShell/PowerShell/issues/21055#issuecomment-1889972976 is great - makes P/Invoke so much easier.
Thanks appreciate the kind words.