Using Powershell to interact with Task Scheduler

Gathering information from Task Scheduler with Powershell

I’ve already written about Running Powershell scripts with Task Scheduler, but I solved a Task Scheduler problem the other day with Powershell and want to write about it.

I love to use Powershell to solve problems and questions that I have. These are questions that are, for all intents and purposes, impossible to do manually because of the time that it would take.

I’ve undertaken a project to implement rotating the local Administrator passwords on our work servers. One of the things that I realized, as I was doing the preliminary investigation, is that there are quite a few scheduled jobs that run as .\Administrator in the Task Scheduler. I wondered:

  • exactly how many jobs are scheduled in Task Scheduler?
  • what do the jobs do?
  • which servers are they on?

I used Powershell and get-adcomputer to identify 543 computers in our domain that should be scanned for Task Scheduler-scheduled jobs. These computers run OSes ranging from Server 2000 to Server 2019. There were some key pieces of information I wanted to pull from them:

  • Computer Name
  • Task Name
  • Credentials it runs under
  • Is it enabled?
  • When did it last run?
  • When will it next run?
  • What command is executed?
  • What actions, if any, are executed?

I started with Get-AllScheduledTasks and modified it for the things I need:

  • I removed ActiveDirectory module lines as they’re not necessary under Powershell 7
  • I removed the variable $OUDistinguishedName
  • I added field to the HashTable storing information about each Task
  • I added filtering when querying the Task - I only want information about Tasks in the root ("")
  • I added a switch condition to resolve well-known SIDs to their names (i.e. LOCAL SYSTEM) and improved the regex query
  • I sent all the output directly to a CSV instead sending it to an array, then sending that to a CSV

