From fda456338f75c7adb0a26df72de53d78b1ccb6da Mon Sep 17 00:00:00 2001 From: Jon Cave Date: Sat, 13 Aug 2016 12:00:44 +0100 Subject: Add a polling mode to Invoke-UserHunter Repeatedly poll a set of target computers for user sessions. This could be a useful technique for building a much better picture of current sessions, but without having to communicate with every host. The -Poll parameter is used to specify the duration for which polling should occur. Each target computer is dedicated with a thread with -Delay and -Jitter specifying how long to sleep between each session enumeration attempt of an individual host. --- Recon/PowerView.ps1 | 190 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 111 insertions(+), 79 deletions(-) (limited to 'Recon') diff --git a/Recon/PowerView.ps1 b/Recon/PowerView.ps1 index 27f87c7..c1828c8 100755 --- a/Recon/PowerView.ps1 +++ b/Recon/PowerView.ps1 @@ -9480,6 +9480,11 @@ function Invoke-UserHunter { The maximum concurrent threads to execute. + .PARAMETER Poll + + Continuously poll for sessions for the given duration. Automatically + sets Threads to the number of computers being polled. + .EXAMPLE PS C:\> Invoke-UserHunter -CheckAccess @@ -9534,6 +9539,13 @@ function Invoke-UserHunter { Executes old Invoke-StealthUserHunter functionality, enumerating commonly used servers and checking just sessions for each. + .EXAMPLE + + PS C:\> Invoke-UserHunter -Stealth -StealthSource DC -Poll 3600 -Delay 5 -ShowAll | ? { ! $_.UserName.EndsWith('$') } + + Poll Domain Controllers in parallel for sessions for an hour, waiting five + seconds before querying each DC again and filtering out computer accounts. + .LINK http://blog.harmj0y.net #> @@ -9623,7 +9635,10 @@ function Invoke-UserHunter { [Int] [ValidateRange(1,100)] - $Threads + $Threads, + + [UInt32] + $Poll = 0 ) begin { @@ -9632,9 +9647,6 @@ function Invoke-UserHunter { $DebugPreference = 'Continue' } - # random object for delay - $RandNo = New-Object System.Random - Write-Verbose "[*] Running Invoke-UserHunter with delay of $Delay" ##################################################### @@ -9705,6 +9717,14 @@ function Invoke-UserHunter { } } + if ($Poll -gt 0) { + Write-Verbose "[*] Polling for $Poll seconds. Automatically enabling threaded mode." + if ($ComputerName.Count -gt 100) { + throw "Too many hosts to poll! Try fewer than 100." + } + $Threads = $ComputerName.Count + } + ##################################################### # # Now we build the user target set @@ -9802,7 +9822,7 @@ function Invoke-UserHunter { # script block that enumerates a server $HostEnumBlock = { - param($ComputerName, $Ping, $TargetUsers, $CurrentUser, $Stealth, $DomainShortName) + param($ComputerName, $Ping, $TargetUsers, $CurrentUser, $Stealth, $DomainShortName, $Poll, $Delay, $Jitter) # optionally check if the server is up first $Up = $True @@ -9810,89 +9830,46 @@ function Invoke-UserHunter { $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName } if($Up) { - if(!$DomainShortName) { - # if we're not searching for foreign users, check session information - $Sessions = Get-NetSession -ComputerName $ComputerName - ForEach ($Session in $Sessions) { - $UserName = $Session.sesi10_username - $CName = $Session.sesi10_cname - - if($CName -and $CName.StartsWith("\\")) { - $CName = $CName.TrimStart("\") - } - - # make sure we have a result - if (($UserName) -and ($UserName.trim() -ne '') -and (!($UserName -match $CurrentUser))) { - - $TargetUsers | Where-Object {$UserName -like $_.MemberName} | ForEach-Object { - - $IPAddress = @(Get-IPAddress -ComputerName $ComputerName)[0].IPAddress - $FoundUser = New-Object PSObject - $FoundUser | Add-Member Noteproperty 'UserDomain' $_.MemberDomain - $FoundUser | Add-Member Noteproperty 'UserName' $UserName - $FoundUser | Add-Member Noteproperty 'ComputerName' $ComputerName - $FoundUser | Add-Member Noteproperty 'IPAddress' $IPAddress - $FoundUser | Add-Member Noteproperty 'SessionFrom' $CName - - # Try to resolve the DNS hostname of $Cname - try { - $CNameDNSName = [System.Net.Dns]::GetHostEntry($CName) | Select-Object -ExpandProperty HostName - $FoundUser | Add-Member NoteProperty 'SessionFromName' $CnameDNSName - } - catch { - $FoundUser | Add-Member NoteProperty 'SessionFromName' $Null - } - - # see if we're checking to see if we have local admin access on this machine - if ($CheckAccess) { - $Admin = Invoke-CheckLocalAdminAccess -ComputerName $CName - $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Admin.IsAdmin - } - else { - $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Null - } - $FoundUser.PSObject.TypeNames.Add('PowerView.UserSession') - $FoundUser + $Timer = [System.Diagnostics.Stopwatch]::StartNew() + $RandNo = New-Object System.Random + + Do { + if(!$DomainShortName) { + # if we're not searching for foreign users, check session information + $Sessions = Get-NetSession -ComputerName $ComputerName + ForEach ($Session in $Sessions) { + $UserName = $Session.sesi10_username + $CName = $Session.sesi10_cname + + if($CName -and $CName.StartsWith("\\")) { + $CName = $CName.TrimStart("\") } - } - } - } - if(!$Stealth) { - # if we're not 'stealthy', enumerate loggedon users as well - $LoggedOn = Get-NetLoggedon -ComputerName $ComputerName - ForEach ($User in $LoggedOn) { - $UserName = $User.wkui1_username - # TODO: translate domain to authoratative name - # then match domain name ? - $UserDomain = $User.wkui1_logon_domain - # make sure wet have a result - if (($UserName) -and ($UserName.trim() -ne '')) { + # make sure we have a result + if (($UserName) -and ($UserName.trim() -ne '') -and (!($UserName -match $CurrentUser))) { - $TargetUsers | Where-Object {$UserName -like $_.MemberName} | ForEach-Object { + $TargetUsers | Where-Object {$UserName -like $_.MemberName} | ForEach-Object { - $Proceed = $True - if($DomainShortName) { - if ($DomainShortName.ToLower() -ne $UserDomain.ToLower()) { - $Proceed = $True - } - else { - $Proceed = $False - } - } - if($Proceed) { $IPAddress = @(Get-IPAddress -ComputerName $ComputerName)[0].IPAddress $FoundUser = New-Object PSObject - $FoundUser | Add-Member Noteproperty 'UserDomain' $UserDomain + $FoundUser | Add-Member Noteproperty 'UserDomain' $_.MemberDomain $FoundUser | Add-Member Noteproperty 'UserName' $UserName $FoundUser | Add-Member Noteproperty 'ComputerName' $ComputerName $FoundUser | Add-Member Noteproperty 'IPAddress' $IPAddress - $FoundUser | Add-Member Noteproperty 'SessionFrom' $Null - $FoundUser | Add-Member Noteproperty 'SessionFromName' $Null + $FoundUser | Add-Member Noteproperty 'SessionFrom' $CName + + # Try to resolve the DNS hostname of $Cname + try { + $CNameDNSName = [System.Net.Dns]::GetHostEntry($CName) | Select-Object -ExpandProperty HostName + $FoundUser | Add-Member NoteProperty 'SessionFromName' $CnameDNSName + } + catch { + $FoundUser | Add-Member NoteProperty 'SessionFromName' $Null + } # see if we're checking to see if we have local admin access on this machine if ($CheckAccess) { - $Admin = Invoke-CheckLocalAdminAccess -ComputerName $ComputerName + $Admin = Invoke-CheckLocalAdminAccess -ComputerName $CName $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Admin.IsAdmin } else { @@ -9904,10 +9881,61 @@ function Invoke-UserHunter { } } } - } + if(!$Stealth) { + # if we're not 'stealthy', enumerate loggedon users as well + $LoggedOn = Get-NetLoggedon -ComputerName $ComputerName + ForEach ($User in $LoggedOn) { + $UserName = $User.wkui1_username + # TODO: translate domain to authoratative name + # then match domain name ? + $UserDomain = $User.wkui1_logon_domain + + # make sure wet have a result + if (($UserName) -and ($UserName.trim() -ne '')) { + + $TargetUsers | Where-Object {$UserName -like $_.MemberName} | ForEach-Object { + + $Proceed = $True + if($DomainShortName) { + if ($DomainShortName.ToLower() -ne $UserDomain.ToLower()) { + $Proceed = $True + } + else { + $Proceed = $False + } + } + if($Proceed) { + $IPAddress = @(Get-IPAddress -ComputerName $ComputerName)[0].IPAddress + $FoundUser = New-Object PSObject + $FoundUser | Add-Member Noteproperty 'UserDomain' $UserDomain + $FoundUser | Add-Member Noteproperty 'UserName' $UserName + $FoundUser | Add-Member Noteproperty 'ComputerName' $ComputerName + $FoundUser | Add-Member Noteproperty 'IPAddress' $IPAddress + $FoundUser | Add-Member Noteproperty 'SessionFrom' $Null + $FoundUser | Add-Member Noteproperty 'SessionFromName' $Null + + # see if we're checking to see if we have local admin access on this machine + if ($CheckAccess) { + $Admin = Invoke-CheckLocalAdminAccess -ComputerName $ComputerName + $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Admin.IsAdmin + } + else { + $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Null + } + $FoundUser.PSObject.TypeNames.Add('PowerView.UserSession') + $FoundUser + } + } + } + } + } + + if ($Poll -gt 0) { + Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) + } + } While ($Poll -gt 0 -and $Timer.Elapsed.TotalSeconds -lt $Poll) } } - } process { @@ -9922,6 +9950,9 @@ function Invoke-UserHunter { 'CurrentUser' = $CurrentUser 'Stealth' = $Stealth 'DomainShortName' = $DomainShortName + 'Poll' = $Poll + 'Delay' = $Delay + 'Jitter' = $Jitter } # kick off the threaded script block + arguments @@ -9937,6 +9968,7 @@ function Invoke-UserHunter { Write-Verbose "[*] Total number of active hosts: $($ComputerName.count)" $Counter = 0 + $RandNo = New-Object System.Random ForEach ($Computer in $ComputerName) { @@ -9946,7 +9978,7 @@ function Invoke-UserHunter { Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) Write-Verbose "[*] Enumerating server $Computer ($Counter of $($ComputerName.count))" - $Result = Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $Computer, $False, $TargetUsers, $CurrentUser, $Stealth, $DomainShortName + $Result = Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $Computer, $False, $TargetUsers, $CurrentUser, $Stealth, $DomainShortName, 0, 0, 0 $Result if($Result -and $StopOnSuccess) { -- cgit v1.2.3 From 9b365e82b1bcf9957179ada3e1df4f6feb1c5888 Mon Sep 17 00:00:00 2001 From: Jon Cave Date: Sat, 13 Aug 2016 12:05:12 +0100 Subject: Continuously collect output from background threads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PowerShell.BeginInvoke(PSDataCollection, PSDataCollection) method[1] is used to collect output from each job into a buffer. This can be read whilst the jobs are still running. Being able to return partial results is particularly useful for long running background threads, such as Invoke-UserHunter -Poll. PowerShell 2.0 doesn't play nicely with generic methods. The technique described in [2] is used to allow this version of BeginInvoke() to be used. [1] https://msdn.microsoft.com/en-us/library/dd182440(v=vs.85).aspx [2] http://www.leeholmes.com/blog/2007/06/19/invoking-generic-methods-on-non-generic-classes-in-powershell/ --- Recon/PowerView.ps1 | 61 ++++++++++++++++++++++++----------------------------- 1 file changed, 27 insertions(+), 34 deletions(-) (limited to 'Recon') diff --git a/Recon/PowerView.ps1 b/Recon/PowerView.ps1 index c1828c8..796b5d7 100755 --- a/Recon/PowerView.ps1 +++ b/Recon/PowerView.ps1 @@ -9277,11 +9277,16 @@ function Invoke-ThreadedFunction { $Pool = [runspacefactory]::CreateRunspacePool(1, $Threads, $SessionState, $Host) $Pool.Open() - $Jobs = @() - $PS = @() - $Wait = @() + $method = $null + ForEach ($m in [PowerShell].GetMethods() | Where-Object { $_.Name -eq "BeginInvoke" }) { + $methodParameters = $m.GetParameters() + if (($methodParameters.Count -eq 2) -and $methodParameters[0].Name -eq "input" -and $methodParameters[1].Name -eq "output") { + $method = $m.MakeGenericMethod([Object], [Object]) + break + } + } - $Counter = 0 + $Jobs = @() } process { @@ -9297,54 +9302,42 @@ function Invoke-ThreadedFunction { } # create a "powershell pipeline runner" - $PS += [powershell]::create() + $p = [powershell]::create() - $PS[$Counter].runspacepool = $Pool + $p.runspacepool = $Pool # add the script block + arguments - $Null = $PS[$Counter].AddScript($ScriptBlock).AddParameter('ComputerName', $Computer) + $Null = $p.AddScript($ScriptBlock).AddParameter('ComputerName', $Computer) if($ScriptParameters) { ForEach ($Param in $ScriptParameters.GetEnumerator()) { - $Null = $PS[$Counter].AddParameter($Param.Name, $Param.Value) + $Null = $p.AddParameter($Param.Name, $Param.Value) } } - # start job - $Jobs += $PS[$Counter].BeginInvoke(); + $o = New-Object Management.Automation.PSDataCollection[Object] - # store wait handles for WaitForAll call - $Wait += $Jobs[$Counter].AsyncWaitHandle + $Jobs += @{ + PS = $p + Output = $o + Result = $method.Invoke($p, @($null, [Management.Automation.PSDataCollection[Object]]$o)) + } } - $Counter = $Counter + 1 } } end { + Write-Verbose "Waiting for threads to finish..." - Write-Verbose "Waiting for scanning threads to finish..." - - $WaitTimeout = Get-Date - - # set a 60 second timeout for the scanning threads - while ($($Jobs | Where-Object {$_.IsCompleted -eq $False}).count -gt 0 -and $($($(Get-Date) - $WaitTimeout).totalSeconds) -lt 60) { - Start-Sleep -MilliSeconds 500 + Do { + ForEach ($Job in $Jobs) { + $Job.Output.ReadAll() } + } While (($Jobs | Where-Object { ! $_.Result.IsCompleted }).Count -gt 0) - # end async call - for ($y = 0; $y -lt $Counter; $y++) { - - try { - # complete async job - $PS[$y].EndInvoke($Jobs[$y]) - - } catch { - Write-Warning "error: $_" - } - finally { - $PS[$y].Dispose() - } + ForEach ($Job in $Jobs) { + $Job.PS.Dispose() } - + $Pool.Dispose() Write-Verbose "All threads completed!" } -- cgit v1.2.3