Monday, 27 January 2014

Powershell: Retrieve Run Keys, Startup items, Local Admins

In this post I'll talk about retrieving run keys, start menu items and local admins with Powershell.

Note: Scripts below have been created/tested for Win7. Modifications will be needed for XP!


Listing Run Keys

There are many many registry locations that can be used to auto-run software on system start-up. I decided to start with two of the most common:

- \HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
- \HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run

In this script I enumerate the run keys for the local machine and each user. I've used a notmatch filter to remove the noise from known good registry keys. Also at the start of the script you'll notice the threading function that I mentioned in my last Powershell blog post. It is possible to use StartupCommand to grab run keys but this doesn't actually cover all possible auto-run locations and I wanted to provide a more generic registry key enumeration example that you guys can adapt.

To use, just create a text file with a list of machine names and point the $Computers variable at it. Start with a small number of machines and gradually add known good keys to the exclusions list, if the path contains a curved bracket or forward slash you need to escape it with a \.

#Threading function
function ForEach-Parallel {
    param(
        [Parameter(Mandatory=$true,position=0)]
        [System.Management.Automation.ScriptBlock] $ScriptBlock,
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [PSObject]$InputObject,
        [Parameter(Mandatory=$false)]
        [int]$MaxThreads=5
    )
    BEGIN {
        $iss = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
        $pool = [Runspacefactory]::CreateRunspacePool(1, $maxthreads, $iss, $host)
        $pool.open()
        $threads = @()
        $ScriptBlock = $ExecutionContext.InvokeCommand.NewScriptBlock("param(`$_)`r`n" + $Scriptblock.ToString())
    }
    PROCESS {
        $powershell = [powershell]::Create().addscript($scriptblock).addargument($InputObject)
        $powershell.runspacepool=$pool
        $threads+= @{
            instance = $powershell
            handle = $powershell.begininvoke()
        }
    }

    END {
        $notdone = $true
        while ($notdone) {
            $notdone = $false
            for ($i=0; $i -lt $threads.count; $i++) {
                $thread = $threads[$i]
                if ($thread) {
                    if ($thread.handle.iscompleted) {
                        $thread.instance.endinvoke($thread.handle)
                        $thread.instance.dispose()
                        $threads[$i] = $null
                    }
                    else {

                        $notdone = $true
                    }
                }
            }
        }
    }
}
#End of threading function!

$ErrorActionPreference = "Stop";
$start = Get-Date
$Computers = Get-Content c:\temp\machines.txt
$Computers |ForEach-Parallel -MaxThreads 100{
 try{
  if(Test-Path \\$_\C$ -ErrorAction silentlycontinue){
   #List run keys for all users on machine
   $Srv=$_
   $key = ""
   $type = [Microsoft.Win32.RegistryHive]::Users
   $regKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($type, $Srv)
   $regKey = $regKey.OpenSubKey($key)
   Foreach($sub in $regKey.GetSubKeyNames()|where {$_ -notmatch "Classes"}|where {$_.length -gt 9}){
    $key = $sub + "\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
    $regKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($type, $Srv)
    $regKey = $regKey.OpenSubKey($key)
    Foreach($val in $regKey.GetValueNames()){
       $Srv + " - Users - Name: " + $val + " Data: " + $regKey.GetValue("$val")|where {$_ -notmatch "C:\\Program Files \(x86\)\\Microsoft Office\\Office14\\MSOSYNC.EXE|C:\\Windows\\system32\\StikyNot.exe|C:\\Program Files\\Windows Sidebar\\sidebar.exe|C:\\Program Files \(x86\)\\Skype\\Phone\\Skype.exe|C:\\Program Files \(x86\)\\Google\\Chrome\\Application\\chrome.exe|C:\\PROGRA~2\\Yahoo!\\Messenger\\YahooMessenger.exe|\\AppData\\Local\\Google\\Update\\GoogleUpdate.exe|\\AppData\\Roaming\\Google\\Google Talk\\googletalk.exe"}
    }
   }
   #List run keys for local machine
   $key = "SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
   $type = [Microsoft.Win32.RegistryHive]::LocalMachine
   $regKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($type, $Srv)
   $regKey = $regKey.OpenSubKey($key)
   Foreach($val in $regKey.GetValueNames()){
      $Srv + " - LocalMachine - Name: " + $val + " Data: " + $regKey.GetValue("$val")|where {$_ -notmatch "C:\\Windows\\system32\\hkcmd.exe|C:\\Windows\\system32\\igfxtray.exe|C:\\Windows\\system32\\igfxpers.exe|C:\\Program Files\\Realtek\\Audio\\HDA\\RAVCpl64.exe|C:\\Program Files\\Synaptics\\SynTP\\SynTPEnh.exe|C:\\Program Files \(x86\)\\HP\\Digital Imaging\\Fax\\Fax Driver 0.6 Base\\hppfaxprintersrv.exe"}
   }
  } else{
   $_ + " - Machine not accessible"
  }
 }
 Catch{
  "Caught an exception!"
 }
}
$end = Get-Date
"Time taken: " + (New-Timespan $start $end).seconds + " seconds"



