PowerShell icon indicating copy to clipboard operation
PowerShell copied to clipboard

Get-Process Cmdlet -IncludeUserName Requires Elevation

Open blackops786187 opened this issue 2 years ago • 2 comments

Prerequisites

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

blackops786187 avatar Jan 12 '24 12:01 blackops786187

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).

MartinGC94 avatar Jan 12 '24 15:01 MartinGC94

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)
                }
            }
        }
    }
}

jborean93 avatar Jan 12 '24 21:01 jborean93

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.

TobiasPSP avatar Jan 31 '24 14:01 TobiasPSP

The WG discussed this and agree with the statement made by @TobiasPSP above:

JamesWTruher avatar Feb 21 '24 17:02 JamesWTruher

@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?

MartinGC94 avatar Feb 21 '24 17:02 MartinGC94

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.

mklement0 avatar Mar 04 '24 18:03 mklement0

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.

jborean93 avatar Mar 04 '24 19:03 jborean93