朝日ネット 開発者ブログ

朝日ネットのエンジニアによるリレーブログ。今、自分が一番気になるテーマで書きます。

脆弱性スキャナ Vuls の紹介と実運用時の工夫

はじめに

開発部の tasaki です。

皆さんはソフトウェアの脆弱性の検知をどのように行っていますか? NVD や JVN、Linux ディストリビューションの公式 Web サイトで脆弱性情報を逐一確認し、現在マシンにインストールされているパッケージと照らし合わせる……、というのは非常に骨の折れる作業です。 更にサーバが複数台あり、ディストリビューションもインストールされているパッケージもまちまち、という状況ではとても人の手でできたものではありません。

このような時に役に立つのがオープンソース (GPLv3) の脆弱性検知ツールである Vuls です 1 。 今回は Vuls の基本的な使い方を紹介し、加えて弊社で実際に運用してみて得た知見について書いていきます。

インストール

Vuls で脆弱性検知を行うには最低限以下の 3 つのソフトウェアが必要です。

  • vuls
    • サーバの脆弱性をスキャンし、メールや Slack で開発者に通知するソフトウェア
  • go-cve-dictionary
    • 脆弱性情報を取得するソフトウェア
  • goval-dictionary
    • 脆弱性を検知し対策するために必要な情報を取得するソフトウェア

amd64 な Linux であれば GitHub の Releases ページから最新版のバイナリをダウンロードして実行ファイルを PATH の通った場所に配置するだけで OK です。

より詳しいインストール方法については公式サイトをご参照ください。

vuls.io

今回は使いませんが、Docker Hub には Vuls 公式のコンテナも用意されています。

試してみる

例として CentOS 7 と Debian 9 (stretch) に対するスキャンをしてみましょう。

脆弱性検知の対象となるサーバを立てる

今回は説明の都合上 Docker コンテナとして CentOS 7 と Debian 9 (stretch) をベースとした ssh サーバを立てます。

このリポジトリの使い方については本筋ではないので割愛しますが(README.md をご参照ください)、 Dockerfile (CentOS 7, Debian 9) と docker-compose.yml を使って CentOS 7 サーバと Debian 9 サーバを立て、localhost の 2222, 2223 番ポートから ssh アクセスできるようにしています。 以下このサーバを対象としてスキャンを行います。

なお Vuls は Docker コンテナ内の脆弱性を検知する機能を標準で備えており、本来はわざわざ ssh サーバを立てる必要はありません。 Docker コンテナの正しいスキャン方法については「詳しい使い方:Docker コンテナのスキャン」節で後述します。

Vuls の設定を行う

Vuls の設定は TOML ファイルで行います。 作業ディレクトリに以下の内容の config.toml を作成します。

[servers.centos7]
host = "localhost"
port = "2222"
user = "vuls"
keyPath = "keys/id_ecdsa"
scanMode = ["fast"]

[servers.debian9]
host = "localhost"
port = "2223"
user = "vuls"
keyPath = "keys/id_ecdsa"
scanMode = ["fast"]
  • host : 脆弱性検知対象ホスト
  • port : ssh ポート
    • ここを "local" とすると ssh はせず localhost を対象としたスキャンとなります
  • user : ssh ログインユーザ
  • keyPath : ssh 秘密鍵
  • scanMode : スキャンモード

vuls configtest コマンドで設定のテストを行えます。

vuls configtest

なお、デフォルトのログファイル出力ディレクトリは /var/log/vuls です。 ホスト側に適切なパーミッションで /var/log/vuls ディレクトリを作成しておく必要があります。

sudo mkdir -p /var/log/vuls
sudo chown "$(id -u):$(id -g)" /var/log/vuls

あるいは各コマンドに -log-dir DIR コマンドラインオプションを指定してください。

mkdir logs
vuls configtest -log-dir logs

脆弱性情報を取得する

go-cve-dictionary コマンドで脆弱性データベースから最新の脆弱性情報を取得します。

go-cve-dictionary fetchnvd -last2y
go-cve-dictionary fetchjvn -last2y

実行が完了するとデフォルトでは cve.sqlite3 という SQLite3 のデータベースファイルが作成されます。 -last2y オプションを付けると過去 2 年の脆弱性情報を取得します。 今回は NVD (National Vulnerability Database), JVN (Japan Vulnerability Notes) の 2 つのデータソースを使います。

脆弱性にはそれぞれ一意の CVE-ID が振られています。 CVE については IPA の以下のページをご参照ください。

脆弱性を検知し対策するために必要な情報を取得する

