4

I'm attempting to build a simple "get-ProcessInfo.ps1" module to be used with a PowerShell (forensics/IR) framework called Kansa. The command is a simple one liner that calls Get-WMIObject win32_process and pipes it into Select-Object. Then Kansa SHOULD export the data to Csv via the Export-Csv cmdlet. The script runs without issue on my local host, however it fails when run remotely (on windows) via the Invoke-Command cmdlet in Kansa. My error logging shows get-ProcessInfo.ps1 "Cannot find a process with the process identifier ####", for every processID. Other modules run on the remote hosts without issue, so I know I'm authenticating as an admin. Therefore I think I'm running into a permissions error, or possibly an authentication issue with Wmi. I'm running this in a Windows domain, from a Windows box, via a domain admin account.

Kansa GitHub: https://github.com/davehull/Kansa Kansa Talk: https://www.youtube.com/watch?v=cQmsT9D0kKI

I've attempted to replicate the WmiObject call as seen in another Kansa module, but this still did not produce data from remote hosts. - https://github.com/davehull/Kansa/blob/master/Modules/Process/Get-ProcsWMI.ps1

I attempted to understand what was happening in the InjectedThreads.ps1 script since it uses WmiObject remotely without issue, but its a bit over my head. From what I could understand, it sounds like WmiObject is "unmanaged" (unauthenticated? / not inheriting Kerberos from PowerShell?) - https://github.com/davehull/Kansa/blob/master/Modules/Process/Get-InjectedThreads.ps1

I've attempted multiple variations of Wmi Authentication, Impersonation and Privileges. Which unfortunately still produces no remote data. - https://blogs.msmvps.com/richardsiddaway/2011/08/04/authentication-impersonation-and-privileges/

Finally, since get-WmiObject is technically deprecated, in favor of Get-CIMInstance, I've attempted multiple variations of the Get-CIMInstance cmdlet.

Here is the code from the module I'm attempting to make, get-ProcessInfo.ps1

Get-WmiObject win32_process | Select-Object creationdate,ws,ProcessName,ProcessID,ParentProcessID, @{Name = 'ParentProcessName';Expression = {(Get-Process -Id $_.ParentProcessId).Name}},Path,CommandLine,@{Name = 'ParentProcessPath';Expression = {(Get-Process -Id $_.ParentProcessId).Path}}

Expected results should be a list of processes and their related info, which works on my local machine, and returns no data (just errors) when run remotely via the Invoke-Command within Kansa.ps1

Can someone point me in the proper direction with what exactly is happening here, and how I might go about resolving the issue?

*As a note, this scrip is run via WinRM (Invoke-Command) on a remote host, so asking for credentials is out of the question, and so is hard coded credentials.

mklement0
  • 382,024
  • 64
  • 607
  • 775
Duck
  • 91
  • 7
  • 1
    Have you tried `Get-CimInstance -ComputerName` directly? No need to get PowerShell remoting involved via `Invoke-Command`. – mklement0 Jul 12 '19 at 23:31
  • I would try to run your code inside another job with explicitly specified credentials: `Invoke-Command { Start-Job { Get-WmiObject ... } -Credential ... } -Credential ...` – Zergatul Jul 13 '19 at 07:59

1 Answers1

3

In your computed properties you're assuming that the ParentProcessId is always a valid id:

@{Name = 'ParentProcessPath';Expression = {(Get-Process -Id $_.ParentProcessId).Path}

The Win32_Process documentation states the following:

It is possible that the process identified by ParentProcessId is terminated, so ParentProcessId may not refer to a running process. It is also possible that ParentProcessId incorrectly refers to a process that reuses a process identifier.

That means that ParentProcessId might point to an already terminated process. In that case (Get-Process -Id $_.ParentProcessId).Path will trigger an error (because no process for e.g. 528 is found, therefore (Get-Process -Id $_.ParentProcessId) will be $null and you're trying to call $null.Name).

I tested your one-liner on my machine and didn't receive any error as you described above. Therefore I checked how Kansa invokes a given module, which can be found here:

$Job = Invoke-Command -Session $PSSessions -FilePath $Module -ArgumentList $Arguments -AsJob -ThrottleLimit $ThrottleLimit

As you can see your module file gets invoked as a remote job (see the -AsJob switch). I asked myself if there could be a difference in error handling when invoking a script block via a background. I found this interesting StackOverflow answer stating:

Using throw will change the job object's State property to "Failed". ...

Which makes sense because a job can't ignore errors in comparison to the default PowersShell host. So, I wrapped your command to a job (on my local machine):

$job =start-job -ScriptBlock { Get-WmiObject win32_process | Select-Object creationdate,ws,ProcessName,ProcessID,ParentProcessID, @{Name = 'ParentProcessName';Expression = {(Get-Process -Id $_.ParentProcessId).Name}},Path,CommandLine,@{Name = 'ParentProcessPath';Expression = {(Get-Process -Id $_.ParentProcessId).Path}}  } 
Wait-Job $job
$result =Receive-Job $job

When calling Receive-Job I see the following error on the command line:

Cannot find a process with the process identifier 528. At line:1 char:1 + $result =Receive-Job $job + ~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : ObjectNotFound: (528:Int32) [Get-Process], ProcessCommandException + FullyQualifiedErrorId : NoProcessFoundForGivenId,Microsoft.PowerShell.Commands.GetProcessCommand + PSComputerName : localhost

I also checked the $result for $null :

> $null -eq $result
True

As can be seen above the error mentions NoProcessFoundForGivenId, which is self-explanatory. As you can see the error handling depends on how your code gets invoked (either as Job or either on the PowerShell host).

What can you do?

You can check if ParentProcessId is valid or not. Therefore you've to change your code to:

Get-WmiObject win32_process | ForEach-Object {

    # Create a custom object with the given properties. If Select-Object doesn't find given property it'll create an empty `NoteProperty` with the given name
    $processInfo = $_ | Select-Object creationdate,ws,ProcessName,ProcessID,ParentProcessID, ParentProcessPath, ParentProcessName
    $p = (Get-Process -Id $_.ParentProcessId -ErrorAction SilentlyContinue) 

    if ($null -ne $p){
        # Parent process exists lets fill up the empty properties
        $processInfo.ParentProcessName = $p.Name
        $processInfo.ParentProcessPath = $p.Path
    }

    $processInfo # Return this value to the pipeline
} 

There may be a more sophisticated way to perform $null checking in the computed property of Select-Object, unfortunately, I'm not aware of it.

If we wrap above code in a job, and run it:

    $job = start-job -ScriptBlock { Get-WmiObject win32_process | ForEach-Object {
        $processInfo = $_ | Select-Object creationdate,ws,ProcessName,ProcessID,ParentProcessID, ParentProcessPath, ParentProcessName
        $p = (Get-Process -Id $_.ParentProcessId -ErrorAction SilentlyContinue) 
        if ($null -ne $p){
            $processInfo.ParentProcessName = $p.Name
            $processInfo.ParentProcessPath = $p.Path
        }

        $processInfo # Return this value to the pipeline
    } 
}

Wait-Job $job
$result = Receive-Job $job
if ($null -ne $result){
    $result
}
else {
    Write-Error "Job with id $($job.Id) failed"
}

We'll get all processes without any kind of error.

Hope that helps.

Moerwald
  • 10,448
  • 9
  • 43
  • 83