ModSecurity for CentOS5.x

## pcre-devel libxml2-devel のインストール

```shell-session:
yum install pcre-devel libxml2-devel
```

## apacheのダウンロード

```shell-session:
cd /usr/local/src
wget http://ftp.jaist.ac.jp/pub/apache//httpd/httpd-2.2.25.tar.gz
tar zxvf httpd-2.2.25.tar.gz
```
## mod_unique_id インストール

```shell-session:
cd /usr/local/src/httpd-2.2.25/modules/metadata
/usr/local/apache2/bin/apxs -cia mod_unique_id.c && echo OK
```

## apr インストール

```shell-session:
cd /usr/local/src/httpd-2.2.25/srclib/apr
./configure && make && make install && echo OK
```

## apr-util インストール

```shell-session:
cd /usr/local/src/httpd-2.2.25/srclib/apr-util
./configure --with-apr=/usr/local/apr/bin/apr-1-config && \
make && make install && echo OK
```

## apache インストール

```shell-session:
./configure \
--enable-so \
--enable-rewrite \
--enable-mods-shared="unique_id" \
--with-pcre \
--with-apr=/usr/local/apr/bin/apr-1-config \
--with-apr-util=/usr/local/apr/bin/apu-1-config \
--prefix=/usr/local/apache2 && \
make && make install && echo OK
```

### apache 再構築の場合前回のコンパイルオプションの確認方法

```shell-session:
/usr/local/src/httpd-2.2.xx/config.status --version
```

## modsecurity インストール
2.7系最新を使うとlibxml2が2.6.29でないとだめだといわれる。
CentOS 5.x 標準のlibxml2のバージョンは2.6.26のため2.6系をダウンロードしてくる。

```shell-session:
cd /usr/local/src/
wget http://sourceforge.net/projects/mod-security/files/modsecurity-apache/2.6.8/modsecurity-apache_2.6.8.tar.gz/download
tar zxvf modsecurity-apache_2.6.8.tar.gz
cd modsecurity-apache_2.6.8
./configure
--prefix=/usr/local/apache2 \
--with-apxs=/usr/local/apache2/bin/apxs \
--with-apr=/usr/local/apache2/bin/apr-1-config \
--with-apu=/usr/local/apache/bin/apu-1-config \
--with-pcre=/usr/bin/pcre-config \
--with-libxml=/usr/bin/xml2-config \
--enable-pcre-match-limit=90000 \
--enable-pcre-match-limit-recursion=90000 \
LDFLAGS=-L/usr/local/apache2/lib
```
apacheのエラーログに「Execution error - PCRE limits exceeded (-8)」とエラーを吐かれたので
http://forum.parallels.com/showthread.php?99430-ModSecurity-Rule-execution-error-PCRE-limits-exceeded
を参考にした。

## CRS
最新版のCRSを使うと「Error parsing actions: Unknown action: ver」と怒られるのでｖ2.2.5を探してくる。
参考：http://sourceforge.net/mailarchive/message.php?msg_id=29864155

### CRSの設置

```shell-session:
cd /usr/local/src/
wget https://github.com/SpiderLabs/owasp-modsecurity-crs/tarball/v2.2.5
mkdir -p /usr/local/apache2/conf/modsecurity
tar zxvf SpiderLabs-owasp-modsecurity-crs-v2.2.5-0-g28e4ec8.tar.gz
cd SpiderLabs-owasp-modsecurity-crs-5c28b52
cp -av base_rules/* /usr/local/apache2/conf/modsecurity/
```

日本語をGET/POSTすると攻撃扱いになるのでリストから外す

```shell-session:
diff -u modsecurity_crs_41_sql_injection_attacks.conf.org modsecurity_crs_41_sql_injection_attacks.conf
--- modsecurity_crs_41_sql_injection_attacks.conf.org   2012-09-19 22:24:02.000000000 +0900
+++ modsecurity_crs_41_sql_injection_attacks.conf       2013-09-01 02:08:29.000000000 +0900
@@ -152,7 +152,7 @@
 #
 # This rule attempts to identify when multiple (4 or more) non-word characters are repeated in sequence
 #
-SecRule ARGS "\W{4,}" "phase:2,capture,t:none,t:urlDecodeUni,block,id:'960024',rev:'2.2.5',msg:'SQL Character Anomaly Detection Alert - Repetative Non-Word Characters',logdata:'%{tx.0}',setvar:tx.anomaly_score=+%{tx.warning_anomaly_score},setvar:tx.sql_injection_score=+1,setvar:'tx.msg=%{rule.msg}',setvar:tx.%{rule.id}-WEB_ATTACK/RESTRICTED_SQL_CHARS-%{matched_var_name}=%{tx.0}"
+#SecRule ARGS "\W{4,}" "phase:2,capture,t:none,t:urlDecodeUni,block,id:'960024',rev:'2.2.5',msg:'SQL Character Anomaly Detection Alert - Repetative Non-Word Characters',logdata:'%{tx.0}',setvar:tx.anomaly_score=+%{tx.warning_anomaly_score},setvar:tx.sql_injection_score=+1,setvar:'tx.msg=%{rule.msg}',setvar:tx.%{rule.id}-WEB_ATTACK/RESTRICTED_SQL_CHARS-%{matched_var_name}=%{tx.0}"
```

