1

In asking this question, I'm not looking to be told that I shouldn't be doing this or that it's insecure. Those matters beyond the scope of this question.

Linux respects the setuid andsetgid bits on scripts when they are registered in the binfmt_misc filesystem with the credentials flag (C). This is a demonstration of just that. I'm attempting to use this same facility to make shell scripts, specifically bash scripts, that will work with setuid/setgid. To this end, I crafted a binfmt string that employs file extension matching:

:pbash:E::pbash::/bin/bash:C

To test this, a file named test.pbash that lacks a shebang line, is owned by root, and has the mode 6755:

echo "Effective UID: $(id -u)"
echo "Real UID: $(id -ru)"

Running test.pbash outputs this:

Effective UID: 1000
Real UID: 1000

The effective UID should be displaying 0, so I had to search the web about bash and effective UIDs. I found that bash will change the effective UID to the real UID unless it is run with the -p flag set. This requires that I change the binfmt and create a wrapper script.

The wrapper script, /bin/pbash, with several invocations of -p just in case:

#!/bin/bash -p
set -p
exec bash -p -- "$@"

Delete the old binfmt and load the new new binfmt string:

:pbash:E::pbash::/bin/pbash:C

Run test.pbash and I get:

Effective UID: 1000
Real UID: 1000

Still not working. I figured I'd rewrite /bin/pbash to output some information:

#!/bin/bash -p
set -p
echo "Executing '$0' '$1'" > /tmp/log.txt
exec bash -p -- "$@"

I run test.pbash again and /tmp/log.txt doesn't exist, as if /bin/pbash didn't run at all. I remove the C flag from the binfmt string, run test.pbash again, and get the same output. What's changed is that /tmp/log.txt exists! It says:

Executing '/bin/pbash' '/bin/test.pbash'

So my wrapper script isn't even running when I have the credentials flag set. Why is bash or Linux behaving like this? How do I coerce it to run the wrapper script?

Melab
  • 2,594
  • 7
  • 30
  • 51

1 Answers1

-1

The kernel doc page has a hint that might be important:

If you want to pass special arguments to your interpreter, you can
write a wrapper script for it ....

You might be running into one or both of two issues:

  • an interpreter /bin/bash -p might be being taken literally as looking for a file called "/bin/bash\ -p"
  • echo "Effective UID: $(id -u)" is not the same as echo -n "Effective UID: " ; id -u.
    • Specifically, the $(id -u) invokes id from within another invocation of /bin/bash (likely without a -p argument).

I've not drilled down to a minimal example, but this combination works for me. You might be able to trim it down to something shorter, but this gives an example that addresses both of the above potential issues: Try this code (pbash.c) as the interpreter:

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

int main(int argc, char **argv) {
    char **args = calloc(sizeof(char *), argc+2);
    memcpy(args+2, argv+1, (argc-1)*sizeof(char *));
    args[0] = strdup("bash");
    args[1] = strdup("-p");
    execv("/bin/bash", args);
    perror("exec failed");
}

Compile and install it:

$ gcc -o pbash pbash.c
$ sudo cp pbash /usr/local/bin/
$ ls -l /usr/local/bin/pbash
-rwxr-xr-x. 1 root root 24832 Jul 16 21:35 /usr/local/bin/pbash
$ sudo -s
# echo ':pbash:E::pbash::/usr/local/bin/pbash:C' > /proc/sys/fs/binfmt_misc/register
# exit
$ ls -l /proc/sys/fs/binfmt_misc/
total 0
-rw-r--r--. 1 root root 0 Jul 16 21:29 pbash
--w-------. 1 root root 0 Jul 16 21:29 register
-rw-r--r--. 1 root root 0 Jul 16 20:55 status

And prepare a try.pbash script:

echo -n "Effective UID: " ; id -u
echo -n "Real UID: " ; id -ru

and make it setuid and run it:

$ sudo chown root.root  try.pbash
$ sudo chmod +xs try.pbash
$ ls -l try.pbash
-rwsr-sr-x. 1 root root 64 Jul 16 21:52 try.pbash
$ ./try.pbash
Effective UID: 0
Real UID: 1000
Tinkerer
  • 865
  • 7
  • 9