前節で CVE-ID で管理される脆弱性情報を取得しましたが、これだけでは情報が足りません。 それぞれの Linux ディストリビューション上でどのように脆弱性を検知しどのように対策すればよいのか、ということが記載されたデータ(OVAL 定義データ)も必要となります。 goval-dictionary コマンドでこの情報を取得します。

goval-dictionary fetch-redhat 7
goval-dictionary fetch-debian 9

実行が完了するとデフォルトでは oval.sqlite3 という SQLite3 のデータベースファイルが作成されます。 今回は対象ホストが CentOS と Debian であるため RedHat と Debian の OVAL 定義データを使います。

OVAL については IPA の以下のページをご参照ください。

脆弱性をスキャンする

vuls scan コマンドで実際に対象サーバをスキャンします。

vuls scan

実行が完了すると results/(スキャン日時)/ ディレクトリ以下に JSON 形式で対象ホスト毎に結果が保存されます(例:results/2019-02-12T17:27:30+09:00/centos7.json)。 また、results/current に最新のスキャン結果のディレクトリがシンボリックリンクされます。

脆弱性を表示・通知する

vuls scan の実行結果の JSON ファイルには CVE や OVAL をデータソースとした脆弱性情報は埋めこまれていません。 後段でこの JSON ファイルを扱う必要がある場合は vuls report 系のコマンドを一度実行し、脆弱性情報の埋め込まれた完全な JSON にしておく必要があります。

ここで、vuls report コマンドを実行し検知された脆弱性情報を表示してみます。

vuls report

2019/02/12 現在の最新の Docker イメージをスキャンした場合、以下のような結果となりました。

centos7 (centos7.6.1810)
========================
Total: 5 (High:1 Medium:3 Low:1 ?:0), 5/5 Fixed, 157 installed, 14 updatable, 0 exploits, en: 0, ja: 0 alerts

+----------------+------+------+----------+---------+-------------------------------------------------+---------+
|     CVE-ID     | CERT | CVSS |  ATTACK  |  FIXED  |                       NVD                       | EXPLOIT |
+----------------+------+------+----------+---------+-------------------------------------------------+---------+
| CVE-2018-15688 |      |  9.8 |  Network |   Fixed | https://nvd.nist.gov/vuln/detail/CVE-2018-15688 |         |
| CVE-2018-16864 |      |  7.8 |    Local |   Fixed | https://nvd.nist.gov/vuln/detail/CVE-2018-16864 |         |
| CVE-2018-16865 |      |  7.8 |    Local |   Fixed | https://nvd.nist.gov/vuln/detail/CVE-2018-16865 |         |
| CVE-2018-5742  |      |  5.9 |  Network |   Fixed | https://nvd.nist.gov/vuln/detail/CVE-2018-5742  |         |
| CVE-2019-3815  |      |  3.3 |    Local |   Fixed | https://nvd.nist.gov/vuln/detail/CVE-2019-3815  |         |
+----------------+------+------+----------+---------+-------------------------------------------------+---------+


debian9 (debian9.7)
===================
Total: 0 (High:0 Medium:0 Low:0 ?:0), 0/0 Fixed, 229 installed, 0 exploits, en: 0, ja: 0 alerts

No CVE-IDs are found in updatable packages.
229 installed

(ちなみに今回はコンテナ内で sshd しか起動していないのでこれらの CVE については問題はありませんでした。また、yum upgrade すると脆弱性の表示は消えます。)

その他、vuls report には表示形式に関するオプションがあり、vuls tui を使うと結果を TUI (Text User Interface) で表示できます。

vuls report -format-full-text  # テキスト形式で詳細情報も表示
vuls report -format-json       # JSON 形式で保存
vuls report -to-slack          # Slack 通知(後述)
vuls tui                       # TUI 表示

config.toml に以下のような設定をして vuls report -to-slack コマンドを実行すると Slack Webhooks を利用した通知を行えます。

[slack]
hookURL = "https://hooks.slack.com/services/..."
authUser = "..."
channel = "#..."

f:id:ikasat:20190212101308p:plain

Slack 通知の詳しい設定方法については以下のページをご参照ください。

詳しい使い方

CPE 名指定での脆弱性検知

ディストリビューション標準のパッケージマネージャを経由せずインストールしたパッケージや、ソフトウェアではない周辺機器などの脆弱性を検知するには CPE (Common Platform Enumerations) を使います。 例えば nginx 1.15.2 の脆弱性を検知したい場合は config.toml のサーバ設定に cpeNames を以下のように追加します。

[servers.foo]
...
cpeNames = [
    "cpe:2.3:a:nginx:nginx:1.15.2:*:*:*:*:*:*:*"
]

CPE は NVD の以下のページから検索できます。 nginx 1.15.2 の CPE 名は cpe:2.3:a:nginx:nginx:1.15.2:::::::* です。

