実行順序から解き明かす、sudoコマンドの脆弱性(CVE-2019-14287)が発生した理由:OSS脆弱性ウォッチ(16)(1/2 ページ)
連載「OSS脆弱性ウォッチ」では、さまざまなオープンソースソフトウェアの脆弱性に関する情報を取り上げ、解説する。今回は、2019年10月14日に発生したsudoコマンドの脆弱性(CVE-2019-14287)について。
「OSSセキュリティ技術の会」の面和毅です。本連載「OSS脆弱性ウォッチ」では、さまざまなオープンソースソフトウェア(OSS)の脆弱(ぜいじゃく)性に関する情報を取り上げ、解説しています。
2019年10月14日に、「sudo」コマンドの脆弱性情報(Important:CVE-2019-14287)と新バージョン(1.8.28)が公開されました。広範囲のsudoを対象とし、「特定のアクセス制限を迂回(うかい)できる」という問題の内容から、あちこちで反響が広がっています。そこで今回は、この脆弱性を詳しく掘り下げ、「どのような理由で脆弱性が発生しているのか」について、動きと対象をより詳細に探ります。
なお説明の際のソースコードは、本家からダウンロードしたものを参照しており、それぞれ下記として説明しています。
- 脆弱性のあるバージョン:sudo-1.8.28rc2
- 修正されたバージョン: sudo-1.8.28
通常のsudoは、どのように動作するのか
今回の脆弱性の前に、通常のsudoは、どのように動作するのかについて、ソースコードを参照しながら簡単に見ていきます。
sudoは下記のようにSUIDビットが立っているため、一般ユーザーで起動しても「root」として実行されます。そのため、以降のプロセスはrootとして実行されます。
jsosug@localhost:~$ ls -l /bin/sudo -rwsr-xr-x 1 root root 586560 10月 30 10:49 /bin/sudo
sudoの実行順序を追っていくのはなかなか骨が折れますが、脆弱性のあるバージョン(1.8.28 rc2)について見ていきましょう。
【Case1】ユーザー名が「/etc/sudoers」に列記されている場合
1.まずsudoでコマンドを実行する途中で「plugins/sudoers/parse.c」ファイル中の「sudoers_lookup_check()」関数を使って、「/etc/sudoers」に設定された各ユーザー(sudoを使用したユーザー)の設定を見ていきます。
この中で、/etc/sudoersファイル中の「ユーザーに関する設定」を見る際に下記で、runaslist_matches()関数を呼び出します。
152 matching_user = NULL; 153 runas_match = runaslist_matches(nss->parse_tree, 154 cs->runasuserlist, cs->runasgrouplist, &matching_user, 155 NULL);
2.plugins/sudoers/match.cファイル中にある「runaslist_match()」関数を使って、/etc/sudoersに設定されたユーザーの情報を見ていきます。
この中で、「user_list」(sudoersでrunasに設定されたユーザー制限の部分の文字列)をチェックします。例として下記のような/etc/sudoersが設定されているとします。
test ALL=(hoge, fuga, jsosug,foo,!test) ALL
この状態で下記を実施したとします。
test@localhost:~$ sudo -u jsosug /bin/cat /home/jsosug/hidden
この場合、ユーザーに関しては後ろからチェックしていく処理になっていて、まず「!test」を「m->name」に入れて処理を行います。
ここで、member構造体「m」は「plugins/sudoers/parse.h」ファイルで定義されていて下記のようになっています。
struct member { TAILQ_ENTRY(member) entries; char *name; /* member name */ short type; /* type (see gram.h) */ short negated; /* negated via '!'? */ };
チェックを行い、「!test」文字列自体は、「ALL」などの特別なものではない単語になっているため、runaslist_matches()中の下記処理を行います。
202 case WORD: 203 if (userpw_matches(m->name, runas_pw->pw_name, runas_pw)) 204 user_matched = !m->negated; 205 break;
以降、ユーザー名がマッチするまで「foo」、最後に「jsosug」を処理していきます。ここで、jsosugが設定されていることを見つけます。またユーザー名を確認する際に「!」で始まっているかどうかのチェックも行っており、下記のように「negated」という変数(初期値は0です)を1または0で指定する処理を行います。
- !が付いている→negated=1
- !が付いていない→negated=0
ここでは、「m->name」としてjsosugが入り、negated=0が入るので、上述のuser_matchedには1が入るようになります。ここで、もしもuser_matched=1(すなわち、!jsosug)になっていた場合は下記に引っ掛かり、「DENY(0)」が返されます。それ以外は「user_matched(1)」が返されます。
280 if (user_matched == DENY || group_matched == DENY) 281 debug_return_int(DENY);
3.元のsudoers_lookup_check()に処理が戻り、下記のようになって、「ユーザー名がsudoersに記載されており、大丈夫」ということになったのでコマンド処理が進みます。
156 if (runas_match == ALLOW) { 157 cmnd_match = cmnd_matches(nss->parse_tree, cs->cmnd); 158 if (cmnd_match != UNSPEC) {
4.さまざまな(割愛)処理を行った後にsrc/exec.c中の処理が戻り、下記でuidを、「details->uid」(今回の場合はjsosugのUID)、「details->euid」(今回の場合はjsosugのUID)として使って、setresuid()を呼び出します。
203 #if defined(HAVE_SETRESUID) 204 if (setresuid(details->uid, details->euid, details->euid) != 0) { 205 sudo_warn(U_("unable to change to runas uid (%u, %u)"), 206 (unsigned int)details->uid, (unsigned int)details->euid); 207 goto done; 208 }
setresuid()は、Linux Manページにもあるように、ユーザーやグループの実効、保存IDを設定するシステムコールです。
この結果、sudoを使って実行したコマンド「/bin/cat」のUIDがjsosugに設定され、src/exec.c中の「exec_cmnd()」で実行されます。
test@localhost:~$ sudo -u jsosug /bin/cat /home/jsosug/hidden This is hidden file. test@localhost:~$
【Case2】ユーザー名に「ALL」がある場合
1.sudoers_lookup_check()での処理は【Case1】と同じです。
2.plugins/sudoers/match.cファイル中にある「runaslist_match()」関数を使って、/etc/sudoersに設定されたユーザーの情報を見ていきます。
この中で、user_list(sudoersでrunasに設定されたユーザー制限の部分の文字列)をチェックします。例として下記のような/etc/sudoersが設定されているとします。
test ALL=(hoge, fuga, ALL,!root) ALL
※注
先述のLinux Manページを見て分かるように「ALL,!root」で「ALLからのroot除外」を表すものになるため、同じ意味(rootを除外した全てのユーザーという意味)で使うならば、「!root,ALL」のように記述を入れ替えて使うことはできません。
この状態で下記を実施したとします。
test@localhost:~$ sudo -u jsosug /bin/cat /home/jsosug/hidden
この状態ですと、ユーザーを後ろから確認していくため、まず!rootが処理されます。そして【Case1】のときと同じロジックで、まずrootでの実行は否定されます。
3.runaslist_match()が呼び出されます。この際に下記のようになり、user_matchedには初期値0の否定の1が入ります。
174 if (user_list != NULL) { 175 TAILQ_FOREACH_REVERSE(m, user_list, member_list, entries) { 176 switch (m->type) { 177 case ALL: 178 user_matched = !m->negated; 179 break;
結果、ALLがあった場合には「sudoersにユーザーが許可される形で存在していた」と見なされてコマンド処理が進みます。
4.【Case1】のときと同様、さまざまな(割愛)処理を行った後にsrc/exec.c中の処理に戻り、下記でuidをdetails->uid(今回の場合にはjsosugのUID)、details->euid(今回の場合にはjsosugのUID)を使ってsetresuid()を呼び出します。
203 #if defined(HAVE_SETRESUID) 204 if (setresuid(details->uid, details->euid, details->euid) != 0) { 205 sudo_warn(U_("unable to change to runas uid (%u, %u)"), 206 (unsigned int)details->uid, (unsigned int)details->euid); 207 goto done; 208 }
この結果、sudoを使って実行したコマンド(/bin/cat)のUIDがjsosugに設定され、src/exec.c中のexec_cmnd()で実行されます。その結果、「UID:jsosug」として「/bin/cat /home/jsosug/hiden」が実行され、下記のように表示されます。
test@localhost:~$ sudo -u jsosug /bin/cat /home/jsosug/hidden This is hidden file. test@localhost:~$
Copyright © ITmedia, Inc. All Rights Reserved.