Scheduled Powershell Scripts without storing credentials

2 minute read

TL;WR: Use a G/MSA and a Base64 encoded task to execeute a script as a service account in a scheduled task without manually saving credentials or leaving files.

Note: The following isn’t normally a good practice for a lot of reasons. In fact (minus the G/MSA account) it’s practically a low-effort malware persistence technique. Normally you’d want a repository style folder so that you can view/track the scripts (and potentially include them in your CD pipelines).
This could be appropriate if your already-change-controlled scripts need to create temporary sub-tasks/jobs. Even then, SECOPS will probably raise their eyebrows at you for suggesting it.

Sometimes I want to schedule a script to run with specific domain credentials/service account without storing credentials locally. With G/MSA’s an attacker needs to use AD/LDAP to steal the G/MSA password blob, which is easier to log and identify centrally. Credentials saved to a scheduled task can be really easily extracted with something like NetPass.

Anyways, digressing from the caviats: here I wanted to schedule maintenance notifications to users that have been logged in for so long that their hosts are up for replacement as part of our lifecycle automations, but wanted to put together a generic helper utility for creating these tasks.

You can’t select a G/MSA account in the Task Scheduler UI, only with SC or PowerShell; and since nobody like a process that reads “Do a dozen things by hand in the UI, then write some lines to modify it” I figured the generic helper would be a lot more useful.

The next step to low footprint bliss is to say goodbye to all the files and ACLs! Inline the scripts (if they’re short enough)! Here are the key parts for a file-less, sort-of-credential-less PowerShell scheduled task. In this case it just schedules desktop messages for Citrix sessions. Ironically this specific example drops transcript copies but you get the point.

(Sorry for not sharing the full helper, but these are the guts of it. Pretty simple to generalize based on your needs. Besides, I’d feel bad if someone out there is B64 encoding all of their maintenance scripts.)

#Encode script as Base64, send a Citrix message in this example
function EncodeMessageTaskScript ($MessageText, $AdminAddress)
    $TaskScript = @"
    Start-Transcript "C:\ScriptLogs\ScheduledPS.log" -Append
    & { Add-Pssnapin @('Citrix.Host.Admin.V2','Citrix.Broker.Admin.V2')}

    `$CurrentSessions = Get-BrokerSession -AdminAddress "$AdminAddress" -MaxRecordCount 1000 | ? DesktopGroupName -eq 'Nope'
    `$CurrentSessions | % { Send-BrokerSessionMessage -AdminAddress "$AdminAddress" -InputObject `$_ -MessageStyle Critical -Text "$MessageText"`nMessage Sent [`$(Get-Date)]" -Title "Maintenance Warning"}


"@ #Let's pretend that this is indented...


function CreateScheduledGMSATask ($EncodedPsScript, [datetime]$TriggerDateTime, $TaskName)

    $Action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-EncodedCommand `"$EncodedPsScript`" -NoLogo -NoProfile -ExecutionPolicy Bypass"
    $Principal = New-ScheduledTaskPrincipal -LogonType Password -RunLevel Limited -UserId 'DOMAIN\[G]MSA$'
    $Settings = New-ScheduledTaskSettingsSet -Compatibility Win8
    $Trigger = New-ScheduledTaskTrigger -Once -At $TriggerDateTime

    $TaskObj = New-ScheduledTask -Action $Action -Principal $Principal -Trigger $Trigger -Settings $Settings
    Register-ScheduledTask -TaskName $TaskName -InputObject $TaskObj


$EncodedReminderTask = EncodeMessageTaskScript -MessageText "Your Message Text" -AdminAddress $CitrixAdminAddress
CreateScheduledGMSATask -EncodedPsScript $EncodedReminderTask -TriggerDateTime $MaintReminder -TaskName "MessageTask $($MaintStartTime.ToString('yyyyMMdd.HHmmss'))"