Why is SUID disabled for shell scripts but not for binaries?

The name of the pictureThe name of the pictureThe name of the pictureClash Royale CLAN TAG#URR8PPP

.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty{ margin-bottom:0;
}

up vote
34
down vote

favorite

13

While I understand the idea of SUID is to let an unprivileged user run a program as a privileged user, I have found that SUID usually doesn’t work on a shell script without some workarounds. My question is, I don’t really understand the dichotomy between a shell script and a binary program. It seems that whatever you can do with a shell script, you can also do it with C and compile it into a binary. If SUID is not secure for a shell script, then it’s also not secure for binaries. So why would shell scripts but not binaries be prohibited from using SUID?

share|improve this question

  • BTW. How does this work for binfmt_misc?
    – el.pescado
    Sep 21 at 9:49

  • 1

    @el.pescado See manpages.debian.org/jessie/binfmt-support/… (^F setuid).
    – forest
    Sep 21 at 10:03

up vote
34
down vote

favorite

13

While I understand the idea of SUID is to let an unprivileged user run a program as a privileged user, I have found that SUID usually doesn’t work on a shell script without some workarounds. My question is, I don’t really understand the dichotomy between a shell script and a binary program. It seems that whatever you can do with a shell script, you can also do it with C and compile it into a binary. If SUID is not secure for a shell script, then it’s also not secure for binaries. So why would shell scripts but not binaries be prohibited from using SUID?

share|improve this question

  • BTW. How does this work for binfmt_misc?
    – el.pescado
    Sep 21 at 9:49

  • 1

    @el.pescado See manpages.debian.org/jessie/binfmt-support/… (^F setuid).
    – forest
    Sep 21 at 10:03

up vote
34
down vote

favorite

13

up vote
34
down vote

favorite

13
13

While I understand the idea of SUID is to let an unprivileged user run a program as a privileged user, I have found that SUID usually doesn’t work on a shell script without some workarounds. My question is, I don’t really understand the dichotomy between a shell script and a binary program. It seems that whatever you can do with a shell script, you can also do it with C and compile it into a binary. If SUID is not secure for a shell script, then it’s also not secure for binaries. So why would shell scripts but not binaries be prohibited from using SUID?

share|improve this question

While I understand the idea of SUID is to let an unprivileged user run a program as a privileged user, I have found that SUID usually doesn’t work on a shell script without some workarounds. My question is, I don’t really understand the dichotomy between a shell script and a binary program. It seems that whatever you can do with a shell script, you can also do it with C and compile it into a binary. If SUID is not secure for a shell script, then it’s also not secure for binaries. So why would shell scripts but not binaries be prohibited from using SUID?

linux privilege-escalation

share|improve this question

share|improve this question

share|improve this question

share|improve this question

asked Sep 20 at 18:13

Cyker

853713

853713

  • BTW. How does this work for binfmt_misc?
    – el.pescado
    Sep 21 at 9:49

  • 1

    @el.pescado See manpages.debian.org/jessie/binfmt-support/… (^F setuid).
    – forest
    Sep 21 at 10:03

  • BTW. How does this work for binfmt_misc?
    – el.pescado
    Sep 21 at 9:49

  • 1

    @el.pescado See manpages.debian.org/jessie/binfmt-support/… (^F setuid).
    – forest
    Sep 21 at 10:03

BTW. How does this work for binfmt_misc?
– el.pescado
Sep 21 at 9:49

BTW. How does this work for binfmt_misc?
– el.pescado
Sep 21 at 9:49

1

1

@el.pescado See manpages.debian.org/jessie/binfmt-support/… (^F setuid).
– forest
Sep 21 at 10:03

@el.pescado See manpages.debian.org/jessie/binfmt-support/… (^F setuid).
– forest
Sep 21 at 10:03

2 Answers
2

active

oldest

votes

up vote
58
down vote

accepted