### apacheの設定

```shell-session:
vi /usr/local/apache2/conf/httpd.conf
```

httpd.confに追記する

```shell-session:httpd.conf
#----------------------------------------------------------
# ModSecurity
#----------------------------------------------------------
LoadModule security2_module modules/mod_security2.so
LoadFile   /usr/lib64/libxml2.so

<IfModule mod_security2.c>

    #----------------------------------------------------------
    # メイン
    #----------------------------------------------------------
    #--- 参考: https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual
    #--- 参考: http://open-groove.net/category/modsecurity/

    #--- 動作モード
    # (On:遮断, Off:無効, DetectionOnly:検知のみ)
    SecRuleEngine On
    #SecRuleEngine DetectionOnly

    #--- デフォルトアクション
    # (検出パターンに合致したときの動作を定義)
    # phase
    #   1.Request headers   (REQUEST_HEADERS)
    #   2.Request body      (REQUEST_BODY)
    #   3.Response headers  (RESPONSE_HEADERS)
    #   4.Response body     (RESPONSE_BODY)
    #   5.Logging           (LOGGING)
    SecDefaultAction "phase:2,deny,log,status:406"

    #--- テンポラリファイル格納先
    SecTmpDir /tmp/

    #--- 永続的データの格納先
    SecDataDir /tmp/

    #--- リクエストボディのバッファリング指示
    SecRequestBodyAccess On
    #--- 最大リクエストボディサイズを指定
    SecRequestBodyLimit 13107200
    #--- アップロードファイルサイズを除いた最大リクエストボディサイズ
    SecRequestBodyNoFilesLimit 131072
    #--- 最大メモリバッファサイズ
    SecRequestBodyInMemoryLimit 131072
    #--- リミット時の動作
    SecRequestBodyLimitAction Reject

    #----------------------------------------------------------
    # ルール
    #----------------------------------------------------------
    # SecRule VARIABLES OPERATOR [ACTIONS]
    # VARIABLES : チェックを実行する変数を指定
    # OPERATOR  : フィルタリングルールを記述
    #   @rx         : 正規表現によるパターンマッチング
    #   @pm         : パラレルパターンマッチング
    #   @pmFromFile : 対象文学列をリスト化したファイルを対象にパターンマッチング
    #   @@beginsWith: 対象文学列の始まりにおいてパターンマッチング
    #   @contains   : 対象文学列の全体からパターンマッチング
    #   @endWith    : 対象文学列の終わりにおいてパターンマッチング
    #   @streq      : 対象文学列が完全一致しているかどうかをチェック
    #   @within     : 指定した範囲内でパターンマッチング
    # ACTIONS  : ルールにマッチした場合のアクションを指示
    SecRule \
     REQUEST_HEADERS:Content-Type \
     "text/xml" \
     "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML"

    Include /usr/local/apache2/conf/modsecurity/modsecurity_crs_41_sql_injection_attacks.conf

    #----------------------------------------------------------
    # ログ
    #----------------------------------------------------------
    #--- 監査ログの動作モード
    # (On:すべて記録, Off:無効, RelevantOnly:SecAuditLogRelevantStatusに合致したステータスコードのみを記録)
    SecAuditEngine  On

    #---監査ログの記録対象とするサーバ応答コード
    # (SecAuditEngine が RelevantOnly)
    SecAuditLogRelevantStatus "^(?:5|4(?!04))"

    #--- 監査ログの出力タイプ
    # (Serial:1ファイルに記載, Concurrent:セッション毎に個別ファイルを生成)
    SecAuditLogType Serial

    #--- 監査ログの出力場所
    SecAuditLog     /var/log/modsecurity.log

    #--- 監査ログの出力項目
    #   A:AuditLog ヘッダー
    #   B:リクエストヘッダー
    #   C:リクエストボディ
    #   D:Reserved
    #   E:レスポンスボディ
    #   F:レスポンスヘッダー
    #   G:Reserved
    #   H:追加情報。パターンにマッチしたアクセスだとここにタグが付与される。
    #   I:ファイルを除外した、コンパクトなリクエストボディ
    #   J:Reserved
    #   K:トランザクションにマッチした全てのルール
    #   Z:最後の境界線
    SecAuditLogParts "ABIFHKZ"

    #----------------------------------------------------------
    # デバッグ
    #----------------------------------------------------------
    #--- デバッグログ
    SecDebugLog /var/log/modsecurity_debug.log

    #--- デバッグログレベル
    #   0:no logging.
    #   1:errors (intercepted requests) only.
    #   2:warnings.
    #   3:notices.
    #   4:details of how transactions are handled.
    #   5:as above, but including information about each piece of information handled.
    #   9:log everything, including very detailed debugging information
    SecDebugLogLevel 3

</IfModule>
```

## apacheの再起動
/usr/local/apache2/bin/apachectl configtest
/etc/init.d/httpd restart


## 確認

wget -S --spider "http://localhost/?and%201=1;--"

## 確認用WEBアプリ