Vuls における CPE の詳しい設定方法については以下を参照してください。

CPE とは何か、ということを知りたい場合は IPA の以下のページをご参照ください。

未修正の脆弱性の検知 (gost)

Vuls v0.5 以降のバージョンではディストリビューション公式のセキュリティ修正パッチが提供されていない脆弱性の検知を gost を使うことで行えるようになりました。

gost のビルドには Go 処理系のインストールが必要です。 go get では一部の依存ライブラリのインストールに失敗するので dep を使う必要があります。 対象リポジトリ内で make install すると自動的に dep が使われます。

mkdir -p "${GOPATH}/src/github.com/knqyf263"
cd "${GOPATH}/src/github.com/knqyf263"
git clone https://github.com/knqyf263/gost.git
cd gost
make install

gost を利用する前にログ出力ディレクトリを作っておきます。 Vuls と同様に -log-dir コマンドラインオプションによる指定も可能です。

sudo mkdir -p /var/log/gost
sudo chown "$(id -u):$(id -g)" /var/log/gost

gost fetch で脆弱性情報を取得します。

gost fetch redhat
gost fetch debian

gost fetch redhat では --after オプションで期間の指定が可能です [^1] 。 CentOS (RHEL) と Debian については以下の API を利用しているようです。

実行が完了するとデフォルトでは gost.sqlite3 という SQLite3 のデータベースファイルが作成されます。

あとは先ほどと同様に vuls scan を行えば OK です。

fast-root scan / deep scan

Vuls には fast-root scan や deep scan というモードがあります。 fast-root scan ではパッケージリポジトリのデータ同期や再起動が必要なプロセスの検知、deep scan では更に ChangeLog のパースなどより時間のかかる処理が行われます。 config.toml のサーバ設定の scanMode を変更すればこれらのモードでサーバをスキャンできます。

scanMode = ["fast-root"]  # fast-root scan
# scanMode = ["deep"]     # deep scan

fast-root scan や deep scan モードではスキャン時に root 権限が必要な操作も行われるため、/etc/sudoers (または /etc/sudoers.d/...)に以下のページを参考にして設定を追加する必要があります。

fast scan, fast-root scan, deep scan の詳しい差異については以下のページを参照してください。

Docker コンテナのスキャン

Docker コンテナをスキャンしたい場合は config.toml のサーバ設定に containersIncluded = ["${running}"] を追加します。

[servers.foo]
...
containersIncluded = ["${running}"]

なお、Docker コンテナをスキャンするには vuls scan 対象ホストのログインユーザが sudo なしで docker コマンドを使える必要があり、 すなわち root でログインするか docker グループに含まれたユーザでログインすることになります。 もちろん一般ユーザを docker グループに含めるのはそれ相応のリスクを伴いますので実運用時には注意してください。

  • Docker コンテナに対して fast-root scan または deep scan を行う場合、cap_addSYS_PTRACE を付与しておく必要があります。
  • vuls scan -containers-only とすることでホストは無視してコンテナのみを対象としてスキャンできます。

実運用時の工夫

弊社では一昨年の秋頃より脆弱性検知ツールとして Vuls を利用し始め、社内の複数のサーバを対象として毎日脆弱性検知を行っています。 この節では実際に運用している最中に気付いたこと・改善したことについて述べます。

スキャン結果の集約

複数のサーバで同じ脆弱性が検知された場合(仮に CVE-XXXX-YYYY, CVE-XXXX-ZZZZ, ... とします)、出力結果の JSON は以下のような構造となります。

* サーバAのスキャン結果
    * CVEの一覧 (scannedCves)
        * CVE-XXXX-YYYY
            * 影響を受けるパッケージ一覧 (affectedPackages)
                * 影響を受けるパッケージ 1
                    * パッケージ名 (name)
                    * 未修正かどうかのフラグ (notFixedYet)
                * 影響を受けるパッケージ 2
                * ...
            * CVE の詳細一覧 (cveContents)
                * CVE の詳細 1
                    * データソースの種別 (type)
                    * タイトル (title)
                    * CVSS ベクタ (cvss3Vector, cvss2Vector)
                    * CVSS スコア (cvss3Score, cvss2Score)
                    * リンク (sourceLink)
                    * 参照先一覧 (references)
                * CVE の詳細 2
                * ...
        * CVE-XXXX-ZZZZ
        *  ...
* サーバBのスキャン結果
    * CVEの一覧
        * CVE-XXXX-YYYY
        * CVE-XXXX-ZZZZ
        *  ...
