← ブルーチーム / 検知・調査

SIEM  /  Search Processing Language

ログは集めた。
あとは訊き方
覚えるだけ。

Splunk 検索入門 — ゼロから SPL を書けるようになるまで

数百万件のイベントから、必要な17件を取り出す。Splunk の画面の見方から始めて、調査クエリを白紙から組み立てられるところまで。前提知識はいりません。

SPL検索の3層構造stats / eval / rex脅威ハンティング
01 なぜ集めるのか

Splunk は何をする道具か

Splunk を一言でいうと、バラバラの機器のログを1か所に集めて、まとめて検索できるようにする道具です。SIEM と呼ばれる製品の一種です。

1.1なぜ grep ではダメなのか

ログを読むだけなら grep で足ります。実際、1台のサーバの1本のログを見るなら、そのほうが速い。問題は調査が1台で終わらないことです。

攻撃者が Web サーバに侵入して、そこから内部の端末に横移動して、ドメインコントローラの管理者権限を取った。この流れを追うには、Web サーバのアクセスログ、端末のプロセス起動ログ、DC の認証ログを突き合わせる必要があります。形式も置き場所もバラバラの3種類を。

grep でやるとSplunk でやると
複数機器1台ずつ SSH して回る1回の検索で横断する
形式の違いログごとに違う正規表現を書くフィールド名で統一的に扱える
時系列の突合手で並べ替える時刻順に自動で混ざる
集計awk や sort で自作コマンド1つ
ログの消失侵入者に消される転送済みなので残る

最後の行が地味に重要です。攻撃者は痕跡を消しますが、すでに転送されたログには手が届きません。ログを集約する理由の半分はこれです。

1.2取り込みから検索までの流れ

Splunk がやっていることは大きく2段階です。

① 取り込み(INDEX TIME) 機器 web / 端末 / DC Forwarder ログを送る常駐 Indexer 時刻を切り出して保管 index(保管庫) index=main / botsv3 … ② 検索(SEARCH TIME) あなたが SPL を書く index=main status=404 Search Head indexer に問い合わせる ここでフィールドを切り出す 保管時ではなく、検索するたびに実行される
取り込み時に保管するのは、ほぼ生のログ。フィールドへの切り出しは検索するたびに行われる。
この仕組みの利点

切り出しルールを後から足しても、過去のログに遡って新しいフィールドが生えます。取り込み直しは不要です。「あのとき見落としていた項目を今から使いたい」が通るのは、この設計のおかげです。

02 触る前に

画面の見方 — 6つの領域

SPL の文法より先に、画面のどこに何があるかです。ここを知らないまま本のクエリを写経しても、結果が出たのか失敗したのかすら分かりません。

使うのは Search & Reporting というアプリです。ログイン後の左メニューから開きます。

Search & Reporting
① 検索バー
index=web sourcetype=access_combined status=404 | stats count by uri
② 時間範囲
Last 24 hours ▾
④ フィールド
host 3
source 5
sourcetype 2
clientip 41
status 6
uri 128
method 2
⑤ 結果タブ
EventsStatisticsVisualization
⑥ 結果
8/24 16:03:11 40.80.148.42 "GET /uploads/… " 200
8/24 16:04:02 40.80.148.42 "POST /uploads/… " 200
8/24 16:11:20 40.80.148.42 "GET /uploads/… " 404
③ 1,482,391 events  (2.14s)  ·  ジョブの進捗と件数はここ
1検索バー/SPL を書く場所。ここに書いた内容が全て。
2時間範囲/既定は直近24時間などに設定されている。前回の値が残るので毎回確認する。
3件数と所要時間/絞り込みが効いているかの一次判断はここ。
4フィールドサイドバー/実際に生えているフィールド名の一覧。最重要
5結果タブ/生ログを見るなら Events、集計結果は Statistics に出る。
6結果本体/イベントの一覧、または表。

2.1フィールドサイドバーが最重要な理由

本や記事に載っているクエリをコピーしても動かないことがよくあります。原因のほとんどはフィールド名が自分の環境と違うことです。同じ Apache のログでも、環境によって uri だったり uri_path だったり url だったりします。

サイドバーには、いま検索で返ってきたイベントに実際に存在するフィールド名が並びます。ここに無い名前を書いても絶対に動きません。

