ModSecurity(mod_security) の Core Rule Set(CRS)を読み解く【その2:検知ルール】
ModSecurity Core Rule Set(CRS)を読み解く【その1:global_config, config】に続いて、Core Rule Set(CRS)の検知ルールを見てみた。
- 確認したバージョン
- modsecurity-crs v2.0.2(2009年10月2日現在)
CRS の base_rules 以下の .conf ファイルが ModSecurity が HTTP/HTTPS 通信を検査するための検知ルールである。この日記では、modsecurity_crs_41_sql_injection_attacks.conf を例として、どのようにルールが定義されているか確認した。
./base_rules: modsecurity_40_generic_attacks.data modsecurity_41_sql_injection_attacks.data modsecurity_46_et_sql_injection.data modsecurity_46_et_web_rules.data modsecurity_50_outbound.data modsecurity_crs_20_protocol_violations.conf modsecurity_crs_21_protocol_anomalies.conf modsecurity_crs_23_request_limits.conf modsecurity_crs_30_http_policy.conf modsecurity_crs_35_bad_robots.conf modsecurity_crs_40_generic_attacks.conf modsecurity_crs_41_phpids_filters.conf modsecurity_crs_41_sql_injection_attacks.conf modsecurity_crs_41_xss_attacks.conf modsecurity_crs_45_trojans.conf modsecurity_crs_46_et_sql_injection.conf modsecurity_crs_46_et_web_rules.conf modsecurity_crs_47_common_exceptions.conf modsecurity_crs_48_local_exceptions.conf modsecurity_crs_49_enforcement.conf modsecurity_crs_50_outbound.conf modsecurity_crs_60_correlation.conf
modsecurity_crs_41_sql_injection_attacks.conf
SQLインジェクションの検知ルールを見てみると、大体以下の流れで SQL インジェクション攻撃の有無を検査しているようだ。
- ルール(1)で HTTP リクエストにおいて、SQL インジェクション攻撃に含まれるキーワードが含まれるか検査する。含まれている場合、pm_sqli_score 変数に 1 を加算する
- ルール(2)で、pm_sqli_score 変数の値が 0 であるか検査する。0 であった場合(特定のキーワードが HTTP リクエストに含まれない)、以降の RegEx Rules で HTTP リクエストを検査する。含まれていない場合、RegEx Rules で HTTP リクエストを検査せずにマーカ:END_SQL_INJECTION_PM まで移動する。
- ルール(3)、(4)でHTTPリクエスト・ヘッダを検査する
- ルール(5)で、ここまで検査したルールが 1 つでも合致したか検査する。合致していた場合、ルール(6)〜(9)で合致した HTTP 通信を検査する。合致していない場合、ルール(6)〜(9)で HTTP 通信を検査せずにマーカ:END_SQL_INJECTION_WEAK まで移動する
まず特定のキーワードが含まれているか検査することで、正規表現を使用する RegEx Rules で検査する HTTP 通信を最小限に抑えるように工夫しているようだ。
# # Prequalify Request Matches # ### ルール(1) SecRule REQUEST_URI|REQUEST_BODY|REQUEST_HEADERS|XML:/*|!REQUEST_HEADERS:Referer \ "@pmFromFile modsecurity_41_sql_injection_attacks.data" \ "phase:2,t:none,t:urlDecodeUni,t:htmlEntityDecode,t:lowercase,t:replaceComments, t:compressWhiteSpace,nolog,pass,setvar:tx.pm_sqli_score=+1, setvar:tx.pm_sqli_data_%{matched_var_name}=%{matched_var}" ### ルール(2) SecRule TX:PM_SQLI_SCORE "@eq 0" "phase:2,t:none,pass,skipAfter:END_SQL_INJECTION_PM,nolog" # # Begin RegEx Checks for target locations that matched the prequalifier checks # ### RegEx Rules Start ----------------------------------- # # Blind SQL injection # ### RegEx ルール(A) SecRule TX:/^PM_SQLI_DATA_*/ "\bsys\.user_catalog\b" \ "phase:2,capture,t:none,ctl:auditLogParts=+E,block,nolog,auditlog, msg:'Blind SQL Injection Attack',id:'959517',tag:'WEB_ATTACK/SQL_INJECTION', logdata:'%{TX.0}',severity:'2',setvar:'tx.msg=%{rule.msg}', setvar:tx.sqli_score=+1,setvar:tx.anomaly_score=+20, setvar:tx.%{rule.id}-WEB_ATTACK/SQL_INJECTION-%{matched_var_name}=%{matched_var}" ... # # SQL injection # SecRule TX:/^PM_SQLI_DATA_*/ "\'msdasql\'" \ "phase:2,capture,t:none,ctl:auditLogParts=+E,block,nolog,auditlog, msg:'SQL Injection Attack',id:'959020',tag:'WEB_ATTACK/SQL_INJECTION', logdata:'%{TX.0}',severity:'2',setvar:'tx.msg=%{rule.msg}', setvar:tx.sqli_score=+1,setvar:tx.anomaly_score=+20, setvar:tx.%{rule.id}-WEB_ATTACK/SQL_INJECTION-%{matched_var_name}=%{matched_var}" ... ### RegEx Rules End ------------------------------------- ### マーカ: END_SQL_INJECTION_PM SecMarker END_SQL_INJECTION_PM ### ルール(3) SecRule REQUEST_HEADERS|XML:/*|!REQUEST_HEADERS:Referer \ "\b(\d+) ?= ?\1\b|[\'\"](\w+)[\'\"] ?= ?[\'\"]\2\b" \ "phase:2,capture,t:none,t:urlDecodeUni,t:htmlEntityDecode,t:replaceComments, t:compressWhiteSpace,t:lowercase,ctl:auditLogParts=+E,block,nolog,auditlog, msg:'SQL Injection Attack',id:'950001',tag:'WEB_ATTACK/SQL_INJECTION', logdata:'%{TX.0}',severity:'2',setvar:'tx.msg=%{rule.msg}', setvar:tx.sqli_score=+1,setvar:tx.anomaly_score=+20, setvar:tx.%{rule.id}-WEB_ATTACK/SQL_INJECTION-%{matched_var_name}=%{matched_var}" ### ルール(4) SecRule REQUEST_HEADERS|XML:/*|!REQUEST_HEADERS:Referer|!REQUEST_HEADERS:via \ "\b(?:coalesce\b|root\@)" \ "phase:2,capture,t:none,t:urlDecodeUni,t:htmlEntityDecode,t:replaceComments, t:compressWhiteSpace,t:lowercase,ctl:auditLogParts=+E,block,nolog,auditlog, id:'950908',tag:'WEB_ATTACK/SQL_INJECTION',setvar:'tx.msg=%{rule.msg}', setvar:tx.sqli_score=+1,setvar:tx.anomaly_score=+20, setvar:tx.%{rule.id}-WEB_ATTACK/SQL_INJECTION-%{matched_var_name}=%{matched_var}" ### マーカ: BEGIN_SQL_INJECTION_WEAK SecMarker BEGIN_SQL_INJECTION_WEAK ### ルール(5) SecRule &TX:/SQL_INJECTION/ "@eq 0" "phase:2,t:none,nolog,pass,skipAfter:END_SQL_INJECTION_WEAK" ### ルール(6) SecRule TX:/SQL_INJECTION/ "\b(?:rel(?:(?:nam|typ)e|kind)|a(?:ttn(?:ame|um)|scii)| c(?:o(?:nver|un)t|ha?r)|s(?:hutdown|elect)|to_(?:numbe|cha)r|u(?:pdate|nion)| d(?:elete|rop)|group\b\W*\bby|having|insert|length|where)\b" \ "phase:2,chain,capture,t:none,ctl:auditLogParts=+E,block,nolog,auditlog, msg:'SQL Injection Attack',id:'950001',tag:'WEB_ATTACK/SQL_INJECTION', logdata:'%{TX.0}',severity:'2'" ### ルール(7) SecRule MATCHED_VAR "(?:[\\\(\)\%#]|--)" \ "t:none,setvar:'tx.msg=%{rule.msg}',setvar:tx.sqli_score=+1,setvar:tx.anomaly_score=+20, setvar:tx.%{rule.id}-WEB_ATTACK/SQL_INJECTION-%{matched_var_name}=%{matched_var}" ### ルール(8) SecRule TX:/SQL_INJECTION/ "\b(?:benchmark|encode)\b" \ "phase:2,chain,capture,t:none,ctl:auditLogParts=+E,block,nolog,auditlog, msg:'Blind SQL Injection Attack',id:'950007',tag:'WEB_ATTACK/SQL_INJECTION', logdata:'%{TX.0}',severity:'2'" ### ルール(9) SecRule MATCHED_VAR "(?:[\\\(\)\%#]|--)" \ "t:none,setvar:'tx.msg=%{rule.msg}',setvar:tx.sqli_score=+1,setvar:tx.anomaly_score=+20, setvar:tx.%{rule.id}-WEB_ATTACK/SQL_INJECTION-%{matched_var_name}=%{matched_var}" ### マーカ: END_SQL_INJECTION_WEAK SecMarker END_SQL_INJECTION_WEAK
ModSecurity Reference Manual*1 によると、SecRule は以下の通り 3 つの引数で構成される。
引数 | 必須/任意 | 意味 |
---|---|---|
VARIABLES | 必須 | 検査対象とする HTTP 通信の特定データを格納した変数を指定する |
OPERATOR | 必須 | 検査項目を指定する |
ACTIONS | 任意 | OPERATOR の条件が真となった場合に実行する処理を指定する |
この日記では、ルール(1)、(3)を例に検知ルールをみていく。
ルール(1)
ルール(1): VARIABLES
ルール(1)は、HTTP リクエストにおける次の項目を検査対象とする。各変数を OR(「|」)で連結しているため、検査対象としたどれかで OPERATOR の条件に真となれば、ACTION が実行される。
変数 | 意味 |
---|---|
REQUEST_URI | URI。クエリストリングを含む |
REQUEST_BODY | HTTP リクエスト・ボディ。POST メソッドにおけるパラメータが該当する |
REQUEST_HEADERS | HTTP リクエスト・ヘッダ |
XML:/* | XML のすべてのエンティティ |
!REQUEST_HEADERS:Referer | Referer ヘッダを除外する |
ルール(1): OPERATORS
ルール(1)では、modsecurity_41_sql_injection_attacks.data に保存されているキーワードが検査対象である VARIABLES に合致するか検査する。ルール(1)は pmFromFiles*2 OPERATOR キーワード(?)を指定することで、引数で指定したファイルに含まれる文字列が検査対象に合致するか検査する。OPERATOR キーワードを OPERATORS で指定する場合、キーワードの語頭に「@」を付与する*3。
modsecurity_41_sql_injection_attacks.data に保存されているキーワードは、RDBMS におけるシステムテーブル・アカウント、SQL 文の予約語等である。SQL インジェクション攻撃の HTTP リクエストに含まれる可能性の高いキーワードを選別しているようだ。
sys.user_objects sys.user_triggers @@spid msysaces instr ... sp_makewebtask utl_http dbms_java benchmark xp_regread xp_regwrite
ルール(1): ACTIONS
ModSecurity のルールの Action は、実行処理に関する設定だけではなく、ルールそのものに関する設定も定義される。ルール(1)の Action をまとめてみると、「OPERATOR が真になった回数、真になった時の変数名および値を記録する」という感じだろうか。ルール(1)の Action の詳細を以下の通りである。
Action | 意味 |
---|---|
phase:2 | ModSecurity における処理フェーズ 2 でこのルールを適用する。フェーズ 2 は HTTP リクエスト・ボディを処理するフェーズ |
t:none | 変換関数をすべて無効化する。ルール(1)では他の変換関数より先に定義しているため、変換関数の初期化と考えられる |
t:urlDecodeUni | 変換関数:URI を URL デコードする。%xx だけでなく、%uXXXX もデコードする |
t:htmlEntityDecode | 変換関数:エスケープしたHTML 特殊文字(「<」、「>」等)を元に戻す |
t:lowercase | 変換関数:英大文字を英小文字に変換する |
t:replaceComments | 変換関数:/* ... */ を 1 スペースに変換する |
t:compressWhiteSpace | 変換関数:改行コード等や連続したスペースを 1 スペースに変換する |
nolog | このルールに関してログに記録しない |
pass | OPERATOR が真となっても、以降のルールで検査を継続する |
setvar:tx.pm_sqli_score=+1 | OPERATOR が真となる度に tx.pm_sqli_score を 1 増加する |
setvar:tx.pm_sqli_data_%{matched_var_name}=%{matched_var} | OPERATOR が真となったら、真となった時の VARIABLE の値をtx.pm_sqli_data_ |
ルール(3)
ルール(3): VARIABLES
ルール(3)は、Referer ヘッダを除くHTTP リクエスト・ヘッダを検査対象とする。ルール(3)で定義された VARIABLES については、ルール(1): VARIABLES でまとめているため、割愛する。
ルール(3): OPERATOR
正規表現にて、次のどちらかの文字列を含むか否かを検査する。「=」前後に空白(スペース)はあってもなくてもよい。SQL インジェクションの脆弱性があるか否かを調べる際に、WHERE を必ず真とするために使用される文字列だ。SQL インジェクションの検査通信を検出するルールといったところだろう。
- <数字> = <数字>
- 例:1 = 1、1=1、234=234
- "文字列" = "文字列"、または '文字列' = '文字列'
- 例:"a"="a"、'a' = 'a'
ルール(3): ACTIONS
OPERATOR が真となった場合、SQL Injection の攻撃があったと判断し、audit log に HTTP レスポンス・ボディ等を記録し、該当 HTTP トランザクションを遮断する。
Action | 意味 |
---|---|
capture | OPERATOR の正規表現で合致したパターンの後方参照を有効にする。TX.0-TX.9 まで |
ctl:auditLogParts=+E | 一時的にHTTPレスポンス・ボディを audit log に記録するように設定を変更する |
block | 合致した HTTP トランザクションを遮断する |
auditlog | audit log を記録する |
msg:'SQL Injection Attack' | ルール固有メッセージに 'SQL Injection Attack' を設定する |
id:'950001' | ルール番号として、950001 を割り当てる。Core Rule Set には、900,000-999,999 が予約されているようだ*4 |
tag:'WEB_ATTACK/SQL_INJECTION' | ルールに 'WEB_ATTACK/SQL_INJECTION' タグを設定する |
logdata:'%{TX.0}' | ログに指定したデータを記録する。ここでは OPERATOR の正規表現全体に合致した文字列(TX.0)を記録する |
severity:'2' | Severity に CRITICAL を設定する |
setvar:'tx.msg=%{rule.msg}' | tx.msg に msg に設定した 'SQL Injection Attack' を格納する |
setvar:tx.sqli_score=+1 | tx.sqli_score に 1 を加算する |
setvar:tx.anomaly_score=+20 | tx.anomaly_score に 20 を加算する。modsecurity_crs_60_correlation.conf でアノーマリ判定に利用するようだ |
setvar:tx.%{rule.id}-WEB_ATTACK/SQL_INJECTION-%{matched_var_name}=%{matched_var} | tx.950001-WEB_ATTACK/SQL_INJECTION-<変数> に <変数値> を格納する |
検知ルールを読んでみて
ModSecurity の検知ルールをきちんと読んだのは初めてだが、パフォーマンスを向上する工夫があって興味深かった。ModSecurity で最小限のルールを利用するだけなら、パフォーマンス低下を最小限を抑えられるのではないだろうか。インターネット上で ModSecurity について調べていると、トラックバック・スパムを防ぐ用途でも利用されているようだ(今もそうなのか?)。*5
CRS の検知ルールを読んでみると、オープンソースの IDS である Snort を思い出した。検知ルールの定義方法がよく似ている。だから Snort を触ったことがある人なら、キーワードだけ理解すればすぐに検知ルールが書けそうだ。
【収集用メールアドレス】:q1w2e3w2@gmail.com
*1:ModSecurity〓 Reference Manual, SecRule
*2:ModSecurity〓 Reference Manual, pmFromFiles
*3:「The operator syntax uses the @ symbol followed by the specific operator name.」との記述あり