Inheriting privilege

This article discusses how privilege is inherited with capabilities under Linux. It uses a set of libcap tools (setcap, getcap, capsh) that generally come standard on Linux distributions, and a more recent libcap distributed binary, captree, that may not be part of your standard install (yet, but see here to build it yourself). To work through all of the examples here, you will also need sudo or some equivalent.

Note: that there is a pam_cap.so module that is distributed with libcap that can configure users and/or groups to add inheritable capabilities to processes at application login time. This module supports Inheritable, Ambient and Blocked (~Bounding) capabilities for Linux-PAM compliant applications.

Legacy privilege

While still very much in use today, we first explain what we mean by legacy privilege. It is the idea that there is one special user on a Linux system, root (whose UID is 0). This one user wields all privilege. Legacy privilege means other users have no privilege. A tool like sudo provides a regular user (morgan in these examples, UID of 1000) the opportunity to do things as a different user, specifically (by default) the different user root.

When user root launches an executable program, that program runs as root too, and thus has privilege. The inheritance of this user=root through program execution thus creates the expectation that privilege should be naively inherited.

Non-legacy privilege, specifically the draft POSIX.1e Capabilities, do not inherit that way. More on that front below.

How can we see this naive inheritance in action? Let's do an experiment (we assume bash is the user shell we start with, running as an unprivileged user):

$ id

uid=1000(morgan) gid=1000(morgan) groups=1000(morgan),10(wheel)

$ sudo -s

# id

uid=0(root) gid=0(root) groups=0(root)

# bash

# bash

# id

uid=0(root) gid=0(root) groups=0(root)

# captree bash

--bash(89746)

  +-sudo(145106) "=ep"

    +-bash(145110) "=ep"

      +-bash(145140) "=ep"

        +-bash(145170) "=ep"

          +-captree(145825+{145826,145827,145828,145829}) "=ep"

For a more complete explanation of the output of captree, we've provided another article. Suffice it to say, "..." means the running process has some capabilities and specifically the e and p parts mean actual privilege to do things. The value "=ep" means all privilege.

Clearly, sudo has some special something to seemingly create privilege from nothing. That special attribute comes from it being setuid-root. That is, a property of the sudo program generates the privilege for the sudo  running process to execute with. The property is that the setuidness causes the program to effectively execute as root, and root is equivalent to the all privilege role. So, sudo doesn't so much inherit privilege, but receives it from the way its executable is stored on the filesystem. While sudo's children inherit the UID of root, and execute as root, they retain this privilege through inheritance.

What the captree output shows is that our sudo and bash invocations have created a tree of parent (above and to the left) -> child (below and to the right) processes and we can see that everything after we invoked sudo is inheriting, and thus executing, with the privilege sudo provided.

We can try the following sequence to see that sudo is also able to stop naive inheritance by changing user from privileged to not again (these commands are a continuation of the above sequence):

# sudo -u morgan -s

$ captree bash

--bash(89746)

  +-sudo(145106) "=ep"

    +-bash(145110) "=ep"

      +-bash(145140) "=ep"

        +-bash(145170) "=ep"

          +-sudo(146186) "=ep"

            +-bash(146188)

              +-captree(146227+{146228,146229,146230,146231,146232})

$ exit

# exit

# exit

# exit

To summarize, in the default mode of operation of processes under Linux, the simple Legacy inheritance of UID=0 through program execution, and its equivalence to privilege, is how privilege is inherited by processes.

Finally, all the exit commands at the end of this sequence back us out of the nested shells back to the top level again in preparation for the next section.

Ambient inheritance