表示中身
Selected Fields初期状態では host / source / sourcetype の3つ。表示したいものを自分で追加できる
Interesting Fields返ってきたイベントの2割以上に出現するフィールドが自動で並ぶ
最初にやること

新しいログを触るときは、必ずこれを投げてサイドバーを眺めてください。フィールド名を目で確認してから本文を書き始めます。

index=web sourcetype=access_combined
| head 20

2.2結果が Events に出るか Statistics に出るか

ここでつまずく人が多い。クエリの内容によって、結果が出るタブが変わります。

クエリ結果が出る場所
index=web status=404Events タブ(生ログの一覧)
… | stats count by uriStatistics タブ(表)
… | table _time, uriStatistics タブ(表)
… | timechart countStatistics と Visualization

「クエリを実行したのに何も出ない」と思ったら、Events タブが空なだけで Statistics タブに結果が入っていることがあります。この違いの正体は第9章で説明します。

03 場所を指定する

データの住所 — index と sourcetype

Splunk には何種類ものログが同居しています。だから検索するときは「どのログを見るか」を指定します。その指定に使うのが4つのフィールドです。

フィールド意味
index保管庫の名前。ログの置き場所そのものweb / security / botsv3
sourcetypeログの形式を表す名札。これでフィールドの切り出し方が決まるaccess_combined / WinEventLog:Security
source取り込み元のパス/var/log/apache/access.log
hostそのログを出した機器web01 / dc01

3.1index を書く癖をつける

index を書かないと、その利用者が読める全ての index を横断して検索します。データ量が多い環境では、これだけで検索が数分かかることがあります。

# 全部の保管庫を舐める
sourcetype=access_combined
# 保管庫を指定する
index=web sourcetype=access_combined

どんな index があるか分からないときは、これで一覧が出ます。

| eventcount summarize=false index=*
| table index, count

あるいは、どの index にどんな種類のログが入っているかを一気に見るならこちら。新しい環境を渡されたら最初に投げるクエリです。

index=*
| stats count by index, sourcetype
| sort - count
注意

上のクエリは全 index を舐めるので重いです。時間範囲を直近15分などに絞ってから実行してください。どんな種類のログがあるかを知るのが目的なので、期間は短くて構いません。

3.2sourcetype がフィールドを決める

第1章で「フィールドは検索時に切り出される」と書きました。どの切り出しルールを当てるかを決めているのが sourcetype です。

たとえば sourcetype=access_combined という名札が付いていると、Splunk は「これは Apache のアクセスログ形式だ」と判断して、あらかじめ用意されたルールを当てます。その結果、次のフィールドが自動で生えます。

フィールド値の例
clientip40.80.148.42
methodGET
uri/uploads/sp-client-document-manager/1.php
versionHTTP/1.1
status200
bytes1043
useragentMozilla/5.0 …
refererhttp://…
覚えておくと後で効く

パスは uri、プロトコルは version最初から別のフィールドに分かれています。他のログ分析ツールの記事だと HTTP/1.1 を文字列置換で削る処理が出てきますが、Splunk では通常その処理が要りません。

04 検索の材料

イベントとフィールド

4.11行 = 1イベント

Splunk に取り込まれたログは、1件ずつバラバラの「イベント」として保管されます。Apache のアクセスログなら、リクエスト1件が1イベントです。

192.168.250.70 - - [24/Aug/2016:16:02:51] "GET /wp-admin/admin-ajax.php HTTP/1.1" 200 1043
192.168.250.70 - - [24/Aug/2016:16:02:53] "POST /wp-admin/admin-ajax.php HTTP/1.1" 200 88
40.80.148.42   - - [24/Aug/2016:16:03:11] "GET /uploads/sp-client-…/1.php HTTP/1.1" 200 12

この3行は、Splunk の中では独立した3つのイベントです。検索するというのは、この山から条件に合うものを取り出す作業です。

4.2どのログにも必ずある5つのフィールド

名前中身
_timeイベントの発生時刻。内部では UNIX 時間(数値)で持っている
_raw元のログ1行そのまま。切り出す前の文字列
hostそのログを出した機器
source取り込み元のパス
sourcetypeログの種類を表す名札

