Linux/fail2banでアクセスログ監視~その2

ありもしないWordPressへのアクセスが止まらないよ~(2017-03-08、追記:2019-10-20)


fail2banでアクセスログ監視 が導入できたので、その続きです。
また、明らかに同じ接続元からの攻撃が連続で来る場合は
静的なIPフィルタで接続拒否し続ける こともご検討ください。



botが蔓延しているせいなのか、
脆弱なURLを探すアクセスが、時に大量やってきます。

(一部抜粋)
/admin/config.php
/mysqladmin/scripts/setup.php
/phpadmin/scripts/setup.php
/pma/scripts/setup.php
/wordpress/
/wp/wp-admin/
/wp-login.php

404 Not Found で終わるから支障は無い・・・ですが、
脆弱性を探られた挙句に想定外の攻撃を食らうのは嫌なので、
「想定外のアクセス=即 ban」とするのが今回の目標です。

片っ端から即 ban  ~ignoreregex 以外、全拒否編

URL や ステータスを元に fail2banのフィルタを作ります。
設定が上手くいかない場合は 前回(導入編) もご参照ください。

まず新しいフィルタルール、
/etc/fail2ban/filter.d/apache-whitelist.conf を作成します。

[Definition]
# ここはログに合わせる。ログの中から HOST が特定できる正規表現にすること。
failregex = <HOST>

# 見づらいから改行しましたが、本来は一行で書く。
# スペース有無に注意。パイプ「|」区切りは先頭行末には不要。
ignoreregex = .css.php[ \?]|index.html[ \?]|index.php[ \?]| / |
                .gif |.png |.jpg |.ico |.css |.js |
                robots.txt |\?plugin=sitemap |/sitemap.xml | 408 |
                /directory1/|/directory2/

# 日付書式を変更している場合は、
# datepattern も作成する

failregex はHOST名ありのログ全てを対象にします、

ignoreregex で無視するログ、
つまり今回は「接続許可する」ログの正規表現を書きます。
 「|」は「または(or)」の意味、
 「?」は前文字の0回か1回の繰り返しの意味なので、そうならないように「\?」
 「[ \?]」は、スペース か ?のどちらか、という意味、
 「.」も任意の一文字という意味ですが、今回は「\.」には直しません。面倒なので。
それを踏まえて、
 index.htmlなどクエリ文字「?」を許可するファイルと、
 それ以外のクエリを認めないファイル・拡張子を分けて指定する。
 検索用のrobots.txtファイルやsitemapも忘れずに足して、
 408 はリクエストTime Outを無視するため、
 (408 Time Out 時はメソッドもURLも出ない)
 最後にディレクトリごと許可する場合を指定しています。

・・・と、いきなり記入例を挙げてしまいましたが
正規表現の選別方法の例は後述します。
最初は、もっと緩い条件から徐々に細かい ignoreregex に変えてもよろしいかと。

これらをfail2ban の jail に設定します。
今回は /etc/fail2ban/jail.d/local.conf に直接書いています。

[apache-whitelist]
enabled = true
port    = http,https
usedns = no
logpath = %(apache_access_log)s tail
filter = apache-whitelist
maxretry = 1
findtime = 1
bantime = 1
backend = pyinotify

maxretry を1に指定して、一回のアクセスで即 ban します。
bantime は上記では1にしていますが、
設定確認後に数分~数時間に増やしましょう。

これで設定を読み込みます。

# service fail2ban reload

あとは、「tail -f /var/log/fail2ban.log 」などしながら、
Webブラウザや wget 等で不正なURLへアクセスして確認してみましょう。

認証失敗に対するフィルタ ~HTTP応答ステータスで拒否編

うちのページはWikiなので、編集機能と認証機能もあります。
それらへの不正アクセスも減らすために、
HTTPの応答ステータスでもフィルタを検討します。
(Apache の LogFormat だと %s か %>s の部分)


ここで脱線しますが、apache の LogFormat について。

LogFormat は デフォルトだと
「%h %l %u %t \"%r\" %>s %b」だと思いますが、
%s 等のよく使う値は、%r 等の書式自由な値より前に持ってくると便利です。
なぜかというと、
%r はスペースを含む数が変わるので
スペース区切りで前から何番目という指定ができなくて面倒くさいからです。
(同じく、日時 %t も使いやすい形に変えられます)

そして、書式を変える場合は、
https通信についても確認しておきましょう(SSLの有無と、そのログの書式)。
私の環境では http-ssl.conf で TransferLog が別途定義されていたため、
こちらの書式も直す必要がありました。
(TransferLogの前に、ニックネーム無しの LogFormat を定義する)


