FutureVuls Blog

内部と外部をつなぐ脆弱性管理|FutureVulsでASM・Nmap・Nessusなどの診断結果を活用する方法

eyecatch

はじめに

こんにちは、FutureVuls開発チームの和田皓翔(わだ ひろか)です。

10月21日のリリースで、新たにネットワークスキャンツールやWeb脆弱性診断結果をFutureVulsに取り込む機能が追加されました。これにより、ASM (Attack Surface Management)、Qualys、Nessus、Nmapなどで取得したインターネットからの外部スキャン結果をFutureVulsにインポートし、内部の脆弱性とインターネットに露出している脆弱性を紐付けて管理できるようになりました。

脆弱性のスキャン方法には、インターネットからの外部スキャンと、FutureVulsのような内部情報に基づくスキャンの2つの方法があります。それぞれのメリット・デメリットを以下の表にまとめました。

スキャンタイプ メリット デメリット
外部スキャン - インターネット上の脆弱性をスキャンできる
- 導入が簡単(内部にセットアップ不要)
- 検知精度が低く、簡易診断(網羅性が低い)
- 内部の具体的な脆弱性は追加調査が必要
内部スキャン - 完全な資産情報を基に脆弱性を検知(精度・網羅性が高い) - 内部にセットアップが必要で導入工数が高い
- インターネット上の脆弱性は不明

では、どちらか一方のスキャンだけで十分なのでしょうか?

いいえ、そうではありません。理想は両方の方式を併用し、それぞれのメリット・デメリットを理解して効果的に組み合わせることです。

先日、ISOG-Jから公開された「ASM導入検討を進めるためのガイダンス(基礎編)」の「2.2 ASMのプロセス」には、次のように記載されています。

従来の管理方法(ルールベースでの管理)でIT資産を網羅的に把握し、把握した全てのIT資産、少なくとも全てのアタックサーフェスに対して脆弱性診断によるリスク評価を行うことができれば、理想的な管理状態に近いと考えられます。しかし、従来の管理方法での網羅的な把握も、全てのアタックサーフェスへの脆弱性診断も、コスト的な面から見て現実的ではありません。

IT資産を網羅的に把握して脆弱性を管理するFutureVuls方式と、インターネットからのスキャンを組み合わせる方法が理想的ですが、コスト面での制約があることが指摘されています。

ASMでのIT資産の検出+リスク評価のプロセスが有効に働けば、従来の管理方法では拾いきれなかったIT資産が発見され、高リスクなIT資産を優先して対策可能になるため、ASMだけでよいと考える方もいると思います。しかし、ASMでは資産検出は検索やクローリングなどにより稼働しているシステムを探すアプローチが多いことから、全てのIT資産を把握しきることは難しく、内部情報との連携も難しいと考えられます。また、リスク評価においても脆弱性診断と同程度ではなく、簡易的なものであることが多いです。
そのため、従来の管理方法(内部で管理している情報)とASM(外部から取得した情報)を組み合わせて、従来の管理方法では漏れてしまっていたIT資産を把握するようにIT資産管理プロセスを組み立てることをお勧めします。

ASMのようなインターネット側からのチェックだけでは、資産の列挙、内部情報の連携、検出精度に問題があり簡易的なチェックでとなることが指摘されています。

この記事では、このレポートに書かれている、「FutureVulsによる網羅的な管理とアタックサーフェス診断結果を組み合わせて、理想に近い脆弱性管理」を実現する方法を説明します。

具体的には、外部スキャン結果をFutureVulsにインポートし、インターネットに露出した脆弱性への対応を強化する方法について詳しく解説します。また、大量のIPや高頻度の外部スキャンが必要な場合、FutureVulsのREST APIを使って継続的なインポートを自動化する方法についても紹介します。

新機能の概要

Qualys、Nessus、NmapなどのネットワークスキャンツールやWeb診断、ASM (Attack Surface Management) の結果をFutureVulsに取り込むことができるようになりました。

機能の概要

この機能により、以下の利点があります:

  • 外部に露出した脆弱性を即座に把握
    FutureVulsで検知した脆弱性の中から、外部に露出しているものを画面上で素早く特定できます。外部に露出している脆弱性が、実際のどのサーバやアプリケーションに影響があるのかを画面上でドリルダウンで確認できるため迅速に対応可能です。

  • チケットでの一元管理
    インポートしたスキャン結果をFutureVulsのチケット機能で一元管理できます。担当者、期限、優先度を設定し、進捗状況や対応履歴をチーム全体で共有できます。また、外部露出した脆弱性が放置されるのを防ぎます。

  • 自動リスク評価で優先順位の明確化
    FutureVulsのSSVC (Stakeholder-Specific Vulnerability Categorization) 機能により、インポートしたスキャン結果を自動でリスク評価し、優先順位付けできます。

  • 詳細な情報の表示
    FutureVulsが保持する脆弱性情報や最新の脅威情報を表示し、より精度の高い対応をサポートします。

外部スキャン結果のインポート手順

スキャン結果の取得と準備

まず、外部スキャンツールを使って脆弱性診断を行い、CVE-IDリストを取得します。取得した結果はCSV形式で保存し、ExcelやGoogleスプレッドシートで管理すると便利です。

cveIDのリストを取得

FutureVulsへのインポート方法

取得した外部スキャン結果をFutureVulsにインポートするには、以下の手順に従います:

  1. FutureVulsのサーバ詳細画面で「外部スキャン」ボタンをクリックします。
    外部スキャン結果一覧

  2. 「外部スキャン結果の追加」ボタンを押して、追加ダイアログを開きます。
    外部スキャン結果の追加ボタン

  3. スキャン名とCVE-IDリストを入力し、「外部スキャン」としてインポートします。

外部スキャン結果の追加ダイアログが開きます。各項目について説明します。

項目 詳細
外部スキャン名 追加する外部スキャン結果の名前
CVE-IDのリスト 外部スキャンで出力されたCVE-IDのリスト
外部スキャン 追加する外部スキャン結果の脆弱性が外部から露出しているものであるかどうか。デフォルトでチェックが入っている。

外部スキャン結果の入力例

以上でインポートが完了しました。

一覧画面での確認方法

外部スキャン結果をインポートした後、脆弱性はFutureVulsコンソールに即時反映されます。タスクについては次回スキャン時に反映されますが、「サーバ詳細>手動スキャン」を行うことで即時反映させることも可能です。

外部スキャン列

「外部スキャン」列は以下の一覧画面に追加されています。

タブ名 サブタブ名
脆弱性 脆弱性一覧
脆弱性詳細 > タスク x サーバ
タスク タスク一覧 (グループセットを除く)
ロール ロール詳細 > 脆弱性 x タスク
サーバ サーバ詳細 > 脆弱性 x タスク

トリアージ編

疑似サーバを作成し、スキャン結果をインポートする

では、実際にインターネットからのスキャン結果をインポートし、外部の脆弱性スキャン結果と内部サーバの情報を紐付けてみましょう。

一般的に、ASMやNessusなどのスキャン結果が内部のどのサーバやライブラリに該当するのかを特定するには追加の調査が必要です。しかし、FutureVulsのロール機能やサーバタグ機能を活用すれば、この紐付けを画面上で簡単に行うことが可能です。

ここでは、すでに内部サーバがFutureVulsに登録されている環境を前提とし、外部スキャン結果をインポートしていきます。

まずは、前述の手順に従い、スキャン結果をインポートするための「疑似サーバ」を作成し、その上でスキャン結果をインポートしてください。

外部露出用のロールとサーバタグを作成

次に、インターネットに露出するサーバと、スキャン結果をインポートした疑似サーバに「ロール」や「サーバタグ」を設定していきます。

FutureVulsのロール機能を活用することで、インターネットに露出しているサーバ専用のロールを作成します。今回は、ロール名「Internet-Facing」を作成し、SSVCのExposureを「Open」に設定しました。

外部露出用ロールの作成

作成したロールを以下のサーバに設定してください。

  • インターネットに露出するサーバ
  • スキャン結果をインポートした疑似サーバ

また、FutureVulsのサーバタグ機能を使うことで、サーバに自由にラベル付けが可能です。ロールは各サーバに対して1つしか設定できませんが、サーバタグは複数つけることができるため、柔軟な管理が可能です。

今回は、インターネットに露出するサーバに対して以下のサーバタグを設定しました。

  • インターネットに露出するサーバpublic
  • スキャン結果をインポートした疑似サーバpublic, nessus

外部露出用サーバタグの設定

脆弱性タブを使った外部に露出する脆弱性のサーバ特定手順

次に、FutureVulsの脆弱性管理で外部スキャンの結果をどのように処理し、外部に露出している脆弱性を持つサーバを特定する手順について説明します。今回は、脆弱性タブを使って実際にトリアージを進める流れを解説します。

外部スキャン結果を使った脆弱性トリアージ

脆弱性一覧画面で「✓」がついている項目は、外部スキャンの結果で脆弱性が検出されたサーバが存在していることを示しています。重要なのは、この外部スキャン結果に基づいて脆弱性があるサーバやライブラリをさらに画面上で深掘りできる点です。これにより、ユーザーはどのサーバやアプリケーションのどのライブラリに対して対応すべきかをひと目で把握できます。

外部スキャン列

該当サーバを深掘りする

次に、脆弱性タブで「外部スキャン列」に「✓」がついている行をクリックして、外部に露出する脆弱性を持つサーバを特定します。この操作で第2ペインが開き、「タスクxサーバ」を選択すると、脆弱性が検出されたサーバの一覧が表示されます。この一覧には、インターネットに露出しているサーバとそうでないサーバが混在しています。

ここで注目すべきポイントは、「外部スキャン列」に「✓」がついているサーバや、「Internet-Facing」ロールが設定されているもの、またはサーバタグに「public」がつけられているサーバです。

外部スキャン列クリック

「外部スキャン列」に「✓」がついている項目は、外部スキャンツールで検出された疑似サーバのタスクを示しています。一方で、外部スキャン列が空で「Internet-Facing」ロールが設定されている、もしくはサーバタグに「public」がつけられているサーバは、インターネットに露出している可能性があるサーバです。これらのサーバに存在する脆弱性がインターネットに露出している可能性があることを画面上で確認できます。

外部スキャン列クリック

さらに、「プロセス実行中」列には「▶」や「📡」のマークが表示されることがあります。これらは、脆弱なソフトウェアのプロセスが実行中であり、さらにポートをリッスンしている状態であることを示しています。このポートがインターネットに露出している可能性が高いことも示唆されています。

なお、外部スキャン列に「✓」がなく、ロール名やサーバタグが空のサーバに対して検知されている同じ脆弱性は、インターネットには露出していないと判断できます。

サーバ内の脆弱性の詳細を確認する

さらに詳しく見ていきましょう。第2ペインの「タスクxサーバ」で該当する行をクリックすると、第3ペインが開き、タスクの詳細が表示されます。この詳細画面では、サーバ内で該当するパッケージやOSSライブラリのリストを確認できます。また、右側には該当パッケージから起動されたプロセスの情報や、リスンしているポート番号も表示されます。

タスク詳細

今回の例からは以下の情報が読み取れます:

  • サーバ名「dev」上で、
  • openssh-clientopenssh-serveropenssh-sftp-serverの3つのパッケージが、インターネットに露出する「CVE-2023-38408」に該当
  • openssh-serverからプロセス「sshd」(PID:800)が起動中
  • 22番ポートをリッスンしている

つまり、この一連の流れで次のことが分かります:

  • 外部スキャンの結果として検出された「インターネットに露出している脆弱性」が、内部のどのサーバに存在しているのか
  • そのサーバ内のどのパッケージやライブラリが影響を受けているのか
  • 影響を受けているパッケージやライブラリから脆弱なプロセスが起動しているかどうか
  • さらに、その脆弱なプロセスがインターネットに露出するポートをリスンしているかどうか

これにより、脆弱性の状況を詳細に把握し、適切な対策を講じるための情報を得ることができます。

脆弱性対応の次のステップ

FutureVulsのこの機能を使えば、外部スキャンの結果から、内部のどのモジュールが脆弱性を持っているかをドリルダウン形式で簡単に追跡できます。また、先程のタスク詳細画面では、脆弱性のステータス変更、対応期限の設定、担当者のアサイン、タスクコメントの登録など、必要な対応を画面上で直接行うことができます。

担当者を割り振ったタスクのソートとフィルタで対応状況をチェックする

セキュリティ管理者にとって、外部に露出している脆弱性が放置されることは最も避けたい事態の1つです。FutureVulsの「タスクタブ」を活用することで、対応状況を効率的に追跡できます。

タスクのフィルタリングとソートによる効率的な確認

タスクタブを利用して、外部に露出している脆弱性が適切に管理されているかを確認しましょう。例えば、以下の条件でタスクをフィルタリングして、対応の優先度を確認することが可能です:

  • 外部スキャンが「✓」であるタスク:外部に露出する脆弱性が特に重要であるため、優先的に対応が必要です。
  • 対応期限が切れているもの:期限を過ぎた脆弱性はリスクが高いため、緊急対応が求められます。
  • 対応予定日が設定されていないもの:計画が未定のタスクは放置される可能性があるため、スケジュールを確定する必要があります。
  • タスクステータスが初期状態のもの:まだ何も手を付けていないタスクは、優先的に作業を開始すべきです。
  • 担当者が設定されていないもの:誰も対応に責任を持っていないタスクが存在することは大きなリスクです。適切に担当者を割り振りましょう。

タスク詳細

一括操作で効率的に対応

これらの条件でフィルタリングしたタスクは、一括での操作も可能です。例えば、チケットのステータスをまとめて更新したり、未対応の担当者に対して対応の催促を行うことができます。この一括操作機能を使うことで、手動で個々のタスクを更新する手間を省き、対応の効率を大幅に向上させることができます。

タスク催促

FutureVuls APIから外部スキャン結果をインポートする

ここまで、外部スキャンの結果を手動でインポートする方法を記載しました。
しかし、スキャン対象のIP数が多かったり、スキャン頻度が高い場合、手動でのインポートは非常に手間がかかります。
FutureVulsでは、REST APIを提供しており、このインポート作業を自動化することが可能です。

FutureVuls APIを利用することで、外部スキャン結果のインポートを簡単に行うことができます。
APIを活用することで、外部スキャン結果の取得からFutureVulsへのインポートまでの流れを自動化でき、作業効率を大幅に向上させることができます。

以下に、FutureVuls APIを使用して外部スキャン結果をインポートするコマンドの例を示します。

1
2
3
4
#新規登録
curl -X 'POST' 'https://rest.vuls.biz/v1/scanImport' \
-H 'accept: application/json' -H 'Authorization: {トークン}' -H 'Content-Type: application/json' \
-d '{"serverID": {外部スキャン結果を追加するサーバのID},"scanImportName": "Web脆弱性診断結果","isExternalScan": {脆弱性が外部に露出しているかどうか}, "cveList": ["CVE-2021-20424"]}'
1
2
3
4
#更新
curl -X 'PUT' 'https://rest.vuls.biz/v1/scanImport/{scanImportID}' \
-H 'accept: application/json' -H 'Authorization: {トークン}' -H 'Content-Type: application/json' \
-d '{"cveList": ["CVE-2020-21313"]}'

定期的にNmapを実行し結果をFutureVulsにインポートする手順

ここでは、Nmapによる外部スキャン結果を取得し、FutureVulsに登録・更新するためのPythonスクリプトのサンプルを紹介します。このサンプルは自由に改変可能なので、FutureVuls APIを活用して外部スキャン結果のインポートを自動化する際の参考にしてください。

スキャン結果の取得には、Nmapのvulnersスクリプトを使用しています。なお、使用しているPythonのバージョンは3.13です。
スクリプトは以下の図が示す流れで実行されます。
スクリプトの全体図

サンプルスクリプトの使い方

以下に、サンプルスクリプトの使い方を示します。スクリプト名はuploadScanResult.pyです。

外部スキャン結果を新規登録する場合:

1
Usage for add: python uploadScanResult.py add <IP_ADDRESS> <SCANRESULT_NAME> <SERVER_ID> <IS_EXTERNAL_SCAN>

外部スキャン結果を新規登録する場合はサンプルスクリプトにaddオプションを指定してコマンドを実行します。
必要なコマンドライン引数は以下のとおりです。
スキャン結果を登録するサーバはすでに作成しており、サーバIDが取得できていることが前提になります。
サーバIDの取得方法はこちらを参照してください。

  • スキャン対象のIPアドレス(IP_ADDRESS)
    • CIDR表記など、複数のIPアドレスをスキャン対象にとることには対応していません
  • 外部スキャン結果の名前(SCANRESULT_NAME)
  • 外部スキャン結果を追加するサーバのID(SERVER_ID)
  • 外部スキャン結果に紐づく脆弱性が外部に露出しているものかどうか(IS_EXTERNAL_SCAN)

実行後スクリプトと同階層にip_and_id_mapping.txtというファイルが作成され、スキャン対象のIPアドレスと追加した外部スキャン結果のID、外部スキャン結果が追加されたサーバのIDの組み合わせが記録されます。
すでにファイルが存在している場合は追記されます。
このファイルにはサーバIDが記録されるため、どのサーバにどの外部スキャン結果が登録されているかという対応関係の確認にも使えます。
このファイルは外部スキャン結果の更新に必要なもののため編集せず、削除しないでください。

1
2
3
4
5
# 作成されるip_and_id_mapping.txtのサンプル
# {ip_address} {scan_result_id} {server_id}
192.168.0.1 271 8399232
192.168.0.2 272 2249287
192.168.0.3 273 8742822

外部スキャン結果を更新する場合

1
$ python uploadScanResult.py update

上記のコマンドを実行するとaddコマンドの実行で作成されたip_and_id_mapping.txtを読み込み、ファイルに記載されている全てのIPアドレスに対して再度スキャンを行います。
スキャン後FutureVuls APIを用いて外部スキャン結果を更新します。

一度addコマンドで登録したスキャン対象は、次回以降updateコマンドを実行するだけで外部スキャン結果の更新を一括で行うことができます。
cronコマンドなどでpython uploadScanResult.py updateを定期的に実行するような設定を行うことで、外部スキャン結果の取得->FutureVulsへのインポートの流れを自動化できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# 外部スキャン結果自動登録のサンプル
import subprocess
import json
import requests
import sys

FVULS_TOKEN = "{FutureVulsのトークン}"
MAPPING_FILE = "ip_and_id_mapping.txt"
ENDPOINT_URL = "https://rest.vuls.biz/v1"

# Nmapの脆弱性スキャン結果からCVE-IDリストを取得
def get_cve_list(ip_address):
# Nmapのスキャンを実行し、結果をフィルタリングしてCVE-IDを抽出
command = "nmap -sV --script vulners " + ip_address
result = subprocess.run(command, shell=True, capture_output=True, text=True)

# スキャンが失敗した場合
if result.returncode != 0:
print(f"Error: Nmap scan failed for IP address {ip_address}")
print(f"Error details: {result.stderr}")
print(f"{ip_address}のスキャンが失敗したためskipします")
return None

# "0 hosts up" が含まれているかを確認して、スキャン対象の状態を判定
if "0 hosts up" in result.stdout:
print(f"Error: All ports are closed or filtered on IP address {ip_address}")
print(f"{ip_address}がスキャンできない状態のためskipします")
return None

# スキャン結果からCVE-IDを抽出
cve_command = "echo '" + result.stdout + "' | grep -o 'CVE-[0-9]\\{4\\}-[0-9]\\+' | sort | uniq"
cve_result = subprocess.run(cve_command, shell=True, capture_output=True, text=True)
cve_list = cve_result.stdout.splitlines()

return json.dumps(cve_list)