アンダースコアで始まる名前は、Splunk が内部で使う特別なフィールドという印です。_time_raw がそれにあたります。

_time は数値である

画面には「8/24 16:03:11」と表示されますが、中身は 1472054591 のような数値です。そのまま表に載せても読めません。読める形にするには変換が必要で、これが第8章の strftime の話につながります。

4.3自動で生えないフィールドは自分で作る

sourcetype のルールでカバーされない項目は、自分で切り出します。使うのは rex(正規表現で抜く)と eval(計算・変換して作る)の2つです。第8章で扱います。

どちらを使うかの判断はこうです。

やりたいこと使うもの
文字列の一部を抜き出したいrex
既にあるフィールドを計算・変換したいeval
05 構造をつかむ

検索は3層でできている

ここが本章の山場です。SPL のクエリは必ずこの3層でできています。この構造が頭に入ると、あとは部品を当てはめるだけになります。

LAYER 1 — 時間範囲 いつのログを見るか 画面右の時間ピッカーで指定。クエリ本文には書かない。最も強力な絞り込み。 Last 24 hours ▾ LAYER 2 — ベース検索 どのイベントを持ってくるか 最初のパイプまで。索引が効くので速い。絞れる条件はここに書く。 index=web sourcetype=access_combined LAYER 3 — パイプ 持ってきたものをどう加工するか 1件ずつ処理する。コマンド名から始める。ここは遅い。 この層だけ文法が違う ── ここを混同するのが最初の壁。 | stats count by status | sort - count
上から順に絞られていく。上の層で絞るほど速く、下の層に降りるほど自由に加工できる。

5.1層1と層2の文法は違う

ベース検索は field=value やキーワードを並べるだけ。パイプの後ろは必ずコマンド名から始まります。この2つは別の言語だと思ったほうがいいくらい書き方が違います。

┌─ ベース検索:field=value を並べる
index=web sourcetype=access_combined status=404
| eval ...   
| stats ...  ├─ パイプ:コマンド名から始める
| sort ...   

5.2なぜ層を分けているのか

速度のためです。ベース検索は索引を引いて一気に絞れますが、パイプの後ろのコマンドは絞り込んだ全イベントを1件ずつ処理します。

だから同じ結果になる書き方でも、どちらに条件を書くかで速度が桁で変わります。

遅い ── 150万件を全部取り出してから捨てている
index=web
| where status=404
速い ── 索引の段階で絞っている
index=web status=404
原則

絞れる条件は、できるだけ上の層に、できるだけ左に。この一言で書けるクエリの質がかなり変わります。

06 層2の文法

絞り込みの書き方

ベース検索で覚えることは7つです。

6.1キーワードをそのまま書く

単語を書くと、その単語を含むイベントが返ります。どのフィールドかは指定しなくて構いません。

index=web failed
index=web "POST /admin"    ← 空白を含むならクォート

6.2フィールド名=値

ベース検索では不等号も使えます。

status=404
status!=200
bytes>10000
index=web status=404      ← 並べるだけで AND

6.3AND / OR / NOT

並べるだけで AND なので AND は普通は省略します。OR と NOT は必ず大文字で書きます。小文字だとただの検索語として扱われます。

status=404 OR status=500
index=web NOT clientip=10.0.0.5
(status=404 OR status=500) clientip=10.0.0.5

同じフィールドで値を並べるなら IN が読みやすい。

status IN (404, 500, 503)

6.4ワイルドカード

アスタリスクが0文字以上の任意の文字列にあたります。

uri="/uploads/*"
host="web*"
先頭ワイルドカードは避ける

uri="*.php" のように先頭に * を置くと索引が効きません。全件を舐めることになるので、他の条件で範囲を狭めてから使ってください。

6.5大文字小文字のルール

対象区別する?
フィールド名するstatusStatus は別物
フィールドの値しないmethod=getGET も拾う
キーワード検索しないerrorERROR も拾う
AND / OR / NOTする小文字の or は演算子にならない

区別して探したいときだけ CASE() で囲みます。

host=CASE(localhost)   ← 小文字の localhost だけ

6.6記号を含む文字列の罠

Splunk は取り込み時に、ピリオドやスラッシュやハイフンの位置で単語を区切って索引を作ります。

