1.はじめに
WAFをサービス等で導入するにはコストが。。。
なんとかOSSで対応できないか。。。
そんな時に出会った「ModSecurity」について少しご紹介したいと思います。
上記の画像からもわかる通り、OSSで開発されているWeb Application Firewall(WAF)の1つで、2011年より、TurstwaveのSpiderLabsというところが開発を行っているそうです。
ModSecurityでは、攻撃を検知するための基本的なルールセット「Core Rule Set」(CRS)が準備されており、現在はOWASP(Open Web Application Security Project)がCRSをメンテナンスしているということで、とても心強いです。
そんな素敵なModSecurityですが、日本語の文献があまりないためハードルが少し高く感じられます。。。
そこで、CRSのほかに簡単なルールセットを自分で作る方法を考えてみました。
2.環境
- OS => CentOS Linux release 7.5.1804 (Core)
- Kernel => 3.10.0-862.el7.x86_64
- Apache => Apache/2.4.35 (Unix)
- ModSecurity => 2.9.1
3.ModSecurityのインストール
まずはModSecurityのインストール。
yumは使わずソースインストールしていきます。
まずはソースファイルをサーバに格納します。WinSCP等で格納してもいいですし、wgetで格納してもいいです。
ソースは下記公式のGithubからDL出来ます。
https://github.com/SpiderLabs/ModSecurity/releases
下記の場合wgetで/usr/local/src内にファイルをダウンロードする想定です。
cd /usr/local/src
wget https://github.com/SpiderLabs/ModSecurity/releases/download/v2.9.1/modsecurity-2.9.1.tar.gz
格納したtarファイルを解凍し、解凍されたファイルに移動します。
tar xvfz modsecurity-2.9.1.tar.gz
cd modsecurity-2.9.1/
移動したら、configureを実行します。
この時、prefixでapxs、apr、apr-utilのパスを指定します。
./configure --with-apxs=/usr/bin/apxs --with-apr=/usr/local/src/source/httpd-2.4.35/srclib/apr --with-apu=/usr/local/src/source/httpd-2.4.35/srclib/apr-util
apxsのパスがわからない場合はwhichコマンドで調べましょう。
[root@www ~]# which apxs
/usr/bin/apxs
configureが終わったらmake、make installを実行します。
make
make install
make installが終わったら/usr/localにmodsecurityが出来ていると思います。
ll /usr/local
4.Apacheの設定
次にApacheの設定です。
まず、httpd.confに以下モジュールが記載されていることを確認し、コメントアウトになっている場合は有効化しましょう。
#LoadModule unique_id_module /usr/lib64/httpd/modules/mod_unique_id.so
#LoadModule security2_module /usr/lib64/httpd/modules/mod_security2.so
↓
LoadModule unique_id_module /usr/lib64/httpd/modules/mod_unique_id.so
LoadModule security2_module /usr/lib64/httpd/modules/mod_security2.so
今回、ModSecurityのconfはhttpd.confとは別にextra内に配置しますので、IfModuleディレクティブを記載します。
<IfModule security2_module>
Include conf/extra/security.conf
</IfModule>
apacheの設定が終わったらモジュールが読み込まれているか確認しましょう。
httpd -M
5.ModSecurityのconfを書く
いよいよModSecurityのconfを書いていきます。
今回はカンタンに以下の設定をWAFとして機能させます。
ステータスコード | 内容 | 想定される原因 | 検知時間 | 検出回数 | ブロック時間 |
---|---|---|---|---|---|
400 | BadRequest | リクエストフォーマットエラー | 30秒 | 50回 | 4時間 |
403 | Forbidden | 権限なし | 30秒 | 50回 | 2時間 |
404 | Not found | 対象ページなし | 30秒 | 50回 | 2時間 |
500 | Internal server error | DBエラー、SQL異常 | 30秒 | 50回 | 4時間 |
「400エラーのリクエストを30秒間に50回実行したIPを2時間ブロックする」というような感じです。
この辺の閾値については、アクセス数やリソースの容量など様々な要因が関係してくると思いますので、それぞれでチューニングが必要になってくると思います。
ひとまず今回は、以上のルールに合わせてconfを作成してみました。
#モード切替(DetectionOnlyは検知のみ)
#SecRuleEngine On
SecRuleEngine DetectionOnly
SecDataDir /var/log/httpd/mod_security/data
SecAuditEngine RelevantOnly
SecAuditLog /var/log/httpd/audit.log
SecAuditLogParts ABCFHZ
SecDebugLog /var/log/httpd/modsec-debug.log
SecResponseBodyAccess On
# 無効なリクエストステータスを403で拒否する
SecDefaultAction "phase:2,log,auditlog,deny,status:403"
# テキストファイルに記載したホワイトリストを読み込み
SecRule REMOTE_ADDR "@pmFromFile whitelist_ip.txt" "phase:1,id:1,nolog,allow,ctl:ruleEngine=Off,ctl:auditEngine=Off"
# ブラックリストに追加されているか確認し、該当する場合ブロッキングする。
SecRule REMOTE_ADDR "@pmFromFile blacklist.txt" "phase:1,id:2,deny,msg:'Blacklisted IP address'"
# 設定ファイルに記載の文字列を含む不正なアクセスを403で拒否する
SecRule REQUEST_URI "@pmFromFile block_uri.txt" "phase:1,id:3,deny,log,status:403"
#クライアントのIPアドレスを格納する変数REMOTE_ADDR を準備する
SecAction phase:2,id:4,nolog,pass,initcol:ip=%{REMOTE_ADDR}
# 400のアクセスを監視する。
SecRule RESPONSE_STATUS "@streq 400" "phase:5,id:5,t:none,log,pass,setvar:ip.400_counter=+1,deprecatevar:ip.400_counter=50/30,expirevar:ip.400_counter=14400"
SecRule ip:400_counter "@gt 50" "log,drop,phase:2,id:6,msg:'upper limit of 400 has been exceeded'"
# 403のアクセスを監視する
SecRule RESPONSE_STATUS "@streq 403" "phase:5,id:7,t:none,log,pass,setvar:ip.403_counter=+1,deprecatevar:ip.403_counter=50/30,expirevar:ip.403_counter=7200"
SecRule ip:403_counter "@gt 50" "log,drop,phase:2,id:8,msg:'upper limit of 403 has been exceeded'"
# 404のアクセスを監視する
SecRule RESPONSE_STATUS "@streq 404" "phase:5,id:9,t:none,log,pass,setvar:ip.404_counter=+1,deprecatevar:ip.404_counter=50/30,expirevar:ip.404_counter=7200"
SecRule ip:404_counter "@gt 50" "log,drop,phase:2,id:10,msg:'upper limit of 404 has been exceeded'"
# 500のアクセスを監視する
SecRule RESPONSE_STATUS "@streq 500" "phase:5,id:11,t:none,log,pass,setvar:ip.500_counter=+1,deprecatevar:ip.500_counter=50/30,expirevar:ip.500_counter=14400"
SecRule ip:500_counter "@gt 50" "log,drop,phase:2,id:12,msg:'upper limit of 500 has been exceeded'"
上からざっくり解説していきます。
SecRuleEngine
SecRuleEngineはOn|Off|DetectionOnlyを設定することができ、ModSecurityのON/OFFとログ記録のみのモードに設定できます。
ログ記録モード「DetectionOnly」では、ログのみを記録し検知後に設定された動作は行いません。
conf内に記述のある400アクセス監視の部分を例にすると、閾値を超えるアクセスが発生した場合にはFINパケットを送信してTCP接続の即時終了を実行するよう設定しているのですが、DetectionOnlyモードであればログに証跡は残すが接続のドロップ処理は発生しない、といった感じです。
はじめはDetectionOnlyモードを使用し、意図した検知内容がログに上がっているか確認してからONに変更するといいと思います。
SecDataDir
永続データ(IPアドレスデータ、セッションデータなど)を格納するパスを指定します。
ここに検知されたIPなどが格納され、管理されます。
apacheが書き込み可能な場所に設定し、パーミッションも設定しましょう。
以下、confの通り/var/log/httpd/内にディレクトリを作成する場合。
mkdir /var/log/httpd/mod_security/
mkdir /var/log/httpd/mod_security/data/
chown -R apache:apache /var/log/httpd/mod_security
chmod -R 755 /var/log/httpd/mod_security
SecAuditEngine
監査ログのエンジンを設定します。
このディレクティブは全てのトランザクションを記録する監査ログのエンジンを設定するために使われます。
RelevantOnlyに設定した場合、エラーや警告の場合、もしくはSecAuditLogRelevantStatusディレクティブのルールにマッチするトランザクションのみ記録します。
SecAuditLog
監査ログの吐き出し先を指定します。
SecAuditLogParts
監査ログにどのパートの情報をロギングするかをアルファベットで指定します。
パートは以下の通りです。
A AuditLogヘッダー
B リクエストヘッダー
C リクエストボディ
D Reserved
E レスポンスボディ
F レスポンスヘッダー
G Reserved
H 追加情報。パターンにマッチしたアクセスだとここにタグが付与される。
I ファイルを除外した、コンパクトなリクエストボディ
J Reserved
K トランザクションにマッチした全てのルール
Z 最後の境界線
SecDebugLog
デバッグログの吐き出し先を指定します。
SecResponseBodyAccess
レスポンスボディをバッファするかどうかをON|OFFで設定します。
レスポンスボディに監査対象が存在する場合、SecAuditLogPartsで「C リクエストボディ」を指定する場合はONがいいのだと思います。
SecDefaultAction
同じ設定範囲にあるルールへ継承するデフォルトの動作を定義します。
無効なリクエストステータスがあった場合はこのルールが適用されます。
SecRule
これで処理を実行するルールなどを定義します。
SecAction
無条件で処理を実行します。
今回の場合、アクセスしてきたクライアントのipを変数に格納しています。
変数の初期化をするときにも使用するようです。
ホワイトリスト作成
ホワイトリストは以下の部分で定義しています。
# テキストファイルに記載したホワイトリストを読み込み
SecRule REMOTE_ADDR "@pmFromFile whitelist_ip.txt" "phase:1,id:1,nolog,allow,ctl:ruleEngine=Off,ctl:auditEngine=Off"
ホワイトリストを記載するテキストファイルはModSecurityのconfと同じ場所に配置しましょう。
この場合、ModSecurityのconfは/etc/httpd/conf/extra/security.confにあるので以下コマンドでテキストファイルを作成します。
touch /etc/httpd/conf/extra/whitelist_ip.txt
中身にはホワイトリスト対象のipアドレスを箇条書きで記載していけばOKです。
192.168.1.1
192.168.1.2
192.168.1.3
pmFromFileで指定されたテキストファイル内のIPアドレスをREMOTE_ADDRで保管し、以下の設定を適用することでテキストファイルに記載のIPアドレスを検知対象外に設定しています。
nolog=>ログとらない
allow=>アクセス許可
ctl:ruleEngine=Off=>ルール検知しない
ctl:auditEngine=Off=>監査ログ記録しない
ブラックリスト作成
同じ要領でブラックリストを作成します。
touch /etc/httpd/conf/extra/blacklist.txt"
xxx.xxx.xxx.xxx
xxx.xxx.xxx.xxx
xxx.xxx.xxx.xxx
# ブラックリストに追加されているか確認し、該当する場合ブロッキングする。
SecRule REMOTE_ADDR "@pmFromFile blacklist.txt" "phase:1,id:2,deny,msg:'Blacklisted IP address'"
ホワイトリストと同じ要領で、テキストファイル内のブラックリストを保管しdeny(アクセス拒否)することでアクセスを遮断しています。msgはログに残すメッセージです。
不正なURL文字列を裁く
アクセスログを見ていると、よくある[wp-admin]や[login.php]等へのプログラムを使用した不正アクセスが目立ちます。このようなアクセスを検知するべく作成したものです。
security.confの以下部分になります。
# 設定ファイルに記載の文字列を含む不正なアクセスを403で拒否する
SecRule REQUEST_URI "@pmFromFile block_uri.txt" "phase:1,id:3,deny,log,status:403"
ここではSecRuleにREQUEST_URIを使用しています。
REQUEST_URIはクエリ文字列データを含む完全なリクエストURLを保持します。(例:/index.php?p = X)。
※ドメイン名は含まれません。
このSecRuleによって、block_uri.txtに記載されている文字列がリクエストにあった場合に403でdeny(アクセス拒否)しています。
もちろん、公開しているページに文字列が該当してしまった場合見れなくなってしまうので要注意ですが。。。
e.g.
root
admin
login
config
user
閾値でアクセス遮断
例として400ステータスの場合で説明します。
# 400のアクセスを監視する。
SecRule RESPONSE_STATUS "@streq 400" "phase:5,id:5,t:none,log,pass,setvar:ip.400_counter=+1,deprecatevar:ip.400_counter=50/30,expirevar:ip.400_counter=14400"
SecRule ip:400_counter "@gt 50" "log,drop,phase:2,id:6,msg:'upper limit of 400 has been exceeded'"
まず、一行目のRuleではRESPONSE_STATUSでクライアントのリクエストに対するレスポンスステータスを保管します。
RESPONSE_STATUSに保管されたステータスコードはstreqで文字列比較を実行し、パラメータ文字列と入力文字列が同じ場合はtrueを返します。
次にphaseですが、今回のRuleにはphase:5が設定されています。
各フェーズのざっくりとしたイメージは以下になります。
(見づらくてすみません。。)
- phase5
この1行目のRuleはあくまでもレスポンスステータスが400であるかどうかをチェックしています。
そのため、phase5での実行タイミングを指定しています。
- t:none
t:noneは公式ページの説明で以下のようにあったため適用しています。。
SecRuleで指定した変換関数は、前のSecDefaultActionで指定したものに追加されます。ルールでは常にt:noneを使用することをお勧めします。これにより、デフォルトの設定によってはこれらが防止されます。
- log
apacheのエラーログとModSecurity監査ログ(audit.log)へ記録します。nologの場合は記録しません。
- pass
このRuleにマッチした場合も次の処理(次のRule)も実行することを指定。この場合400が発生した場合でも一行下のRuleも実行されるということです。
- setvar
setvarは変数を作成、削除、更新するときに使用します。大文字と小文字を区別しないので要注意。
今回の場合、「setvar:ip.400_counter=+1」なので、このRuleを通るたびに変数ip.400_counterが+1されていきます。
- deprecatevar
deprecatevarはメモリに格納されている変数に対して、時間の経過とともに値を減らします。
今回の場合、「ip.400_counter=50/30」なので30秒ごとに変数の値をマイナス50します。この処理により30秒以内に50回以上のアクセスがあるかを検出できます。
- expirevar
expirevarは指定された期間の経過後に有効期限が切れるように変数を構成します。
今回の場合、「expirevar:ip.400_counter=14400」なのでこの変数の有効期限はRule通過時から14400秒となり、すなわち4時間の有効期限を設定しています。
そして二行目のRuleで変数が50以上かどうかをチェックし、50以上である場合はphase:2(リクエスト受取時)にdropを指定(FINパケットを送信してTCP接続の即時終了)しているため、アクセスが遮断されます。
アクセスの遮断はexpirevarで設定した時間の間保持されますので、30秒間に50回以上の不正リクエストステータスを送信したユーザーのアクセスは指定時間の間遮断することができます。
Apache再起動
confの作成、設定が終わったらapacheを再起動しましょう!
これでModSecurityは稼働します!!
systemctl restart httpd
6.まとめ
今回は既存CRSは使わずに設定を作成してみましたが、そもそもCRSにはどのようなルールがあるのかもっと調べていきたいです。。
そのうえで、WAFとして防ぐべき攻撃や手法を洗い出し、攻撃から守ることが大事ですね。
もっとModSecurityのことを知り、使いこなしていきたいです。
また、閾値のチューニング方法や検討方法なども全然知らないので、そこのところも要勉強です。
ご存知の方がいましたらご教示いただけると幸いです。
PLUS ULTRA!!