KITA Eng.

北海道でサーバー技術者として歩み出したひとが綴るblog。

iLogScannerで攻撃兆候を検出

 IPAから公開されている、ウェブサイトの攻撃兆候検出ツール iLogScannerを使って、NGINXのアクセスログを元にした攻撃兆候検出をサーバー上で行うようにしました。

環境

  • Ubuntu 18.04 Server
  • NGINX
  • iLogScanner

準備

1. IPAのページからオフライン版iLogScannerをダウンロード

https://www.ipa.go.jp/security/vuln/iLogScanner/ からダウンロードできる。 ダウンロードの直リンクURLは、さくっとは拾えない。利用規約に同意したよのチェックがついているか確認される兼ね合いなのだろう。 頑張れば拾えるけど

ZIPで圧縮されているので、適当なディレクトリに展開しておく。
以下は、~/iLogScannerとして展開したものとしてコマンド類は記載。

2. JAVAの実行環境を用意

OpenJDK 11 以上がオフライン版iLogScannerの実行には必要。
Ubuntu 18.04 であれば、以下のコマンドでインストールできる。今回はサーバー上で解析させる(GUI不要)ので、headless版をインストール。

user@localhost:~$ sudo apt-get install openjdk-11-jre-headless

3. 試しに動かしてみる

オプション引数は、https://www.ipa.go.jp/security/vuln/iLogScanner/の利用方法(CUI版)のパラメーター一覧を参照。

user@localhost:~$ ./iLogScanner/1_bin/iLogScanner.sh mode=cui logtype=apache accesslog=/var/log/nginx/access.log outdir=/home/user/ reporttype=text level=detail

./iLogScanner/1_bin/iLogScanner.sh --help とか打っても、何も出てくれない悲しみがある。

出力された解析結果の文字コードは、Shift_JISなので、nkf文字コード変換して表示するのが良いでしょう。

user@localhost:~$ nkf -w iLogScanner_20191100_000000.txt | less

4. 自動実行用のスクリプトを用意する

こんなスクリプトにして、CRONで1時間に1回ぐらいのペースで実行するようにした。
アクセスログが肥大化するにつれて実行時間が延びるのが辛み。

#!/bin/sh
iLogScanner=/home/user/iLogScanner/1_bin/iLogScanner.sh 
LOGDIR=/home/user/logs/iLogScanner
ACCESS_LOG=/var/log/nginx/access.log

$iLogScanner mode=cui logtype=apache accesslog=$ACCESS_LOG outdir=$LOGDIR/ reporttype=text level=detail

LOGFILE="${LOGDIR}/$(ls -1t $LOGDIR | head -n1)"

if [ $(/usr/bin/nkf -w $LOGFILE | grep -c -E "^解析結果ログ") -ge 1 ];then
    /usr/bin/nkf -w $LOGFILE
fi

find ${LOGDIR} -daystart -mtime +7 -type f -exec rm {} \;

LOGDIRにひたすら解析ログが溜まっていく(7日分)。
何か引っかかると「解析結果ログ」という字句が入るので、それを拾うことで、検出されたかを判定するようにしています。

PyCon JP 2019でポスター発表しました——「Djangoで業務改善 〜子どもの福祉・教育に関わるNPOでの挑戦〜」

PyCon JP 2019でポスター発表しました。 諸般の都合によりがっつりポスターではなく、スライド資料をA3で8枚印刷したものを掲示しました。 (Slideshareで、掲示したスライド+αのものを公開しています)

www.slideshare.net

技術的なこと

今回の発表ではあまり技術的なバックグラウンドの事を盛り込んでいなかったので、いくつか技術的な部分をご紹介しておきます。

フロントエンド

Djangoのデフォルトテンプレートエンジンを使用し、CSSフレームワークとしてBootstrap3系を使用しています。
活動予定のカレンダー表示には、FullCalendarを使用しています。

プロジェクト構成

基本的に大まかな機能単位で、アプリ分割しています。
アプリを横断するようなものは、ひとまずcoreアプリにまとめる方針としています。
これらを一つのDockerコンテナの中で稼働させています。

.
├── Dockerfile
├── Dockerfile-development
├── bbs
├── chiebukuro
├── contribution
├── core
├── enquete
├── entrypoint.sh
├── gakubora_report
├── juken
├── kacomen
├── kacotabu
├── kacotam
├── manage.py
├── news_banner
├── requirements.in
├── requirements.txt
├── scheduler
├── sensei
├── static
├── sutasapo_report
├── tasks.sh
└── yurukichi

