以前のtokyo.cljで話した内容ですが、Stormを使った不正アクセス検知の仕組みの実装をちゃんと作りなおしてみたので、ここに詳細書いておきます。
最近またリスト型攻撃が増えてきているようで、対策に頭を悩まされているエンジニアも少なくないのではないでしょうか。リスト型攻撃はファイアウォールやWAFで防ぐことが難しく、インテリジェントな判断が必要になります。
そこで、複数のサイトからログインのログ(Database)をリアルタイムで収集し、リスト型アクセスっぽいものを解析しアラートを、監視コンソールにあげるという仕組みをプロトタイプとして作ってみました。
ログインログの収集
ログインログは、ユーザのログイン試行時に、各Webアプリのデータベースのテーブルにその成否が書き込まれる想定です。
create table login_log (
logined_at timestamp not null,
account varchar2(100) not null,
ip_address varchar2(15) not null,
success char(1) not null);
これを既存のWebアプリに変更を加えることなく、取得するにはデータベースのEvent機能を使うとよいです。
Oracleの場合、DBMS_ALERTを使うと、Webアプリとは別コネクションで、テーブルへの更新がリアルタイムに取得できるようになります。
CREATE OR REPLACE TRIGGER login_log_after_insert
AFTER INSERT
ON login_log
FOR EACH ROW
BEGIN
DBMS_ALERT.SIGNAL('NEW_LOGIN',
(cast(SYS_EXTRACT_UTC(:new.logined_at) as date)
- cast(from_tz(timestamp '1970-01-01 00:00:00', '00:00')
- as date)) * 24 * 60 * 60 || ' '
|| :new.account || ' '
|| :new.ip_address || ' '
|| :new.success);
END;
/
変更はトリガを仕込んでおいて、DBMS_ALERT.SIGNAL
でアラートを発生させるようにします。
これを別のコネクションからキャッチします。
(reset! oracle-conn (j/get-connection oracle-db))
(let [stmt (.prepareCall @oracle-conn "{call DBMS_ALERT.REGISTER(?)}")]
(doto stmt
(.setString 1 "NEW_LOGIN")
(.executeUpdate)
(.close)))
(let [stmt (.prepareCall @oracle-conn "{call DBMS_ALERT.WAITANY(?,?,?,?)}")]
(doto stmt
(.registerOutParameter 1 Types/VARCHAR)
(.registerOutParameter 2 Types/VARCHAR)
(.registerOutParameter 3 Types/INTEGER)
(.setInt 4 300)))
これでLOGIN_LOG
テーブルにINSERTがされた瞬間に、ALERTをキャッチできるようになります。これをログサーバに送信してあげます。
ここではflumeを使います。これは冒頭のスライドに書いてあるように、flumeの設定やオリジナルのSourceやSinkをClojureで実装できるようにしてあります。
ログインログの送信
これも冒頭のスライドに書いていますが、flumeとstormを連携は間にメッセージキューを挟むのがよくある実装例っぽいのですが、別立てするの面倒なので、WebSocketを使った自前の組み込み型のメッセージ送信のライブラリulon-colon
を使っています。
そうすると、送信側
(defsink ulon-colon-sink
:start (fn [] (start-producer))
:process (fn [event]
(produce (String. (.getBody event)))))
受信側は
(consume-sync consumer
(fn [msg]
(emit-spout! collector [msg]))))
とたったこれだけのコードで、メッセージ送信できるようになります。
不正アクセスの検知
Stormを使って収集したログインログを解析します。これ用のSpoutとBoltのセットをmine-canaryとして作りました。
こんな感じで一定期間に5回以上同一ユーザでログイン失敗した人を検出するBoltを書きます。
;;; 一定時間内に同一ユーザの大量のログイン失敗を検出する
(defbolt failures-by-same-user ["account" "times"] {:prepare true}
[conf context collector]
(let [counts (atom {})]
(bolt
(execute [tuple]
(let [[tm account ip-address success?] (.getValues tuple)
tm (long (Double/parseDouble tm))
success? (= success? "1")]
(when-not success?
(let [failures-tm (get @counts account [])]
(reset! counts
(assoc @counts account (->> (conj failures-tm tm)
(sort >)
(take 5)
vec))))
(when (in-the-period? (@counts account))
(emit-bolt! collector [account (@counts account)] :anchor tuple)))
(ack! collector tuple))))))
監視コンソールへの通知
Webアプリとしてmine-canaryの監視コンソールを作っているので、不正アクセスの通知をリアルタイムに受けることができます。
監視コンソールはWebSocketでmine-canaryにコネクションを貼っていて、不正アクセスの発生イベントのPushを待ちます。ここの連携も前述のulon-colonを使っています。
ulon-colonにはClojureScriptのconsumerも付いているので、直接メッセージをブラウザに配信できるのです。
ちなみに、監視コンソールはomを使って書いていますが、このulon-colonとomを組み合わせると、サーバからpushされたメッセージを受け取り順次表示するみたいなコードも、簡潔に書けます。
(defcomponent main-app [app owner]
(will-mount
[_]
(consume (:consumer app)
(fn [msg]
(om/transact! app :alerts #(conj % msg)))))
(render
[_]
(html
[:div.ui.list
(for [{:keys [account times]} (:alerts app)]
[:div.item
[:i.icon.frown]
[:div.content
[:div.header "Suspicious attack"]
[:div.description (format-times account times)]]])])))
(om/root main-app {:alerts [] :consumer (make-consumer "ws://localhost:56293")}
{:target (.getElementById js/document "app")})
こうして、mine-canaryであげた不正アクセスの兆しが、リアルタイムに監視コンソールに表示されるようになります。
ログイン用のexampleアプリもfriendを使って作ってみましたので、そこからログインを何回か失敗してみます。
すると、監視コンソールにメッセージがリアルタイムであがってきます。
実際に使うには…
実際のリスト型攻撃は、より巧妙になってきているのでmine-canaryのBoltのルール調整が必要です。どのくらいのユーザ・期間を監視対象とするかにもよりますが、Stormはインメモリにデータを溜め込むのが基本なので、それなりのノード数が必要になると思われます。