話を戻して、
アクセスログはこの書式に変えました。
「LogFormat "%{%y%m%d/%H:%M:%S}t %h %>s ~以下省略~"」
(時刻、IP、ステータス。欲しいのは3番目)

170201/11:22:33 127.0.0.1 200 (~以降省略~)

それをふまえて、追加する fai2banの filter の値は

failregex = ^[\d:/]* <HOST> [45]\d\d

400番台はクライアントエラー、500番台はサーバエラーです。
([45]\d\dではなく、(401|403) のように明確な方が良いかもしれません)
404 Not Found については、
「正常」な接続もあるのでフィルタする場合は慎重に。
(例えば favicon.ico や apple-touch-icon をはじめとした、
 有無にかかわらずブラウザ標準で取りに来ては 404 になるファイルがある*1

banできないよ!? ~接続済みセッションを 一旦 Closeするには

ログを見ながらのフィルタなので、
常に初回アクセスは許可しています。*2
・・・そう、最初のセッションで一気に連続でやられたら、
止めることができないのです。やられました・・・

fail2ban.log で、なぜか バンバンうるさいと思ったら・・・
[apache-whitelist] Ban 162.243.242.xxx
[apache-whitelist] 162.243.242.xxx already banned
[apache-whitelist] 162.243.242.xxx already banned

以下は wget で1秒間隔で(-w 1)アクセスした例です。
index.html(正常)、 bad_urlcgi(攻撃)、 index.html(正常) の順に同一セッション内でアクセスしています。

# wget null-i.net/index.html -w 1  null-i.net/bad_url.cgi -w 1  null-i.net/index.html
(2つ目 bad_url.cgi でfail2ban が有効になるが、3つ目が接続できてしまう)
# wget null-i.net/index.html
(続けてコマンド実行し、今度はfail2banで接続拒否済みであることを確認)

firewallで drop できるのは新規セッションなので、
接続済みのセッションは一度、切断させないといけません。
さて、どうしようか・・・

KeepAlive*3を無効にする手もあるけど、正常な通信の効率が落ちるのは避けたいし・・・
404エラーの場合だけ、セッションを切断できないか?
・・・という訳で試しに ErrorDocument ディレクディブで cgi からの closeを試みます。

まずは、404 Not Found で呼ばれる bad_url.cgi(今回はperlスクリプト)を作ります。

#!/bin/perl

# refresh での10秒後ジャンプ先「http://任意のURL」は書き換えてください!!
# もっとも、banする場合はジャンプできないので、この行は削除でも構いませんが。
my $body=<<'EOS';
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Not Found</title>
<meta http-equiv="refresh"content="10; url=http://任意のURL">
<body>
Not Found, and some access will be dropped by firewall, sorry.
</body></html>
EOS

# connection close することを明示する
print "Content-type: text/html; charset=UTF-8\r\n";
print "Content-Length: " . length($body) . "\r\n";
print "Connection: close\r\n";
print "\r\n";
print $body;

close(STDOUT);
close(STDERR);

一行目の perl のPATHは環境に合わせて変更し、cgiファイルの chmod と chown を忘れずに。
もちろん perl 以外でも構いません、Connection close を投げるのが目的です。
(初 cgi の場合は、少し苦戦するかもしれませんが、
 apacheのサイトにある cgiチュートリアルを参考にがんばりましょう。)

このファイルを、404 の時に参照するように
http.conf で指定します。PATHは例なので cgiを置いた場所に変更してください。*4

ErrorDocument 404 /cgi-bin/bad_url.cgi

そして、「service httpd restart」(あるいは graceful)などでコンフィグを更新したら、
もう一度、動作確認します。

# wget null-i.net/index.html -w 1  null-i.net/bad_url.cgi -w 1  null-i.net/index.html
(2つ目 bad_url.cgi でfail2ban が起動して、3つ目が拒否されることを確認する)

念のために tcpdumpで確認したところ、
404 Not Found 時にサーバ側からTCP-Finを返していることは確認できました。
つまり、apache が一旦接続を切る旨を wget へ伝えています。
そのため、wgetの3つ目は新規セッションになり、firewallで drop されます。

ただし、さらに言えば、
wget の秒間隔「-w 1」を消せば、やはり3つとも通ることが確認できます。
これは単に、
アクセスログ出力~fail2ban検知~firewall設定
まで(1秒くらい?)のタイムラグでしょう。
これをログ監視で防ぐのは難しいので、止めるなら・・・WAFか?*5

ここまでできたら、 bantime を変えつつ、様子を見ましょう。
一応、数日様子を見ましたが、
これだけでも 脆弱性探しの 404の数がグッと減りました。


後日談:
という訳で、様子を見たのですが、

  1. 明らかに効果があるのは、定番のアクセス
    WordPress、DBなどの定番な脆弱性探しはキーワードが決まっているので
    止める価値はある
  2. ただ、不正アクセスのバリエーションは多いので、
    正規表現で静的に止めるのは限界はある
  3. fail2ban が発動する前に連続で来る、
    最初の一秒間で一気にくるアクセスは煩わしい

2つ目はいたちごっこなので、一旦おいておいて
(企業ならちゃんとした製品を導入すれば良いのでしょうけれど)
3番については、結局 「squid を導入してのTCP_RESET で切断」という手で様子を見ています。
もちろん、Apacheのプラグインを探しても良いですし、
snort を間に挟んで(監視ではなく中継させて)不正アクセスは切断するとか、色々できそうです。

(参考)フィルタルールの絞り込み作業例

ここからはフィルタ正規表現を足したり削ったりする作業例です。
何から手を付けようか思い浮かばない場合に、
案の1つとして参考にして頂ければと思います。

まず、
特定のアクセスだけを許すのか(ホワイトリスト)、
特定アクセスだけを止めるのか(ブラックリスト)。
今回は前者を採用して、
想定外のアクセスはすべて拒否する方針です。
(次から次へと違うURLでアクセスしてくるので、ブラックリストは難しい)

ちなみに、
うちのサイトはPukiwikiで作っています。
cgiも自分でアレンジしたもの以外は、Pukiwikiがベースです。
結果、index.php にクエリが続く、というのが基本書式です。
(index.php を index.htmlに置き換えて動作させています)

よって正常なWebアクセスは、こんなものを想定します、
(そもそも(HEADとかGETとか)何言ってるのか分からない方、これらはHTTPリクエストの一行目の構文なので、Webアクセスを監視するならばぜひ調べてみてください)

HEAD  /                            HTTP/1.1
GET   /aaa/image.png               HTTP/1.1
POST  /index.php?aaa=bbb&ccc=ddd   HTTP/1.1
※スペースを強調して書いています。

なお、「クエリ」というのは上記3行目の「?aaa=bbb&ccc=ddd」の部分で、
Webブラウザから Webサイトへ情報を渡すときに付きます。
(Pukiwikiだとサイト間の移動や文字列検索など、色々使っています)
一方で cgiではないファイル、画像ファイルとかにはクエリは付きません。

そこで、あくまで一例になりますが・・・
ディレクトリ、ファイル名や拡張子、クエリ有無
に着目するのはどうでしょうか。
つまり以下のパターンをホワイトリストの基本形とします。

「/」のみ
「/」+「ディレクトリ名」+「/」
「特定のファイル名・または拡張子」+「スペース or クエリ」 ※クエリを許可する
「特定のファイル名・または拡張子」+「スペース」      ※クエリを許可しない

それはさておき、
細かくは、のちのち調整するとして、
まず試しにファイル名のリスト regexp.txt を作ってみます。

# cat regexp.txt
[ ]/[ ]
[ ]//[ ]
index.html[ \?]
index.php[ \?]
.gif[ ]
.png[ ]
.jpg[ ]
.ico[ ]
.css[ ]

上記の[ \?] はスペース or ?という意味で、?は「\?」と表現します。
これをアクセスログにかけてみます。
以下、apacheの例です。nginx など別Webサーバの場合も流れは同じですが。

 軽く素振りしてみましょう。
 regexpでフィルタした結果を数行出してみます。

# egrep -f regexp.txt  /var/log/httpd/access_log | head

このログか、あるいは httpd.conf などの LogFormat で
応答ステータス(%s)と、リクエスト内容(%r)が何フィールド目か確認します。
ステータスやリクエストがログ出力に無い場合は追加しましょう。
私の環境だとそれぞれ3番目、6番目以降だったので、

# egrep -f regexp.txt  /var/log/httpd/access_log | awk '{ printf("%s: %s %s %s\n", $3, $6, $7, $8);
# egrep -vf regexp.txt  /var/log/httpd/access_log | awk '{ printf("%s: %s %s %s\n", $3, $6, $7, $8);
期待する出力の例
200: "GET / HTTP/1.1"
200: "GET /index.html HTTP/1.1"
200: "GET /index.php?cmd=list HTTP/1.1"
200: "GET /image/rss.png HTTP/1.0"

egrep は 正規表現を使える grep です。
「egrep -f 」はファイル内の文字列リストで絞り込むためのオプション
「-v 」は ”含まない”場合に指定、つまり除外条件指定。
awkで $3番目(ステータス)、$6~$8番目(HTTPリクエスト)を出しています。
この出力結果でステータス・リクエストが無事表示できたら、更に絞り込みをかけます。

egrep -vf regexp.txt  /var/log/httpd/access_log* |\
 awk '{ printf("%s: %s %s %s\n", $3, $6, $7, $8); }' |\
 egrep '^2' |\
 sort -u

先頭が 200番台で始まるリクエスト(egrep '^2')を表示します。
sort -u は重複行を排除して表示行数を減らす為です。

つまり、
この出力が0件になるのが目標です。正常な通信はすべてregexp.txtに含まれる状態にします。
(regexp.txt が正常系通信を網羅した状態、をつくりたい)

regexp.txt の過不足を直して egrep を繰り返しましょう。
たとえば私の場合、 robots.txt や favicon.ico 等が抜けていることが分かりました。

この出力が0件になったら、逆にリストの内容が甘くないかを確認します。

egrep -f regexp.txt  /var/log/httpd/access_log* |\
 awk '{ printf("%s: %s %s %s\n", $3, $6, $7, $8); }' |\
 egrep '^[45]' |\
 sort -u

400、500番台で始まるリクエスト(egrep '^[45]')を表示します。
表示されたものは正常な通信として許可されてしまうので、
許容できないものまで regexp.txt リストに入らないように条件を見直しましょう。

regexp.txt が確定したら、前述の fail2ban フィルタへ戻ります。
各行を「パイプ(|)」で繋いで一行にして、ignoreregex に渡しましょう。

別の例として、
fail2banに渡すのではなく、
ログを正規表現で読み取って、IPアドレス一覧を作る例はこちら。
阻止対象のIPアドレス一覧があるのなら、fail2banではなく firewall-cmd で静的に止めるという方針です。

いずれにせよ、egrep でも、awkでも perlでも何でも良いので、
正規表現を使っていい感じにログを読み取る作業には
慣れておいて損は無いと思います。

(参考)正規のアクセスと不正アクセスを分けられない場合は・・・

正規表現をどう書くかについては・・・慣れです [smile] (私もよく間違えます)
ただ、
ファイル名やディレクトリ構成は工夫できます。
ファイル名変更が困難でない限りは(仕様上できなかったりしなければ)、
初期の名前・構成は攻撃対象になるだけなので、
変えてしまうことをお勧めします。
例えば、

「/cgi-bin/setup.cgi」の置き場所を「/cgi-bin/my_secret/setup.cgi」に変える~
(mkdir と mv するだけです)

これだけでも bot の追跡は減りますし、
ホワイトリストで仕訳もしやすくなります。(/my_secret/で仕訳できる)

(参考)フィルタで許可するべきリクエスト?

まず「分かりません」と言い切った上での話ではあるのですが...
前述の通り、ホワイトリスト方式(想定したものを許可する。他は全拒否)
の設定にはしたものの、想定外の「正規の?」リクエスト、クエリが多すぎます...
他にも困っている方も居ると思うので、もう少しメモを追記します。

詳細については各々の情報を探していただくとして、検討の際の手がかりになればと。

robots.txt
sitemap.xml

サイトの情報を渡します。
ボットが検索する場所の許可/拒否を伝えるrobotsとサイトの構成を教えるsitemapですが、
メジャーな会社のクローラーも含めて、Disallowとか無視してくるので、正直意味があるのか疑問。

favicon.ico

アイコン画像。Webブラウザでブックマーク追加した時に表示されたりするやつ。
サイトに置こうが置くまいが検索される画像ファイルです。

ads.txt
app-ads.txt

広告に関するやつ。他にもあるはず。年々増える。
悪意のサイトやアプリが、正規品を詐称して広告費をかすめとる等を抑止する為の付与情報らしい。

security.txt

そのサイトにセキュリティに関する脆弱性が有った際に、
研究者(researchers)と会社/組織(companies)との橋渡しを担う目的の付与情報で、
2019.10現在でRFCドラフト段階らしい。詳細は securitytxt.org 参照。

apple-touch-icon.png
apple-touch-icon-precomposed.png
apple-touch-icon-120x120.png など

iPhone等が使用するアイコン用の画像。
これもfaviconのように、サイトに置こうが置くまいが検索されるらしい。

browserconfig.xml

IE11あたりが何か探しに来るらしく、私は試していませんが公式の情報によるとHTMLタグに
「<meta name="msapplication-config" content="none"/> 」を付ければ
探さずにそっとしておいてくれるらしい。

"fbclid=なんかのID"

クエリ(URLで"?"のあとに続く情報)として載ってくる、
Facebook絡みのアクセスで付与されてくる識別情報。謎。
HTTPヘッダー(Cookieやリファラー)ならともかく、こんなものクエリに載せられても...*6

.well-known/

ディレクトリ。RFCに規定されているよく知られた(うぇるのうん)情報を置く用の場所なのだが、問題は、
そこにある情報を攻撃材料として漁ってくるアクセスが多い点・・・

autodiscover/autodiscover.xml

これは余談ですが、Outlookでメール設定する際に
間違えてPOPやIMAPではなくOutolookとかを選んでしまった際に
このURLがWebサーバにリクエストされました(メール関連だけどhttpsに飛んできた)。
前述のFacebookもですが、様々なアプリがそれぞれhttpsへ汎用的に情報連携をしてくるようですね。

リクエストに何が飛んでくるか事前に予測するのなんて不可能だよなぁ、とあらためて思います。

http(s)はファイアウォールを通過しやすいポートです。事情は色々あると思いますが、

  • 会社にせよ施設にせよ、通常使うであろうメールやWebアクセス以外のすべてのポートはファイアウォールで塞いでしまったほうが安心できる。
  • 自分で勝手なポートで通信しようとしても途中でどこかのファイアウォールで止められる。
  • だから、確実に通過できそうな http / https をWebブラウジング以外の用途でも流用する。

情報のやり取りにはとりあえずhttpを使う場合が多いことをふまえると、Webサーバ側に間違った?リクエストが飛んでくることも多くなるのだと思います。


...というわけで、いま把握している「正規の?」アクセスだけでもこんなにあって、
まだ他にも色々有あるのだと思います(が、バンバンbanしてます)

これらファイルの「公式な(?)」情報を探しても見つからない時には、
"what is なんとか" みたいに、ためしに英語でも検索してみて下さい。
新しい情報とかは英語の方がヒットします。

私はひとまずバンバンbanするスタイルですが、
企業とかだとそうは行かないと思うので
(Facebookからのアクセスをbanしてユーザを取り逃がすわけにも行かない〜、とか)
自動でやってくれそうなセキュリティ機器を早々に導入したほうが工数的には安くつくか、
あるいは自力でやるなら少し期間を多めにとって、最低でも一ヶ月以上はかけてログを追って調整する必要が、
あるようなないような、そんな感想です。



・・・ここまで長くなってしまいましたが、
おつかれさまでした!

私も日々、手探りの話題なので、少しでもご参考になれば幸いです。


*1 他には .well-known/assetlinks.json に apple-app-site-association ・・・あと、何だ? 自分の知らないファイルにアクセスされても、不正も正常も区別付きませんよ、ねぇ?
*2 そう、プロレスラーは初撃は全身で雄々しく受け止める掟なのです!・・・しかし私はSEなので初撃もらったら斃れてしまいます!このままでは・・・
*3 HTTPのKeepAliveについて。Webサーバから情報を取得する時の流れで 「1.TCP接続する、2.HTTPで送受信する、3.TCP切断する」、という流れを「1.TCP接続する、2.HTTPで送受信する、3.HTTPで更に何度も送受信する、4.最終的にTCP切断する」と、一回のTCP接続で複数のHTTP送受信し、いちいちTCPを繋いだり切ったりする負荷を減らすための仕組みです。TCPのKeepAliveというのもありますが、それは少し別の話になります。とにかく、HTTPリクエストのKeepAliveヘッダ や 応答のConnectionヘッダ がこれに該当します。
*4 cgi-binが初期ディレクトリとして用意されていると思いますが、できれば名前変更した方が良いかと。推測されて標的にされてしまうので。
*5 Webアプリケーションファイアウォール、もフリーウェアはあります。あとはWAFでは無いですが、squid等をリバースプロキシにしてURLフィルタをかける、とか?
*6 アプリをまたいで情報渡す手段がURLしか無いのかもしれないけど、敢えてhttpsで暗号化されない部分に載せるのは、ファイアウォールで止めるなり盗聴・詐称するなり、煮るなり焼くなり好きにせよという事だろうか?