2017年の初期リリースから、適宜要望に応じて機能を追加し続けてきたので、だんだんカオスになってきています......。

RDB

RDBには、PostgreSQLを採用しています。特に深い採択理由はありません。

未来図の実現に向けて

未来図に向けて、ポータルの改修作業をちまちまと進めています。 その中で、単純に外に出せるcore部分を以下で順次公開しています。 github.com

ポータルで使っている、Djangoからチャットワークに通知したり、タスク追加したりするためのユーティリティは、django-chatworkとして、PyPiで公開しています。Issue、プルリク大歓迎です!!

pypi.org

github.com

FAQ

Q: 2015、2016年頃Djangoでゼロから作るのしんどくなかったですか?

A: Railsには浮気できない訳がありまして...、やり抜きました。bottle.pyを途中少し触って雰囲気をつかんだ感があります。

Q: インフラどうしてるんですか?

A: Cloud Garage の Develpper Assist Plan(開発者支援の無償利用プラン)を利用してきました(が、サービス終了を迎えるので、IDCFクラウドにお引っ越しします。月額3千円ほどでの構成になる予定)。

Q: サービスとして提供しているのですか?

A: 今はまだしていません。上述の未来図の通り後々にはしていきたいと思っています。コアのソースはOSSとして、カスタマイズや運用保守周りをサポートするようなモデルを想定しています。

Q: 実際どのくらい使ってますか?

A: 「もう無い頃には戻れない」というぐらいに使ってます。特に出欠集約周りは、自動のリマインドや月次集計レポート通知など人力から自動化された部分は、事務作業軽減に大きく寄与しています。

BIND9 ゾーンファイルの編集の罠

BIND9にて、ゾーン情報の編集を以下の流れでやった場合に、変更後の情報が反映されない事象が発生しました。

  1. 現在のゾーンファイル(zone.example.com)を別名にコピー(zone.example.com_copy)して、内容を編集(シリアル値はもちろんあげておく)
  2. 1.の作業から少ししてから、現在のゾーンファイル(zone.example.com)を編集して、BINDに再読み込みさせる
  3. 2.の作業の後に、現在のゾーンファイル(zone.example.com)を別名(zone.example.com_back)にして、zone.example.com_copyをzone.example.comにして、BINDに再読み込みさせる

この場合、2. においては、意図したとおりゾーンファイルの情報が再読み込みされるにも関わらず、3. の場合には、 再読み込みがされません(もちろんシリアル値は、大きくなっている)。

コマンドで上記を追うと、以下のようになる。

# cat zone.example.com
;$ORIGIN .
;$TTL 600           ; 10 minutes
$TTL 60             ; 1 minutes
@           IN SOA  ns.example.com. abuse.example.com. (
                2019090900 ; serial
                3600       ; refresh (1 hour)
                900        ; retry (15 minutes)
                604800     ; expire (1 week)
                86400      ; minimum (1 day)
                )
            NS  ns.example.com.
ns          A   172.16.3.1
test1           A   172.16.3.1


# dig +norec +short @172.16.3.1 test1.example.com
172.16.3.1
# dig +norec +short @172.16.3.1 test2.example.com   # 登録がないのでレスポンスなし
# dig +norec +short @172.16.3.1 test3.example.com   # 登録がないのでレスポンスなし


# cp -iva zone.example.com zone.example.com_copy
'zone.example.com' -> 'zone.example.com_copy'

# vi zone.example.com_copy 
# cat zone.example.com_copy 
;$ORIGIN .
;$TTL 600           ; 10 minutes
$TTL 60             ; 1 minutes
@           IN SOA  ns.example.com. abuse.example.com. (
                2019090910 ; serial    ; <-- 大きめに変更
                3600       ; refresh (1 hour)
                900        ; retry (15 minutes)
                604800     ; expire (1 week)
                86400      ; minimum (1 day)
                )
            NS  ns.example.com.
ns          A   172.16.3.1
test1           A   172.16.3.1
test3           A   172.16.3.1      ; <-- 追加した


# ls -la
total 16
drwxr-xr-x 2 root root 4096 Sep  9 18:01 .
drwxrwxr-x 3 root bind 4096 Sep  9 17:54 ..
-rw-r--r-- 1 bind bind  334 Sep  9 17:56 zone.example.com
-rw-r--r-- 1 bind bind  355 Sep  9 18:01 zone.example.com_copy