つまり 192.168.1.1 を検索すると、内部では「192 かつ 168 かつ 1 かつ 1」を含むイベントを探しにいきます。関係ないものが混ざる可能性があるということです。

1つのかたまりとして扱わせたいときは TERM() で囲みます。

TERM(192.168.1.1)

ただし実用上は、クォートで囲んで書けば大抵うまくいきます。候補を広めに拾ってから絞る動きになるだけです。

6.7時間範囲をクエリに書く場合

普段は時間ピッカーで指定しますが、保存する検索などではクエリに書きます。

index=web earliest=-24h latest=now
index=web earliest="08/24/2016:00:00:00"
書き方意味
-15m / -24h / -7d15分前 / 24時間前 / 7日前から
-1d@d昨日の0時ちょうどから(@d で日単位に切り下げ)
@w0今週の日曜0時
now
07 体系で覚える

コマンドは2種類しかない

SPL には百個以上のコマンドがあります。全部覚えるのは無理だし、その必要もありません。コマンドを2種類に分類するだけで、大半の混乱が消えます。

TYPE A — イベントを返す 形は「イベントの束」のまま 絞ったり、フィールドを足したりするだけ。 元のフィールドは全部残っている。 evalフィールドを足す rex正規表現で切り出す search / whereさらに絞る sort並べ替える dedup重複を消す head / tail件数を減らす rename名前を変える fields列を残す / 捨てる TYPE B — 表を返す ここで形が「表」に変わる 集計してしまうので、 書いていないフィールドは消える。 stats数える・まとめる table列を選ぶ timechart時間ごとに集計 chart2軸で集計 top / rare多い順・少ない順 eventstats※例外・下記参照 Statistics タブに結果が出るのは この種類を通したときだけ。
左は形を変えない。右は形を変える。第2章で触れた「結果が出るタブが変わる」の正体はこれ。

7.1この分類が効く理由

TYPE B を通した後は、元のフィールドが存在しません。だからこう書くとエラーも出ないまま結果が空になります。

index=web
| stats count by status      ← ここで status と count だけになる
| table _time, clientip     ← どちらも存在しない

逆に言えば、TYPE B は最後に置くのが基本形になります。詳しくは第9章で図解します。

例外 — eventstats

eventstats は集計しつつイベントの束のまま返す珍しいコマンドです。「全体の平均を出しつつ、平均を超えるイベントだけを残す」といった処理ができます。最初は使わなくて構いませんが、存在だけ覚えておくと後で助かります。

| eventstats avg(bytes) as 平均
| where bytes > 平均 * 5
08 道具箱

よく使うコマンド12個

実際の調査で使うのは、ほぼこの12個で足ります。

8.1eval — フィールドを作る TYPE A

使用頻度が最も高いコマンドです。= の左が新しく作るフィールド名、右が中身。既存の名前を左に書くと上書きになります。

| eval kb = bytes / 1024
| eval time = strftime(_time, "%Y/%m/%d %H:%M:%S")
| eval 判定 = if(status >= 400, "エラー", "正常")

よく使う関数

関数やること
strftime(時刻, 書式)UNIX 時間を読める文字列に
strptime(文字列, 書式)文字列を UNIX 時間に戻す
replace(元, 正規表現, 置換後)マッチ部分を置き換える
if(条件, 真, 偽)2択で振り分ける
case(条件1, 値1, 条件2, 値2 …)多択で振り分ける
lower(x) / upper(x)小文字化・大文字化
len(x)文字数
coalesce(a, b, c)最初に中身があるものを返す
round(x, 桁)四捨五入
mvcount(x)複数値フィールドの個数

時刻の書式

記号意味記号意味
%Y年(4桁)%H時(24時間)
%m%M
%d%S
replace は正規表現である

第2引数は文字列ではなく正規表現です。. は「任意の1文字」なので、ピリオドそのものを消したいならエスケープが要ります。

✕ . が任意の文字として解釈される
eval x = replace(version, "HTTP/1.1", "")
○ 正しい
eval x = replace(version, "HTTP/1\.1", "")

8.2rex — 正規表現で切り出す TYPE A

自動で生えていないフィールドを自分で作ります。名前付きキャプチャがそのままフィールド名になります。

