Sunday, April 18, 2021

Ambient Capabilities

Support for ambient capabilities has been added to the most recent libcap-ng release (0.8+). Ambient capabilities are designed to let a privileged process bestow capabilities to a non-privileged child process. Normally when you hit execve  in a non-privilged process, all capabilities are stripped unless you do a very specific sequence of instructions. This is nicely wrapped up in capng_change_id(). But what if you want to execute a helper process that also needs capabilities?

Ambient capabilities allows this to happen. If you are the privileged process, you can grant yourself ambient capabilities and then capng_change_id() and then calling helper processes is not problem. Systemd takes advantage of this to allow services to be started as non-root and with capabilities. All you have to do is add a line with AmbientCapabilities= to your service file and assign yourself the capabilities you want.

But there is a problem. Ambient capabilities are leaky. You may pass them to a child, the child can pass them to a child, and so on. Whole families of processes could leak capabilities to everything they do. And if one of those applications are exploitable, then the attacker has a way to escalate some privileges. One of the updates to libcap-ng is to also allow netcap and pscap display processes that have ambient capabilities so that we can hunt down the source of leaks.

Let's test this and ways to solve it. The test scenario is like this, we have a privileged process as root with all capabilities. It selects a set of capabilities, including ambient, so that its child can use the capabilities and changes to uid 99 (nobody). But then suppose that program is exploitable and the attacker pops a shell, the third process? What are the possible mitigations?

In this first run, we have the privileged root process starting off the second as follows:


    capng_clear(CAPNG_SELECT_ALL);
    capng_updatev(CAPNG_ADD,
               CAPNG_EFFECTIVE|CAPNG_PERMITTED|CAPNG_INHERITABLE|
               CAPNG_BOUNDING_SET|CAPNG_AMBIENT, CAP_KILL, CAP_IPC_LOCK, -1);
    capng_change_id(99, 99, CAPNG_DROP_SUPP_GRP);


It drops supplemental groups as a precaution.

# ./first
first
Effective:    00000000, 00004020
Permitted:    00000000, 00004020
Inheritable:  00000000, 00004020
Bounding Set: 00000000, 00004020
Ambient :     00000000, 00004020
second
Effective:    00000000, 00004020
Permitted:    00000000, 00004020
Inheritable:  00000000, 00004020
Bounding Set: 000000FF, FFFFFFFF
Ambient :     00000000, 00004020
third
Effective:    00000000, 00004020
Permitted:    00000000, 00004020
Inheritable:  00000000, 00004020
Bounding Set: 000000FF, FFFFFFFF
Ambient :     00000000, 00004020


As we can see, the attacker has inherited privileges because of the ambient capabilities. We can see that Effective and Permitted have values. 

Does clearing the bounding set in the privileged process have any effect?
 

The code to do that looks like this:

    capng_change_id(99, 99, CAPNG_DROP_SUPP_GRP|CAPNG_CLEAR_BOUNDING);

Now running our test program we see the evolution of permissions as follows:


# ./first
first
Effective:    00000000, 00004020
Permitted:    00000000, 00004020
Inheritable:  00000000, 00004020
Bounding Set: 00000000, 00000000
Ambient :     00000000, 00004020
second
Effective:    00000000, 00004020
Permitted:    00000000, 00004020
Inheritable:  00000000, 00004020
Bounding Set: 00000000, 00000000
Ambient :     00000000, 00004020
third
Effective:    00000000, 00004020
Permitted:    00000000, 00004020
Inheritable:  00000000, 00004020
Bounding Set: 00000000, 00000000
Ambient :     00000000, 00004020


Nope. That changes nothing. The attacker still has privileges in Effective and Permitted capabilities.

Next let's see what happens if we drop the inheritable capabilities

    capng_updatev(CAPNG_DROP, CAPNG_INHERITABLE, CAP_KILL, CAP_IPC_LOCK, -1);
    capng_apply(CAPNG_SELECT_CAPS);


The results are as follows:


# ./first
first
Effective:    00000000, 00004020
Permitted:    00000000, 00004020
Inheritable:  00000000, 00004020
Bounding Set: 00000000, 00004020
Ambient :     00000000, 00004020
second
Effective:    00000000, 00004020
Permitted:    00000000, 00004020
Inheritable:  00000000, 00000000
Bounding Set: 000000FF, FFFFFFFF
Ambient :     00000000, 00000000
third
Effective:    00000000, 00000000
Permitted:    00000000, 00000000
Inheritable:  00000000, 00000000
Bounding Set: 000000FF, FFFFFFFF
Ambient :     00000000, 00000000
attempting to regain ambient capabilities
Effective:    00000000, 00000000
Permitted:    00000000, 00000000
Inheritable:  00000000, 00000000
Bounding Set: 000000FF, FFFFFFFF
Ambient :     00000000, 00000000

This works but that means the program needs to be capabilities aware. If the program was capabilities aware it would not need ambient capabilities because it could start as root and set itself up and change uid.

Maybe just clearing the ambient capabilities in the second process works?
 

This can be done with the following code:

    prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0);


 The test results are as follows:

# ./first
first
Effective:    00000000, 00004020
Permitted:    00000000, 00004020
Inheritable:  00000000, 00004020
Bounding Set: 00000000, 00004020
Ambient :     00000000, 00004020
second
Effective:    00000000, 00004020
Permitted:    00000000, 00004020
Inheritable:  00000000, 00004020
Bounding Set: 000000FF, FFFFFFFF
Ambient :     00000000, 00000000
third
Effective:    00000000, 00000000
Permitted:    00000000, 00000000
Inheritable:  00000000, 00004020
Bounding Set: 000000FF, FFFFFFFF
Ambient :     00000000, 00000000
attempting to regain ambient capabilities
Effective:    00000000, 00000000
Permitted:    00000000, 00000000
Inheritable:  00000000, 00004020
Bounding Set: 000000FF, FFFFFFFF
Ambient :     00000000, 00000000

This works. This is probably the best solution for programs that have ambient capabilities and do not want to become capabilities aware. All that's needed is to just make a 1 line call to prctl. You do not need to link against any new library.