Startup Folder Items

The startup folder is an oldy but still a goody for malware. I found relatively few entries in this location so it's probably dead easy for most people to spot anything suspicious here.

The script will first retrieve the names of user folders present in C:\Users then retrieve the startup values for each profile. I've omitted the threading function in the script below you will need to copy and paste it from above for the script to work!!! Also you will need to point $Computers to your text file containing hostnames.

#Insert threading function here!
$ErrorActionPreference = "Stop";
$start = Get-Date
$Computers = Get-Content c:\temp\machines.txt
$Computers |ForEach-Parallel -MaxThreads 100{
 try{
  if(Test-Path \\$_\C$ -ErrorAction silentlycontinue){
   #Retrieve usernames from machine
   $Srv = $_
   $Users = Get-ChildItem \\$Srv\C$\Users -force | Where-Object {$_.mode -match "d"} | foreach { $_.Name } | where {$_ -notmatch "Search|Public"}
   #For each user retrieve Startup items
   ForEach ($User in $Users){
   Get-ChildItem -Force "\\$Srv\C$\Users\$User\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup" -name | where {$_ -notmatch "desktop.ini|Dropbox.lnk"} | foreach {"$Srv - $User - " + $_}
   }
  } else{
   $_ + " - Machine not accessible"
  }
 }
 Catch{
  "Caught an exception!"
 }

}
$end = Get-Date
"Time taken: " + (New-Timespan $start $end).seconds + " seconds"



Local Admins

Whether you're using XP or Windows7 giving your users local admin privileges is not a good idea as most of the time they don't need the access to do their jobs. But whether you lock things down or not, it can be useful to know what admin accounts currently exist on your systems.

In the script I used invoke-command inside the threading code, I found this sped things up and made error handling easier. Invoke-command simply ran "net localgroup administrators" and then I filter the results. To run the code below you will need to add the threading function from the first example to the start and also update $Computers.

#Insert threading function here!
$ErrorActionPreference = "Stop";
$start = Get-Date
$Computers = Get-Content c:\temp\test.txt
$Computers |ForEach-Parallel -MaxThreads 100{
 try{
  if(Test-Path \\$_\C$ -ErrorAction silentlycontinue){
   $Srv = $_
   Invoke-Command -ComputerName $Srv -ScriptBlock { net localgroup Administrators } | where {$_ -ne ""} | where {$_ -notmatch "Alias Name|Administrators have complete|Members|--------|knownaccount1|The command completed"} | foreach {"$Srv - " + $_}

  } else{
   $_ + " - Machine not accessible"
  }
 }
 Catch{
  "Caught an exception!"
 }
}
$end = Get-Date
"Time taken: " + (New-Timespan $start $end).seconds + " seconds"

For something a bit more formal check the post here. Also I did play around with WMI but it kept hanging on some machines and WMI in Powershell has no timeout parameter by default. In case you're curious try:

gwmi win32_groupuser -computer $_ | ? groupcomponent -match 'administrators' |% {$_.partcomponent} | where {$_ -match "UserAccount"} | where {$_ -notmatch "known1|known2"}



Final Thoughts

Whether for incident response, active defense or monitoring Powershell can be one of the most useful tools in your security toolbox. As is usually the case I tried Googling for these scripts, couldn't find something I liked, so wrote my own. The scripts could definitely be improved or even combined. For more scripts check out the Microsoft repository here.

I also wanted to plug PoshSec again (https://github.com/PoshSec/PoshSecFramework) as it just looked so good, although I've yet to play with it myself. And again thanks to Tome for his threading code.

Hope you guys have found this post useful. Comments/questions always appreciated.

1 comment: