Polkit: wat

:: computer, security

What polkit is, why you should worry about it, some ways to defang it.

What polkit is

Polkit1 is part of the freedesktop.org project. The documentation for polkit describes what it does:

polkit provides an authorization API intended to be used by privileged programs (“MECHANISMS”) offering service to unprivileged programs (“SUBJECTS”) often through some form of inter-process communication mechanism. In this scenario, the mechanism typically treats the subject as untrusted. For every request from a subject, the mechanism needs to determine if the request is authorized or if it should refuse to service the subject. Using the polkit APIsu, a mechanism can offload this decision to a trusted party: The polkit authority.

In other words, polkit provides a mechanism by which applications can run parts of themselves with elevated privilege, in a similar way that sudo and other mechanisms do. There are no limits to the privilege that can be gained using polkit, and in particular there is nothing preventing it from allowing programs to run as any user, including root via the pkexec utiity. As well as polkit’s own documentation the Wikipedia article on it is fairly good.

An example of the sort of problem that polkit wants to solve, I think, is that it’s desirable that someone using a desktop system should be able to turn it off without needing to be a privileged user. But it’s rather undesirable that someone using the same machine via ssh for instance should be able to turn it off, even if they are the same user. So there needs to be some framework which lets you express the idea that ‘if this person is using a GUI on the console of this machine, they should be able to shut it down, but they should not be able to do that if they are not using the GUI on the console (for instance, they should almost certainly not be able to set up a cron or at job to turn the machine off)’. There are enough other such operations, such as connecting USB disks to machines, which need to have similar controls around them to make a general framework worth having.

Polkit ships as part of the basic installs of several Linux distributions, including (but not limited to):

  • RHEL 7;
  • Ubuntu 19.10 (older version of polkit);
  • CentOS 7 & 8.

Polkit is included as part of server as well as desktop installs of these platforms. I’m not sure what purpose it serves on server installs: I suspect that it may be used for device management.

A simple example of pkexec

pkexec is a command-line tool which uses polkit to decide whether a user is allowed to run a command as another user, with that other user being, by default, root:

$ groups
tfb wheel
$ id -u
1000
$ pkexec id -u
==== AUTHENTICATING FOR org.freedesktop.policykit.exec ====
Authentication is needed to run `/usr/bin/id' as the super user
Authenticating as: Tim Bradshaw (tfb)
Password:
==== AUTHENTICATION COMPLETE ====
0
$

So you can see that pkexec is doing the same thing that sudo would do: it has some rules which say that tfb is allowed to do things as root and is then asking that user to authenticate themselves. In fact, as configured on the machine this ran on, tfb is allowed to become root by virtue of being in the wheel group (sudo has equivalent rules on this machine).

Enough polkit to be dangerous

Polkit is a big complicated system and part of an even bigger and more complicated system: in order to understand it you need to read the manuals, and also to understand about how things like D-bus work. I don’t understand all of those things, but here is enough information to be able to poke around in the configuration files and get some idea about what is going on. This is not a definitive guide: reading the manuals or the source is the only way to get that.

There have been at least two versions of polkit: I’m mostly describing the newer one here. As of 19.10, Ubuntu still uses an older version.

The names of things

  • An unprivileged program making a request to polkit to do something is known as a subject.
  • What the unprivileged program is asking for is an action.
  • A privileged program which performs an action is a mechanism.
  • The thing that verifies whether a given subject can get a given mechanism to perform a given action is the authority.
  • An authentication agent is something which is asked by the authority to get someone or something authenticate themselves.

An overview of polkit

Polkit overview

Polkit overview

In this figure:

  • links in red are (usually?) mediated by dbus;
  • polkitd is the authority at the centre of the process, and deals with checking if an action is allowed, and getting authentication for it;
  • the policies files describe what actions exist;
  • the rules files provide rules which tell you if a given requested action should be allowed.

The most important part of the process is polkitd, together with the rules and policies files it consults.

I am fairly sure that the requesting program (subject) and the privileged program (mechanism) can be the same: this is the case for pkexec for instance. However it could be the intent is that the subject is whatever invoked pkexec in this case.

polkitd

polkitd is the daemon which is at the centre of polkit. Its job is to serve as the authority: it answers the question of whether a given request should be allowed or not and deals with any required authentication by talking to an authentication agent. polkitd does not itself have any particular privilege, and runs as the polkitd user: the questions it answers can be very critical to security however.

polkitd is configured by two sets of files:

  • policy files, also known as action files which describe what sort of ‘actions’ polkit knows about;
  • rules files, which describe the conditions under which a given action should be allowed.

Policy files

Policy files live in the /usr/share/polkit-1/actions/ directory, and have extension policy. All the files in that directory are read, and I’m reasonably sure that polkitd watches for changes in the directory and reads or rereads things appropriately.

Policy files are XML, and their content is described in polkit(8). The important elements are <action>s, which specify what the actions are. A given policy file can specify many actions. Because the files are XML and also because they often have a lot of internationalisation support they are fairly hard to read. However there’s a nice utility called pkaction which will tell you what actions exist and display them in a more readable format: pkaction on its own will list all of the available actions and pkaction --verbose will display details about them. You can also use the --action-id option to specify an individual action to display, as here:

$ pkaction --verbose --action-id org.freedesktop.policykit.exec
org.freedesktop.policykit.exec:
  description:       Run a program as another user
  message:           Authentication is required to run a program as another user
  vendor:            The polkit project
  vendor_url:        http://www.freedesktop.org/wiki/Software/polkit/
  icon:
  implicit any:      auth_admin
  implicit inactive: auth_admin
  implicit active:   auth_admin 

This corresponds to the following XML fragment2:

<action id="org.freedesktop.policykit.exec">
  <description>Run a program as another user</description>
  <message>Authentication is required to run a program as another user</message>
  <defaults>
    <allow_any>auth_admin</allow_any>
    <allow_inactive>auth_admin</allow_inactive>
    <allow_active>auth_admin</allow_active>
  </defaults>
</action>

The org.freedesktop.policykit.exec action is the one that pkexec uses to do things: the policy file that specifies it is probably /usr/share/polkit-1/actions/org.freedesktop.policykit.policy.

The interesting part of action specifications in policy files is their defaults: these tell you what is required to perform the action in various circumstances. pkaction reports these defaults as implicit ... at the end. It’s not completely clear from the documentation, but I strongly assume that these are minimum requirements for the action to be performed. In the example above, anything requesting the action is required to authenticate as an administrative user, and that authentication is not remembered for any period.

Additionally there can be annotations added, which are key/value pairs which let you specify various things like paths.

Rules files

Rules files live in two locations: /etc/polkit-1/rules.d and  /usr/share/polkit-1/rules.d, and have extension rules. All files in both directories are read, after being sorted in lexical order by filename, with files in /etc being read first when there’s a tie. The daemon watches for changes in the directories and rereads everything in that case.

The contents of rules files is JavaScript. Polkit defines an object called polkit and there are various methods on this object which do useful things:

  • addRule(fn) adds a rule, which is a function which, given arguments representing an action and a subject, is responsible for saying if the action is allowed and what authorisation is needed to run it;
  • addAdminRule(fn)adds a rule — a function again — which gets to say what counts as being an administrator;
  • log(message)will log things in some suitable way;
  • spawn(argv) will spawn a program, capturing its output.

The functions added by addRule are called in the order they were added, until one returns a non-null result, which can either unconditionally allow or deny the action, or require authorisation of various kinds.

The functions added by addAdminRule are called in the order they were added until one returns a description of what an administrator is.

These functions can call polkit.log(...) to log things and polkit.spawn(...) to run programs.

There are bounds on how long a rule may run for, and also on how long programs spawned by polkit.spawn(...) can run for.

More details on the rules files are in the documentation.

Example rules and actions

Here is a sample rule which tries to require administrator authentication to run pkexec:

polkit.addRule(function(action, subject) {
    if (action.id == "org.freedesktop.policykit.exec") {
        polkit.log("pkexec rule hit\n");
        return polkit.Result.AUTH_ADMIN;
    } else {
        polkit.log("pkexec rule missed\n");
        return polkit.Result.NOT_HANDLED;
    }});

If this is installed as, for instance /usr/share/polkit-1/rules.d/00-pkexec.rules then it will try to ensure that anyone trying to use pkexec requires administrator authorisation (equivalently: is required to authenticate themselves as an administrator). Since it is almost certainly first in the sort order, it also gets to control things before any other rules get their hands on things.

Except this rule does not work: it does catch actions whose id is org.freedesktop.policykit.exec, but these are not the only actions which pkexec can use: it can also use actions which have an org.freedesktop.policykit.exec.path annotation. For instance this policy file

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD polkit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/software/polkit/policyconfig-1.dtd">
<policyconfig>
  <vendor>The sinister TFEB organisation</vendor>
  <vendor_url>https://www.tfeb.org/</vendor_url>
  <action id="org.tfeb.tc.explode">
    <description>Explode</description>
    <message>Authentication is not required to explode</message>
    <annotate
        key="org.freedesktop.policykit.exec.path">/usr/sbin/explode</annotate>
    <defaults>
      <allow_any>yes</allow_any>
      <allow_inactive>yes</allow_inactive>
      <allow_active>yes</allow_active>
    </defaults>
  </action>
</policyconfig>

will allow /usr/sbin/explode to be run by pkexec with no authentication at all:

$ /usr/sbin/explode
exploded as UID 1000 GID 1000
$ pkexec /usr/sbin/explode
exploded as UID 0 GID 0

To catch this, one approach is to rely on the fact that the Action objects passed to the rule have properties which can be looked up with a lookup method, and pkexec sets a program property. So the following version of the above rule should catch all pkexec rules:

polkit.addRule(function(action, subject) {
    if (action.id == "org.freedesktop.policykit.exec"
        || action.lookup("program")) {
        polkit.log("pkexec rule hit\n");
        return polkit.Result.AUTH_ADMIN;
    } else {
        polkit.log("pkexec rule missed\n");
        return polkit.Result.NOT_HANDLED;
    }});

A similar rule can simply disable pkexec altogether3:

polkit.addRule(function(action, subject) {
    if (action.id == "org.freedesktop.policykit.exec"
        || action.lookup("program")) {
        polkit.log("pkexec rule hit\n");
        return polkit.Result.NO;
    } else {
        polkit.log("pkexec rule missed\n");
        return polkit.Result.NOT_HANDLED;
    }});

And now:

$ pkexec /usr/sbin/explode
Error executing command as another user: Not authorized

This incident has been reported.

Why polkit is a security disaster

There are at least two reasons why the way polkit works is a security disaster:

  • expressing rules in JavaScript (or any general programming language) is a terrible idea;
  • the implementation is deficient.

Writing rules in a general-purpose language is a terrible idea

It might seem like a clever idea to write rules in JavaScript:

  • using a general-purpose programming language means that very general rules can be implemented;
  • given that decision JavaScript is a common language which is not entirely awful.

But in fact this is a terrible idea, just because it means that very general rules can be implemented. In particular it is not possible, even in principle, to statically determine what polkit will allow or deny. JavaScript is a fully-fledged programming language which means that the only way you can know what a program will do, in general, is to run it. There is, at least, no halting problem since the execution time of the rules is bounded, but all of the other problems associated with general-purpose programming languages are still present.

What this means is that any kind of security analysis of a system needs to

  • check the rules are valid JavaScript, which can be done statically;
  • check what the rules do, which can’t be done statically, but requires the rules to be run.

A possible counter argument to this is

Well, only very simple rules will ever be written: no-one is actually going to make use of all this power. In particular the rules people actually write will be so simple that they can in fact be analysed statically.

That’s exactly the same argument as

Well, no-one is ever going to do anything bad, so they can all have the root password.

and it’s equally stupid. Secure systems should make it impossible to do things which are undesirable, not rely on people just not doing them. The language in which rules are expressed should be just expressive enough that allows the options needed, but no more expressive than that, and it should certainly always be possible to statically analyse a rule to know what it will allow. Using a general-purpose programming language for rules is just dumb.

Just to drive home this point it turns out that the rules supplied with the system are indeed mildly hard to analyse: here is /etc/polkit-1/rules.d/49-polkit-pkla-compat.rules from a CentOS 8 system:

polkit.addAdminRule(function(action, subject) {
        //polkit.log('Starting pkla-admin-identities\n');
        // Let exception, if any, propagate to the JS authority
        var res = polkit.spawn(['/usr/bin/pkla-admin-identities']);
        //polkit.log('Got "' + res.replace(/\n/g, '\\n') + '"\n');
        if (res == '')
                return null;
        var identities = res.split('\n');
        //polkit.log('Identities: ' + identities.join(',') + '\n');
        if (identities[identities.length - 1] == '')
                identities.pop()
        //polkit.log('Returning: ' + identities.join(',') + '\n');
        return identities;
});

polkit.addRule(function(action, subject) {
        var params = ['/usr/bin/pkla-check-authorization',
                      subject.user, subject.local ? 'true' : 'false',
                      subject.active ? 'true' : 'false', action.id];
        //polkit.log('Starting ' + params.join(' ') + '\n');
        var res = polkit.spawn(params);
        //polkit.log('Got "' + res.replace(/\n/g, '\\n') + '"\n');
        if (res == '')
                return null;
        return res.replace(/\n$/, '');
});

Well, it’s possible to work out what this is doing, if you try hard. But note that, in particular what it is doing is deferring to completely separate programs both to work out who administrative users are, and whether an action should be allowed. So now you need to understand that program as well. And yes, it is doing all sorts of string hacking to parse the output of that program, which is always a really good sign.

The implementation is deficient

Even given the design, polkit’s implementation is deficient.

The first and most obvious sign of deficiency is that rules can invoke external programs: those programs run as the polkitd user and can do anything it can do, including writing to the filesystem.

If SELinux is enabled on the system (which can be checked with sestatus), and if the correct policy is loaded, then it may well prohibit this, as polkit’s rules run under a policy which prevents them writing to the filesystem. But polkitd doesn’t check that SELinux is enforcing, or that the correct policy is in place: it just blunders on, trusting whatever external programs it runs to be well-behaved.

But this is only the start of the horrors. The actions, and even more so the rules that polkitd uses are security-critical. If I can install an early rule such as, for instance

polkit.addRule(function(action, subject) {
    return polkit.Result.YES;
});

then I have completely bypassed security on the system, because pkexec will let me do anything with no authentication at all.

So polkit, and specifically polkitd should be very careful about the ownership and permissions of the files and directories it looks at. In particular everything in the path down to any file it looks at should be owned by a privileged user and writable only by that user, and polkitd. That user should almost certainly be root. polkitd should check this every time it reads anything.

It doesn’t do that. In fact it doesn’t check at all:

$ id
uid=1000(tfb) gid=1000(tfb) groups=1000(tfb),10(wheel)
$ pwd
/usr/share/polkit-1/rules.d
$ ls -ld .
drwxrwx---. 2 polkitd tfb 80 Feb 24 14:23 .
$ cat > 00-bypass.rules
polkit.addRule(function(action, subject) {
    return polkit.Result.YES;
});
$ pkexec
#

In the presence of a massive, easily-detectible, security compromise like this, polkitd should refuse to do anything at all and log security alerts. It doesn’t: it just blunders on.

Finally, the default owner of, for instance, /usr/share/polkit-1/rules.d/ is polkitd: this might seem reasonable, except that it means that any external program spawned by a rule could, for instance write a rule (unless SELinux prevents this, which it will only do if it’s enabled). This is an acceptable risk only if you assume that no external program is ever compromised, even momentarily, and that if it is then all is immediately lost. It would also help if rules were easy to analyse: it’s quite possible to imagine a rule which could be persuaded to execute some program of an attacker’s choosing. This is all just extremely brittle: secure systems are not brittle.

I found these problems on rather casual inspection of polkit. There may very well be others, and I’d assume since I found these so easily that there are.

Conclusion

Polkit is yet another mechanism which allows privilege escalation on Linux systems: it has functionality broadly equivalent to programs like sudo. Every additional mechanism for privilege escalation increases the attack surface of the system and increases the burden on people who need to ensure the security of systems, and is thus undesirable of itself.

Additionally, polkit:

  • is significantly complicated;
  • has rules which govern privileged access which can’t be statically analysed in general by design, and which can invoke arbitrary programs during their evaluation;
  • has serious security problems in its implementation.

Polkit almost certainly contains other security problems. Red Hat, and probably other vendors, now ship polkit as part of core installs and will not support systems without it4. This means it’s hard to remove: a safe approach is therefore to defang it by installing a rule which simply denies access altogether: install a file in /etc/polkit-1/rules.d/00-defang.rules which contains

polkit.addRule(function(action, subject) {
    return polkit.Result.NO;
});

Such a rule should minimise the security risk from polkit, if it can’t be removed.


Appendices

Disclaimer

All of this is what I’ve worked out by playing around with polkit. Any of it may be wrong, and in particular all of the rules or actions above are only samples: you should check them yourself, and I’m not responsible if they don’t work.

If this happens5:

$ pkexec id -u
==== AUTHENTICATING FOR org.freedesktop.policykit.exec ====
Authentication is needed to run `/usr/bin/id' as the super user
Authenticating as: Tim Bradshaw (tfb)
Password:
polkit-agent-helper-1: error response to PolicyKit daemon: GDBus.Error:org.freedesktop.PolicyKit1.Error.Failed: No session for cookie
==== AUTHENTICATION FAILED ====
Error executing command as another user: Not authorized

This incident has been reported.

then this seems to be because of some problem with the authentication agent. Here is a terrible hack to make it work so you can test things.

  1. Open another terminal window to the same machine.
  2. In the main terminal window find the PID of the shell by echo $$.
  3. In the second window run pkttyagent --process PID, using the PID from the previous step.
  4. When you authenticate you will now get prompted by the pkttyagent running in the second window.

Yes, this is as horrid as it sounds, but it’s enough to get by.

Wat?

Wat.


  1. Previously known as ‘PolicyKit’. 

  2. The actual XML is more complicated than this as it includes versions of the description & message in several languages. The <action> element is also not the top-level element. 

  3. DISCLAIMER: while I believe this rule disables pkexec completely, I don’t warrant that it does: caveat emptor

  4. This raises questions about the approach of these companies to security, of course, which I’m not addressing here. 

  5. This seems to be a problem with RHEL 8, but not RHEL 7 (based on experiments with CentOS 8 & 7 respectively).