
Cベースの概念実証エクスプロイト for CVE-2021-4034 (PwnKit)。pkexecの範囲外書き込みを介してローカル特権昇格を可能にします。ビルド手順と脆弱性の説明を含みます。
CVE-2021-4034
debian@debian:~/PwnKit-Exploit$ make
cc -Wall exploit.c -o exploit
debian@debian:~/PwnKit-Exploit$ whoami
debian
debian@debian:~/PwnKit-Exploit$ ./exploit
Current User before execute exploit
hacker@victim$whoami: debian
Exploit written by @luijait (0x6c75696a616974)
[+] Enjoy your root if exploit was completed succesfully
root@debian:/home/debian/PwnKit-Exploit# whoami
root
root@debian:/home/debian/PwnKit-Exploit#
| Command | Use |
|---|---|
sudo chmod 0755 pkexec | CVE 2021-4034 の修正 |
git clone https://github.com/luijait/PwnKit-Exploit
cd PwnKit-Exploit
make
./exploit
whoami
| Command | Utility |
|---|---|
make clean | 修正したコードをテストするためのビルドをクリーンアップ |
qualys.com のブログに基づく
pkexec の main() 関数の先頭では、コマンドライン引数を処理し (534-568行)、実行するプログラムのパスが絶対パスでない場合、PATH 環境変数のディレクトリ内で検索します (610-640行):
435 main (int argc, char *argv[])
436 {
...
534 for (n = 1; n < (guint) argc; n++)
535 {
...
568 }
...
610 path = g_strdup (argv[n]);
...
629 if (path[0] != '/')
630 {
...
632 s = g_find_program_in_path (path);
...
639 argv[n] = path = s;
640 }
残念ながら、コマンドライン引数の数 argc が 0 の場合、つまり execve() に渡す引数リスト argv が空 ({NULL}) の場合、argv[0] は NULL になります。これが引数リストの終端です。したがって:
534行目で、整数 n は恒久的に 1 に設定されます; 610行目で、ポインタ path が argv[1] から範囲外読み取りされます; 639行目で、ポインタ s が argv[1] に範囲外書き込みされます。 しかし、この範囲外の argv[1] からは正確に何が読み取られ、書き込まれるのでしょうか?
この質問に答えるには、少し脱線する必要があります。新しいプログラムを execve() すると、カーネルは引数、環境文字列、ポインタ (argv と envp) を新しいプログラムのスタックの末尾にコピーします。例:
|---------+---------+-----+------------|---------+---------+-----+------------|
| argv[0] | argv[1] | ... | argv[argc] | envp[0] | envp[1] | ... | envp[envc] |
|----|----+----|----+-----+-----|------|----|----+----|----+-----+-----|------|
V V V V V V
"program" "-option" NULL "value" "PATH=name" NULL
明らかに、argv と envp のポインタはメモリ上で連続しているため、argc が 0 の場合、範囲外の argv[1] は実際には envp[0]、つまり最初の環境変数「value」へのポインタになります。その結果:
610行目で、実行するプログラムのパスが argv[1] (すなわち envp[0]) から範囲外読み取りされ、「value」を指します; 632行目で、このパス「value」は g_find_program_in_path() に渡されます (629行目で「value」がスラッシュで始まらないため); 次に、g_find_program_in_path() は PATH 環境変数のディレクトリ内で「value」という名前の実行可能ファイルを検索します; そのような実行可能ファイルが見つかると、そのフルパスが pkexec の main() 関数に返されます (632行目); 最後に、639行目で、このフルパスが argv[1] (すなわち envp[0]) に範囲外書き込みされ、最初の環境変数が上書きされます。 つまり、より正確に言うと:
PATH 環境変数が「PATH=name」で、ディレクトリ「name」が (カレントワーキングディレクトリに) 存在し、その中に「value」という名前の実行可能ファイルがある場合、「name/value」という文字列へのポインタが envp[0] に範囲外書き込みされます; または PATH が「PATH=name=.」で、ディレクトリ「name=.」が存在し、その中に「value」という名前の実行可能ファイルがある場合、「name=./value」という文字列へのポインタが envp[0] に範囲外書き込みされます。 言い換えれば、この範囲外書き込みにより、「安全でない」環境変数 (例えば LD_PRELOAD) を pkexec の環境に再導入できます。これらの「安全でない」変数は通常、main() 関数が呼び出される前に ld.so によって SUID プログラムの環境から削除されます。次のセクションでは、この強力なプリミティブを悪用します。
直前の注意: polkit は Solaris や *BSD などの非 Linux オペレーティングシステムもサポートしていますが、それらの悪用可能性は調査していません。ただし、OpenBSD は悪用できないことに注意してください。そのカーネルは argc が 0 の場合にプログラムの execve() を拒否するためです。