sudoの脆弱性 CVE-2019-14287について実装を追ってみる 2019年10月16日
注意書き
あくまでも個人が,研究の息抜きに趣味程度に調べたことである.隅々まで調べたわけではないため誤りを含んでいる可能性がある.(間違いを見つけたらこっそり教えて下さい)鵜呑みにしないこと.検証すること.
体系的に記述しておらず,また飛び飛びで分かりづらいのは申し訳ない.
TL; DR;
- 2019年10月14日に公開された,sudoの脆弱性CVE-2019-14287をコードレベルで追ってみた
- 公開されている情報のとおり,uidが-1かどうかのcheckをせず,setresuidに渡しているため,sudoersの制約( “!root”; rootとしての実行を禁止) を回避しrootとして実行できていた
- patch自体はrepoではすでにできている様子なので,そのうち各Linuxのpackage managerでも入るようになると思われる
- patchを待てない人は,sudoersにおいて 次のような,root以外の人のユーザでコマンドが実行できる,といった記述がある場合,特定のユーザの権限としてのみ実行可能,と言った記述などに修正する(可能なら)
- x
user1 ALL=(ALL, !root) /path/to/cmd
- o
user1 ALL=(user2) /path/to/cmd
どのような脆弱性か
JPCERTでの注意喚起にもあるとおり,
sudoersにおいて,あるユーザに対し,「root以外の任意のユーザでコマンドを実行可能」と記述されている場合において,「root以外の」という制約を回避することができる脆弱性,である.
JPCERTの例ではあるコマンドとしているが,もちろん以下のようにすべてのコマンドを実行可能としてもよい.この例では,user “test”に対し,任意のコマンドをroot以外の任意のユーザで実行可能としている.
test ALL=(ALL, !root) ALL
(このようなruleがどのような条件下で設定されるのか,という点で懐疑的であるため実例が知りたい)
この条件下において,以下のように脆弱性が再現できる.
$ id
uid=1001(test) gid=1001(test) groups=1001(test)
$ sudo -u root id
[sudo] password for test:
Sorry, user test is not allowed to execute '/bin/id' as root on localhost
$ sudo -u#-1 id
[sudo] password for test:
uid=0(root) gid=1001(test) groups=1001(test)
だれが影響を受るのか
JPCERTのページでも記述されている通り,また上でも記述したとおり,
「root意外の任意のユーザとしてコマンドの実行を許可」
するようなsudoersになっていれば影響を受ける.一方で,そうでない人や,パッチがあたっている人は影響を受けない.おおよそ,ほとんどの人には無関係な脆弱性のはずである(未確認).
patchを確認する
commit logを追う限り,次の修正が本脆弱性に対するpatchであるように見える.
https://github.com/sudo-project/sudo/commit/f752ae5cee163253730ff7cdf293e34a91aa5520
id_tは環境に応じてlongまたはlong longとなるようで,環境に応じて修正が異なる.(再現環境のCentOS 8, x86_64 ではlongであった.)
なお,最新のcommitにおいては,id_tがlongかlong longかのmacroが消失している.
この修正で,-1を入力することができなくなった. また,id_tがlongな環境に置いては,errno=ERANGEを見るようになり,4294967295を含めることもできなくなった.
脆弱性が起きる流れ
ざっくりいうと,uidに-1を指定した状態でsudoすると,setresuidで権限をsetする際,-1がuidにそのまま渡ることに起因する.
-1をuidに渡した場合,uidは変更しない,という処理になることから,rootのまま実行される.(sudoにはsetuid bitが立っているので,sudoそのものはrootとして実行される)
なお,正確にはsetresuidのuidの方はuid_tであるので,実際の値は4294967295である.
後述するが,ここのようにid_t -> uid_tのcastをしていることから,id_tがlongな環境に置いては,-1はcastされて4294967295になる.(-1でも4294967295でもいいのは,このcastによる.)
-u#-1が渡されたときのざっくりとした内部処理
ダラダラ書きすぎた,すみません… / 処理の概要は本記事には書いておらず,下記の記述は部分部分の記述のため,codeを読みながら見たほうがよいと思われrる.これらはcommit id : fd5d0f511efa009cd93edcdabc693f839818daadを元に作成している.
- set_runaspw()で,#-1から-1をsudo_strtoid()に投げる
- 4294967295を投げた場合はlong_maxを超えてしまって-1が返るsudo_strtoidはid_tを返すが,uid_tにcastするので,-1は4294967295になる
- set_runaspw()のその後の処理で,uidが4294967295のユーザを探しに行く.
- いる場合はpwにそのユーザの情報を含むpasswd構造体へのポインタ が入る
- いない場合はfakeなpwエントリが生成されてpwにが入る
- fakeなエントリは, pw_nameが#-1,pw_uidが4294967295となる
- runas_pw (global variable)に生成されたpwエントリのポインタを保存
- sudoersをparseして各エントリを確認するところはここ
- matchしなければ次のエントリを読む
- matchしたらエントリの中をチェックする
- allowed userlistに対する(ALL, !root)の評価はここ
- 権限設定はここ
- このuidは,ここでrunas_pwからcommand_infoにcopyされている. command_info_to_details()で,command_infoからこのdetailsにcopyされる.
- command_infoからpw_uid(=4294967295)を取り出して再度strtoidしてdetails->uidに入る
- なんでわざわざuidをstringにしてまた戻すんだ… :thinking_face:
- command_infoからpw_uid(=4294967295)を取り出して再度strtoidしてdetails->uidに入る
- uidは4294967295 (signed intで-1)
- setresuidでは,uidに-1 (4294967295) が与えられた場合uidを変更しない
- sudoはsetuid bitが立っているので,この時点でのuidはroot
- 結果として,コマンドがrootで実行される
- このuidは,ここでrunas_pwからcommand_infoにcopyされている. command_info_to_details()で,command_infoからこのdetailsにcopyされる.
所感
- 流し読み程度にsudoの実装を読んでみたが思っていたよりもカオスだった
- 脆弱性そのものについては,原因はまぁそういうことですよね,という感じで大方予想からは外れていなかった
- とはいえcode読むのは楽しい
- ガッツリ読んだわけではないので,codeの意図は深くは捉えられていない
- 上記にも書いたが, (ALL, !root) を書かなければならないケースが思いつかない
- みんな割と騒いでいるように見えるので,僕が知らないだけで実はそういうケースに満ちあふれているのだろうか...?
- id_tがlong longな環境って何があるんだろう...
P.S.
先週誕生日でしたのでほしいものリストを置いておきますね.
Leave a Reply