* サーバCのスキャン結果
    * CVEの一覧
        * CVE-XXXX-YYYY
        * CVE-XXXX-ZZZZ
        *  ...
* ...

これをそのまま vuls report -to-slack で Slack 通知すると CVE のデータが重複して見辛くなってしまいます。 また、Red Hat から RHSA (Red Hat Security Advisory) が出ている場合、同じ RHSA に属する CVE をまとめて表示できればより見やすくなりそうです。

というわけで、出力結果の JSON をうまくピボット&マージし、以下のような構造を持つ別の JSON に変換します。

* スキャン結果(脆弱性一覧)
    * 脆弱性 1
        * タイトル(RHSA 名、または影響パッケージから自動生成)
        * 影響を受けるサーバ一覧
            * サーバA
            * サーバB
            * サーバC
            * ...
        * 影響を受けるパッケージ一覧
            * 影響を受けるパッケージ 1
                * パッケージ名
                * 未修正かどうかのフラグ
            * 影響を受けるパッケージ 2
            * ...
        * CVE の詳細一覧(タイトルが同じ CVE を集約する)
            * CVE-XXXX-YYYY
                * CVSS スコア
                * リンク
            * CVE-XXXX-ZZZZ
            * ...
        * RHSA の参照先(あれば)
    * 脆弱性 2
    * 脆弱性 3
    * ...

この JSON を元に、以下のような Slack 通知を行うようにしています。

f:id:ikasat:20190212174706p:plain

なお、先ほど「うまくピボット&マージして」とひとことで書きましたが、実際には「typeredhat な cveContent が無い場合は nvdjvn にフォールバックする 2 」 「cvss3Vector 3 が空である場合は cvss2Vector にフォールバックする」「前回スキャンとの差分を取る」「cveContent が更新されていた場合はその旨を通知する」「Vuls v0.5 以降の JSON 出力に対応する 4 」などといった処理を足していくうちに非常に泥臭いソースコードになってしまいました……。ちなみにこの JSON の変換と Slack への通知を行うプログラムは Python のスクリプトとして書いてありますが 5 、スキーマが比較的定まっている巨大な JSON を別の巨大な JSON を変換するような処理は静的型付き言語で書いた方がよかったという後悔もややあります。もし同じような処理を書こうとしている方がいらっしゃればご参考までに……。

スキャン結果の HTML 出力

Slack には前回スキャンとの差分だけ投稿していますが、同時に Jinja2 (Python で動作するテンプレートエンジン)で以下のような静的 HTML を生成し全結果を閲覧できるようにしています。

f:id:ikasat:20190212105905p:plain

VulsRepo を使う

今回紹介しきれませんでしたが Vuls には VulsRepo というフロントエンドがあり、上記の静的 HTML 出力とは別にこちらも利用しています。

VulsRepo を使うとよりグラフィカルに Vuls の結果を閲覧できます。 以下のような vulsrepo-config.toml を書いて vulsrepo/server/vulsrepo-server を実行すると localhost:5111 に Web サーバが立ち上がります。 注意点として、指定する rootPathresultPath は絶対パスである必要があるようです。

[server]
rootPath = "/path/to/vulsrepo"
resultsPath = "/path/to/results"
serverPort = "5111"

おわりに

今回は脆弱性スキャナである Vuls を紹介しました。

この記事を執筆している 2019/02/12 には runc の脆弱性 (CVE-2019-5736) が公開されています。 ソフトウェアを利用する限り脆弱性対応は避けては通れませんが、Vuls は非常に手軽に導入できる脆弱性スキャナであり、少ない労力で脆弱性検知の手間を確実に減らしてくれます。 もちろん脆弱性を検知すること自体よりも検知した後のアクションの方が重要です。 Vuls を導入する際には是非ソフトウェアのテストとアップデートを適切に行えるような環境も同時に整えていきましょう。

採用情報

朝日ネットでは新卒採用・キャリア採用を行っております。


  1. gost fetch redhat --after="$(date -d '2 years ago' +'%Y-%m-%d')" のように(GNU coreutils の)date コマンドを使うとよいでしょう。

  2. gost を使う場合更に redhat_api, debian_security_tracker という type が加わります。

  3. CVSS については 共通脆弱性評価システムCVSS概説:IPA 独立行政法人 情報処理推進機構 をご参照ください。

  4. Vuls v0.4 までは出力結果の JSON のフィールド名は UpperCamelCase (PascalCase) でしたが、Vuls v0.5 以降 lowerCamelCase に変更されています。

  5. 余談ですが 11 月に書いた Python の記事(2019年に向けてPythonのモダンな開発環境について考える)へのアクセス数が 1 月になって急激に伸び、大変驚きました(本当に余談)。ありがとうございます。