su without ROOT

It has been a while since I worked on anything PAM related, but I recently became interested in exploring how to convert the su binary to work with capabilities only, and not require it being setuid-root. I was primarily interested in what it would take to get su to work in the environment libcap mode=PURE1E. (Recall, in this environment, being root comes with no super user privilege. However, we shouldn't ever forget that root owns a lot of system files!)

The path I took was to dig out a very old set of sources for su from the SimplePAMApps tar ball. That was a set of applications those of us, that originally developed Linux-PAM, wrote to prototype modules and libpam improvements against. I had a prototyping project related to libcap now and, while a couple of decades had elapsed, it was fun to take that code out for a spin again.

In the end, there were two pieces to making this work:

  1. modifying pam_unix.so to work without the hard-coded assumption that setuid-root was the only way to read the /etc/shadow file;

  2. and rewriting su to use capabilities for privileged operations.

Updating pam_unix.so and helper

It didn't take me long to realize that pam_unix.so would also be part of the porting effort, so I started to explore what was needed to get pam_unix.so to work with an application that isn't running as root.

First, the simplest change is to not install unix_chkpwd as setuid-root, but have it owned by some user and configure it with a single file capability:

$ sudo setcap cap_dac_read_search=ep /sbin/unix_chkpwd

Ideally, unix_chkpwd wouldn't need the legacy, e, bit enabled but we can live with that for now. This is sufficient for unix_chkpwd to be able to read the content of the /etc/shadow file, and takes care of the case where a user is re-authenticating as them self.

A trickier situation is getting the privileged mode of pam_unix.so to authenticate any user. Which is what we actually need to have su correctly function. A few layers deep in the pam_unix authentication code was the reason. There was this check (a 20+ year younger me likely wrote it!):

if (geteuid() || SELINUX_ENABLED)

return PAM_UNIX_RUN_HELPER;

*spwdent = pam_modutil_getspnam(pamh, name);

if (*spwdent == NULL || (*spwdent)->sp_pwdp == NULL)

return PAM_AUTHINFO_UNAVAIL;

What this code was doing was deciding if the running code is setuid-root or not and forcing the running of the the unix_chkpwd program if it wasn't. In PURE1E mode the su program won't be running as setuid-root, but we want the code to authenticate other users... So, that code needed to change.

To get pam_unix.so to work in a PURE1E environment, we needed to remove the geteuid() test and to try to fetch the shadow entry for the authenticating user unconditionally before failing over to invoking the helper. Such a change was committed to the Linux-PAM sources and is included in Linux-PAM 1.5.2.

Restructuring su to be fully capable

Next up rewriting some version of su to have no requirement to be run-as-root.

Back when the Linux world was young, a few of us developed an implementation of PAM (Pluggable Authentication Modules) for Linux. Bootstrapping acceptance of its abstraction and validating that modules were compatible by design vs. by implementation, we wrote minimal versions of various system utilities: the SimplePAMApps. They were useful for development purposes, but were never adopted as actual replacements for system tools, instead, folk modified the standard tools to support PAM abstractions. However, since I had some memory of working with that code, I decided to refactor the version of su from that tar ball.

The net result was the contrib/sucap sources inside the libcap distribution. (As was the case with the SimplePAMApps, we don't expect this binary to become standard on any distribution but, we're happy to receive bug reports and or patch fixes against it.)

Before closing this section, since we're not actually interested in using the root identity, this version of su does not support changing user identity to that of root. This is by design, not because it couldn't, but because we've added code to explicitly prevent it from doing so. Our reason for this is that we want to explore executing programs in a PURE1E environment and having a hybrid notion of privilege stemming from expectations of being root confuses things.

Validating su can be fully capable

So, on a system with the Linux-PAM 1.5.2+ package (you will also need the PAM development package which includes the API headers etc) installed, we can build and try out our capable implementation of su.

This walk through explicitly builds the code from within a clone of the upstream sources, so all of the needed source APIs are available.

Building our fully capable su can be done as follows:

$ mkdir foo

$ cd foo

$ git clone git://git.kernel.org/pub/scm/libs/libcap/libcap.git

$ cd libcap

$ make DYNAMIC=yes

$ cd contrib/sucap

$ make

Note, we specify DYNAMIC=yes to ensure that capsh is linked dynamically. This is to work around a longstanding bug in glibc affecting getpwuid() and friends when linked statically (gentoo, fedora, ...).

You might want to read over the contrib/sucap/Makefile to see why it invokes sudo. This is to add file capabilities to the built binary. Also, the Makefile builds this version of su in a way that it identifies to libpam as the application "sucap". As such, you need to install a dedicated /etc/pam.d/sucap config file. An example version is provided in the contrib/sucap directory, but that one requires pam_cap.so be setup.

To start with for this present exercise, I would recommend something simpler still (install this as /etc/pam.d/sucap):

#%PAM-1.0

auth required pam_unix.so

account required pam_unix.so

password required pam_unix.so

session required pam_unix.so

You can explore adding other modules or even copying the default config for su on your system after you have validated that this simple setup works.

At this point, we have a capable binary, su, that has its own PAM config installed. We can give it a try:

$ ls -l su

-rwxr-xr-x. 1 morgan morgan 37128 Aug 06 17:22 su

$ ../../progs/getcap ./su

./su cap_chown,cap_dac_read_search,cap_setgid,cap_setuid,cap_setpcap=p

$ ./su $(whoami)

Password:

$ ../../go/captree ${SHELL##*/}

--bash(394872)

+-su(757650) "cap_chown,cap_dac_read_search,cap_setgid,cap_setuid,cap_setpcap=p"

+-bash(757653)

+-captree(757695+{757696,757697,757698,757699,757700})

$ exit

The captree command assumes that your system has built the captree utility. Alternatively, your installed libcap may have provided the captree binary for you (in which case, you can run captree ${SHELL##*/} or /sbin/captree ${SHELL##*/}). If neither of these two things are true, see here to build and understand the output of captree yourself.

If this configuration of su doesn't work for you, please double check you followed the above instructions, and if it still fails, file a bug. We're always interested in debugging corner cases, improving this article and fixing source bugs.

Finally, to satisfy the purpose of this article, let's try that all again but in a PURE1E mode of operation.

We'll use the local build of capsh to get us into PURE1E mode, and then observe that the setuid-root version of su fails in this environment (this is essentially the same observation as the party trick at the end of the article on Inheriting privilege). We'll then demonstrate that our sucap version still works.

$ sudo LD_LIBRARY_PATH=../../libcap ../../progs/capsh --mode=PURE1E --user=$(whoami) -+

$ su $(whoami)

Password:

su: Authentication failure

$ ./su $(whoami)

$ ../../go/captree sudo

--sudo(22077) "=ep"

+-capsh(22078) "=p"

+-bash(22079)

+-su(22101) "cap_chown,cap_dac_read_search,cap_setgid,cap_setuid,cap_setpcap=p"

+-bash(22102)

+-captree(22115+{22116,22117,22118,22119})

$ exit

$ exit

Notes:

When invoking capsh, we have forced the use of the in-build-tree version of libcap. This is to ensure we can run the in-tree version of capsh which uses modern API features of libcap that may not (yet) be part of your default install.

Above, we have used the capsh ... -+ argument to launch a shell. This is similar to, but not the same as, the -- argument insofar as it does launch a user shell, but keeps the parent capsh binary running as opposed to simply exec*()ing over it. We use this variant to provide more visibility into what privilege the independent binaries in our chain of execution have.

An earlier version of this article observed that where bash was invoked, it printed an error (bash: /root/.bashrc: Permission denied) but this has been removed. Since libcap-2.65 was released, capsh no longer generates this error.

The proof is visible from the captree output that our fully capable su binary is able work even in this PURE1E environment. The regular setuid-root su does not work, and claims an Authentication failure, because being EUID of root is insufficient privilege to read the /etc/shadow file in PURE1E mode:

$ ls -l /etc/shadow

----------. 1 root root 1445 Oct 20 2021 /etc/shadow

Finally, a note on the file capabilities our capsu variant of su is configured with. As you can see, we have given it 5 specific file-permitted capabilities ("cap_chown,cap_dac_read_search,cap_setgid,cap_setuid,cap_setpcap=p"). These are needed to read the shadow file, adjust the terminal settings, grant Inheritable capabilities (via pam_cap.so) and change UID and GID values in order to launch a shell as the target user. Other capabilities, needed to change network settings or insert kernel modules are not needed by the su application, so they are not provided. This configuration is intended to support only the privilege that su needs to do its work, and the source code can be audited with this in mind. This is the Fully Capable way.

That being said, and the Makefile contains a comment to this effect, if you were to want to use this version of su to support granting arbitrary Ambient capabilities (via pam_cap.so), you would need to grant the su binary all capabilities ("=p"). If you want that feature, you can also use a more traditional, but actually Linux-PAM compliant, setuid-root version of su.

Further, some modules quietly hope to be able to use capabilities in some specific ways but silently proceed when those capabilities are not available. In the libcap-2.66 release we include a new helper binary, captrace, that can help you explore that sort of detail:

$ sudo ../../go/captrace capsh --user=$(whoami) -- -c "exec ./su $(whoami)"

2022/09/24 15:55:08 capsh 651456 opt=4 "cap_setgid" -> 0

2022/09/24 15:55:08 capsh 651456 opt=4 "cap_setgid" -> 0

2022/09/24 15:55:08 capsh 651456 opt=4 "cap_setuid" -> 0

2022/09/24 15:55:08 su 651456 opt=0 "cap_dac_read_search" -> -1 (operation not permitted)

2022/09/24 15:55:08 su 651456 opt=0 "cap_dac_override" -> -1 (operation not permitted)

2022/09/24 15:55:08 su 651456 opt=4 "cap_setuid" -> 0

Password:

2022/09/24 15:55:24 su 651456 opt=0 "cap_dac_read_search" -> 0

2022/09/24 15:55:24 su 651456 opt=0 "cap_audit_write" -> -1 (operation not permitted)

2022/09/24 15:55:24 su 651456 opt=0 "cap_dac_read_search" -> 0

2022/09/24 15:55:24 su 651456 opt=0 "cap_audit_write" -> -1 (operation not permitted)

2022/09/24 15:55:24 su 651456 opt=4 "cap_setgid" -> 0

2022/09/24 15:55:24 su 651456 opt=4 "cap_setgid" -> 0

2022/09/24 15:55:24 su 651456 opt=4 "cap_setuid" -> 0

2022/09/24 15:55:24 su 651456 opt=0 "cap_audit_write" -> -1 (operation not permitted)

2022/09/24 15:55:24 su 651456 opt=0 "cap_audit_write" -> -1 (operation not permitted)

2022/09/24 15:55:24 su 651456 opt=4 "cap_setuid" -> 0

[inner shell]$ exit

2022/09/24 15:57:18 su 651456 opt=0 "cap_audit_write" -> -1 (operation not permitted)

2022/09/24 15:57:18 su 651456 opt=0 "cap_audit_write" -> -1 (operation not permitted)

2022/09/24 15:57:19 su 651456 opt=4 "cap_setuid" -> 0

Which loosely translates to the observation that some things in the PAM stack of modules are trying to passively benefit from the effective privilege of the process without following the capability rules directly (cap_dac_read_search, cap_audit_write). Since we are only using one module, pam_unix.so, it is likely one or more of the many many shared libraries, perhaps over-zealously, linked to pam_unix.so.