0

I am trying to create a shutdown command that can be run as a user, i.c., a flask web page that is not running as root. Sounds simple, just put a shutdown command in a SETUID script. Because SETUID does not work on shell scripts, I created an executable from a C program.

The problem is that this does not work on the target machine, a Raspberry Pi Zero W. I tested the same stuff on my Ubuntu 20.4 pc, and there it runs flawlessly. So the method in itself seems correct, but there is a raspberry pi issue.

The Pi runs this OS:

cat /etc/issue
-->
Raspbian GNU/Linux 10 \n \l

This is usershutdown.c :

#include <stdio.h>
#include <stdlib.h>

int main(){
   system("/sbin/poweroff");
}

These are the permissions of the executable:

-rwsr-xr-x 1 root   root   7988 Dec 20 23:59 usershutdown

I checked the mount options of the root disk in /etc/fstab, where I added ,suid and rebooted:

PARTUUID=738a4d67-02  /               ext4    defaults,noatime,suid  0       1

And these are the error messages on the Pi when calling the exec as intended:

$ ./usershutdown 
Failed to set wall message, ignoring: Interactive authentication required.
Failed to power off system via logind: Interactive authentication required.
Failed to open initctl fifo: Permission denied
Failed to talk to init daemon.
$

This is what does work on the Pi, when calling the exec as root/sudo, the ssh connection to it is closed and the device shuts down without error:

$ sudo ./usershutdown 
$ Connection to picamhq closed by remote host.
Connection to picamhq closed.
$ 

How do I fix this?

Roland
  • 4,619
  • 7
  • 49
  • 81
  • I don't want to test it right now, but I seem to recall the shell contains code to notice when its real and effective user IDs are different (as they are for a suid program, since that only sets the effective uid), and drop privileges if so. This is to make it a little harder for 31337 d00dZ to h4x0r your b0x0r. If you insist, run `poweroff` with `exec*()` instead of `system()`. – Nate Eldredge Dec 21 '20 at 02:57
  • But it would be much better to just use sudo instead, where you can impose proper restrictions on who can do it, and avoid many common suid vulnerabilities. – Nate Eldredge Dec 21 '20 at 02:58
  • Trying to run any shell (which `system()` does) with elevated privileges is asking for trouble; there are just too many ways for the user to convince it to do things that you didn't want. For instance, you might like to look up the various abuses of the `IFS` variable. – Nate Eldredge Dec 21 '20 at 03:03
  • 1
    They can't do anything else **if** your suid program is secure and free of bugs. But in general this is hard to accomplish, especially if you are not an expert. I mean, if the shell didn't drop privileges as I mentioned, your first attempt with `system()` would already have created a local root vulnerability, maybe remote root depending on what your flask app could be persuaded to do. So I'm hesitant to go on the record recommending this. – Nate Eldredge Dec 21 '20 at 15:34
  • But what's wrong with sudo? With an appropriate entry in the sudoers file, you can arrange that whatever user your flask app runs as can run `sudo poweroff` without a password, without giving it the ability to run any other commands. – Nate Eldredge Dec 21 '20 at 15:39
  • 1
    You can use the `NOPASSWD` option in `/etc/sudoers` to have `sudo` allow specified users to execute specified commands without giving a password. That's what I've been trying to say. No need to store a password anywhere. Your suid program may be only one line, but already you're allowing every local user on the system to run `poweroff`, and for instance they can pass it arbitrary environment variables, which for some commands can be dangerous. `sudo` will clean the environment and take many other precautions that you might not think of. – Nate Eldredge Dec 21 '20 at 18:25
  • `sudo` configuration is probably more on topic for Unix.SE than here, however. – Nate Eldredge Dec 21 '20 at 18:25

1 Answers1

1

system() runs the program via a shell, and running a shell from any setuid program is extremely dangerous from a security perspective; there are lots of ways a user can hijack it to do other things (look up ways of abusing IFS, for instance). To mitigate this, commonly used shells will try to notice when they are being run under setuid (by noticing that the real and effective uids are different) and drop privileges.

So a safer alternative is to run poweroff via execle instead. (execl will also work, but it is risky to pass through environment variables that you don't control, so better to have your program set an environment that's known to be safe.)

You still have a problem in that your setuid program is runnable by anyone, so any unprivileged local user on the server would be able to shut it down. You'll want to make sure that it can only be executed by the user your app runs as. Since the owner of the file has to be root for setuid to work, this will require some juggling with groups.

You can avoid all this hassle and risk by instead setting up sudo to allow your app's user to run the poweroff command without a password, using the NOPASSWD directive in /etc/sudoers. This will:

  • restrict it to the desired user only

  • clean the environment

  • avoid the risks inherent to having an extra setuid program around that you have to maintain (e.g. future libc bugs, or you get tempted to add features, etc)

See https://unix.stackexchange.com/questions/18830/how-to-run-a-specific-program-as-root-without-a-password-prompt for more.

Nate Eldredge
  • 48,811
  • 6
  • 54
  • 82
  • Execl fixed my original problem, but sudo with NOPASSWD is even simpler, and more secure. Nate thanks for steering me to a better solution! – Roland Dec 21 '20 at 19:32