# vi zone.example.com
# cat zone.example.com
;$ORIGIN .
;$TTL 600           ; 10 minutes
$TTL 60             ; 1 minutes
@           IN SOA  ns.example.com. abuse.example.com. (
                2019090901 ; serial
                3600       ; refresh (1 hour)
                900        ; retry (15 minutes)
                604800     ; expire (1 week)
                86400      ; minimum (1 day)
                )
            NS  ns.example.com.
ns          A   172.16.3.1
test1           A   172.16.3.1
test2           A   172.16.3.1      ; <-- 追加した


# vi zone.example.com
# ls -la
total 16
drwxr-xr-x 2 root root 4096 Sep  9 18:02 .
drwxrwxr-x 3 root bind 4096 Sep  9 17:54 ..
-rw-r--r-- 1 bind bind  355 Sep  9 18:02 zone.example.com   # <-- こっちのタイムスタンプの方が新しい
-rw-r--r-- 1 bind bind  355 Sep  9 18:01 zone.example.com_copy


# rndc reload example.com    # 【リロード(1)】
zone reload queued

# dig +norec +short @172.16.3.1 test1.example.com
172.16.3.1
# dig +norec +short @172.16.3.1 test2.example.com   # <-- 追加したので出るようになった(意図通り)
172.16.3.1
# dig +norec +short @172.16.3.1 test3.example.com


# mv -iv zone.example.com zone.example.com_back
'zone.example.com' -> 'zone.example.com_back'
# mv -iv zone.example.com_copy zone.example.com
'zone.example.com_copy' -> 'zone.example.com'

# ls -la
total 16
drwxr-xr-x 2 root root 4096 Sep  9 18:04 .
drwxrwxr-x 3 root bind 4096 Sep  9 17:54 ..
-rw-r--r-- 1 bind bind  355 Sep  9 18:01 zone.example.com   # <-- mvで入れ替えたのでタイムスタンプが古いママ
-rw-r--r-- 1 bind bind  355 Sep  9 18:02 zone.example.com_back

 
# rndc reload example.com   # 【リロード(2)】
zone reload up-to-date
# dig +norec +short @172.16.3.1 test1.example.com
172.16.3.1
# dig +norec +short @172.16.3.1 test2.example.com   # <-- でないはずなのに出る
172.16.3.1
# dig +norec +short @172.16.3.1 test3.example.com   # <-- でるはずなのに出ない


# touch zone.example.com    # <-- タイムスタンプを新しくする
# ls -la
total 16
drwxr-xr-x 2 root root 4096 Sep  9 18:04 .
drwxrwxr-x 3 root bind 4096 Sep  9 17:54 ..
-rw-r--r-- 1 bind bind  355 Sep  9 18:05 zone.example.com
-rw-r--r-- 1 bind bind  355 Sep  9 18:02 zone.example.com_back
# rndc reload example.com   # 【リロード(3)】
zone reload queued


# dig +norec +short @172.16.3.1 test1.example.com
172.16.3.1
# dig +norec +short @172.16.3.1 test2.example.com   # <-- でなくなった(意図通り)
# dig +norec +short @172.16.3.1 test3.example.com   # <-- でるようになった(意図通り)
172.16.3.1

syslogには、以下のようにログが出力される。
【リロード(2)】のときには、「zone example.com/IN: loaded serial 2019090901」のように読み込んだファイルのシリアル値のログが出ていない。

Sep  9 18:03:50 localhost named[25597]: received control channel command 'reload example.com'
Sep  9 18:03:50 localhost named[25597]: zone example.com/IN: loaded serial 2019090901
Sep  9 18:04:52 localhost named[25597]: received control channel command 'reload example.com'
Sep  9 18:05:13 localhost named[25597]: received control channel command 'reload example.com'
Sep  9 18:05:13 localhost named[25597]: zone example.com/IN: loaded serial 2019090910

これは、ゾーンファイルの再読込について、ゾーンファイルの中身(シリアル値)が確認される前に、ゾーンファイルのタイムスタンプをBINDが確認しているためで、現在読み込まれているファイルのタイムスタンプよりも新しくなっていないと、再読込されない仕様なようです。

Tuning your BIND configuration effectively for zone transfers (particularly with many frequently-updated zones) - Performance

上のページの3段落目に、

the decision on whether or not the zone has changed is the serial number that is maintained in the zone's SOA record (although when a master reloads, it does also check the timestamp on the zone data file first, before checking the zone's SOA).

と書かれています。

普通に更新したら、ファイルのタイムスタンプは新しくなるだろうけど、上のように事前に用意しておいたファイルと置き換えるときには注意が必要なようです。