| rex field=uri "(?<filename>[^/]+\.\w+)$"

(?<filename>...)filename が新しいフィールド名です。マッチしなかったイベントには生えません(エラーにはなりません)。field= を省略すると _raw つまりログ全文が対象になります。

sed モードもある

置換だけしたいときは mode=sed が使えます。evalreplace より読みやすいことが多い。

| rex field=uri mode=sed "s/ HTTP\/1\.1//"

8.3search / where — 途中で絞る TYPE A

書き方使いどころ
searchベース検索と同じ(ワイルドカード可)単純な値の一致
whereeval と同じ式(関数が使える)フィールド同士の比較、計算結果での絞り込み
| search status=404
| where len(uri) > 100
| where bytes > 平均             ← フィールド同士は where だけ
where のクォート忘れ

where で文字列を比較するときは必ずダブルクォートで囲みます。囲まないとフィールド名として解釈され、静かに0件になります。

where user = "admin"    ← 文字列 admin と比較(正しい)
where user = admin      ← admin という名前のフィールドと比較(0件)

8.4stats — 数える・まとめる TYPE B

SQL の GROUP BY にあたります。調査で最も出番の多い集計手段です。

| stats count by clientip

by を省くと全体でひとつの数字になります。

関数やること
count件数
dc(x)x の種類数(重複を除いた数)
values(x)出てきた値を全部並べる(重複なし)
list(x)出てきた値を出現順に全部並べる
sum(x) / avg(x)合計・平均
min(x) / max(x)最小・最大(時刻に使えば初回と最終)
earliest(x) / latest(x)最初のイベントの値・最後のイベントの値

複数の集計を同時に書けます。as で列名を付けられます。

| stats count, dc(uri) as 種類数, min(_time) as 初回 by clientip

8.5table — 列を選ぶ TYPE B

指定した列だけの表を作ります。書いた順に左から並びます。書かなかった列は消えます。

| table _time, clientip, uri, status

8.6fields — 列を残す・捨てる TYPE A

table と似ていますが、こちらは形を変えません。マイナスを付けると「捨てる」。

| fields clientip, uri     ← この2つだけ残す
| fields - _raw          ← _raw を捨てて軽くする

途中で軽くしたいときは fields、最後に見た目を整えるときは table と使い分けます。

8.7sort — 並べ替える TYPE A

| sort count             ← 昇順(少ない順)
| sort - count           ← 降順(多い順)
| sort - count, uri      ← 同数なら uri で昇順
必ず踏む地雷 — 1万件で切れる

sort既定で先頭1万件しか返しません。それ以上あっても黙って切り捨てられ、エラーも警告も出ません。

外すにはコマンド名の直後に 0(上限なし)を書きます。

| sort 0 _time          ← 全件
| sort 100 - count       ← 上位100件だけ

8.8dedup / head / tail TYPE A

| dedup clientip     ← clientip の重複を1つに
| head 20            ← 先頭20件
| tail 20            ← 末尾20件

8.9rename — 列名を変える TYPE A

| rename clientip AS "接続元IP"

8.10top / rare TYPE B

stats count by して sort するのを1コマンドでやる省略形。件数と割合も出ます。

| top 10 uri
| rare 10 uri           ← 珍しいもの=ハンティングの主戦場
| top 5 uri by clientip

8.11timechart — 時間ごとに集計 TYPE B

「時間で区切った stats」です。深夜に跳ねている、といった異常が視覚的に出ます。

| timechart span=1h count by status

span1m 5m 1h 1d のように指定します。

8.12eventstats — 集計しつつ形は保つ 例外

集計結果を「全イベントに列として配る」コマンドです。全体平均と各イベントを比較したいときに使います。

| eventstats avg(bytes) as 平均, stdev(bytes) as 標準偏差
| where bytes > 平均 + 標準偏差 * 3

「全体から見て極端に大きいレスポンス」を探す型です。データ持ち出しの検知などに使えます。

09 核心

データの形が変わる

コマンドを覚えても書けるようにならない人が、ほぼ全員つまずくのがここです。逆にここを掴めば、クエリを自分で組み立てられるようになります。