There are some problems with the script - namely there’s no handling of Tasks with multiple triggers or actions. They show up as “System.Object[]` in the log - but for the purpose of my high-level discussions on how to handle Tasks running as .\Administrator, the script works. There were also one or two servers where the script just hung, and this script obviously doesn’t work on older OSes like Windows Server 2003.

  • 1604 jobs were found… wow! Those are a lot of jobs that bypass our enterprise scheduling process.
  • 543 domain-joined systems were scanned by this script. NOTE: some systems couldn’t be queried. I’d estimate this at 10-15% (50-75 systems).
  • 899 jobs (56%) run on DB systems (Oracle, SQL servers)
  • 159 jobs run under the local Administrator credentials
  • 158 jobs are to reboot the server
  • 598 jobs have not run since 2019
  • 614 jobs are currently disabled and not set to run. Some of them are obsolete, but some of them are purposely created & disabled so that an admin can easily run a predefined task by manually triggering it.

With these stats, I can create a plan to deal with all these Tasks that won’t run once I rotate the local Administrator password.

I’ve included my full script at the end of this post.

As I was writing this script, I found that a number of systems still had Symantec jobs running regularly, even though Symantec was uninstalled from our servers about a year ago. The tasks ran, but didn’t actually do anything since the executable they were calling had been deleted. That seemed pretty harmless but pointless, so of course I had to fix it!

Now I had a new quest - disable all the Symantec tasks across our organization!
What tool can I use? Powershell!

I wrote a quick script that uses 2 methods to query all 543 servers, find any Tasks in the \Symantec Endpoint Protection\ folder and disable them… then I ran it. Mission accomplished!

#Disable-ScheduledSEPTasks

<#
There are a lot of servers with enabled SEP-related tasks in Task Scheduler.
The tasks don't run because the executable doesn't exist.
Disable these tasks to clean up log noise and prevent someone from placing a malicious file in the path

Greg Beifuss
2021-08-25 12:20
#>


#Grab specific computers
$Computers = Get-ADComputer -Filter { (name -notlike "AC*") -and (name -notlike "WIFI*") -and (name -notlike "DON*") -and (Enabled -eq "True") } | Select-Object -ExpandProperty DNSHostName | Sort-Object


#Do this the Powershell 7 way
$Computers | ForEach-Object -Parallel {
    Test-Connection -ComputerName $Computer -Count 1 -Quiet -ErrorAction Stop
    Get-ScheduledTask -CimSession $_ -TaskPath "\Symantec Endpoint Protection\" }

#Do this the legacy way.
#If pingable, try to connect and disable tasks
ForEach ($Computer in $Computers) {
    # Test connection with computer
    Test-Connection -ComputerName $Computer -Count 1 -Quiet -ErrorAction Stop

    # Disable SEP tasks on machines
    $scriptblock = {Get-ScheduledTask -TaskPath "\Symantec Endpoint Protection\" | Disable-ScheduledTask}
    Invoke-Command -ComputerName $Computer -ScriptBlock $scriptblock | Out-Null
}

And here is the script that generated the statistics about Task Scheduler:

#Get-ScheduledRootTasks

<#
Get all scheduled tasks at the root level "\" (this is where staff typically create them and avoids OS/Application-created tasks)
Export the results to a CSV

Modified from https://github.com/exevolution/Get-AllScheduledTasks/blob/master/Get-AllScheduledTasks.ps1

Greg Beifuss
2021-08-25 12:25
#>


Function Connect-TaskScheduler {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $True, ValueFromPipeline = $True)]
        [String]$ComputerName
    )

    Begin {
        $objScheduledTask = New-Object -ComObject("Schedule.Service")
    }
    Process {
        Try {
            Write-Verbose "Connecting to $ComputerName"
            $objScheduledTask.Connect("$ComputerName")
            Write-Verbose "Connected: $($objScheduledTask.Connected)"
        }
        Catch { Write-Error "Failed to connect to $ComputerName" }
    }
    End {
        Return $objScheduledTask
    }
}

Function Get-AllScheduledTasks {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $True)]
        [ValidateScript( { If ($_.GetType().BaseType.Name -eq "MarshalByRefObject" -and $_.Connected -eq $True) { $True }Else { Throw "Not a valid Task Scheduler connection object" } })]
        $Session,

        [Parameter(ValueFromPipelineByPropertyName = $True)]
        [ValidateNotNullOrEmpty()]
        [String[]]$Path = "\",

        [Parameter()]
        [Switch]$Recurse = $False
    )

    Begin {
        $Tasks = @()
        $Paths = @()
    }

    Process {
        $Paths += Get-TaskSchedulerPaths -Session $Session -Path $Path -Recurse:$Recurse
        $Tasks += $Paths | ForEach-Object { $_.Path | Get-TaskSchedulerTasks -Session $Session }
    }
    End {
        Return $Tasks
    }
}

Function Get-TaskSchedulerPaths {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline = $True)]
        [ValidateNotNullOrEmpty()]
        [String[]]$Path = "\",

        [Parameter(Mandatory = $True)]
        [ValidateScript( { If ($_.GetType().BaseType.Name -eq "MarshalByRefObject" -and $_.Connected -eq $True) { $True }Else { Throw "Not a valid Task Scheduler connection object" } })]
        $Session,

        [Parameter()]
        [Switch]$Recurse = $False
    )

    Begin {
        $Paths = @()
    }
    Process {
        $BasePath = $Session.GetFolder("$Path")
        $Paths += $BasePath
        $Paths += $BasePath.GetFolders(1)
        If ($Recurse) {
            ForEach ($P in $Paths) {
                If ($P -eq $BasePath) { Continue }
                Else { $Paths += Get-TaskSchedulerPaths -Session $Session -Path $P.Path -Recurse }
            }
        }
    }
    End {
        Return $Paths
    }
}

Function Get-TaskSchedulerTasks {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline = $True)]
        [ValidateNotNullOrEmpty()]
        [String[]]$Path = "\",

        [Parameter(Mandatory = $True)]
        [ValidateScript( { If ($_.GetType().BaseType.Name -eq "MarshalByRefObject" -and $_.Connected -eq $True) { $True }Else { Throw "Not a valid Task Scheduler connection object" } })]
        $Session
    )

    Begin {
        $AllTasks = @()
    }

    Process {
        $Folder = $Session.GetFolder("$Path")
        $FolderTasks = $Folder.GetTasks(0)

        ForEach ($Task in $FolderTasks) {
            If ((($Task.Path.ToCharArray() | Where-Object { $_ -eq '\' } | Measure-Object).Count -eq 1) -and ($Task | Where-Object -Property Name -NotMatch '^(SensorFramework|Optimize Start Menu|ShadowCopyVolumeUser_Feed|User_Feed|OneDrive|MicrosoftEdgeUpdateTask|GoogleUpdateTask|Adobe Acrobat Update Task)')) {
                $RunAsID = $Task | Select-Object @{Name = "RunAs"; Expression = { [xml]$xml = $_.xml ; $xml.Task.Principals.Principal.UserId } } | Select-Object -ExpandProperty RunAs -ErrorAction Stop
                If ($RunAsID -match "S-\d-(?:\d+-){1,14}\d+") {
                    Switch ($RunAsID) {
                        "S-1-5-18" { $RunAs = "LOCAL SYSTEM"; break }
                        "S-1-5-19" { $RunAs = "LOCAL SERVICE"; break }
                        "S-1-5-20" { $RunAs = "NETWORK SERVICE"; break }
                        Default {
                            Try { $RunAs = Get-ADUser -Identity $RunAsID | Select-Object -ExpandProperty SamAccountName -ErrorAction Stop }
                            Catch {
                                Try { $RunAs = Get-ADGroup -Identity $RunAsID | Select-Object -ExpandProperty SamAccountName -ErrorAction Stop }
                                Catch { $RunAs = $RunAsID }
                            }
                        }
                    }
                }
                Else { $RunAs = $RunAsID }

                $HashTable = [Ordered]@{
                    ComputerName = $Session.TargetServer
                    Name         = $Task.Name
                    RunAs        = $RunAs
                    Action       = $Task | Select-Object @{Name = "Actions"; Expression = { [xml]$xml = $_.xml ; $xml.Task.Actions.Exec.Command } } | Select-Object -ExpandProperty Actions -ErrorAction Stop
                    Arguments    = $Task | Select-Object @{Name = "Arguments"; Expression = { [xml]$xml = $_.xml ; $xml.Task.Actions.Exec.Arguments } } | Select-Object -ExpandProperty Arguments -ErrorAction Stop
                    LastRunTime  = $Task.LastRunTime
                    NextRunTime  = $Task.NextRunTime
                    TaskEnabled  = $Task.Enabled
                }
                $AllTasks += New-Object PSObject -Property $HashTable
            }
        }
    }

    End {
        Return $AllTasks
    }
}

$Tasks = @()

ForEach ($Computer in ( Get-ADComputer -Filter { (name -notlike "AC*") -and (name -notlike "WIFI*") -and (name -notlike "DON*") -and (Enabled -eq "True") } | Select-Object -ExpandProperty DNSHostName | Sort-Object)){
#ForEach ($Computer in $Computers[355..543]) {
    $Connection = Connect-TaskScheduler -ComputerName $Computer -Verbose
    Get-AllScheduledTasks -Session $Connection -Recurse | Export-CSV c:\users\gbadmin\desktop\tasks.csv -NoTypeInformation -Append
}
Built with Hugo
Theme Stack designed by Jimmy