SSHの接続元IP制限(国単位)をhosts.allow, hosts.denyで

まとめ

  • iptablesIPアドレスを大量に登録するのは、いろいろ大変なので、動的に制御できたら嬉しい
  • Debian/Ubuntuだと、/etc/hosts.allow/etc/hosts.denyaclexec というオプション1を使用してプログラムの終了コードでアクセス制御ができる
  • GeoIPデータベースを利用してIPアドレスの国判定(IPv4/v6対応)のスクリプト書いた
  • OpenSSH自体では、6.7tcpwrappers/libwrapが除かれた2が、Debian/Ubuntu版openssh-serverではtcpwrappers/libwrapのサポートが継続中3という実は微妙な状況

書いたスクリプト

#!/usr/bin/env python3
#
# install required packeges:
# sudo apt-get install python3-geoip geoip-database libgeoip1
#
# download script:
# sudo curl -sS -o /usr/local/bin/check_geoip.py https://gist.githubusercontent.com/kacchan822/f9240646cfd78a5290a2ec95d844b1a0/raw/a5d7047beb493dfdfc63c70b00b3b3c5595e1d64/check_geoip.py
# sudo chmod +x /usr/local/bin/check_geoip.py
#
# setting up hosts.allow and hosts.deny:
# sudo sh -c 'echo "sshd: ALL: aclexec /usr/local/bin/check_geoip.py %a" >> /etc/hosts.allow'
# sudo sh -c 'echo "sshd: ALL" >> /etc/hosts.deny'
#
import ipaddress
import sys
import GeoIP

# CHANGE if allow from other countory.
ALLOWED_COUNTORY = ['JP',]

# Check Value
try:
    ip = ipaddress.ip_address(sys.argv[1])
except ValueError:
    sys.exit(1)

# Local IP is permitted
if ip.is_private:
    sys.exit(0)

# Check IP address version
if ip.version == 4:
    gi = GeoIP.new(GeoIP.GEOIP_STANDARD)
    cc = gi.country_code_by_addr(str(ip))
else:
    gi = GeoIP.open('/usr/share/GeoIP/GeoIPv6.dat', GeoIP.GEOIP_STANDARD)
    cc = gi.country_code_by_addr_v6(str(ip))

# Chaeck Countory Code
if cc in ALLOWED_COUNTORY:
    sys.exit(0)
else:
    sys.exit(1)

https://gist.github.com/kacchan822/f9240646cfd78a5290a2ec95d844b1a0

ポイント - ALLOWED_COUNTORYに許可したい国コード(2文字)を入れれば、日本以外も許可できる - IPv4,IPv6の両方に対応 - ローカルIPは許可

つかいかた

上記スクリプトの上部にもあるけど...

# install required packeges
ubuntu@ubuntu1604:~$ sudo apt-get install python3-geoip geoip-database libgeoip1

# download script
ubuntu@ubuntu1604:~$ sudo curl -sS -o /usr/local/bin/check_geoip.py https://gist.githubusercontent.com/kacchan822/f9240646cfd78a5290a2ec95d844b1a0/raw/a5d7047beb493dfdfc63c70b00b3b3c5595e1d64/check_geoip.py
ubuntu@ubuntu1604:~$ sudo chmod +x /usr/local/bin/check_geoip.py

# setting up hosts.allow and hosts.deny
ubuntu@ubuntu1604:~$ sudo sh -c 'echo "sshd: ALL: aclexec /usr/local/bin/check_geoip.py %a" >> /etc/hosts.allow'
ubuntu@ubuntu1604:~$ sudo sh -c 'echo "sshd: ALL" >> /etc/hosts.deny'

おわりに

openssh-serverのTCPwrapper対応がいつからなくなるのかには気をつけないと...。


  1. http://manpages.ubuntu.com/manpages/artful/man5/hosts_options.5.html

  2. apt-get changelog openssh-server で確認できるchangelogに次の記載がある(Ubuntu 17.10で確認)。”openssh (1:6.7p1-1) unstable; urgency=medium … * Restore TCP wrappers support, removed upstream in 6.7. It is true that dropping this reduces preauth attack surface in sshd. On the other hand, this support seems to be quite widely used, and abruptly dropping it (from the perspective of users who don’t read openssh-unix-dev) could easily cause more serious problems in practice. It’s not entirely clear what the right long-term answer for Debian is, but it at least probably doesn’t involve dropping this feature shortly before a freeze. …”

  3. http://www.openssh.com/txt/release-6.7

