実行順序から解き明かす、sudoコマンドの脆弱性(CVE-2019-14287)が発生した理由OSS脆弱性ウォッチ(16)(1/2 ページ)

連載「OSS脆弱性ウォッチ」では、さまざまなオープンソースソフトウェアの脆弱性に関する情報を取り上げ、解説する。今回は、2019年10月14日に発生したsudoコマンドの脆弱性(CVE-2019-14287)について。

» 2019年11月18日 05時00分 公開
[面和毅OSSセキュリティ技術の会]

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

 「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:~$
       1|2 次のページへ

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。