0

I'm hardening a Linux system and wanted to test (setuid-based) shell escapes using common binaries, like awk, vim, etc., supporting command executing.

However, all binaries I've tested except sh and bash don't respect their setuid bit.

In particular, awk continues to execute as a normal user:

$ ls -lL /usr/bin/awk
-rwsr-xr-x 1 root root 121976 Mar 23  2012 /usr/bin/awk
$ id
uid=1000(bob) gid=1000(bob) groups=1000(bob)
$ awk 'BEGIN{system("id")}'
uid=1000(bob) gid=1000(bob) groups=1000(bob)

In contrast, bash executes as root when given the -p option:

$ ls -la /bin/bash
-rwsr-xr-x 1 root root 1168776 Apr 18  2019 /bin/bash
$ /bin/bash -p
# id
uid=1000(bob) gid=1000(bob) euid=0(root) groups=1000(bob)

Is there any way to make awk, vim, less, etc. respect the setuid bit and execute the command as root?

OS:

# cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 10 (buster)"
NAME="Debian GNU/Linux"
VERSION_ID="10"
VERSION="10 (buster)"
VERSION_CODENAME=buster
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

Update:

parallels@debian-gnu-linux-vm:~$ ls -la /proc/self/fd/0 /dev/fd/0 /dev/stdin
lrwx------ 1 parallels parallels 64 Mar 26 08:15 /dev/fd/0 -> /dev/pts/1
lrwxrwxrwx 1 root      root      15 Mar 20 19:56 /dev/stdin -> /proc/self/fd/0
lrwx------ 1 parallels parallels 64 Mar 26 08:15 /proc/self/fd/0 -> /dev/pts/1
Stephen Kitt
  • 411,918
  • 54
  • 1,065
  • 1,164
Shuzheng
  • 4,023
  • 1
  • 31
  • 71
  • @StephenKitt - will you elaborate? I only know of real uid and effective uid. Furthermore, how can I check the ruid? And how can I turn my command into e.g. a `bash` root shell? – Shuzheng Mar 24 '20 at 15:33
  • `awk` works as intended. Try this, `awk 1 /etc/shadow` and if it honours its setuid bit it will display the contents of the otherwise protected shadow file. – roaima Mar 24 '20 at 15:35
  • id shows you the effective UID, "id -r" the real one. – Gerard H. Pille Mar 24 '20 at 15:37

1 Answers1

7
$ ls -lL /usr/bin/awk
-rwsr-xr-x 1 root root 121976 Mar 23  2012 /usr/bin/awk
$ awk 'BEGIN{system("id")}'
uid=1000(bob) gid=1000(bob) groups=1000(bob)

In your example, it's not awk which is dropping privileges or not "respecting its setuid bit", but the /bin/sh command that awk uses to implement its system() function.

Just like its C counterpart, awk's system() does not parse and run the command directly, but by passing it as an argument to /bin/sh -c. If /bin/sh is bash (or the Debian version of dash, or a couple of other shells which copied this misfeature from bash), it will reset its effective uid back to the real one.