イベントの束 TYPE A のコマンドを通しても、この形のまま 16:02:51 GET /wp-admin/admin-ajax.php 200 1043 _time · method · uri · status · bytes · clientip · version · useragent · _raw … 16:02:53 POST /wp-admin/admin-ajax.php 200 88 _time · method · uri · status · bytes · clientip · version · useragent · _raw … 16:03:11 GET /uploads/sp-client-…/1.php 404 12 _time · method · uri · status · bytes · clientip · version · useragent · _raw … … あと 1,482,388 件 | stats count by status ← TYPE B。ここで形が変わり、書いていないフィールドは捨てられる 集計結果。もう元のログではない status count 2001,204,881 404271,402 5006,108 ✕ clientip も uri も _time も _raw も、ここではもう存在しない 残っているのは status と count の2列だけ。後ろのパイプで取り戻すことはできない。
TYPE B を通った瞬間、データは集計結果に置き換わる。捨てられた情報は戻らない。

9.1だから静かに空になる

この2つはどちらもエラーが出ません。結果が空か、列が全部空欄になるだけです。

stats の後で消えたフィールドを使っている
index=web
| stats count by status
| table _time, clientip, status, count
table の後で消えたフィールドで並べ替えている
index=web
| table uri, status
| sort 0 _time

9.2解決は2通り

①捨てる前に処理を済ませる。

index=web
| sort 0 _time          ← 先に並べる
| table uri, status     ← 見せる列は最後に決める

②集計の中で一緒に拾っておく。

index=web
| stats count, values(clientip) as 接続元, min(_time) as 初回 by status

9.3組み立ての型

ほとんどの調査クエリは、この4段階に収まります。迷ったらこの型に当てはめてください。

やること使うもの種類
1 絞る対象のログだけ取り出すベース検索・時間範囲
2 足す必要なフィールドを作るeval / rexTYPE A
3 整える並べ替え・重複削除・追加の絞り込みsort / dedup / whereTYPE A
4 見せる集計する・列を選ぶstats / table / timechartTYPE B
ひとことで言うと

TYPE B は最後。これだけ守れば、消えたフィールドを触る事故はほぼ起きません。

10 手を動かす

実践 — 1行ずつ組み立てる

10.1まず日本語で書く

SPL をいきなり書き始めないでください。やりたいことを日本語で書くと、そのまま構造になります。

今回の調査

Web サーバのアップロード先 /uploads/sp-client-document-manager へのアクセスを全部拾って、いつ・何が・どうなったかを時系列で並べたい。

第9章の4段階に割り当てます。

集計しない、というのがこの調査の性格です。攻撃の流れを追うときは、数を数えるより1件ずつ時系列で読むほうが分かります。

10.2ボタンで1行ずつ進める

各段階でデータの形がどう変わるかに注目してください。

STEP 1 / 5

      
      

10.3完成形

index=web sourcetype=access_combined "/uploads/sp-client-document-manager"
| eval time = strftime(_time, "%Y/%m/%d %H:%M:%S")
| sort 0 _time
| table time, status, uri
なぜ必要か
1対象を絞る。index を書かないと全保管庫を舐める
2_time は数値。人が読む表に載せるには変換が要る
3時系列で読みたい。0 が無いと1万件で切れる。並べ替えは _timetime は文字列なので文字列順になる)
4TYPE B は最後。ここで他の列は消えるが、もう使わない
11 事故防止

ハマりどころ

エラーが出れば直せます。厄介なのはエラーが出ないまま間違った結果が返るケースです。ここに挙げるのは全部そのタイプです。

11.1sort が1万件で黙って切れる

件数の多い調査では条件反射で 0 を書いてください。切り捨てられても何も表示されません。

11.2TYPE B の後で消えたフィールドを使う

結果が空になるか、列が全部空欄になります。結果が意味不明なときは、まず TYPE B のコマンドより前に戻して確認。

11.3フィールド名が環境で違う

本や記事のクエリが動かない原因の大半です。ベース検索だけ投げて、サイドバーで実際の名前を確認してからコピーしてください。

11.4_time と time を混同する

名前正体
_timeSplunk が持つ本物の時刻。数値。並べ替えや時間計算はこれ
timeeval で自分が作った表示用の文字列。並べ替えに使うと文字列順になる

年月日の書式なら文字列順でも偶然そろいますが、%m/%d のような書式にした瞬間に順番が壊れます。