```php:sql_injection.php

<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<html>
<head><title>SQL Injection TEST</title></head>
<body>
<?php
#-----------------------------------------------------------
# SQLインジェクションテストクラス
#-----------------------------------------------------------
class SQL_INJECTION_TEST{

    var $conn;
    var $data;
    var $sql;

    #-----------------------------------------------------------
    # SQLITEに接続
    #-----------------------------------------------------------
    function db_connect(){
        $dsn = "sqlite::memory:";
        $this->conn = new PDO($dsn);
    }

    #-----------------------------------------------------------
    # CREATE TABLE
    #-----------------------------------------------------------
    function db_table_check(){
        $sql         = "CREATE TABLE IF NOT EXISTS
                            tablename
                            (id       INTEGER PRIMARY KEY AUTOINCREMENT,
                             name     TEXT    UNIQUE,
                             password TEXT    UNIQUE)";
        #--- テーブル作成
        $stmt = $this->conn->prepare($sql);
        $stmt->execute();
        #--- 初期データー登録
        $this->db_insert("root",    "qwerty");
        $this->db_insert("yamada",  "wD?mZwrcr37n");
        $this->db_insert("tanaka",  "neeAnbH=4k7K");
        $this->db_insert("sato",    "F2s&jQ8eTtwQ");
        $this->db_insert("yamaguti","y8=bZ7CVkCsd");
        $this->db_insert("sugimoto","t7mRPZ&3fVsE");
        $this->db_insert("oda",     "7zU4@LYPyJmw");
    }

    #-----------------------------------------------------------
    # INSERT
    #-----------------------------------------------------------
    function db_insert($name, $password){
        $sql = "INSERT INTO tablename (name, password) VALUES ('{$name}', '{$password}')";
        if ($name){
            $stmt = $this->conn->prepare($sql);
            $flag = $stmt->execute();
        }else{
            echo "INSERT FALSE";
        }
    }

    #-----------------------------------------------------------
    # SELECT
    #-----------------------------------------------------------
    function db_select($select_sql){

        $stmt = $this->conn->prepare($select_sql);
        $stmt->execute();
        echo "<table border=\"1\" rules=\"all\">";
        echo "<tr><th>ID</th><th>NAME</th><th>PASSWORD</th></tr>";
        while ($row = $stmt->fetch()) {
            echo "<tr>";
            echo "<td>".$row["id"]   . "</td>";
            echo "<td>".$row["name"] . "</td>";
            echo "<td>".$row["password"] . "</td>";
            echo "</tr>";
        };
        echo "</table>";
    }

    #-----------------------------------------------------------
    # マジッククオート Off
    #-----------------------------------------------------------
    function magic_quotes_off($str) {
        if (get_magic_quotes_gpc()) {
            return stripslashes($str);
        }else{
            return $str;
        }
    }

    #-----------------------------------------------------------
    # POST/GET データーからSQLを生成
    #-----------------------------------------------------------
    function set_data($post_data='', $get_data=''){
        if ($post_data != ''){
            $data = $post_data;
        }
        if ($get_data != ''){
            $data = $get_data;
        }
        $this->data =  $this->magic_quotes_off($data);
        $this->sql  = "SELECT id, name, password FROM tablename WHERE password = '{$this->data}'";
    }

    #-----------------------------------------------------------
    # POST/GET データーを出力
    #-----------------------------------------------------------
    function get_data(){
        return $this->data;
    }

    #-----------------------------------------------------------
    # POST/GET データーからSQLを生成されたものを出力
    #-----------------------------------------------------------
    function get_sql(){
        return $this->sql;
    }

    #-----------------------------------------------------------
    # POST/GET データーからSQLを実行
    #-----------------------------------------------------------
    function get_injection(){
            $this->db_select($this->sql);
        #if ($this->data){
        #    $this->db_select($this->sql);
        #}else{
        #    echo "POST/GET Data None <br />";
        #}
    }
}
?>

<hr>パスワード入力<br />
<br />
<form action="?" method="post">
PASSWORD:<input type="text" name="data" size="20" value="">
<input type="submit" value="ログイン(POST)">
</form>
<form action="?" method="get">
PASSWORD:<input type="text" name="data" size="20" value="">
<input type="submit" value="ログイン(GET)">
</form>
SQLインジェクションコード「' OR 'A' = 'A' --」
<hr>

<?php
$post_data  = $_POST['data'];
$get_data   = $_GET['data'];
$hoge       = new SQL_INJECTION_TEST();
$hoge->db_connect();
$hoge->db_table_check();
$hoge->set_data($post_data, $get_data);
echo "～～結果～～<br />";
echo "発行したSQL : " . $hoge->get_sql()  . "<br />";
$hoge->get_injection();
echo "<hr>～～登録全データー～～<br />";
echo "発行したSQL : SELECT id, name, password FROM tablename<br />";
$hoge->db_select("SELECT id, name, password FROM tablename");
```

## 参考
http://www.modsecurity.org/
http://dev.classmethod.jp/cloud/aws/ec2-amazon-linux-waf-modsecurity-install/
http://www.ipa.go.jp/files/000017312.pdf