-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathInvokePsExec.psm1
393 lines (385 loc) · 20.6 KB
/
InvokePsExec.psm1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
#requires -version 3
function Invoke-PsExec {
<#
.SYNOPSIS
Svendsen Tech's Invoke-PsExec for PowerShell is a function that lets you execute PowerShell
and batch/cmd.exe code asynchronously on target Windows computers, using PsExec.exe.
Versions of PsExec.exe after about 2015 some time (don't quote me on the date) use
encrypted credentials when connecting to remote computers.
Online documentation: http://www.powershelladmin.com/wiki/Invoke-PsExec_for_PowerShell
Copyright (C) 2015-2017, Joakim Borger Svendsen
All rights reserved.
Svendsen Tech.
MIT license. http://www.opensource.org/licenses/MIT
.PARAMETER ComputerName
IP address or computer name.
.PARAMETER Command
PowerShell or batch/cmd.exe code to execute.
.PARAMETER IsPSCommand
This indicates that the specified command string is pure PowerShell code (you will usually want single quotes around that to avoid escaping).
.PARAMETER IsLongPSCommand
Use this if the PowerShell code produces a base64-encoded string of a length greater than 260, so you get
'Argument to long' [SIC] from PsExec. This uses a temporary file that's created on the remote computer.
.PARAMETER CustomPsExecParameters
Custom parameters for PsExec.
.PARAMETER PSFile
PowerShell file in the local file system to be run via PsExec on the remote computer.
.PARAMETER Dns
Perform a DNS lookup.
.PARAMETER Credential
Pass in alternate credentials. Get-Help Get-Credential.
.PARAMETER ContinueOnPingFail
Attempt PsExec command even if ping fails.
.PARAMETER ThrottleLimit
Number of concurrent threads. Default of 8. Lower it if results appear to be missing without reason.
.PARAMETER HideProgress
Do not display progress with Write-Progress.
.PARAMETER Timeout
Timeout in seconds. Causes problems if too short. 30-60 as a default seems OK.
Increase if doing a lot of processing with PsExec.
.PARAMETER HideSummary
Do not display the end summary with start and end time, using Write-Host.
#>
[CmdletBinding()]
param(
# IP address or computer name.
[Parameter(Mandatory=$True,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)][ValidateNotNullOrEmpty()][Alias('PSComputerName', 'Cn')][string[]] $ComputerName,
# PowerShell or batch/cmd.exe code to execute.
[string] $Command,
# This indicates that the specified command string is pure PowerShell code (you will usually want single quotes around that to avoid escaping).
[switch] $IsPSCommand,
# Use this if the PowerShell code produces a base64-encoded string of a length greater than 260, so you get 'Argument to long' [SIC] from PsExec. This uses a temporary file that's created on the remote computer.
[switch] $IsLongPSCommand,
# Custom parameters for PsExec.
[string] $CustomPsExecParameters = '',
# PowerShell file in the local file system to be run via PsExec on the remote computer.
[ValidateScript({Test-Path -Path $_ -PathType Leaf})][string] $PSFile = '',
# Perform a DNS lookup.
[switch] $Dns,
# Pass in alternate credentials. Get-Help Get-Credential.
[System.Management.Automation.PSCredential][System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty,
# Attempt PsExec command even if ping fails.
[switch] $ContinueOnPingFail,
# Number of concurrent threads.
[int] $ThrottleLimit = 8,
# Do not display progress with Write-Progress.
[switch] $HideProgress,
# Timeout in seconds. Causes problems if too short. 60 as a default seems OK. Increase if doing a lot of processing with PsExec.
[int] $Timeout = 60,
# Do not display the end summary with start and end time, using Write-Host.
[switch] $HideSummary)
# PowerShell Invoke-PsExec ("PsExec Wrapper v2").
# Copyright (c) 2015-2017, Joakim Borger Svendsen, All rights reserved. Svendsen Tech.
# Author: Joakim Borger Svendsen
# MIT license - http://www.opensource.org/licenses/MIT
# August 15, 2015. beta1
# August 23, 2015. beta2
# December 02, 2015, beta3, bug fixes, documentation
# 2017-01-23 to -25: Making a module of it, v1.0, rearranging some stuff according to newly learned best practices..
# Setting throttle limit default to 8. The module will require PowerShell v3 due to $PSScriptRoot
# being used. Wish I had used K&R-style blocks now, but keeping them because it's too much work for too little gain.
# 2017-02-14: Trying to conform to PSScriptAnalyzer warnings.
begin
{
Set-StrictMode -Version Latest
$MyEAP = 'Stop'
$ErrorActionPreference = $MyEAP
$StartTime = Get-Date
if ($PsExecExecutable = Get-Item -LiteralPath (Join-Path (Get-Location) 'PsExec.exe') -ErrorAction SilentlyContinue | Select-Object -ErrorAction SilentlyContinue -ExpandProperty FullName)
{
Write-Verbose -Message "Found PsExec.exe in current working directory. Using this PsExec.exe executable: '$PsExecExecutable'."
}
# Missing $PSScriptRoot in PSv2.. Abandoning v2 support for this module.
#Write-Verbose -Message ("MyInvocation: " + ($MyInvocation.MyCommand.Path)) # doesn't exist in my PSv4 ...
elseif ($PsExecExecutable = Get-Item -LiteralPath "$PSScriptRoot\PsExec.exe" -ErrorAction SilentlyContinue | Select-Object -ErrorAction SilentlyContinue -ExpandProperty FullName)
{
Write-Verbose -Message "Found PsExec.exe in directory script was called from. Using this PsExec.exe executable: '$PsExecExecutable'."
}
#>
elseif ($PsExecExecutable = Get-Command -Name psexec -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 | Select-Object -ExpandProperty Definition -ErrorAction SilentlyContinue)
{
Write-Verbose -Message "Found PsExec.exe in `$Env:PATH. Using this PsExec.exe executable: '$PsExecExecutable'."
}
else
{
Write-Error -Message "You need PsExec.exe from Microsoft's SysInternals suite to use this script. Either in the working dir, or somewhere in `$Env:PATH." -ErrorAction Stop
return
}
$RunspaceTimers = [HashTable]::Synchronized(@{})
$Data = [HashTable]::Synchronized(@{})
$Runspaces = New-Object -TypeName System.Collections.ArrayList
$RunspaceCounter = 0
Write-Verbose -Message 'Creating initial session state.'
$ISS = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
$ISS.Variables.Add((New-Object -TypeName System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'RunspaceTimers', $RunspaceTimers, ''))
$ISS.Variables.Add((New-Object -TypeName System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'Data', $Data, ''))
Write-Verbose -Message 'Creating runspace pool.'
$RunspacePool = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool(1, $ThrottleLimit, $ISS, $Host)
$RunspacePool.ApartmentState = 'STA'
$RunspacePool.Open()
# This is run for every computer.
$PsExecScriptBlock =
{
[CmdletBinding()]
param(
[int] $ID,
[string] $ComputerName,
[string] $Command,
[switch] $IsPSCommand,
[switch] $IsLongPSCommand,
[string] $CustomPsExecParameters,
[string] $PSFile,
[switch] $ContinueOnPingFail,
[switch] $Dns,
[string] $PsExecExecutable,
[System.Management.Automation.PSCredential][System.Management.Automation.Credential()] $Credential)
$RunspaceTimers.$ID = Get-Date
if (-not $Data.ContainsKey($ComputerName))
{
$Data[$ComputerName] = New-Object -TypeName PSObject -Property @{ ComputerName = $ComputerName }
}
if ($Dns)
{
Write-Verbose -Message "${ComputerName}: Performing DNS lookup."
$ErrorActionPreference = 'SilentlyContinue'
$HostEntry = [System.Net.Dns]::GetHostEntry($ComputerName)
$Result = $?
$ErrorActionPreference = $MyEAP
#Write-Verbose -Message "`$Result from DNS lookup: $Result (type: $($Result.GetType().FullName))"
# It looks like it's sometimes "successful" even when it isn't, for any practical purposes (pass in IP, get the same IP as .HostName)...
if ($Result)
{
## This is a best-effort attempt at handling things flexibly.
if ($HostEntry.HostName.Split('.')[0] -ieq $ComputerName.Split('.')[0])
{
$IPDns = @($HostEntry | Select-Object -ExpandProperty AddressList | Select-Object -ExpandProperty IPAddressToString)
}
else
{
$IPDns = @(@($HostEntry.HostName) + @($HostEntry.Aliases))
}
$Data[$ComputerName] | Add-Member -MemberType NoteProperty -Name 'IP/DNS' -Value $IPDns
}
else
{
$Data[$ComputerName] | Add-Member -MemberType NoteProperty -Name 'IP/DNS' -Value $Null
}
}
Write-Verbose -Message "${ComputerName}: Pinging."
if (-not (Test-Connection -ComputerName $ComputerName -Count 1 -Quiet))
{
$Data[$ComputerName] | Add-Member -MemberType NoteProperty -Name Ping -Value $False
if (-not $ContinueOnPingFail)
{
continue
}
}
else
{
$Data[$ComputerName] | Add-Member -MemberType NoteProperty -Name Ping -Value $True
}
if ($null -ne $Credential.Username)
{
[string] $CommandString = "-u `"$($Credential.Username)`" -p `"$($Credential.GetNetworkCredential().Password)`" /accepteula $CustomPsExecParameters \\$ComputerName"
}
else
{
[string] $CommandString = "/accepteula $CustomPsExecParameters \\$ComputerName"
}
if ($IsLongPSCommand -or $PSFile)
{
if ($IsLongPSCommand)
{
$TempPSFile = [System.IO.Path]::GetTempFileName()
$Command | Out-File -LiteralPath $TempPSFile
}
elseif ($PSFile)
{
$TempPSFile = $PSFile
}
# Try to handle multiple people running the script at the same time (race condition not handled, but it's better than nothing).
$Destination = "\\${ComputerName}\ADMIN`$\SvendsenTechInvokePsExecTemp.ps1"
if (Test-Path -LiteralPath $Destination)
{
Write-Verbose -Message "${ComputerName}: Destination file '$Destination' already exists. Tacking on numbers until it doesn't."
[bool] $GotAvailableFileName = $False
foreach ($i in 0..10000)
{
$TempDest = $Destination -replace '\.ps1$', "$i.ps1"
if (-not (Test-Path -LiteralPath $TempDest))
{
$Destination = $TempDest
$GotAvailableFileName = $True
break
}
}
if (-not $GotAvailableFileName)
{
Write-Warning -Message "${ComputerName}: All 10,000 temp file names already present in the file system. What are you up to? Skipping this computer."
continue
}
}
try
{
Copy-Item -LiteralPath $TempPSFile -Destination $Destination -ErrorAction Stop
}
catch
{
Write-Warning -Message "${ComputerName}: Unable to copy (temporary) PowerShell script file to destination: '$Destination': $_"
if ($IsLongPSCommand)
{
Write-Verbose -Message "${ComputerName}: Deleting local temporary PS script file: '$TempPSFile'."
Remove-Item -LiteralPath $TempPSFile -Force -ErrorAction Continue
}
continue
}
if ($IsLongPSCommand)
{
Write-Verbose -Message "${ComputerName}: Deleting temporary PS script file: '$TempPSFile'."
Remove-Item -LiteralPath $TempPSFile -Force -ErrorAction Continue
}
$CommandString += " cmd /c `"echo . | powershell.exe -ExecutionPolicy Bypass -File $Env:SystemRoot\$($Destination.Split('\')[-1])`""
}
elseif ($IsPSCommand)
{
$EncodedCommand = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($Command))
$CommandString += " cmd /c `"echo . | powershell.exe -ExecutionPolicy Bypass -EncodedCommand $EncodedCommand`""
}
else
{
$CommandString += " cmd /c `"$Command`""
}
$TempFileNameSTDOUT = [System.IO.Path]::GetTempFileName()
$TempFileNameSTDERR = [System.IO.Path]::GetTempFileName()
Write-Verbose -Message "${ComputerName}: Running PsExec command."
$Result = Start-Process -FilePath $PsExecExecutable -ArgumentList $CommandString -Wait -NoNewWindow -PassThru -RedirectStandardOutput $TempFileNameSTDOUT -RedirectStandardError $TempFileNameSTDERR -ErrorAction Continue
$Data[$ComputerName] | Add-Member -MemberType NoteProperty -Name ExitCode -Value $Result.ExitCode
$Data[$ComputerName] | Add-Member -MemberType NoteProperty -Name STDOUT -Value ((Get-Content -LiteralPath $TempFileNameSTDOUT) -join "`n")
#Write-Verbose -Message ('Content of temp STDERR file: ' + ((Get-Content -LiteralPath $TempFileNameSTDERR) -join "`n"))
$Data[$ComputerName] | Add-Member -MemberType NoteProperty -Name STDERR -Value ((Get-Content -LiteralPath $TempFileNameSTDERR) -join "`n")
Write-Verbose -Message "${ComputerName}: Deleting local STDOUT temporary file: '$TempFileNameSTDOUT'."
Remove-Item -LiteralPath $TempFileNameSTDOUT -Force -ErrorAction Continue
Write-Verbose -Message "${ComputerName}: Deleting local STDERR temporary file: '$TempFileNameSTDERR'."
Remove-Item -LiteralPath $TempFileNameSTDERR -Force -ErrorAction Continue
if ($IsLongPSCommand -or $PSFile)
{
Write-Verbose -Message "${ComputerName}: Deleting remote temporary PowerShell file: '$Destination'."
Remove-Item -LiteralPath $Destination -ErrorAction Continue
}
}
function Get-Result
{
[CmdletBinding()]
param(
[switch] $Wait
)
do
{
$More = $false
foreach ($Runspace in $Runspaces) {
$StartTime = $RunspaceTimers[$Runspace.ID]
if ($Runspace.Handle.IsCompleted)
{
#Write-Verbose -Message ('Thread done for {0}' -f $Runspace.IObject)
$Runspace.PowerShell.EndInvoke($Runspace.Handle)
$Runspace.PowerShell.Dispose()
$Runspace.PowerShell = $null
$Runspace.Handle = $null
}
elseif ($null -ne $Runspace.Handle)
{
$More = $true
}
if ($Timeout -and $StartTime)
{
if ((New-TimeSpan -Start $StartTime).TotalSeconds -ge $Timeout -and $Runspace.PowerShell) {
Write-Warning -Message ('Timeout {0}' -f $Runspace.IObject)
$Runspace.PowerShell.Dispose()
$Runspace.PowerShell = $null
$Runspace.Handle = $null
}
}
}
if ($More -and $PSBoundParameters['Wait'])
{
Start-Sleep -Milliseconds 100
}
foreach ($Thread in $Runspaces.Clone())
{
if (-not $Thread.Handle) {
Write-Verbose -Message ('Removing {0} from runspaces' -f $Thread.IObject)
$Runspaces.Remove($Thread)
}
}
if (-not $HideProgress)
{
$ProgressSplatting = @{
Activity = 'Running PsExec Commands'
Status = 'Processing: {0} of {1} total threads done' -f ($RunspaceCounter - $Runspaces.Count), $RunspaceCounter
PercentComplete = ($RunspaceCounter - $Runspaces.Count) / $RunspaceCounter * 100
}
Write-Progress @ProgressSplatting
}
}
while ($More -and $PSBoundParameters['Wait'])
} # end of Get-Result
}
process
{
foreach ($Computer in $ComputerName)
{
Write-Verbose -Message "Processing $Computer."
++$RunspaceCounter
$psCMD = [System.Management.Automation.PowerShell]::Create().AddScript($PsExecScriptBlock)
[void] $psCMD.AddParameter('ID', $RunspaceCounter)
[void] $psCMD.AddParameter('ComputerName', $Computer)
[void] $PSCMD.AddParameter('Command', $Command)
[void] $PSCMD.AddParameter('IsPSCommand', $IsPSCommand)
[void] $PSCMD.AddParameter('CustomPsExecParameters', $CustomPsExecParameters)
[void] $PSCMD.AddParameter('PSFile', $PSFile)
[void] $PSCMD.AddParameter('IsLongPSCommand', $IsLongPSCommand)
[void] $PSCMD.AddParameter('Dns', $Dns)
[void] $PSCMD.AddParameter('PsExecExecutable', $PsExecExecutable)
[void] $PSCMD.AddParameter('ContinueOnPingFail', $ContinueOnPingFail)
[void] $PSCMD.AddParameter('Credential', $Credential)
[void] $psCMD.AddParameter('Verbose', $VerbosePreference)
$psCMD.RunspacePool = $RunspacePool
[void]$Runspaces.Add(@{
Handle = $psCMD.BeginInvoke()
PowerShell = $psCMD
IObject = $Computer
ID = $RunspaceCounter
})
Get-Result
}
}
end
{
Get-Result -Wait
if (-not $HideProgress)
{
Write-Progress -Activity 'Running PsExec Commands' -Status 'Done' -Completed
}
Write-Verbose -Message "Closing and disposing runspace pool."
$RunspacePool.Close()
$RunspacePool.Dispose()
[hashtable[]] $PsExecProperties = @{ Name = 'ComputerName'; Expression = { $_.Name } }
if ($Dns)
{
$PsExecProperties += @{ Name = 'IP/DNS'; Expression = { $_.Value.'IP/DNS' } }
}
$PsExecProperties += @{ Name = 'Ping'; Expression = { $_.Value.Ping } },
@{ Name = 'ExitCode'; Expression = { $_.Value.ExitCode } },
@{ Name = 'STDOUT'; Expression = { $_.Value.STDOUT } },
@{ Name = 'STDERR'; Expression = { $_.Value.STDERR } }
$Data.GetEnumerator() | Select-Object -Property $PsExecProperties
Write-Verbose -Message '"Exporting" $Global:STPsExecData and $Global:STPsExecDataProperties'
$Global:STPsExecData = $Data
$Global:STPsExecDataProperties = $PsExecProperties
if (-not $HideSummary)
{
Write-Host -ForegroundColor Green ('Start time: ' + $StartTime)
Write-Host -ForegroundColor Green ('End time: ' + (Get-Date))
}
}
}