11.5where で文字列をクォートし忘れる

| where user = adminadmin というフィールドとの比較になります。そんなフィールドは無いので0件。エラーは出ません。

11.6絞り込みをパイプの後ろに書く

結果は同じですが遅くなります。ベース検索に書ける条件はベース検索に。

11.7時間範囲が前回のまま

時間ピッカーは設定が残ります。結果が多すぎる・遅すぎると感じたら、まずここを疑ってください。

11.8index を書き忘れる

全保管庫を横断するので、データ量の多い環境では数分待たされます。

デバッグの手順

思った結果が返らないときは、後ろのパイプから1本ずつ消して実行してください。どこで壊れたかが必ず特定できます。組み立てた手順を逆にたどるだけです。

12 実戦

脅威ハンティングで使う型

調査で繰り返し出てくる6つの型です。フィールド名を差し替えればそのまま使えます。

12.1環境に何があるか把握する

知らない環境を渡されたら最初に投げます。時間範囲は15分程度に絞ってください。

index=*
| stats count by index, sourcetype
| sort - count

12.2珍しいものを探す

ハンティングの基本発想です。正常な活動は大量に繰り返されるので、1〜2回しか出ていないものに異常が紛れます。

index=win sourcetype="WinEventLog:Security" EventCode=4688
| stats count, values(host) as ホスト by New_Process_Name
| sort count
| head 50

「全社で1台しか実行していないプロセス」が上に来ます。Web アクセスなら by uri、DNS なら by query に変えるだけです。

12.3起点の前後を時系列で読む

怪しい IP やホストが1つ見つかったら、そこを軸に周辺を洗います。第10章と同じ型です。

index=web clientip=40.80.148.42
| eval time = strftime(_time, "%H:%M:%S")
| sort 0 _time
| table time, method, uri, status, bytes

12.41つの主体がやったことを1行にまとめる

「この IP は結局どこにアクセスしたのか」を一望する型。values() が効きます。

index=web sourcetype=access_combined
| stats count,
        dc(uri) as URL種類数,
        values(status) as ステータス,
        min(_time) as 初回,
        max(_time) as 最終
      by clientip
| eval 初回 = strftime(初回, "%m/%d %H:%M")
| eval 最終 = strftime(最終, "%m/%d %H:%M")
| sort - URL種類数

短時間に大量の URL を叩いている IP が上に来ます。スキャンや探索の痕跡が出る型です。

なぜ strftime が stats の後なのか

min(_time) の結果も数値の UNIX 時間だからです。集計してから読める形に変換します。集計前に文字列にすると min/max が文字列比較になって狂います。

12.5時間ごとの推移を見る

index=web
| timechart span=1h count by status

深夜に跳ねている、平日と休日で形が違う、といった異常が視覚的に出ます。

12.6全体から外れたものを拾う

eventstats の出番です。「レスポンスサイズが全体平均から極端に外れているリクエスト」を探します。データ持ち出しの兆候を拾う型です。

index=web sourcetype=access_combined
| eventstats avg(bytes) as 平均, stdev(bytes) as 偏差
| where bytes > 平均 + 偏差 * 3
| eval time = strftime(_time, "%m/%d %H:%M")
| sort 0 - bytes
| table time, clientip, uri, bytes
13 自分で書く

練習問題

読むだけでは書けるようになりません。手で書いてから答えを開いてください。フィールド名は access_combined の標準的なものを使います。

Q1 — 基礎

index=web のアクセスログから、ステータスが404のリクエストだけを取り出したい。

答えを見る
index=web sourcetype=access_combined status=404

パイプは不要。ベース検索だけで済むならそれが最速です。| where status=404 と書いても同じ結果になりますが、遅くなります。

Q2 — 集計

404 が出ている URL ごとの件数を、多い順に出したい。

答えを見る
index=web sourcetype=access_combined status=404
| stats count by uri
| sort - count

| top uri でもほぼ同じ結果が出ます(件数に加えて割合も付きます)。結果は Statistics タブに出ます。

Q3 — 順番の罠

次のクエリは0件になる。なぜか、そしてどう直すか。

index=web
| table uri, status
| sort 0 _time
答えを見る