Cloud Garageの「接続許可(詳細設定)」でサーバーを守る

特定非営利活動法人Kacotamの内部用ポータルシステムの開発・運用等を担当させてもらっています(2017年5月からシステム稼働)。 このシステムのインフラ部分として、2017年9月からCloud Garage Dev Assist Program (DAP)での支援を受けています。 2017年7月から始まった新しいクラウドサーバーのサービスですが、これまで大きな障害等なく快適に使えています。

サービス開始当初は最小限の機能でのスタートでしたが、ついにCloud Garageにもインスタンス管理画面上で任意のポートに対する接続許可設定ができるようになっていました。

2018.10.05 新機能追加のお知らせ(接続許可 詳細設定)
従来のCloudGarageでは、未使用ポートへの不正アクセス防止のために、特定の開放ポートを選択して制限する方法をとっていましたが、今回のアップデートにより任意のプロトコル、ポート番号、送信元IPアドレスを指定できるようになりました。コントロールパネル上での簡単な操作で、より高いセキュリティレベルでの運用が可能になりましたので、ぜひご活用ください。 cloudgarage.jp

この機能ができるまでは、SSHの接続ポートを標準の22から変更している場合や、特定のIPからのみ全ポートを許可するなどをしたい場合は、インスタンスの管理画面上では、「全解放」にして、インスタンス内でiptablesなどを使って設定する必要がありました。

サーバー上での設定は、インスタンス作成直後や設定ミスで漏れが発生することもあるので、管理画面上からできるのは◎です

早速設定してみた

Cloud Garageのコントロールパネルはシンプルな作りなので、すぐに見つけられるような気がしますが、一応画面付きで順を追って紹介します。 一部、インスタンスの情報やIPアドレスの部分はマスクしています。

1. 変更するインスタンスの詳細画面を表示する

Cloud Garageのログイン画面からログインすると最初に表示される、インスタンスの一覧表示画面から、接続許可設定を変更したいインスタンス名をクリックする。 f:id:kacchan822:20181019133436p:plain

2. 接続許可設定のダイアログを表示する

インスタンス詳細画面の右下にある、「接続許可設定」の鉛筆アイコンをクリックする。 f:id:kacchan822:20181019133439p:plain

3. 接続許可「詳細設定」を行う

「かんたん設定」の範囲以外のポートを空けていたので、「全解放」のラジオボタンが選択されていました。 f:id:kacchan822:20181019133441p:plain

中央にある「詳細設定」のラジオボタンをクリックすると、ルールを入力できるようになります。
親切に説明があるように、ポートを範囲で指定したり、IPアドレスをブロック単位で指定することもできます。 f:id:kacchan822:20181019133444p:plain

入力ができたら、「更新」ボタンをクリックして保存します。

4. インスタンスの詳細画面で確認

正常に設定が完了すると、インスタンスの詳細画面の右下にある「接続許可設定」の部分に設定内容がリスト表示されています。 f:id:kacchan822:20181019133447p:plain

で、どうなの?

インスタンス内での設定は、iptablesのフロントとして、UFWを利用していて、ブロックしたパケットがあるとログに出るようにしています。ログを見ていると毎日毎日あらゆるところから、開いてもいない22や21、25への接続が来ていました(開いてないからつながりはしないけれど....)。
今回の「接続許可(詳細設定)」の設定後は、UFWのログに記録されるブロックパケットが皆無になりました。

Docker-composeを利用していると、ポートバインドの記述ミスで、UFWでは制限をかけていても、docker側のフォワードの関係でつながってしまう状況が発生してしまうこともあり得ます。
インスタンスに届く前で通信を制限してもらえることで、そういったミスの際のリスクを回避することができるので、ステキです!

(編集後記)

すっかりご無沙汰しておりました。
2017年から特定非営利活動法人Kacotamにて、システム開発室室長の銘を受けていました。
2017年春に、団体WEBページのリニューアル、内部向けポータルサイトの稼働などがあり、わたわたとした1年でした。
これまでは、僕自身を含め、Kacotamの主たる活動の学習支援活動と兼務でのメンバーばかりでしたが、最近開発室専従のメンバーが加わり始めました。
子どもの福祉に関わるNPOで、システム開発室を自前で持っているところはないと思うので、少しでも情報発信して行けたらなと考えています。
(後々専用のTech Blogができるかもですが、)しばらくは、Kita Eng. からお伝えしていきます。関連記事には、Kacotam-Si-Teamのタグをつけるようにしていきます。