# 外部スキャン結果登録APIにPOSTリクエストを送信
def call_add_api(cve_list, scan_result_name, server_id, is_external_scan):
headers = {
'Accept': 'application/json',
'Authorization': FVULS_TOKEN,
'Content-Type': 'application/json'
}
data = {
"serverID": server_id,
"scanImportName": scan_result_name,
"isExternalScan": is_external_scan,
"cveList": json.loads(cve_list)
}
response = requests.post(ENDPOINT_URL+'/scanImport', headers=headers, data=json.dumps(data))
return response.json()

# 外部スキャン結果更新APIにPUTリクエストを送信
def call_update_api(cve_list, scan_result_id):
headers = {
'Accept': 'application/json',
'Authorization': FVULS_TOKEN,
'Content-Type': 'application/json'
}
data = {
"cveList": json.loads(cve_list)
}
response = requests.put(ENDPOINT_URL+f'/scanImport/{scan_result_id}', headers=headers, data=json.dumps(data))
return response.json()

# POSTリクエストの結果からIDを取得し、ファイルに保存
def save_id_to_file(response, ip_address):
# レスポンスからIDを取得
scan_result_id = response.get("id")
server_id = response.get("serverID")

# IDが数字であることを確認
if isinstance(scan_result_id, int) and isinstance(server_id, int):
# IPアドレスとIDをファイルに追記
with open(MAPPING_FILE, "a") as file:
file.write(f"{ip_address} {scan_result_id} {server_id}\n")
print(f"IPアドレス: {ip_address} スキャン結果ID: {scan_result_id} サーバID: {server_id}をファイルに追記しました")
else:
print("スキャン結果の登録に失敗しました")
raise ValueError("スキャン結果の登録に失敗しました")

# ファイルからIPアドレスと外部スキャン結果のIDのペアを読み取る
def read_ip_and_scan_result_id_from_file():
ip_id_pairs = []
with open(MAPPING_FILE, "r") as file:
for line in file:
parts = line.strip().split()
if len(parts) == 3:
ip_address, scan_import_id, server_id = parts
if scan_import_id.isdigit():
ip_id_pairs.append((ip_address, int(scan_import_id)))
else:
print(f"無効な外部スキャン結果のIDが見つかりました: {scan_import_id}")
return ip_id_pairs

# メイン処理
if __name__ == "__main__":
# 引数の確認
if len(sys.argv) < 2:
print("Usage: python uploadScanResult.py <add|update>")
sys.exit(1)

# 動作モードとスキャン対象のIPアドレスを取得
mode = sys.argv[1]

if mode == "add":
if len(sys.argv) != 6:
print("Usage for add: python uploadScanResult.py add <IP_ADDRESS> <SCANRESULT_NAME> <SERVER_ID> <IS_EXTERNAL_SCAN>")
print("example: python uploadScanResult.py add 127.0.0.1 nmap 2141494 True")
sys.exit(1)

ip_address = sys.argv[2]
scan_result_name = sys.argv[3]
server_id = int(sys.argv[4])
if sys.argv[5] == "False":
is_external_scan = False
else:
is_external_scan = True

# CVEリストの取得
cve_list = get_cve_list(ip_address)
# APIへのPOSTリクエスト
post_response = call_add_api(cve_list, scan_result_name, server_id, is_external_scan)
print("{ip_address} のスキャン結果を登録しました")

# ファイルに紐付きを保存
save_id_to_file(post_response, ip_address)

elif mode == "update":
# 保存されたIPアドレスとscan_result_idのペアを取得
ip_id_pairs = read_ip_and_scan_result_id_from_file()
# 各IPアドレスに対してNmapスキャンを実行し、PUTリクエストを送信
for ip_address, scan_import_id in ip_id_pairs:
cve_list = get_cve_list(ip_address)
if cve_list:
put_response = call_update_api(cve_list, scan_import_id)
print(f"{ip_address} のスキャン結果を更新しました")
else:
print("モードはaddかupdateを指定してください")
sys.exit(1)

まとめ

以上、10/21にリリースされた外部スキャン結果のインポート機能について解説しました。
詳しい使い方等はマニュアルを参照してください。
本機能は日々改良を続けており、近々グループセットタスク一覧画面の外部スキャン列でのフィルタ機能も追加する予定です。
本記事が読者の皆様の参考になれば幸いです。