There is a race condition inherent to the way shebang (#!) is typically implemented:

  1. The kernel opens the executable, and finds that it starts with #!.
  2. The kernel closes the executable and opens the interpreter instead.
  3. The kernel inserts the path to the script to the argument list (as argv[1]), and executes the interpreter.

If setuid scripts are allowed with this implementation, an attacker can invoke an arbitrary script by creating a symbolic link to an existing setuid script, executing it, and arranging to change the link after the kernel has performed step 1 and before the interpreter gets around to opening its first argument. For this reason, all modern unices ignore the setuid bit when they detect a shebang.

One way to secure this implementation would be for the kernel to lock the script file until the interpreter has opened it (note that this must prevent not only unlinking or overwriting the file, but also renaming any directory in the path). But unix systems tend to shy away from mandatory locks, and symbolic links would make a correct lock feature especially difficult and invasive. I don’t think anyone does it this way.

A few unix systems implement secure setuid shebang using an additional feature: the path /dev/fd/N refers to the file already opened on file descriptor N (so opening /dev/fd/N is roughly equivalent to dup(N)).

  1. The kernel opens the executable, and finds that it starts with #!. Let’s say the file descriptor for the executable is 3.
  2. The kernel opens the interpreter.
  3. The kernel inserts /dev/fd/3 the argument list (as argv[1]), and executes the interpreter.

All modern unix variants including Linux implement /dev/fd, but most do not allow setuid scripts. OpenBSD, NetBSD and Mac OS X support it if you enable a non-default kernel setting. On Linux, people have written patches to allow it but those patches never got merged. Sven Mascheck’s shebang page has a lot of information on shebang across unices, including setuid support.


In addition, programs running with elevated privileges have inherent risks that are typically harder to control in higher-level programming languages unless the interpreter was specifically designed for it. The reason is that the programming language runtime’s initialization code may perform actions with elevated privileges, based on data that’s inherited from the lower-privilege caller, before the program’s own code has had the opportunity to sanitize this data. The C runtime does very little for the programmer, so C programs have a better opportunity to take control and sanitize data before anything bad can happen.

Let’s assume you’ve managed to make your program run as root, either because your OS supports setuid shebang or because you’ve used a native binary wrapper (such as sudo). Have you opened a security hole? Maybe. The issue here is not about interpreted vs compiled programs. The issue is whether your runtime system behaves safely if executed with privileges.

  • Any dynamically linked native binary executable is in a way interpreted by the dynamic loader (e.g. /lib/ld.so), which loads the dynamic libraries required by the program. On many unices, you can configure the search path for dynamic libraries through the environment (LD_LIBRARY_PATH is a common name for the environment variable), and even load additional libraries into all executed binaries (LD_PRELOAD). The invoker of the program can execute arbitrary code in that program’s context by placing a specially-crafted libc.so in $LD_LIBRARY_PATH (amongst other tactics). All sane systems ignore the LD_* variables in setuid executables.

  • In shells such as sh, csh and derivatives, environment variables automatically become shell parameters. Through parameters such as PATH, IFS, and many more, the invoker of the script has many opportunities to execute arbitrary code in the shell scripts’s context. Some shells set these variables to sane defaults if they detect that the script has been invoked with privileges, but I don’t know that there is any particular implementation that I would trust.

  • Most runtime environments (whether native, bytecode or interpreted) have similar features. Few take special precautions in setuid executables, though the ones that run native code often don’t do anything fancier than dynamic linking (which does take precautions).

  • Perl is a notable exception. It explicitly supports setuid scripts in a secure way. In fact, your script can run setuid even if your OS ignored the setuid bit on scripts. This is because perl ships with a setuid root helper that performs the necessary checks and reinvokes the interpreter on the desired scripts with the desired privileges. This is explained in the perlsec manual. It used to be that setuid perl scripts needed #!/usr/bin/suidperl -wT instead of #!/usr/bin/perl -wT, but on most modern systems, #!/usr/bin/perl -wT is sufficient.

Note that using a native binary wrapper does nothing in itself to prevent these problems. In fact, it can make the situation worse, because it might prevent your runtime environment from detecting that it is invoked with privileges and bypassing its runtime configurability.

A native binary wrapper can make a shell script safe if the wrapper sanitizes the environment. The script must take care not to make too many assumptions (e.g. about the current directory) but this goes. You can use sudo for this provided that it’s set up to sanitize the environment. Blacklisting variables is error-prone, so always whitelist. With sudo, make sure that the env_reset option is turned on, that setenv is off, and that env_file and env_keep only contain innocuous variables.

All these considerations apply equally for any privilege elevation: setuid, setgid, setcap.

Recycled from https://unix.stackexchange.com/questions/364/allow-setuid-on-shell-scripts/2910#2910

share|improve this answer

  • Was going to say (though you touch on this towards the end) — I’ve made a habit of binary wrappers in security-sensitive environments the past, but only when using execve() to pass an explicit environment (and called the script an a fully-qualified, hardcoded path not writable by unprivileged users). Using a non-*e exec* call is trouble, but calls that replace the environment very much do exist.
    – Charles Duffy
    Sep 21 at 15:59

  • I have to admit I’m disappointed that #!/usr/bin/suidperl isn’t required as a means of preventing accidentally setting the suid bit on a script not designed to have it.
    – Joshua
    Sep 21 at 18:21

  • 1

    Note that the GNU dynamic linker at least not only ignores the LD_* variables but also unsets them, which means that even if you do a setuid(geteuid()), other programs that you execute from your setuid program/script will not be affected by those.
    – Stéphane Chazelas
    Sep 22 at 12:32

  • 1

    May be worth pointing out that the environment variables are not the only things that are passed along/preserved across execve() (which brings the privilege escalation). There’s also signal disposition (see what happens when you ignore SIGPIPE or SICHLD for instance), open (and closed like for 0,1,2) fds, controlling terminal, cwd, limits (lowering many of those can trip many software), umask… You want to minimize the code that runs with elevated privilege and write it very carefully. Running a whole shell and whole commands within the script is the last thing you want to do.
    – Stéphane Chazelas
    Sep 22 at 12:40

up vote
14
down vote

Primarily because

Many kernels suffer from a race condition which can allow you to
exchange the shellscript for another executable of your choice between
the times that the newly exec()ed process goes setuid, and when the
command interpreter gets started up. If you are persistent enough, in
theory you could get the kernel to run any program you want.

as well as other reasons found at that link (but the kernel race condition being the most pernicious). Scripts, of course, load differently than binary programs, which is where the fault creeps in.

You might be amused to read this 2001 Dr. Dobb’s article – which goes through 6 steps towards writing more secure SUID shell scripts, only to reach step 7:

Lesson Seven — Don’t use SUID shell scripts.

Even after all our work, it is nearly impossible to create safe SUID
shell scripts. (It is impossible on most systems.) Because of these
problems, some systems (e.g., Linux) won’t honor SUID on shell
scripts.

This history talks about which variants had fixes for the race condition; the list is larger than you’d think… but setuid scripts are still largely discouraged because of other problems or because it’s easier to discourage them than to remember whether you’re running on a safe or unsafe variant.

This was a big enough problem, back in the day, that it’s instructive to read about how aggressively Perl approached compensating for it.

share|improve this answer

    Your Answer

    StackExchange.ready(function() {
    var channelOptions = {
    tags: “”.split(” “),
    id: “162”
    };
    initTagRenderer(“”.split(” “), “”.split(” “), channelOptions);

    StackExchange.using(“externalEditor”, function() {
    // Have to fire editor after snippets, if snippets enabled
    if (StackExchange.settings.snippets.snippetsEnabled) {
    StackExchange.using(“snippets”, function() {
    createEditor();
    });
    }
    else {
    createEditor();
    }
    });

    function createEditor() {
    StackExchange.prepareEditor({
    heartbeatType: ‘answer’,
    convertImagesToLinks: false,
    noModals: false,
    showLowRepImageUploadWarning: true,
    reputationToPostImages: null,
    bindNavPrevention: true,
    postfix: “”,
    noCode: true, onDemand: true,
    discardSelector: “.discard-answer”
    ,immediatelyShowMarkdownHelp:true
    });

    }
    });

     
    draft saved
    draft discarded

    StackExchange.ready(
    function () {
    StackExchange.openid.initPostLogin(‘.new-post-login’, ‘https%3a%2f%2fsecurity.stackexchange.com%2fquestions%2f194166%2fwhy-is-suid-disabled-for-shell-scripts-but-not-for-binaries%23new-answer’, ‘question_page’);
    }
    );

    Post as a guest

    2 Answers
    2

    active

    oldest

    votes

    2 Answers
    2

    active

    oldest

    votes

    active

    oldest

    votes

    active

    oldest

    votes

    up vote
    58
    down vote

    accepted

    There is a race condition inherent to the way shebang (#!) is typically implemented:

    1. The kernel opens the executable, and finds that it starts with #!.
    2. The kernel closes the executable and opens the interpreter instead.
    3. The kernel inserts the path to the script to the argument list (as argv[1]), and executes the interpreter.

    If setuid scripts are allowed with this implementation, an attacker can invoke an arbitrary script by creating a symbolic link to an existing setuid script, executing it, and arranging to change the link after the kernel has performed step 1 and before the interpreter gets around to opening its first argument. For this reason, all modern unices ignore the setuid bit when they detect a shebang.

    One way to secure this implementation would be for the kernel to lock the script file until the interpreter has opened it (note that this must prevent not only unlinking or overwriting the file, but also renaming any directory in the path). But unix systems tend to shy away from mandatory locks, and symbolic links would make a correct lock feature especially difficult and invasive. I don’t think anyone does it this way.

    A few unix systems implement secure setuid shebang using an additional feature: the path /dev/fd/N refers to the file already opened on file descriptor N (so opening /dev/fd/N is roughly equivalent to dup(N)).

    1. The kernel opens the executable, and finds that it starts with #!. Let’s say the file descriptor for the executable is 3.
    2. The kernel opens the interpreter.
    3. The kernel inserts /dev/fd/3 the argument list (as argv[1]), and executes the interpreter.

    All modern unix variants including Linux implement /dev/fd, but most do not allow setuid scripts. OpenBSD, NetBSD and Mac OS X support it if you enable a non-default kernel setting. On Linux, people have written patches to allow it but those patches never got merged. Sven Mascheck’s shebang page has a lot of information on shebang across unices, including setuid support.


    In addition, programs running with elevated privileges have inherent risks that are typically harder to control in higher-level programming languages unless the interpreter was specifically designed for it. The reason is that the programming language runtime’s initialization code may perform actions with elevated privileges, based on data that’s inherited from the lower-privilege caller, before the program’s own code has had the opportunity to sanitize this data. The C runtime does very little for the programmer, so C programs have a better opportunity to take control and sanitize data before anything bad can happen.

    Let’s assume you’ve managed to make your program run as root, either because your OS supports setuid shebang or because you’ve used a native binary wrapper (such as sudo). Have you opened a security hole? Maybe. The issue here is not about interpreted vs compiled programs. The issue is whether your runtime system behaves safely if executed with privileges.

    • Any dynamically linked native binary executable is in a way interpreted by the dynamic loader (e.g. /lib/ld.so), which loads the dynamic libraries required by the program. On many unices, you can configure the search path for dynamic libraries through the environment (LD_LIBRARY_PATH is a common name for the environment variable), and even load additional libraries into all executed binaries (LD_PRELOAD). The invoker of the program can execute arbitrary code in that program’s context by placing a specially-crafted libc.so in $LD_LIBRARY_PATH (amongst other tactics). All sane systems ignore the LD_* variables in setuid executables.

    • In shells such as sh, csh and derivatives, environment variables automatically become shell parameters. Through parameters such as PATH, IFS, and many more, the invoker of the script has many opportunities to execute arbitrary code in the shell scripts’s context. Some shells set these variables to sane defaults if they detect that the script has been invoked with privileges, but I don’t know that there is any particular implementation that I would trust.

    • Most runtime environments (whether native, bytecode or interpreted) have similar features. Few take special precautions in setuid executables, though the ones that run native code often don’t do anything fancier than dynamic linking (which does take precautions).

    • Perl is a notable exception. It explicitly supports setuid scripts in a secure way. In fact, your script can run setuid even if your OS ignored the setuid bit on scripts. This is because perl ships with a setuid root helper that performs the necessary checks and reinvokes the interpreter on the desired scripts with the desired privileges. This is explained in the perlsec manual. It used to be that setuid perl scripts needed #!/usr/bin/suidperl -wT instead of #!/usr/bin/perl -wT, but on most modern systems, #!/usr/bin/perl -wT is sufficient.

    Note that using a native binary wrapper does nothing in itself to prevent these problems. In fact, it can make the situation worse, because it might prevent your runtime environment from detecting that it is invoked with privileges and bypassing its runtime configurability.

    A native binary wrapper can make a shell script safe if the wrapper sanitizes the environment. The script must take care not to make too many assumptions (e.g. about the current directory) but this goes. You can use sudo for this provided that it’s set up to sanitize the environment. Blacklisting variables is error-prone, so always whitelist. With sudo, make sure that the env_reset option is turned on, that setenv is off, and that env_file and env_keep only contain innocuous variables.

    All these considerations apply equally for any privilege elevation: setuid, setgid, setcap.

    Recycled from https://unix.stackexchange.com/questions/364/allow-setuid-on-shell-scripts/2910#2910

    share|improve this answer

    • Was going to say (though you touch on this towards the end) — I’ve made a habit of binary wrappers in security-sensitive environments the past, but only when using execve() to pass an explicit environment (and called the script an a fully-qualified, hardcoded path not writable by unprivileged users). Using a non-*e exec* call is trouble, but calls that replace the environment very much do exist.
      – Charles Duffy
      Sep 21 at 15:59

    • I have to admit I’m disappointed that #!/usr/bin/suidperl isn’t required as a means of preventing accidentally setting the suid bit on a script not designed to have it.
      – Joshua
      Sep 21 at 18:21

    • 1

      Note that the GNU dynamic linker at least not only ignores the LD_* variables but also unsets them, which means that even if you do a setuid(geteuid()), other programs that you execute from your setuid program/script will not be affected by those.
      – Stéphane Chazelas
      Sep 22 at 12:32

    • 1

      May be worth pointing out that the environment variables are not the only things that are passed along/preserved across execve() (which brings the privilege escalation). There’s also signal disposition (see what happens when you ignore SIGPIPE or SICHLD for instance), open (and closed like for 0,1,2) fds, controlling terminal, cwd, limits (lowering many of those can trip many software), umask… You want to minimize the code that runs with elevated privilege and write it very carefully. Running a whole shell and whole commands within the script is the last thing you want to do.
      – Stéphane Chazelas
      Sep 22 at 12:40

    up vote
    58
    down vote

    accepted

    There is a race condition inherent to the way shebang (#!) is typically implemented:

    1. The kernel opens the executable, and finds that it starts with #!.
    2. The kernel closes the executable and opens the interpreter instead.
    3. The kernel inserts the path to the script to the argument list (as argv[1]), and executes the interpreter.

    If setuid scripts are allowed with this implementation, an attacker can invoke an arbitrary script by creating a symbolic link to an existing setuid script, executing it, and arranging to change the link after the kernel has performed step 1 and before the interpreter gets around to opening its first argument. For this reason, all modern unices ignore the setuid bit when they detect a shebang.

    One way to secure this implementation would be for the kernel to lock the script file until the interpreter has opened it (note that this must prevent not only unlinking or overwriting the file, but also renaming any directory in the path). But unix systems tend to shy away from mandatory locks, and symbolic links would make a correct lock feature especially difficult and invasive. I don’t think anyone does it this way.

    A few unix systems implement secure setuid shebang using an additional feature: the path /dev/fd/N refers to the file already opened on file descriptor N (so opening /dev/fd/N is roughly equivalent to dup(N)).

    1. The kernel opens the executable, and finds that it starts with #!. Let’s say the file descriptor for the executable is 3.
    2. The kernel opens the interpreter.
    3. The kernel inserts /dev/fd/3 the argument list (as argv[1]), and executes the interpreter.

    All modern unix variants including Linux implement /dev/fd, but most do not allow setuid scripts. OpenBSD, NetBSD and Mac OS X support it if you enable a non-default kernel setting. On Linux, people have written patches to allow it but those patches never got merged. Sven Mascheck’s shebang page has a lot of information on shebang across unices, including setuid support.


    In addition, programs running with elevated privileges have inherent risks that are typically harder to control in higher-level programming languages unless the interpreter was specifically designed for it. The reason is that the programming language runtime’s initialization code may perform actions with elevated privileges, based on data that’s inherited from the lower-privilege caller, before the program’s own code has had the opportunity to sanitize this data. The C runtime does very little for the programmer, so C programs have a better opportunity to take control and sanitize data before anything bad can happen.

    Let’s assume you’ve managed to make your program run as root, either because your OS supports setuid shebang or because you’ve used a native binary wrapper (such as sudo). Have you opened a security hole? Maybe. The issue here is not about interpreted vs compiled programs. The issue is whether your runtime system behaves safely if executed with privileges.

    • Any dynamically linked native binary executable is in a way interpreted by the dynamic loader (e.g. /lib/ld.so), which loads the dynamic libraries required by the program. On many unices, you can configure the search path for dynamic libraries through the environment (LD_LIBRARY_PATH is a common name for the environment variable), and even load additional libraries into all executed binaries (LD_PRELOAD). The invoker of the program can execute arbitrary code in that program’s context by placing a specially-crafted libc.so in $LD_LIBRARY_PATH (amongst other tactics). All sane systems ignore the LD_* variables in setuid executables.

    • In shells such as sh, csh and derivatives, environment variables automatically become shell parameters. Through parameters such as PATH, IFS, and many more, the invoker of the script has many opportunities to execute arbitrary code in the shell scripts’s context. Some shells set these variables to sane defaults if they detect that the script has been invoked with privileges, but I don’t know that there is any particular implementation that I would trust.

    • Most runtime environments (whether native, bytecode or interpreted) have similar features. Few take special precautions in setuid executables, though the ones that run native code often don’t do anything fancier than dynamic linking (which does take precautions).

    • Perl is a notable exception. It explicitly supports setuid scripts in a secure way. In fact, your script can run setuid even if your OS ignored the setuid bit on scripts. This is because perl ships with a setuid root helper that performs the necessary checks and reinvokes the interpreter on the desired scripts with the desired privileges. This is explained in the perlsec manual. It used to be that setuid perl scripts needed #!/usr/bin/suidperl -wT instead of #!/usr/bin/perl -wT, but on most modern systems, #!/usr/bin/perl -wT is sufficient.

    Note that using a native binary wrapper does nothing in itself to prevent these problems. In fact, it can make the situation worse, because it might prevent your runtime environment from detecting that it is invoked with privileges and bypassing its runtime configurability.

    A native binary wrapper can make a shell script safe if the wrapper sanitizes the environment. The script must take care not to make too many assumptions (e.g. about the current directory) but this goes. You can use sudo for this provided that it’s set up to sanitize the environment. Blacklisting variables is error-prone, so always whitelist. With sudo, make sure that the env_reset option is turned on, that setenv is off, and that env_file and env_keep only contain innocuous variables.

    All these considerations apply equally for any privilege elevation: setuid, setgid, setcap.

    Recycled from https://unix.stackexchange.com/questions/364/allow-setuid-on-shell-scripts/2910#2910

    share|improve this answer

    • Was going to say (though you touch on this towards the end) — I’ve made a habit of binary wrappers in security-sensitive environments the past, but only when using execve() to pass an explicit environment (and called the script an a fully-qualified, hardcoded path not writable by unprivileged users). Using a non-*e exec* call is trouble, but calls that replace the environment very much do exist.
      – Charles Duffy
      Sep 21 at 15:59

    • I have to admit I’m disappointed that #!/usr/bin/suidperl isn’t required as a means of preventing accidentally setting the suid bit on a script not designed to have it.
      – Joshua
      Sep 21 at 18:21

    • 1

      Note that the GNU dynamic linker at least not only ignores the LD_* variables but also unsets them, which means that even if you do a setuid(geteuid()), other programs that you execute from your setuid program/script will not be affected by those.
      – Stéphane Chazelas
      Sep 22 at 12:32

    • 1

      May be worth pointing out that the environment variables are not the only things that are passed along/preserved across execve() (which brings the privilege escalation). There’s also signal disposition (see what happens when you ignore SIGPIPE or SICHLD for instance), open (and closed like for 0,1,2) fds, controlling terminal, cwd, limits (lowering many of those can trip many software), umask… You want to minimize the code that runs with elevated privilege and write it very carefully. Running a whole shell and whole commands within the script is the last thing you want to do.
      – Stéphane Chazelas
      Sep 22 at 12:40

    up vote
    58
    down vote

    accepted

    up vote
    58
    down vote

    accepted

    There is a race condition inherent to the way shebang (#!) is typically implemented:

    1. The kernel opens the executable, and finds that it starts with #!.
    2. The kernel closes the executable and opens the interpreter instead.
    3. The kernel inserts the path to the script to the argument list (as argv[1]), and executes the interpreter.

    If setuid scripts are allowed with this implementation, an attacker can invoke an arbitrary script by creating a symbolic link to an existing setuid script, executing it, and arranging to change the link after the kernel has performed step 1 and before the interpreter gets around to opening its first argument. For this reason, all modern unices ignore the setuid bit when they detect a shebang.

    One way to secure this implementation would be for the kernel to lock the script file until the interpreter has opened it (note that this must prevent not only unlinking or overwriting the file, but also renaming any directory in the path). But unix systems tend to shy away from mandatory locks, and symbolic links would make a correct lock feature especially difficult and invasive. I don’t think anyone does it this way.

    A few unix systems implement secure setuid shebang using an additional feature: the path /dev/fd/N refers to the file already opened on file descriptor N (so opening /dev/fd/N is roughly equivalent to dup(N)).

    1. The kernel opens the executable, and finds that it starts with #!. Let’s say the file descriptor for the executable is 3.
    2. The kernel opens the interpreter.
    3. The kernel inserts /dev/fd/3 the argument list (as argv[1]), and executes the interpreter.

    All modern unix variants including Linux implement /dev/fd, but most do not allow setuid scripts. OpenBSD, NetBSD and Mac OS X support it if you enable a non-default kernel setting. On Linux, people have written patches to allow it but those patches never got merged. Sven Mascheck’s shebang page has a lot of information on shebang across unices, including setuid support.


    In addition, programs running with elevated privileges have inherent risks that are typically harder to control in higher-level programming languages unless the interpreter was specifically designed for it. The reason is that the programming language runtime’s initialization code may perform actions with elevated privileges, based on data that’s inherited from the lower-privilege caller, before the program’s own code has had the opportunity to sanitize this data. The C runtime does very little for the programmer, so C programs have a better opportunity to take control and sanitize data before anything bad can happen.

    Let’s assume you’ve managed to make your program run as root, either because your OS supports setuid shebang or because you’ve used a native binary wrapper (such as sudo). Have you opened a security hole? Maybe. The issue here is not about interpreted vs compiled programs. The issue is whether your runtime system behaves safely if executed with privileges.

    • Any dynamically linked native binary executable is in a way interpreted by the dynamic loader (e.g. /lib/ld.so), which loads the dynamic libraries required by the program. On many unices, you can configure the search path for dynamic libraries through the environment (LD_LIBRARY_PATH is a common name for the environment variable), and even load additional libraries into all executed binaries (LD_PRELOAD). The invoker of the program can execute arbitrary code in that program’s context by placing a specially-crafted libc.so in $LD_LIBRARY_PATH (amongst other tactics). All sane systems ignore the LD_* variables in setuid executables.

    • In shells such as sh, csh and derivatives, environment variables automatically become shell parameters. Through parameters such as PATH, IFS, and many more, the invoker of the script has many opportunities to execute arbitrary code in the shell scripts’s context. Some shells set these variables to sane defaults if they detect that the script has been invoked with privileges, but I don’t know that there is any particular implementation that I would trust.

    • Most runtime environments (whether native, bytecode or interpreted) have similar features. Few take special precautions in setuid executables, though the ones that run native code often don’t do anything fancier than dynamic linking (which does take precautions).

    • Perl is a notable exception. It explicitly supports setuid scripts in a secure way. In fact, your script can run setuid even if your OS ignored the setuid bit on scripts. This is because perl ships with a setuid root helper that performs the necessary checks and reinvokes the interpreter on the desired scripts with the desired privileges. This is explained in the perlsec manual. It used to be that setuid perl scripts needed #!/usr/bin/suidperl -wT instead of #!/usr/bin/perl -wT, but on most modern systems, #!/usr/bin/perl -wT is sufficient.

    Note that using a native binary wrapper does nothing in itself to prevent these problems. In fact, it can make the situation worse, because it might prevent your runtime environment from detecting that it is invoked with privileges and bypassing its runtime configurability.

    A native binary wrapper can make a shell script safe if the wrapper sanitizes the environment. The script must take care not to make too many assumptions (e.g. about the current directory) but this goes. You can use sudo for this provided that it’s set up to sanitize the environment. Blacklisting variables is error-prone, so always whitelist. With sudo, make sure that the env_reset option is turned on, that setenv is off, and that env_file and env_keep only contain innocuous variables.

    All these considerations apply equally for any privilege elevation: setuid, setgid, setcap.

    Recycled from https://unix.stackexchange.com/questions/364/allow-setuid-on-shell-scripts/2910#2910

    share|improve this answer

    There is a race condition inherent to the way shebang (#!) is typically implemented:

    1. The kernel opens the executable, and finds that it starts with #!.
    2. The kernel closes the executable and opens the interpreter instead.
    3. The kernel inserts the path to the script to the argument list (as argv[1]), and executes the interpreter.

    If setuid scripts are allowed with this implementation, an attacker can invoke an arbitrary script by creating a symbolic link to an existing setuid script, executing it, and arranging to change the link after the kernel has performed step 1 and before the interpreter gets around to opening its first argument. For this reason, all modern unices ignore the setuid bit when they detect a shebang.

    One way to secure this implementation would be for the kernel to lock the script file until the interpreter has opened it (note that this must prevent not only unlinking or overwriting the file, but also renaming any directory in the path). But unix systems tend to shy away from mandatory locks, and symbolic links would make a correct lock feature especially difficult and invasive. I don’t think anyone does it this way.

    A few unix systems implement secure setuid shebang using an additional feature: the path /dev/fd/N refers to the file already opened on file descriptor N (so opening /dev/fd/N is roughly equivalent to dup(N)).

    1. The kernel opens the executable, and finds that it starts with #!. Let’s say the file descriptor for the executable is 3.
    2. The kernel opens the interpreter.
    3. The kernel inserts /dev/fd/3 the argument list (as argv[1]), and executes the interpreter.

    All modern unix variants including Linux implement /dev/fd, but most do not allow setuid scripts. OpenBSD, NetBSD and Mac OS X support it if you enable a non-default kernel setting. On Linux, people have written patches to allow it but those patches never got merged. Sven Mascheck’s shebang page has a lot of information on shebang across unices, including setuid support.


    In addition, programs running with elevated privileges have inherent risks that are typically harder to control in higher-level programming languages unless the interpreter was specifically designed for it. The reason is that the programming language runtime’s initialization code may perform actions with elevated privileges, based on data that’s inherited from the lower-privilege caller, before the program’s own code has had the opportunity to sanitize this data. The C runtime does very little for the programmer, so C programs have a better opportunity to take control and sanitize data before anything bad can happen.

    Let’s assume you’ve managed to make your program run as root, either because your OS supports setuid shebang or because you’ve used a native binary wrapper (such as sudo). Have you opened a security hole? Maybe. The issue here is not about interpreted vs compiled programs. The issue is whether your runtime system behaves safely if executed with privileges.

    • Any dynamically linked native binary executable is in a way interpreted by the dynamic loader (e.g. /lib/ld.so), which loads the dynamic libraries required by the program. On many unices, you can configure the search path for dynamic libraries through the environment (LD_LIBRARY_PATH is a common name for the environment variable), and even load additional libraries into all executed binaries (LD_PRELOAD). The invoker of the program can execute arbitrary code in that program’s context by placing a specially-crafted libc.so in $LD_LIBRARY_PATH (amongst other tactics). All sane systems ignore the LD_* variables in setuid executables.

    • In shells such as sh, csh and derivatives, environment variables automatically become shell parameters. Through parameters such as PATH, IFS, and many more, the invoker of the script has many opportunities to execute arbitrary code in the shell scripts’s context. Some shells set these variables to sane defaults if they detect that the script has been invoked with privileges, but I don’t know that there is any particular implementation that I would trust.

    • Most runtime environments (whether native, bytecode or interpreted) have similar features. Few take special precautions in setuid executables, though the ones that run native code often don’t do anything fancier than dynamic linking (which does take precautions).

    • Perl is a notable exception. It explicitly supports setuid scripts in a secure way. In fact, your script can run setuid even if your OS ignored the setuid bit on scripts. This is because perl ships with a setuid root helper that performs the necessary checks and reinvokes the interpreter on the desired scripts with the desired privileges. This is explained in the perlsec manual. It used to be that setuid perl scripts needed #!/usr/bin/suidperl -wT instead of #!/usr/bin/perl -wT, but on most modern systems, #!/usr/bin/perl -wT is sufficient.

    Note that using a native binary wrapper does nothing in itself to prevent these problems. In fact, it can make the situation worse, because it might prevent your runtime environment from detecting that it is invoked with privileges and bypassing its runtime configurability.

    A native binary wrapper can make a shell script safe if the wrapper sanitizes the environment. The script must take care not to make too many assumptions (e.g. about the current directory) but this goes. You can use sudo for this provided that it’s set up to sanitize the environment. Blacklisting variables is error-prone, so always whitelist. With sudo, make sure that the env_reset option is turned on, that setenv is off, and that env_file and env_keep only contain innocuous variables.

    All these considerations apply equally for any privilege elevation: setuid, setgid, setcap.

    Recycled from https://unix.stackexchange.com/questions/364/allow-setuid-on-shell-scripts/2910#2910

    share|improve this answer

    share|improve this answer

    share|improve this answer

    answered Sep 20 at 20:10

    Gilles

    37.4k1090144

    37.4k1090144

    • Was going to say (though you touch on this towards the end) — I’ve made a habit of binary wrappers in security-sensitive environments the past, but only when using execve() to pass an explicit environment (and called the script an a fully-qualified, hardcoded path not writable by unprivileged users). Using a non-*e exec* call is trouble, but calls that replace the environment very much do exist.
      – Charles Duffy
      Sep 21 at 15:59

    • I have to admit I’m disappointed that #!/usr/bin/suidperl isn’t required as a means of preventing accidentally setting the suid bit on a script not designed to have it.
      – Joshua
      Sep 21 at 18:21

    • 1

      Note that the GNU dynamic linker at least not only ignores the LD_* variables but also unsets them, which means that even if you do a setuid(geteuid()), other programs that you execute from your setuid program/script will not be affected by those.
      – Stéphane Chazelas
      Sep 22 at 12:32

    • 1

      May be worth pointing out that the environment variables are not the only things that are passed along/preserved across execve() (which brings the privilege escalation). There’s also signal disposition (see what happens when you ignore SIGPIPE or SICHLD for instance), open (and closed like for 0,1,2) fds, controlling terminal, cwd, limits (lowering many of those can trip many software), umask… You want to minimize the code that runs with elevated privilege and write it very carefully. Running a whole shell and whole commands within the script is the last thing you want to do.
      – Stéphane Chazelas
      Sep 22 at 12:40

    • Was going to say (though you touch on this towards the end) — I’ve made a habit of binary wrappers in security-sensitive environments the past, but only when using execve() to pass an explicit environment (and called the script an a fully-qualified, hardcoded path not writable by unprivileged users). Using a non-*e exec* call is trouble, but calls that replace the environment very much do exist.
      – Charles Duffy
      Sep 21 at 15:59

    • I have to admit I’m disappointed that #!/usr/bin/suidperl isn’t required as a means of preventing accidentally setting the suid bit on a script not designed to have it.
      – Joshua
      Sep 21 at 18:21

    • 1

      Note that the GNU dynamic linker at least not only ignores the LD_* variables but also unsets them, which means that even if you do a setuid(geteuid()), other programs that you execute from your setuid program/script will not be affected by those.
      – Stéphane Chazelas
      Sep 22 at 12:32

    • 1

      May be worth pointing out that the environment variables are not the only things that are passed along/preserved across execve() (which brings the privilege escalation). There’s also signal disposition (see what happens when you ignore SIGPIPE or SICHLD for instance), open (and closed like for 0,1,2) fds, controlling terminal, cwd, limits (lowering many of those can trip many software), umask… You want to minimize the code that runs with elevated privilege and write it very carefully. Running a whole shell and whole commands within the script is the last thing you want to do.
      – Stéphane Chazelas
      Sep 22 at 12:40

    Was going to say (though you touch on this towards the end) — I’ve made a habit of binary wrappers in security-sensitive environments the past, but only when using execve() to pass an explicit environment (and called the script an a fully-qualified, hardcoded path not writable by unprivileged users). Using a non-*e exec* call is trouble, but calls that replace the environment very much do exist.
    – Charles Duffy
    Sep 21 at 15:59

    Was going to say (though you touch on this towards the end) — I’ve made a habit of binary wrappers in security-sensitive environments the past, but only when using execve() to pass an explicit environment (and called the script an a fully-qualified, hardcoded path not writable by unprivileged users). Using a non-*e exec* call is trouble, but calls that replace the environment very much do exist.
    – Charles Duffy
    Sep 21 at 15:59

    I have to admit I’m disappointed that #!/usr/bin/suidperl isn’t required as a means of preventing accidentally setting the suid bit on a script not designed to have it.
    – Joshua
    Sep 21 at 18:21

    I have to admit I’m disappointed that #!/usr/bin/suidperl isn’t required as a means of preventing accidentally setting the suid bit on a script not designed to have it.
    – Joshua
    Sep 21 at 18:21

    1

    1

    Note that the GNU dynamic linker at least not only ignores the LD_* variables but also unsets them, which means that even if you do a setuid(geteuid()), other programs that you execute from your setuid program/script will not be affected by those.
    – Stéphane Chazelas
    Sep 22 at 12:32

    Note that the GNU dynamic linker at least not only ignores the LD_* variables but also unsets them, which means that even if you do a setuid(geteuid()), other programs that you execute from your setuid program/script will not be affected by those.
    – Stéphane Chazelas
    Sep 22 at 12:32

    1

    1

    May be worth pointing out that the environment variables are not the only things that are passed along/preserved across execve() (which brings the privilege escalation). There’s also signal disposition (see what happens when you ignore SIGPIPE or SICHLD for instance), open (and closed like for 0,1,2) fds, controlling terminal, cwd, limits (lowering many of those can trip many software), umask… You want to minimize the code that runs with elevated privilege and write it very carefully. Running a whole shell and whole commands within the script is the last thing you want to do.
    – Stéphane Chazelas
    Sep 22 at 12:40

    May be worth pointing out that the environment variables are not the only things that are passed along/preserved across execve() (which brings the privilege escalation). There’s also signal disposition (see what happens when you ignore SIGPIPE or SICHLD for instance), open (and closed like for 0,1,2) fds, controlling terminal, cwd, limits (lowering many of those can trip many software), umask… You want to minimize the code that runs with elevated privilege and write it very carefully. Running a whole shell and whole commands within the script is the last thing you want to do.
    – Stéphane Chazelas
    Sep 22 at 12:40

    up vote
    14
    down vote

    Primarily because

    Many kernels suffer from a race condition which can allow you to
    exchange the shellscript for another executable of your choice between
    the times that the newly exec()ed process goes setuid, and when the
    command interpreter gets started up. If you are persistent enough, in
    theory you could get the kernel to run any program you want.

    as well as other reasons found at that link (but the kernel race condition being the most pernicious). Scripts, of course, load differently than binary programs, which is where the fault creeps in.

    You might be amused to read this 2001 Dr. Dobb’s article – which goes through 6 steps towards writing more secure SUID shell scripts, only to reach step 7:

    Lesson Seven — Don’t use SUID shell scripts.

    Even after all our work, it is nearly impossible to create safe SUID
    shell scripts. (It is impossible on most systems.) Because of these
    problems, some systems (e.g., Linux) won’t honor SUID on shell
    scripts.

    This history talks about which variants had fixes for the race condition; the list is larger than you’d think… but setuid scripts are still largely discouraged because of other problems or because it’s easier to discourage them than to remember whether you’re running on a safe or unsafe variant.

    This was a big enough problem, back in the day, that it’s instructive to read about how aggressively Perl approached compensating for it.

    share|improve this answer

      up vote
      14
      down vote

      Primarily because

      Many kernels suffer from a race condition which can allow you to
      exchange the shellscript for another executable of your choice between
      the times that the newly exec()ed process goes setuid, and when the
      command interpreter gets started up. If you are persistent enough, in
      theory you could get the kernel to run any program you want.

      as well as other reasons found at that link (but the kernel race condition being the most pernicious). Scripts, of course, load differently than binary programs, which is where the fault creeps in.

      You might be amused to read this 2001 Dr. Dobb’s article – which goes through 6 steps towards writing more secure SUID shell scripts, only to reach step 7:

      Lesson Seven — Don’t use SUID shell scripts.

      Even after all our work, it is nearly impossible to create safe SUID
      shell scripts. (It is impossible on most systems.) Because of these
      problems, some systems (e.g., Linux) won’t honor SUID on shell
      scripts.

      This history talks about which variants had fixes for the race condition; the list is larger than you’d think… but setuid scripts are still largely discouraged because of other problems or because it’s easier to discourage them than to remember whether you’re running on a safe or unsafe variant.

      This was a big enough problem, back in the day, that it’s instructive to read about how aggressively Perl approached compensating for it.

      share|improve this answer

        up vote
        14
        down vote

        up vote
        14
        down vote

        Primarily because

        Many kernels suffer from a race condition which can allow you to
        exchange the shellscript for another executable of your choice between
        the times that the newly exec()ed process goes setuid, and when the
        command interpreter gets started up. If you are persistent enough, in
        theory you could get the kernel to run any program you want.

        as well as other reasons found at that link (but the kernel race condition being the most pernicious). Scripts, of course, load differently than binary programs, which is where the fault creeps in.

        You might be amused to read this 2001 Dr. Dobb’s article – which goes through 6 steps towards writing more secure SUID shell scripts, only to reach step 7:

        Lesson Seven — Don’t use SUID shell scripts.

        Even after all our work, it is nearly impossible to create safe SUID
        shell scripts. (It is impossible on most systems.) Because of these
        problems, some systems (e.g., Linux) won’t honor SUID on shell
        scripts.

        This history talks about which variants had fixes for the race condition; the list is larger than you’d think… but setuid scripts are still largely discouraged because of other problems or because it’s easier to discourage them than to remember whether you’re running on a safe or unsafe variant.

        This was a big enough problem, back in the day, that it’s instructive to read about how aggressively Perl approached compensating for it.

        share|improve this answer

        Primarily because

        Many kernels suffer from a race condition which can allow you to
        exchange the shellscript for another executable of your choice between
        the times that the newly exec()ed process goes setuid, and when the
        command interpreter gets started up. If you are persistent enough, in
        theory you could get the kernel to run any program you want.

        as well as other reasons found at that link (but the kernel race condition being the most pernicious). Scripts, of course, load differently than binary programs, which is where the fault creeps in.

        You might be amused to read this 2001 Dr. Dobb’s article – which goes through 6 steps towards writing more secure SUID shell scripts, only to reach step 7:

        Lesson Seven — Don’t use SUID shell scripts.

        Even after all our work, it is nearly impossible to create safe SUID
        shell scripts. (It is impossible on most systems.) Because of these
        problems, some systems (e.g., Linux) won’t honor SUID on shell
        scripts.

        This history talks about which variants had fixes for the race condition; the list is larger than you’d think… but setuid scripts are still largely discouraged because of other problems or because it’s easier to discourage them than to remember whether you’re running on a safe or unsafe variant.

        This was a big enough problem, back in the day, that it’s instructive to read about how aggressively Perl approached compensating for it.

        share|improve this answer

        share|improve this answer

        share|improve this answer

        edited Sep 20 at 20:01

        answered Sep 20 at 19:43

        gowenfawr

        50.4k10107153

        50.4k10107153

             
            draft saved
            draft discarded

             

            draft saved

            draft discarded

            StackExchange.ready(
            function () {
            StackExchange.openid.initPostLogin(‘.new-post-login’, ‘https%3a%2f%2fsecurity.stackexchange.com%2fquestions%2f194166%2fwhy-is-suid-disabled-for-shell-scripts-but-not-for-binaries%23new-answer’, ‘question_page’);
            }
            );

            Post as a guest

            Related Post

            Leave a Reply

            Your email address will not be published. Required fields are marked *