Labs

Cron Job Hijacking: Escalating a World-Writable Script

Docker lab Privilege Escalation

Updated August 4, 2026 · Written by PWNMI — see About.

Scheduled tasks are a blind spot in a lot of permission audits, because the script itself often gets attention while the job that runs it doesn't. This lab reproduces a specific, common version of that gap: a script executed periodically by root, with permissions loose enough that anyone can rewrite what it does before the next tick. No CVE, no specific product — this is a generic Linux privesc technique class.

What you'll practice: finding what root actually executes on a schedule (not just what's in your own crontab), recognizing that a script's permissions matter independently of who triggers it, and the patience to wait out a real timing window instead of assuming a technique failed.

Set up the lab

New to Docker or git, or unsure what the commands below are actually doing? See Docker for Security Labs and Git for Security Work first.

Get the lab files from pwnmihq/resources:

git clone --filter=blob:none --sparse https://github.com/pwnmihq/resources
cd resources
git sparse-checkout set technique-labs/cron-job-hijacking
cd technique-labs/cron-job-hijacking
docker compose up -d --build

Drop into a shell as analyst, an ordinary unprivileged user:

docker compose exec -u analyst app bash
id

Understanding the technique

Cron doesn't care who wrote the script it's about to run, or what permissions it has — it runs as whatever user the crontab entry specifies, root in this case, and executes the script path exactly as written. If that script (or, just as commonly, the directory containing it) is writable by someone other than root, that someone gets to decide what root runs the next time the schedule fires.

This lab's misconfiguration: /opt/scripts/backup.sh, run every minute by root's crontab, was copied into place with chmod 777 instead of something tighter — plausibly because a deploy script set permissive defaults on an entire directory rather than the one file that needed to be executable. The script's actual content (a placeholder backup command) doesn't matter; the fact that anyone can overwrite it does.

Exploitation

Confirm the permissions problem directly — this is the step that matters, not assuming it's exploitable because a cron job exists at all:

ls -la /opt/scripts/backup.sh
-rwxrwxrwx 1 root root 145 <date> /opt/scripts/backup.sh

World-writable (rwxrwxrwx), owned by root, and — confirm this part too, since a writable script root never actually runs isn't a finding — check /etc/cron.d/ or crontab -l as root isn't available to you yet, but the script's own presence and permissions are enough to proceed. Replace it with a payload that gives you something persistent to use once it runs:

echo 'cp /bin/bash /tmp/rootbash && chmod u+s /tmp/rootbash' > /opt/scripts/backup.sh

Now wait. The job is scheduled * * * * * — every minute — so the wait is never longer than 60 seconds, but it is a real wait, not instant:

watch -n 2 -- ls -la /tmp/rootbash

(Or just poll manually every few seconds — ls -la /tmp/rootbash will fail with "No such file" until root's next cron tick actually runs your replaced script.) Once it appears:

ls -la /tmp/rootbash
-rwsr-xr-x 1 root root 1265648 <date> /tmp/rootbash

SUID-root, owned by root, created by the cron job — not by you directly, which is exactly the point. Use it:

/tmp/rootbash -p -c 'id; head -1 /etc/shadow'
uid=1000(analyst) gid=1000(analyst) euid=0(root) groups=1000(analyst)
root:*:20647:0:99999:7:::

Same -p requirement as the SUID lab, for the same reason: without it, the spawned shell drops the elevated privileges the setuid bit would otherwise grant it.

Cleanup

/tmp/rootbash is a real artifact left on the target — a persistent SUID-root binary, not something that disappears when the exploit finishes. On a real engagement, remove it and restore the original script content once you're done, rather than leaving a live backdoor sitting in /tmp:

rm -f /tmp/rootbash

The original backup.sh content doesn't need forensic-grade restoration for this lab (it's a placeholder), but on a real engagement, restoring the script to what it was — or at minimum documenting exactly what you changed — matters for the client's own cleanup.

Tear the lab down when you're done:

docker compose down -v

Lessons worth keeping

  • What a job runs matters as much as who scheduled it. A cron entry itself being owned and configured correctly by root doesn't help if the script it points at isn't.
  • Waiting is sometimes the actual technique. It's tempting to assume something didn't work when the result isn't instant — cron-based privesc has a real, bounded timing window built into how the exploit resolves, and that's expected, not a failure signal.
  • A world-writable file is a privilege escalation vector the moment anything privileged executes it — not before. The permission bug and the exploitable condition are two different facts; both have to be true, and confirming both (not just the first) is what separates a real finding from a false positive.

Next step

For a related but distinct escalation path through script execution, see the Script Privilege Escalation checklist, which covers this exact technique class (and several others) in more general terms. For the broader mental model, see Privilege Escalation Fundamentals or continue the series with Linux Capabilities Abuse.