2

I'm trying to write a program that will quickly shut down a laptop without prompting for a password or confirmation. The laptop is running Linux, specifically Manjaro.

To that end, I'm trying to make a setuid executable called downnow that executes shutdown with fixed arguments (and also spawns a shell). I'm only using system here to keep the program short, it's a terrible idea from a security standpoint.

// downnow.c
#include <stdlib.h>

int main() {
  system("shutdown --no-wall --halt now");
  return 0;
}

then I compiled downnow, moved it to /bin, changed its owner and gave it setuid and setgid permissions:

$ sudo chown root /bin/downnow 

$ sudo chgrp root /bin/downnow 

$ sudo chmod u+s /bin/downnow 

$ sudo chmod g+s /bin/downnow 

However, when I attempt to execute downnow as an unprivileged user, it can't communicate with systemd.

$ downnow 
Failed to halt system via logind: Interactive authentication required.
Failed to talk to init daemon.

I get the same message before and after ch{own,grp,mod}ing.

stat reports the same permissions on /bin/downnow and /usr/bin/sudo.

$ stat /bin/downnow 
  File: /bin/downnow
  Size: XXXX        Blocks: XX         IO Block: XXXX   regular file
Device: XXXXX/XXXXX Inode: XXXXXX     Links: 1
Access: (6755/-rwsr-sr-x)  Uid: (    0/    root)   Gid: (    0/    root)

$ stat /usr/bin/sudo
  File: /usr/bin/sudo
  Size: XXXXXX      Blocks: XXX        IO Block: XXXX   regular file
Device: XXXXX/XXXXX Inode: XXXXXXX     Links: 1
Access: (4755/-rwsr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root)

Why might downnow fail to elevate its privileges when sudo works perfectly?

1

Probably due to a lack of a setuid(2) call. Here's a before and after around such a call:

#include <err.h>
#include <stdio.h>
#include <unistd.h>

#define GETUIDS(p) (p[0]=getuid(),p[1]=geteuid())

int main(void)
{
    uid_t ids[2];
    GETUIDS(ids);
    fprintf(stderr, "real %lu effective %lu\n", (unsigned long) ids[0],
            (unsigned long) ids[1]);
    if (setuid(0) == -1)
        err(1, "setuid failed");
    GETUIDS(ids);
    fprintf(stderr, "real %lu effective %lu\n", (unsigned long) ids[0],
            (unsigned long) ids[1]);
    return 0;
}

Also your wrapper is not really safe; hopefully it will not escape to multi-user systems. It would be likely much safer to avoid the needless shell call (unless you like shellshock type vulnerabilities or the strange handling of duplicate environment variables by, say, bash...) and instead use an exec(3) call to replace your process with shutdown directly:

#include <err.h>
#include <unistd.h>

int main()
{
    if (setuid(0) == -1)
        err(1, "setuid failed");
    //execl("/usr/bin/echo", "echo", "--no-wall", "--halt", "now",
    execl("/usr/sbin/shutdown", "shutdown", "--no-wall", "--halt", "now",
          (char *) 0);
    err(1, "exec failed");
    return 1;
}

A few accidental shutdowns later...

centos7# make badcode
cc     badcode.c   -o badcode
centos7# mv badcode /badcode
centos7# chmod u+s /badcode
centos7# su - jhqdoe
Last login: Thu Jan 10 01:39:03 UTC 2019 on pts/0
[jhqdoe@centos7 ~]$ /badcode
Connection to 192.168.99.2 closed by remote host.
Connection to 192.168.99.2 closed.
  • The part that everyone is missing is that Interactive authentication required. results from the PolicyKit rules processing, which does not happen in-process and which tests the effective UID not the real UID. – JdeBP Jan 10 at 12:18
  • @JdeBP huh? maybe post how you think policykit is involved in an answer? where did you pull Interactive authentication required from? – thrig Jan 10 at 14:40
  • From the question. – JdeBP Jan 11 at 9:22
  • @JdeBP yes, and? PolicyKit intrudes without the necessary setuid call, and does not with the setuid call and appropriate ownership and u+s bit set, as a trivial test on, say, Centos 7 will show. – thrig Jan 11 at 14:57
2

Setting the permission bit only allows your application to use the setuid call and doesn't, by itself, change your permissions. To do that, you need to set the uid with setuid(uid_t uid). See the man page for details: https://linux.die.net/man/2/setuid

You can use geteuid() to get the current effective uid (i.e. the owner of the file with the setuid permission). See man page: https://linux.die.net/man/2/geteuid

Example:

// downnow.c
#include <stdlib.h>
#include <unistd.h>

int main() {
  setuid(geteuid());
  system("shutdown --no-wall --halt now");
  return 0;
}
  • That explanation will probably just confuse people. The set-ID permissions very much do change one's credentials. Indeed, you are copying those very changed effective credentials onto the real ones. – JdeBP Jan 10 at 11:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.