Privilege Escalation · intermediate · ~11 min

Linux privesc: cron jobs and PATH abuse

**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.

Overview

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.

Why it matters

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.

Core concepts

1. cron running as root

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.

2. Relative paths and PATH abuse

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.

3. Wildcard injection

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.

Threat model

            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

  1. In the diagram, what asset is protected, and where exactly is the trust boundary?
  2. A root cron job runs /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?
  3. Which insecure assumption makes PATH abuse work — what did the programmer wrongly assume about the environment their code runs in?
  4. Why must every reproduction in this lesson happen only on a host you own or are explicitly authorised to test?

Syntax notes

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

Lesson

Scheduled tasks and loose path handling are two more high-frequency routes to root on Linux.

Cron jobs

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:

  • Runs a script you can write to. Put your own code in the script. It then executes as root the next time the job fires.
  • Calls a binary by short name (relying on PATH to find it). Place a malicious binary earlier in PATH so it runs instead.
  • Uses a wildcard in a writable directory, for example tar *. This is "wildcard injection": a carefully named file is interpreted as a command-line option rather than data.

PATH abuse

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.

The fixes

  • Cron scripts should be root-owned and not writable by other users. Jobs should use absolute paths and avoid wildcards on untrusted directories.
  • Privileged programs must call binaries by absolute path and set a clean PATH.
  • Audit every writable file or directory referenced by anything that runs as root.

Code examples

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.

(1) WARNING: intentionally vulnerable — use only in a local, isolated, authorized lab. Do not deploy.

# /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.

(2) The SECURE fix

# /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

(3) VERIFY the fix — it must REJECT bad input and ACCEPT good input

#!/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.

Line by line

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:

  • Redirection : > 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.
  • Root creates 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.

Common mistakes

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

Debugging tips

When enumeration finds nothing but you suspect cron:

  • Confirm the daemon actually runs jobs: check 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.
  • Remember jobs can be defined in five places. If crontab -l is empty, still read /etc/crontab, /etc/cron.d/*, and the cron.{hourly,daily,weekly,monthly} dirs.
  • A job may run a script that runs another script. Follow the chain; the writable link can be two hops down.

When your hardened job "doesn't run":

  • With a pinned 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.
  • cron runs with a minimal environment and no TTY. Something that works in your shell may fail under cron because of a missing variable — set what you need explicitly rather than assuming your login environment.

Questions to ask when a fix seems not to hold:

  • Did I lock the directory as well as the file?
  • Is there a second PATH= set later that overrides mine?
  • Does the job call any helper that itself uses short names or wildcards?
  • Can the low-priv user still write anything the job reads — including config files and the archive destination?

Verify, don't assume: re-run the verify.sh checks after every change. A fix you didn't test is a hypothesis.

Memory safety

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:

  • Timestamp (with timezone), the job identifier, the user it ran as, and its exit status. cron already emits start/finish lines to syslog/journal — make sure they are collected and retained.
  • For each privileged script: the resource it acted on (source dir, destination path), the security decision (which files were included/excluded, whether an integrity check passed), and a correlation id so a job run can be tied to downstream effects.
  • Filesystem-integrity events on the sensitive paths. A file-integrity monitor (e.g. auditd watches or AIDE baselines) on /etc/cron.d/, /etc/crontab, and any root-run script directory should record who modified them and when.
  • Source of change where available: for auditd, the uid/auid and the process that performed the write.

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:

  • Any write to /etc/cron.*, /etc/crontab, or a root-run script by a non-root uid.
  • A permission change that makes a root-referenced file or directory group/world-writable.
  • A cron job whose child processes spawn an unexpected shell or network connection.
  • New executables appearing in a directory that is early in a service's 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.

Real-world uses

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

  • Least privilege: run scheduled work as the lowest user that suffices, not root, whenever the task allows.
  • Secure defaults: scripts root:root, 750; directories not world-writable; PATH pinned; umask 077 in scripts that create sensitive output.
  • Input validation: treat filenames and any environment as untrusted; end option parsing with --; prefer explicit file lists over globs on shared dirs.
  • Logging & error handling: 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."

Practice tasks

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.

  • Objective: produce a complete inventory of scheduled jobs on your lab host.
  • Requirements: list jobs from /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.
  • Output: a table of schedule | user | command | file perms | dir perms | writable-by-non-root?.
  • Hints: ls -la, find / -perm -0002 -type f 2>/dev/null. Concepts: cron locations, permission bits.

Beginner 2 — Harden one writable job.

  • Objective: take a lab job whose script is world-writable and lock it down.
  • Requirements: set the script and its directory to root:root with chmod 750; confirm your low-priv user can no longer write either.
  • Input/output: before/after ls -la; a PASS/FAIL write test as the low-priv user.
  • Constraints: change nothing about what the job does. Hints: reuse the pattern from verify.sh. Concepts: ownership, least privilege, mitigation verification.

Intermediate 1 — Remove PATH dependence.

  • Objective: eliminate PATH abuse from a root script that calls tar, service, or python by short name.
  • Requirements: replace each short name with an absolute path; pin 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.
  • Constraints: lab only; the decoy should be inert (e.g. it just writes a marker to a file you own). Hints: command -v, which. Concepts: PATH search order, absolute paths.

Intermediate 2 — Neutralise wildcard injection.

  • Objective: make a tar -czf out.tgz * job safe without breaking backups.
  • Requirements: move the glob behind 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.
  • Input/output: archive listing showing the option-lookalike name present as a plain entry; backup still contains the real files.
  • Constraints: no exploit payloads — the lookalike file should be inert. Hints: tar -tzf. Concepts: shell globbing, option parsing, --.

Challenge — Detection and prevention pipeline.

  • Objective: make recurrence of these misconfigurations both detectable and blocked.
  • Requirements: (1) add file-integrity monitoring (auditd watch or an AIDE baseline) on /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.
  • Constraints: lab only; log metadata, never file contents or secrets. Defensive conclusion: end with the monitor firing on tampering and staying quiet on an attributed, expected change. Concepts: detection & logging, false positives, secure defaults, verification.

Summary

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.

Practice with these exercises