原因:table は TYPE B なので、書いていない _time がここで捨てられる。次の行で並べ替えに使おうとしても存在しない。

index=web
| sort 0 _time
| table uri, status

直し方:捨てるコマンドを最後に回す。これが第9章の「TYPE B は最後」です。

Q4 — フィールドを作る

アクセスログの uri からファイル名だけを抜き出して、ファイル名ごとの件数を数えたい。
例:/uploads/docs/1.php1.php

答えを見る
index=web sourcetype=access_combined
| rex field=uri "(?<filename>[^/]+\.\w+)$"
| stats count by filename
| sort - count

rex は TYPE A なので、この時点ではまだイベントの束。(?<filename>...) の名前がそのままフィールドになります。[^/]+ は「スラッシュ以外の文字が1つ以上」、$ は末尾。マッチしないイベントには filename が生えないので、集計から自然に外れます。

Q5 — 実戦

直近24時間で、アクセスした URL の種類が多い順に接続元 IP を並べ、それぞれの初回アクセス時刻も読める形で出したい。

答えを見る
index=web sourcetype=access_combined earliest=-24h
| stats dc(uri) as 種類数, count, min(_time) as 初回 by clientip
| eval 初回 = strftime(初回, "%m/%d %H:%M:%S")
| sort - 種類数

ポイントは strftimestatsに置くこと。min(_time) の結果も数値なので、集計してから変換します。先に文字列にすると min が文字列比較になって狂います。時間範囲はピッカーで指定してもかまいません。

14 手元に置く

チートシートと練習環境

14.1コマンド早見表

書き方やること種類
index=x sourcetype=y保管庫と形式で絞る
| eval x = 式新しい列を作るA
| rex field=a "(?<b>…)"正規表現で切り出すA
| search a=1途中で絞る(ベース検索の書き方)A
| where 式途中で絞る(eval の書き方)A
| sort 0 aa で昇順に全件A
| sort 0 - aa で降順に全件A
| dedup aa の重複を1つにA
| head 10 / tail 10先頭・末尾10件A
| fields a, b / fields - a列を残す / 捨てるA
| rename a AS b列名を変えるA
| eventstats avg(a) as m集計値を全イベントに配るA
| stats count by aa ごとに数えるB
| table a, b, cこの列だけの表にするB
| top 10 a / rare 10 a多い順・少ない順B
| timechart span=1h count1時間ごとの件数B

14.2覚えておく6つのルール

  1. 検索は3層。時間範囲 → ベース検索 → パイプ。
  2. 絞れる条件はできるだけ上の層に、できるだけ左に。
  3. コマンドは2種類。TYPE B(stats / table / top / timechart)は最後に置く。
  4. sort には 0 を付ける。付けないと1万件で切れる。
  5. 並べ替えは _time、表示は自分で作った文字列。
  6. フィールド名は環境依存。書く前にサイドバーで確認する。

14.3練習環境

Splunk Enterprise の無償トライアル

公式サイトから落として自分の PC に入れられます。60日間はフル機能で、その後も無償ライセンスに切り替えて使い続けられます(1日あたりの取り込み量に上限あり)。

BOTS データセット

Splunk 公式が CC0 で公開している、実際の攻撃シナリオを含む演習用ログです。GitHub に置かれています。インデックス済みの形で配布されているので、無償ライセンスの取り込み量制限にも引っかかりません。

データセット入手先 / 中身
BOTS v1Apache / Suricata / Sysmon など。最初の版で解説記事も多い
BOTS v2github.com/splunk/botsv2
BOTS v3github.com/splunk/botsv3

攻撃部分だけを抜き出した小さい版(attack-only)もあるので、ディスクが厳しければそちらから始められます。設問と解答も公式から入手できます。

進め方

  1. index=botsv3 | stats count by sourcetype を投げて、何のログがあるか見る
  2. 気になった sourcetype を1つ選び、素で検索してサイドバーのフィールド名を眺める
  3. 第12章の型を、その sourcetype のフィールド名に差し替えて実行する
  4. BOTS の設問を1問ずつ解く
最初の一歩

このページを閉じたら、まずベース検索の1行だけを打って実行してください。結果が返ってきたら、あとは1本ずつパイプを足すだけです。全部いっぺんに書こうとしないこと。それだけで書けるようになります。