The same thing applies to print | "cmd" or "cmd" | getline in awk -- they're implemented with popen(3) which calls /bin/sh -c. Notice that it's always /bin/sh (or the system's shell, eg. /system/bin/sh on Android), not the user's login shell or that from the $SHELL environment variable. [1]

This is different in perl: perl's system, exec, open "|-", open2, open3, etc will run the command directly if they're called with multiple arguments or if the command does not contain shell metacharacters:

$ id -nu
ahq
$ ls -l /tmp/perl
-rwsr-xr-x 1 dummy_user dummy_user 3197768 Mar 24 18:13 /tmp/perl
$ env - /tmp/perl -e 'system("id -nu")'
dummy_user
$ env - /tmp/perl -e 'system("{ id -nu; }")'
ahq

This example is on Debian 10. On other systems like FreeBSD or older Debian, both commands will print the same thing, because their /bin/sh does not drop privileges. [2]


Notes:

[1] Other programs like vim and less do use the $SHELL environment variable, so they're easily "fixable" by pointing it to some wrapper. In vim you could also use :set shcf=-pc to pass the -p option to the shell used for the :! and similar commands.

[2] The perl example will also work on OpenBSD just like on FreeBSD, provided that you replace the env - /tmp/perl 'script' with the more obtuse echo 'script' | /tmp/perl /dev/fd/0.

OpenBSD's perl will reject the -e argument and refuse to read its script from the stdin when running in setuid mode (see this which is ending here -- OpenBSD supposedly has secure setuid scripts).

But that does not apply to /dev/fd/N, which perl is handling itself when given as a script name (only the /dev/fd/N form, not /dev/stdin or /proc/self/fd/N).

obsd66$ ls -l /tmp/perl
-rwsr-xr-x  1 dummy_user  dummy_user  10728 Mar 25 18:34 /tmp/perl

obsd66$ env - /tmp/perl -e 'system("{ id -nu; }")'
No -e allowed while running setuid.

obsd66$ echo 'system("{ id -nu; }")' | env - /tmp/perl
No program input from stdin allowed while running setuid.

obsd66$ echo 'system("{ id -nu; }")' | env - /tmp/perl /dev/stdin
Can't open perl script "/dev/stdin": Operation not permitted

obsd66$ echo 'system("{ id -nu; }")' | env - /tmp/perl /dev/fd/0
dummy_user
debian10$ su - other_user -c 'perl /dev/fd/7' 7<<<'print "OK\n"'
OK
debian10$ su - other_user -c 'perl /proc/self/fd/7' 7<<<'print "OK\n"'
Can't open perl script "/proc/self/fd/7": Permission denied
  • So, the only way to have e.g. `awk` execute commands as `root` is to replace the system's shell with another one that doesn't reset the euid? I guess, that's good for security, but not from a developer's point of perspective. – Shuzheng Mar 25 '20 at 09:47
  • Are you using `/dev/fd/0` as an argument to `perl`, since the piped input is available on that file descriptor? – Shuzheng Mar 25 '20 at 09:51
  • Because OpenBSD's perl will not accept an -e script or reading the script from stdin when running in setuid mode. See the [edit history](https://unix.stackexchange.com/posts/574654/edit); the prev version used the less obtuse `env - /tmp/perl -e 'system("...")'`. I haven't investigated yet if this is a feature in new versions of perl, or it's an OpenBSD addition. `env - /tmp/perl <(echo 'system("id -nu")')` would work too, and not mess with perl's stdin, but not all shells support `<(...)` process substitution. –  Mar 25 '20 at 10:34
  • But isn't `/dev/fd/0` stdin, which you say `perl` cannot read from setuid mode? – Shuzheng Mar 25 '20 at 13:31
  • No, `open("/dev/fd/0", ...)` will ALWAYS return a fd != 0, ie a fd other than stdin. The `/dev/fd/` mechanism works differently in Linux vs BSD/Solaris/etc, but that one thing is 100% sure ;-) As to that "no script from stdin when setuid" in perl being kind of dumb, it's not me who invented it. –  Mar 25 '20 at 13:57
  • On my Debian 10, `env - /usr/bin/perl -e 'system("cat /etc/shadow")'` works just fine - I don't need the `/dev/fd/0`. – Shuzheng Mar 25 '20 at 14:39
  • Please see my update. All `/dev/fd/0`, `/dev/stdin`, and `/proc/self/fd/0` refer to the same character device `/dev/pts/1`. When calling `open("...")` on any of these, one get a `fd != 0` that is effectively `stdin`, thus not having the same integer value (0)? Are we talking about a cloned version of `stdin`? – Shuzheng Mar 26 '20 at 07:23
  • No, you get a "cloned" version of stdin (the same you would get with `dup(2)`) _on *BSD and Solaris_. On Linux, the file would be opened _from scratch_, going through the directory entry and considering the inode's permissions. My point from the extra note and the very last example is that perl is handling itself specially a path like `/dev/fd/N` when given as a script name: perl will NOT open the path, but use the fd directly, perversely bypassing the expected behaviour on Linux (and OpenBSD, where the `/dev/fd/N` cannot be opened from a setuid program). –  Mar 26 '20 at 07:32
  • __tl;dr__ you can ignore everything from the Note: below in the context of this answer. That's a bug I hit upon when trying to create a portable example which demonstrate how system(), etc work in perl. They're not affected by that. –  Mar 26 '20 at 07:34
  • How do you use a fd directly (`/dev/fd/N`) from a program without opening the file first? – Shuzheng Mar 26 '20 at 07:45
  • What kind of program? The perl interpreter is written in C, so it just uses the fd (a short int) directly in functions like `read(2)`, `write(2)`. If you look at the [link](https://github.com/Perl/perl5/blob/f12bbb761071acfd5663d673ad891ffbb2ffdbd9/perl.c#L4017) from the note, you'll see that it passes it to `fdopen()` (actually, perl's internal (PerlIO) `fdopen()`, but that doesn't matter). –  Mar 26 '20 at 07:49
  • I tested `sudo /usr/bin/awk 'BEGIN{system("/bin/bash")}'`, which runs as root on Debian 10. Why doesn't `/bin/sh` drop privileges here? Is it because `sudo` makes both uid and euid to root, before running `awk`, whereas the setuid approach only sets euid to root? – Shuzheng Mar 26 '20 at 11:15
  • 1
    e‎x‎a‎c‎t‎l‎y ‎ –  Mar 26 '20 at 16:16