Privilege Escalation · intermediate · ~11 min
**What you will learn** - Read cron configuration (`/etc/crontab`, `/etc/cron.*`, user crontabs) and recognise the three misconfigurations that turn a scheduled job into a root-code-execution path: a world-writable script, a short (relative) binary name, and a wildcard over an untrusted directory. - Explain how the shell's `PATH` search order lets an attacker-controlled binary run in place of a trusted one when a privileged program calls a command by short name. - Reproduce each weakness safely in an isolated lab and then, more importantly, **remediate** it: correct ownership and permissions, absolute paths, a sanitised `PATH`, and safe wildcard handling. - Prove a fix works with a **mitigation verification** step that shows the job now rejects the malicious input and still performs its legitimate task. - Set up detection: what to log for scheduled jobs and file writes, which events signal abuse, and how false positives arise.
Security objective. The asset is the machine's root privilege — the difference between a low-privilege foothold and total control of a Linux host. The threat is a local attacker who already has an unprivileged shell (through a web bug, a stolen SSH key, a weak service account) and now wants to escalate to root. This lesson teaches you to detect and remediate three closely related misconfigurations in scheduled tasks and path handling, and to verify your fixes hold.
This builds directly on your prerequisite, "Privilege escalation: the enumeration-first mindset" (privesc-intro). There you learned that escalation is mostly enumeration — patiently listing what runs, as whom, and what you can write to — rather than dropping an exploit. Here we apply that mindset to one specific, extremely common surface: cron.
cron is the Linux job scheduler. Many of its jobs run as root. That is normal and useful — backups, log rotation, cleanup. The danger appears when a root job depends on something an unprivileged user controls. If a root cron job runs a script you can overwrite, calls a binary by short name so the lookup depends on your PATH, or globs a wildcard in a directory you can drop files into, then you can make root run your commands on a schedule. The same relative-path weakness applies to any privileged program (a SUID binary, a sudo-allowed script) that runs a command without giving its full, absolute path.
The defensive story is simple hygiene: root-owned, non-writable scripts; absolute paths everywhere; a clean minimal PATH; and no wildcards over untrusted directories. Because these fixes are easy yet frequently missing, both the attack and the defence are worth knowing cold.
In authorized engagements, writable cron scripts and PATH abuse are among the most reliable Linux privilege-escalation findings — and they need no exploit code at all. A single misconfigured schedule or a system("tar ...") call without a path is enough. Automated tools like the LinPEAS/linux-smart-enumeration family flag them precisely because they show up so often in real environments.
For a defender or platform engineer, this is bread-and-butter hardening. Configuration management, backup jobs, and homegrown maintenance scripts accumulate over years; ownership drifts, someone chmod 777s a directory to "make it work," and a quiet root job inherits the risk. Knowing the pattern lets you write a config-management rule or a CI check that keeps it from recurring.
The fixes are cheap. That asymmetry — trivial to exploit, trivial to prevent, yet routinely present — is exactly why these belong in your first pass on any Linux host.
Definition. cron is a daemon that runs commands on a fixed schedule. Job definitions live in several places: the system table /etc/crontab, drop-in directories /etc/cron.d/, the periodic directories /etc/cron.hourly|daily|weekly|monthly/, and per-user tables under /var/spool/cron/ (viewable with crontab -l).
Plain explanation. Each line says when to run and what to run, and — for the system tables — as which user. Many run as root.
How it works as an escalation path. A root cron job is dangerous only when part of what it runs is controlled by a non-root user. The classic case: the job runs /opt/scripts/backup.sh, but that file (or its directory) is writable by your user. You append a line to the script; the next time the job fires, root executes it.
When / when-not. A root job that runs a root-owned, non-writable script with absolute paths is fine. It becomes a finding only when an unprivileged principal can influence the script, the binary lookup, or the arguments.
Pitfall. People check the script's permissions but forget the directory's. If you can write to the directory, you may be able to replace or rename the script even if the file itself looks locked down.
Definition. When a command is given without a leading / (a short/relative name like tar instead of /usr/bin/tar), the shell searches each directory listed in the PATH environment variable, left to right, and runs the first match.
Plain explanation. PATH is just a lookup order. Whoever controls that order — or controls a directory that appears early in it — controls which binary actually runs.
How it works as an escalation path. Suppose a root script or a SUID program calls system("service apache2 restart"). service has no path, so the process searches PATH. If the attacker can prepend a directory they own (e.g. PATH=/tmp/evil:$PATH) and drop an executable named service there, root runs the attacker's service.
When / when-not. This only bites when (a) a command is called by short name and (b) the attacker can influence PATH or write to a directory already early in PATH. Absolute paths defeat it entirely.
Pitfall. sudo normally sanitises PATH (via secure_path), but a script launched by cron may inherit a PATH set at the top of /etc/crontab — and that value is sometimes edited carelessly to include a writable directory.
Definition. A wildcard (*) is expanded by the shell into the list of matching filenames before the command runs. Those filenames are passed as separate arguments.
Plain explanation. If you can create files in the globbed directory, you choose some of the arguments the command receives. Many tools treat arguments beginning with - as options. So a filename can become an option.
How it works. With tar -czf backup.tgz * running in a directory you can write to, you create files named like --checkpoint=1 and --checkpoint-action=exec=sh runme.sh. The shell expands * to include them; tar reads them as options and executes your action as root. (chown, rsync, chmod, and others have analogous argument tricks.)
When / when-not. Only when the wildcard runs in a directory the attacker can write to and the tool has an argument that triggers side effects. A wildcard over a directory only root can write to is safe.
Pitfall. Defenders often "fix" this by escaping the shell but keep the wildcard; the real fix is to not glob untrusted directories, or to prefix the glob with ./ and use -- to stop option parsing.
UNTRUSTED (low-priv user 'alice') | TRUSTED (root)
|
alice's shell --writes--> /opt/scripts/backup.sh |
(entry point) (world-writable file) |
| |
| cron reads & |
=== TRUST BOUNDARY ===|=== runs AS ROOT =|===> root shell
| |
alice drops --> /tmp/evil/service (early in PATH) ---|--> root runs evil 'service'
alice creates-> '--checkpoint-action=...' file ------|--> root's `tar *` runs it
|
Assets protected: root privilege, integrity of scheduled jobs
Entry points: writable script/dir, attacker-controlled PATH, writable glob dir
Boundary crossed: unprivileged file/env influence -> root execution
Knowledge check
/usr/bin/tar -czf /backup/x.tgz * from /root/data, a directory only root can write to. Which of the three weaknesses does this not have, and why?Key places to enumerate cron and the syntax of a job line (all read-only, lab-safe):
# System-wide schedules (note the extra 'user' field vs a personal crontab)
cat /etc/crontab
ls -la /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/
# Your own user crontab
crontab -l
# A system crontab line: minute hour dom month dow USER command
# * * * * * root /opt/scripts/backup.sh
# ^ every minute, as root, runs this script
Check who can write to whatever a root job touches:
# -l long listing shows owner, group, and permission bits
ls -la /opt/scripts/backup.sh /opt/scripts
# -rwxrwxrwx 1 root root ... backup.sh <-- the trailing 'rwx' = world-writable = finding
Inspect the effective PATH and any PATH set inside cron config:
echo "$PATH"
grep -R "PATH=" /etc/crontab /etc/cron.d/ 2>/dev/null
The defensive primitives you will apply:
chown root:root /opt/scripts/backup.sh # correct ownership
chmod 755 /opt/scripts/backup.sh # owner writes; others read/execute only
chmod 750 /opt/scripts # lock the directory too
Scheduled tasks and loose path handling are two more high-frequency routes to root on Linux.
cron runs commands on a schedule, and often runs them as root. The job definitions live in /etc/crontab, the /etc/cron.* directories, and individual user crontabs.
A root cron job becomes an escalation path when it:
PATH to find it). Place a malicious binary earlier in PATH so it runs instead.tar *. This is "wildcard injection": a carefully named file is interpreted as a command-line option rather than data.When a command is given without a full path, the shell searches each directory in PATH in order and runs the first match.
So if a privileged script or SUID program calls a command without an absolute path — for example system("service ...") instead of /usr/sbin/service — it searches PATH. If you can change PATH (or write to a directory that appears early in it), your own service runs with the program's privileges.
This is the weaponised version of the PATH idea from the Computer-Fundamentals lesson.
PATH.The example is a homegrown "nightly backup" cron job. We show the insecure version, the secure fix, and a verification that the fix rejects abuse while still doing its job. Run only in an isolated lab VM or container you control.
# /etc/cron.d/backup (runs as root every minute in the lab)
# THREE separate mistakes packed into one job for teaching:
PATH=/tmp/tools:/usr/bin:/bin # mistake A: attacker-writable dir first in PATH
* * * * * root cd /shared/data && tar -czf /backup/data.tgz *
# ^ mistake B: 'tar' by short name (resolved via PATH)
# ^ mistake C: wildcard over /shared/data
# and suppose /shared/data is world-writable (drwxrwxrwx), so any user can drop files there.
Why this is exploitable, defensively stated: an unprivileged user can (A) place a binary named tar in /tmp/tools, or (C) create files whose names tar interprets as options. Either way root runs attacker-chosen code. We are not publishing the option payload — the point is to recognise and eliminate the conditions.
# /etc/cron.d/backup (hardened)
PATH=/usr/sbin:/usr/bin:/sbin:/bin # fix A: only trusted, root-owned dirs
* * * * * root /usr/local/sbin/backup.sh
#!/bin/sh
# /usr/local/sbin/backup.sh owner root:root, mode 750, dir also root-only
set -eu
umask 077
SRC=/shared/data
DST=/backup/data.tgz
# fix B: absolute path to the binary — no PATH lookup
# fix C: change into the dir, end option parsing with --, and glob with ./ so no
# filename can masquerade as an option
cd "$SRC"
/usr/bin/tar -czf "$DST" -- ./*
And correct the filesystem so untrusted users cannot influence any of it:
chown root:root /usr/local/sbin/backup.sh && chmod 750 /usr/local/sbin/backup.sh
chown root:root /shared/data && chmod 755 /shared/data # not world-writable
#!/bin/sh
# verify.sh — run in the lab as the LOW-PRIV user, then as root, to prove the fix.
set -u
# A) low-priv user can no longer write the script or the data dir -> abuse blocked
if : > /usr/local/sbin/backup.sh 2>/dev/null; then
echo "FAIL: script is writable by non-root"; else echo "PASS: script not writable"; fi
if : > /shared/data/evil 2>/dev/null; then
echo "FAIL: data dir is writable by non-root"; else echo "PASS: data dir not writable"; fi
# B) a filename that looks like an option is treated as DATA, not executed
# (root sets up a legit file; option parsing is stopped by '--' and './')
sudo sh -c 'echo real > /shared/data/report.txt'
sudo /usr/local/sbin/backup.sh && echo "PASS: backup ran"
# C) good input is still archived
if tar -tzf /backup/data.tgz | grep -q './report.txt'; then
echo "PASS: legitimate file present in archive"; else echo "FAIL: backup missing data"; fi
Expected result: every line prints PASS. The first two prove the attacker's entry points (writable script, writable data dir) are gone; the last two prove the job still archives real data and that a hostile filename is handled as a plain file, not an option. If any line prints FAIL, the remediation is incomplete — do not consider it fixed.
Walking the hardened backup.sh and its verification:
| Step | Line | What happens | Why it matters |
|---|---|---|---|
| 1 | set -eu |
Abort on any error or unset variable | A failing archive should stop, not silently continue with a stale/partial file |
| 2 | umask 077 |
New files created only readable by owner | The backup may contain sensitive data; deny others by default |
| 3 | SRC=/shared/data / DST=... |
Fixed, absolute locations | No dependence on the caller's working directory or environment |
| 4 | cd "$SRC" |
Enter the data directory | Combined with ./* this makes globbed names start with ./ |
| 5 | /usr/bin/tar ... |
Absolute binary path | Defeats PATH abuse — the real tar runs regardless of PATH |
| 6 | -- ./* |
-- ends option parsing; ./ prefixes every name |
A file called --checkpoint=1 expands to ./--checkpoint=1, a harmless data name after -- |
How the values flow in the verify script:
: > file tries to truncate file. As the low-priv user it should be denied by the permission bits, so the if body runs the PASS branch. If the bits were still loose, the write would succeed and we'd correctly print FAIL.report.txt, then runs the job. Because the data dir is now 755 (root-only writable), the archive contains exactly what root put there.tar -tzf ... | grep -q './report.txt' lists the archive and confirms the legitimate entry exists. A malicious --checkpoint-style filename, had one been plantable, would appear here as literal data (e.g. ./--checkpoint=1) rather than having executed — but with a root-only dir, it can't be planted at all.The key insight: each fix removes one precondition. PATH abuse needs a short name (removed by the absolute path). Wildcard injection needs option-lookalike filenames to reach the tool as options (removed by -- and ./) and a writable glob dir (removed by 755). Script overwrite needs a writable script/dir (removed by ownership + 750). Defence in depth: even if one control regresses, another still blocks the path.
| Wrong approach | Why it's wrong | Corrected approach | How to recognise / prevent |
|---|---|---|---|
| Checking only the script file's permissions | The parent directory being writable lets an attacker replace or rename the script even if the file is root:root 755 |
Lock both: chmod 750 the dir, chmod 750/755 the file, both root:root |
ls -la the directory, not just the file; audit with find / -perm -0002 -type d |
chmod 777 "just to make the job work" |
World-writable anything referenced by a root job is an instant escalation path | Give the job's own user the minimum it needs; never world-writable | Grep your config management for 0777/chmod 777; add a CI lint rule |
Calling tar, service, python by short name in a root script |
Resolution depends on PATH, which an attacker may influence |
Use absolute paths (/usr/bin/tar) and set an explicit clean PATH at the top of the script |
Static-scan scripts for command calls lacking a leading / |
Trusting cron's inherited PATH |
A PATH= line in /etc/crontab can include a writable dir; jobs inherit it |
Pin PATH=/usr/sbin:/usr/bin:/sbin:/bin in the job/script; keep writable dirs out |
grep -R 'PATH=' /etc/crontab /etc/cron.d/ and review every entry |
Globbing untrusted dirs with * |
Filenames become options (wildcard injection) | cd in, use -- ./*, or better, pass an explicit file list / use find -print0 | xargs -0 |
Search jobs for bare * on tar/chown/chmod/rsync |
| Assuming a passing scanner means "secure" | Scanners miss context (who can write where, custom scripts); a clean scan is not proof | Manually verify ownership, paths, and run the mitigation-verification tests | Treat scanner output as a lead, then confirm by hand |
When enumeration finds nothing but you suspect cron:
systemctl status cron (or crond) and the log. On many systems cron activity lands in /var/log/syslog or the journal: journalctl -u cron / grep CRON /var/log/syslog.crontab -l is empty, still read /etc/crontab, /etc/cron.d/*, and the cron.{hourly,daily,weekly,monthly} dirs.When your hardened job "doesn't run":
PATH, a command you relied on may no longer be found. Fix by using absolute paths, not by widening PATH.set -e will abort the whole script on the first non-zero exit. Test each command's exit status; add || true only where a non-zero result is genuinely acceptable.Questions to ask when a fix seems not to hold:
PATH= set later that overrides mine?Verify, don't assume: re-run the verify.sh checks after every change. A fix you didn't test is a hypothesis.
Security & safety — detection and logging for scheduled jobs and file integrity.
The goal is to notice, quickly, when a root job's inputs are tampered with, and to have enough evidence to reconstruct what happened without capturing secrets.
What to log:
/etc/cron.d/, /etc/crontab, and any root-run script directory should record who modified them and when.What to NEVER log: contents of files that may hold secrets (backup payloads, key material), passwords, API tokens or session cookies, private keys, full payment card numbers, or PII you don't need. Log the fact and metadata of an action ("archived 42 files from /shared/data"), not the sensitive data itself. If a script handles a credential, reference it as <redacted> in logs.
Events that signal abuse:
/etc/cron.*, /etc/crontab, or a root-run script by a non-root uid.PATH.How false positives arise: legitimate deploys and configuration-management runs (Ansible/Puppet) will rewrite cron files and scripts — often as root — and can look identical to tampering. Reduce noise by baselining expected change windows, attributing changes to the automation's service account, and alerting on unexpected actor rather than any change. A backup job that legitimately grows or shrinks its file count will also move your metrics; alert on structure (permissions, ownership, new writers) rather than on benign volume.
Authorized real-world use. On a scoped internal penetration test, after landing an unprivileged shell on a Linux app server, you enumerate cron and find /etc/cron.d/backup running a root job that execs a script in a directory the www-data user can write to. You document the finding, demonstrate impact once in the client's staging environment (writing a marker file as root, not deploying anything persistent), and hand the client the exact remediation: correct ownership, chmod, absolute paths, pinned PATH, and a verification script. That is the whole value — a reproducible finding plus a fix the client can apply and re-test.
Professional best-practice habits
root:root, 750; directories not world-writable; PATH pinned; umask 077 in scripts that create sensitive output.--; prefer explicit file lists over globs on shared dirs.set -eu, meaningful exit codes, and collected cron logs with file-integrity monitoring on the config paths.| Level | Focus |
|---|---|
| Beginner | Enumerate all five cron locations; spot writable scripts/dirs; convert short names to absolute paths; re-run a verification script |
| Advanced | Model PATH inheritance across cron/sudo/service managers; write config-management rules and auditd/AIDE watches; build CI lint that rejects world-writable or short-name/wildcard patterns before they ship |
Authorization checklist (before any lab or engagement): you own the host or hold written authorization; the target and time window are in scope; you use an isolated VM/container/CTF for practice — never third-party systems; you have a rollback/cleanup plan; and you avoid persistence, data exfiltration, and anything outside the agreed scope. Reminder: passing an automated scanner does not prove a system is secure, and nothing is ever "completely secure."
All tasks are lab-only: use a disposable VM or container you own. Each ends by remediating and verifying — the defensive outcome is the point.
Beginner 1 — Enumerate every cron surface.
/etc/crontab, /etc/cron.d/, the periodic dirs, and crontab -l; for each root job, record the script/binary it runs and the permissions of that file and its directory.schedule | user | command | file perms | dir perms | writable-by-non-root?.ls -la, find / -perm -0002 -type f 2>/dev/null. Concepts: cron locations, permission bits.Beginner 2 — Harden one writable job.
root:root with chmod 750; confirm your low-priv user can no longer write either.ls -la; a PASS/FAIL write test as the low-priv user.verify.sh. Concepts: ownership, least privilege, mitigation verification.Intermediate 1 — Remove PATH dependence.
tar, service, or python by short name.PATH=/usr/sbin:/usr/bin:/sbin:/bin; demonstrate that placing a decoy binary named after the command in an early-PATH dir no longer changes what runs.command -v, which. Concepts: PATH search order, absolute paths.Intermediate 2 — Neutralise wildcard injection.
tar -czf out.tgz * job safe without breaking backups.cd/.//--, or replace it with an explicit file list; then create a filename that looks like an option and show it is archived as data, not acted upon.tar -tzf. Concepts: shell globbing, option parsing, --.Challenge — Detection and prevention pipeline.
/etc/cron.d/, /etc/crontab, and your script dir, and trigger it with a benign change to confirm it logs the actor and time; (2) write a small check script (or CI lint) that fails if any root-referenced file/dir is world-writable, or if a root script calls a command by short name or globs a shared dir; (3) document what each alert means and one realistic false positive (e.g. a config-management deploy) and how you'd tune it.Main concepts. A root cron job is an escalation path whenever an unprivileged user can influence it: by writing the script (or its directory), by controlling PATH when the job calls a binary by short name, or by dropping option-lookalike filenames into a directory the job globs with *. The same relative-path weakness applies to any privileged program that runs commands without absolute paths.
Key syntax/commands. Enumerate with cat /etc/crontab, ls -la /etc/cron.d/, crontab -l, and find / -perm -0002. Harden with chown root:root, chmod 750, absolute binary paths (/usr/bin/tar), a pinned PATH=/usr/sbin:/usr/bin:/sbin:/bin, and safe globbing (cd, -- ./*).
Common mistakes. Locking the file but not the directory; chmod 777 for convenience; short command names in root scripts; trusting cron's inherited PATH; globbing untrusted dirs; and treating a clean scanner run as proof of security.
What to remember. Each attack has a small set of preconditions; remediation removes them, and a verification step proves the removal — a fix you didn't test is only a hypothesis. Log job runs and file-integrity events (metadata, never secrets), alert on unexpected writers rather than any change, and only ever practise on systems you own or are authorised to test. Nothing is ever "completely secure"; the goal is to close the easy, common paths and detect the rest.