1

I'd like to have an hg hook that sends email using a gmail account. Obviously I don't want anyone to be able read the email-sending script except me or root, since it has a password in, so here's what I've tried:

-rwsr-xr-x  1 james james   58 Feb 18 12:05 incoming.email.sh
-rwx--x--x  1 james james  262 Feb 18 12:04 send-incoming-email.sh

where incoming.email.sh is the file executed as the hook:

#! /bin/bash
/path/to/send-incoming-email.sh

However, when I try to run as another user I get the error:

/bin/bash: /path/to/send-incoming-email.sh: Permission denied

The send-incoming-email.sh file works fine when I run as myself.

Is what I'm trying to do possible, or will setuid not propagate to commands executed from a shell script?

System is Ubuntu 10.04.2 LTS.

James
  • 290

3 Answers3

3

If you need your solution to work as is, a simple hack would be to use a short C program instead of a shell script:

int main(){
setuid(geteuid());
system("/path/to/send-incoming-email.sh");
}

And have that setuid, thus avoiding the race condition, and at the same time allowing you to pass off execution of the script as root.

This isn't the best solution, by far, but it will solve the problem as described.

2

Linux will ignore the setuid bit for shell scripts to avoid possible race-conditions.


The "proper" way of sending email on Unix/Linux systems is to configure a MTA such as Postfix, Exim4 or Sendmail and let it handle the SMTP authentication mess. There also are "relay-only" MTAs - esmtp, msmtp, ssmtp. All of these can do SMTP relaying ("smarthost") with authentication, for example, through Gmail servers. It becomes trickier on a multi-user machine, but still doable.

(When a MTA is configured, sending an email is done by passing the data to /usr/sbin/sendmail rcpt@address.)

grawity
  • 501,077
0

Almost all systems ignore the setuid and setgid bits on scripts. (This is a not a bug or oversight, it's an important security feature; more on that later.)


The common work-around is to use a small setuid and/or setgid binary wrapper. The basic version in @jeremy-sturdivant 's answer is a good start, but it doesn't allow you to pass any arguments. For that you need to pass argv to execve, after modifying it to point at the actual script:

#include <sys/types.h>
#include <stdio.h>   /* perror */
#include <unistd.h>  /* execve, geteuid, setuid */
int main(int argc, char **argv, char **envp) {
    setuid(geteuid());
    *argv = "/real/path/to/script";
    execve(*argv, argv, envp);
    perror(*argv);
    return 127;
}

If you want setgid instead of setuid, just change the setuid... line to:

    setgid(getegid());

This is still fairly basic, and lacks error checking around setuid/setgid. It also relies on the installer (you) to have checked that there are no other leaks such as writable ancestor directories.


But why are setuid and setgid ignored on scripts?

First it's helpful to understand what happens when a script is invoked.

Like any program, a script is invoked by the execve kernel call, which accepts 3 parameters: filename (a string), argv (an array of strings), and envp (another array of strings). By convention argv[0] is the "same" as filename, but this is only an approximation; it may omit or change the path, and/or it may have a - prefaced to it to indicate that this should be the start of a new login session. (envp contains the environment variables; it isn't important for this discussion.)

The argv and envp parameters are normally passed through unchanged to the argv and envp parameters of main in the new program.

However if a program file begins with #!, the kernel will read the first line of that file to obtain an interpreter filename, and up to one interpreter option. Then it will modify the supplied argv argument:

  • The original argv[0] will be discarded and replaced by filename, then
  • the filename argument is set to the interpreter filename.
  • one or two more elements are inserted at the front of argv[], the interpreter filename, and the interpreter option (if given).

Then execve starts over with these new arguments.

This means that when the interpreter starts, the script filename is passed into its main as an ordinary element of argv, and the interpreter simply opens that filename and starts reading it.

This means that there's a small interval between when the setuid bit is inspected during the execve syscall, and when the script interpreter opens the script for reading. During that, an attacker could replace filename with their own script, which would then run with the new UID or GID.


Wait, you said "almost all" systems. What about the others?

A few systems open the script during the execve syscall and invoke the interpreter with /dev/fd/3 as the "name" of the script. This means there's no security risk from setuid or setgid operation, but it has the drawback that $0 in a script is a lot less useful.