Holding to this idea that privilege comes automatically from your parent, Linux supports the idea of Ambient capabilities. That is, the environment of a process has privilege and thus all of its child processes also share it. (This, again, is not the POSIX.1e model, we'll explain that in the next section.)

To enable Ambient inheritance we'll introduce another program, capsh, which is essentially a "shell wrapper for adjusting and exploring capabilities". We'll do an experiment that mirrors what we did in the previous section:

$ whoami

morgan

$ sudo capsh --user=$(whoami) --iab='^cap_setuid' --

$ whoami

morgan

$ bash

$ capsh --inh= --

$ captree bash

--bash(89746)

  +-sudo(146505) "=ep"

    +-bash(146507) "cap_setuid=eip" [^cap_setuid]

      +-bash(146510) "cap_setuid=eip" [^cap_setuid]

        +-bash(147416)

          +-captree(147417+{147418,147419,147420,147421})

$ exit

$ exit

$ exit

Note: an earlier version of this article included the --norc argument on the bash command lines (and the implicit one at the end of the capsh ... -- commands) to stop bash from printing errors of the form: bash: /root/.bashrc: Permission denied. This article has been updated to no longer include that detail because the capsh provided in libcap-2.65+ does not behave that way.

An alternative work around, which is closer in spirit to what modern versions of capsh do, would have been to reset the environment with a longer command line like this:

sudo capsh --user=$(whoami) --iab='^cap_setuid' \

    --shell=/usr/bin/env -- -i bash -i

Here we see a more interesting inheritance of capabilities through the tree of processes

Ambient capabilities work by augmenting the process state to enhance the privilege of the process (tree) while they are present. They will get reset by any process that can do one of a number of things:

We used the last of these, specifically the Inheritable version.

What remains when capsh exec*()s itself into another instance of bash is a process with no privilege.

What is important to note is that like the Legacy inheritance, the two bash shells in the middle of this tree of execution are running with live privilege. That is, they themselves can perform privileged operations. The youngest bash descendant, however, (PID=147416) has no privilege and neither has captree.

Fully capable inheritance

In this section, we show how Inheritance works, in the way it was intended to work by the POSIX.1e committee.

The key idea that the POSIX.1e committee was trying to stress was that people don't have privilege in computer systems. What has privilege in a system are executable programs. They get this privilege from somewhere, and since privilege somehow risks the overall system security and the integrity of the user data the system manages, those that have it will, ideally, have been audited to use it appropriately. This is fundamentally different from the Legacy model, which tied privilege to user identity: being root. Where any program that user chose to execute would have privilege too.

So the first thing they did was create a concept of a privilege: a capability. Something that any specific user was not supposed to be universally equivalent to. This immediately created an incompatibility with the Legacy notion of privilege. Indeed, the first attempt to manage them together, which put too much emphasis on naive inheritance, didn't turn out so well for Linux. As explained in that linked article, a more robust way to support both models simultaneously did get implemented and we'll leave that aside for the rest of this section. We'll treat root as mostly off-limits for this section, with the exception of using sudo to bootstrap some privilege on programs.

From the perspective of our user morgan, they don't have any permission to freely wield privilege. Instead, they can only execute programs, some of which can summon up (or, force) privilege, and others have inheritable access to privilege.

Let's start by creating a program with all privilege and then explore how it can be used.

$ whoami

morgan

$ cp $(which capsh) ./morgan-capsh

$ chmod 0700 ./morgan-capsh

$ ls -l morgan-capsh

-rwx------. 1 morgan morgan 33368 Sep  4 18:55 morgan-capsh

$ sudo setcap =p ./morgan-capsh

$ ./morgan-capsh --current

Current: =p

Current IAB: 

(Note, libcap-2.52 introduced the capsh --current command line argument. If you are running an earlier version, use --print | grep Current instead.)

In other places, you may be more familiar with adding capabilities in the form of "=ep", but the e part, the Effective flag, is just for binaries that don't know how to use capabilities programmatically---it is the legacy bit. Given the fact that this binary is part of the libcap package, capsh doesn't generally need that.

One thing to note is that we've changed our copy of capsh's access mode to only make it usable by morgan. In Legacy mode, this sort of thing isn't quite as easy, because the ownership of such a setuid-root binary has to be root itself!

We start by trying our first example with this binary and use it instead of sudo:

$ id

uid=1000(morgan) gid=1000(morgan) groups=1000(morgan),10(wheel)

$ ./morgan-capsh --current --

Current: =p

Current IAB: 

$ id

uid=1000(morgan) gid=1000(morgan) groups=1000(morgan),10(wheel)

$ bash

$ bash

$ captree bash

--bash(89746)

  +-bash(170270)

    +-bash(170318)

      +-bash(170355)

        +-captree(170392+{170393,170394,170395,170396})

$ setcap -r morgan-capsh

unable to set CAP_SETFCAP effective capability: Operation not permitted

$ exit

$ exit

$ exit

This might appear confusing. Only while our morgan-capsh binary itself is running does any part of this process tree show any signs of having privilege. That is, the first time we invoke it with --current as an argument, it claims to have privilege. However, by the time the -- argument is evaluated to invoke bash (PID=170270), that privilege has been lost.

This is the POSIX.1e capability model at work. That is, binaries are things that have privilege---users don't. While such a binary is running it has privilege that it can employ on behalf of the user that invoked it, but once it exits (or, as with this case, exec*()s a replacement) that privilege is gone.

It should be clear that a libcap release sealed the feature set of the capsh binary. So, what capsh can be used for is limited to that feature set. For example, it doesn't contain any code for directly manipulating the network, so the fact that morgan-capsh has CAP_NET_ADMIN in its Permitted flag isn't going to have any consequences when it isn't passed on to any child process.

Note, the unprivileged user can't directly remove a capability (setcap -r morgan-capsh) because they don't have sufficient privilege! They can indirectly remove it, by changing the content of the binary---to make it do something else. Or simply deleting the program (they own it after all), but that protects the system from a rogue binary becoming capable without further privilege being employed to reinstall its file capability.

Note: dissatisfaction with these careful semantics and a lack of naive inheritance is what led to the invention of the Ambient capability vector.

This section is titled "Fully capable inheritance", so what kind of inheritance does the POSIX.1e model offer? The model has a whole Inheritable file capability flag for inheritance. Let's see how to use it:

$ cp $(which setcap) morgan-setcap

$ chmod 0700 morgan-setcap

$ sudo setcap cap_setfcap=i morgan-setcap

$ ./morgan-setcap -r morgan-capsh

unable to set CAP_SETFCAP effective capability: Operation not permitted

Clearly that isn't how that works either.

One feature capsh has is to be able to add Inheritable process flags to the running process. So we use this fact to modify the sequence at the start of this section:

$ ./morgan-capsh --inh=cap_setfcap --current --

Current: =p cap_setfcap+i

Current IAB: cap_setfcap

$ bash

$ bash

$ captree bash

--bash(8974)

  +-bash(17261) "cap_setfcap=i"

    +-bash(17265) "cap_setfcap=i"

      +-bash(17269) "cap_setfcap=i"

        +-captree(17277+{17278,17279,17280,17281}) "cap_setfcap=i"

$ setcap -r morgan-capsh

unable to set CAP_SETFCAP effective capability: Operation not permitted

$ ./morgan-setcap -r morgan-capsh

$ echo $?

0

$ exit

$ exit

$ exit

This sequence observes that while the default install of setcap gets permission denied errors, the same operation performed by our morgan-setcap version works in the presence of the process Inheritable flag. The only difference between these two binaries is the addition of a file Inheritable flag capability to morgan-setcap.

This is how the POSIX.1e committee envisaged inheritance of privilege should work. Process trees that are enabled to forward privilege through inheritance (of process Inheritable flag capabilities), would only activate that privilege on binaries specially prepared (with a file Inheritable flag capabilities) to leverage it.

What differentiates this most significantly from Legacy and Ambient inheritance is that no feature of binaries invoked along the way (bash in our examples) can ever directly execute privileged operations. Specifically, no exploitable code bug in these binaries could escalate to an exploit of a non-audited binary.

As a final note, for this section, you should probably do this:

$ rm morgan-capsh morgan-setcap

Our experiment above dropped the file capability on morgan-capsh, but leaving files like this around is bad practice. You can confirm you have cleaned up all the capable binaries under your home directory by finding them all with:

$ getcap -r ~

Hybrid inheritance

The default setup for Linux is one of simultaneously supporting three different inheritance models for privilege. Legacy, Ambient and POSIX.1e. This creates many situations where you can use some feature of one to leverage another to break the independent assumptions of one or all of them. These edge cases are to be expected, require privilege to initiate, and have been discussed elsewhere as evidence that each capability offers full root equivalence. In this hybrid setup, they clearly do.

Perhaps the simplest example is as follows.

Prep:

$ cp $(which capsh) ./morgan-capsh

$ chmod 0700 morgan-capsh

$ sudo setcap cap_setuid=p morgan-capsh

Escalation:

$ ./morgan-capsh --caps="cap_setuid=ep" --uid=0 -+

$ captree bash

--bash(22494)

  +-capsh(2513) "cap_setuid=ep"

    +-bash(2514) "=ep"

      +-captree(2543+{2544,2545,2546,2547,2548}) "=ep"

$ rm morgan-capsh

$ exit

Note: in this command we use the capsh argument -+ which debuted in libcap-2.60. If your version of libcap is older, you can use -- instead, but this will cause capsh to not hang around after it launches bash, so the capsh entry in the captree output will be omitted.

That is, the single capability, CAP_SETUID, can be exploited to escalate to "all privilege" on all binaries through acquisition, and naive inheritance, of the root identity. Some argue that this is evidence that the whole model is flawed. It is our position that the critique is well deserved of the hybrid state of three models coexisting, but not actually any worse of a state than that of the Legacy mode alone.

For this reason, libcap has the notion of modes. (Modes debuted in libcap-2.29.) You can see what modes libcap recognizes with:

$ capsh --modes

Supported modes: NOPRIV PURE1E_INIT PURE1E HYBRID

All but the last of these modes disables Legacy and Ambient Inheritance altogether for all children of the process tree rooted in the PID that ran the (capsh) command to initiate the mode:

$ sudo capsh --mode=PURE1E -+

# id

uid=0(root) gid=0(root) groups=0(root)

# sudo -u morgan -s

sudo: PERM_SUDOERS: setresuid(-1, 1, -1): Operation not permitted

sudo: no valid sudoers sources found, quitting

sudo: error initializing audit plugin sudoers_audit

# captree bash

--bash(22494)

  +-sudo(2566) "=ep"

    +-capsh(2567) "=p"

      +-bash(2568)

        +-captree(2569+{2570,2571,2572,2573})

# exit

$

What is happening here is that root running bash, in a PURE1E environment, has lost its privilege. Naive inheritance of privilege for just being root, or for any other reason, has gone!

This, as written, is an interesting party trick that requires privilege to initiate. However, don't be fooled. In a typical system root owns so many files that an unprivileged root user is still able to cause all sorts of problems for the execution of the system.

However, this mode entry is something we plan to explore in other articles. The first of these introduces an alternative version of su that isn't a setuid-root binary.