「うちのログ」は、何を見ていないか
アクセスログを MongoDB に貯めて、そこそこのことができるようになっていました。 セッションを束ねて訪問者を数え(PVからセッションへ)、 パターンから攻撃者を浮かび上がらせ(攻撃者は、パターンで浮かぶ)、 .htaccess と APCu で実際に止める。
ひととおり揃った気でいたのですが、あるとき素朴な疑問が浮かびました。 遮断した相手は、このログに載っているのだろうか。
載っていませんでした。しかもそれは、設計上そうなるべくしてそうなっていたのです。
盲点は、どこにあったか
うちのログは PHP のモジュールが書いています。つまりPHP が動いたリクエストしか記録できません。 ここに3つの穴がありました。
① 攻撃パスは、記録せずに終了している
モジュールは wp-、.env、admin といった攻撃的なパスを検知すると、
その場で exit します。処理を軽くするための設計ですが、ログを書く前に終わるので記録が残りません。
② .htaccess で弾くと、PHP は起動すらしない
IP や UA による遮断は Apache 層です。403 を返した時点で完結しており、PHP には一切届きません。 遮断が成功しているほど、ログには何も残らないという逆説的な状態でした。
③ 存在しないパスも、PHP を通らない
静的ファイルとして解決できないパスは Apache が 404 を返します。これも PHP の外です。
| 事象 | PHPログ | Apacheログ | 意味 |
|---|---|---|---|
| 通常のアクセス | ✔ | ✔ | 問題なく見えていた |
| botだが通過した | ✔ | ✔ | 攻撃者タブで分類できていた分 |
| 攻撃パスへの要求 | ✕ | 404 | 探索されていること自体が不可視 |
| .htaccess で遮断 | ✕ | 403 | 遮断の実績が分からない |
| 存在しないパス | ✕ | 404 | リンク切れも探索も見えない |
足りない側から、補う
穴の形が分かれば、埋め方は決まります。Apache のログを集計して足す。 収集の実装は前回の記事に書きました—— 追記分だけを読み、ローテーションに追従し、時間帯単位の集計だけを保存する仕組みです。
2つのログは、担当範囲がきれいに分かれています。
| ログ | 見えるもの | 位置づけ |
|---|---|---|
| PHPログ(MongoDB) | セッション・訪問者・bot分類 | 水面上 |
| Apacheログ(集計) | 403・404・5xx・静的ファイル | 水面下 |
片方だけでは全体になりません。2つを足して、はじめて輪郭が出る。 「氷山ビュー」と呼んでいるのはそのためです。見えていた部分の下に、見えていなかった部分がある。
Apache側で保存するのは1時間単位の集計だけです。生ログをDBに入れると容量が破綻しますし、IPが増えるほど扱いも面倒になります。「サーバーは答えだけを返す」という方針はここでも同じです。
何が、見えるようになったか
403 — 遮断の実績
これがいちばん嬉しい変化でした。書いた .htaccess のルールが
実際に何件止めているのかが、初めて数字で分かるようになりました。
それまでは「たぶん効いているはず」でしかなかった。ルールを足しても、効果を確認する手段が無かったのです。 403 の件数が見えるようになると、追加した直後に跳ね、相手が諦めると下がっていく—— そういう動きまで追えます。
404 — 攻撃者の辞書
叩かれたパスの一覧は、そのまま相手が何を探しているかの目録です。
/wp-login.php /.env /xmlrpc.php /phpmyadmin/index.php /.git/config /vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php /actuator/env /static/..%2f..%2f..%2f..%2froot/.aws/credentials
最後の1行は、パストラバーサルでクラウドの認証情報を直接狙いにきています。 こういう傾向が見えると、ブロックリストを更新する材料になります。 「何を守るべきか」を相手が教えてくれる、という妙な構図です。
ステータスの全内訳
2xx / 3xx / 403 / 404 / その他4xx / 5xx を並べ、合計が総リクエスト数と一致するかを 確認できる形にしました。取りこぼしがあれば自分で気づけます。
000 のような応答でした)。
そして、自分が映った
攻撃の見えない部分を映すために作った道具でした。最初に画面に出たのは、これです。
| ステータス | 件数 | 割合 |
|---|---|---|
| 2xx 正常応答 | 7,559 | 31.1% |
| 3xx リダイレクト | 10,985 | 45.2% |
| 403 遮断 | 1,026 | 4.2% |
| 404 探索 | 424 | 1.7% |
| その他 4xx | 853 | 3.5% |
| 5xx サーバエラー | 3,442 | 14.2% |
7件に1件がサーバエラー。 遮断(1,026)と探索(424)を足した数の、2.4倍です。 攻撃を見るために作った道具が、真っ先に映し出したのは自分のサーバの不具合でした。
? ひとつでした。
WordPress マルチサイト用の定番ルールが、条件次第で書き換え前後が同一になり、
内部リダイレクトの無限ループを起こしていた。しかも攻撃者のスキャンが引き金で
顕在化していました。切り分けから修正までは
別記事に詳しく書いています。
この道具が無ければ、error.log に流れる AH00124 は
「気になるけれど原因不明」のまま放置されていたと思います。
可視化とは、相手を見ることであると同時に、自分を見ることでもある——そう実感した一件でした。
視界を、切り替えてみる
同じ1日のトラフィックを、2つの視界で見比べます。 PHPログだけの視界と、Apacheログを足した視界。 切り替えた瞬間に、水面下が立ち上がります。
切り替えると、それまで見えていた棒の下に、遮断と探索とサーバエラーが現れます。 注目してほしいのは攻撃のうち何割が見えていたかという数字です。 うちの場合は6割強でした。氷山は、思ったより小さかった—— これも実際に測ってみるまで分からなかったことです。
ログは、置いた場所しか見ない
当たり前の話に落ち着きました。ログは、それを仕掛けた層のことしか知らない。 アプリ層に置けばアプリに届いたものを、Web サーバー層に置けばそこを通ったものを記録します。 どちらが正しいかではなく、自分のログがどの層にいるのかを把握しているかが問題でした。
この記事のまとめ
- PHPが書くログには 403・404 が残らない。PHPが起動する前に終わっているため。
- 遮断が成功しているほど、アプリ層のログには何も残らないという逆説がある。
- Apacheログの集計を足して、水面上(PHP)と水面下(403/404/5xx)を並べる。
- 403 は遮断の実績、404 は攻撃者が何を探しているかの目録。
- 内訳の合計と総数を並べて表示すると、数え落としに自分で気づける。
- 攻撃を映すつもりの道具が、最初に映したのは自分の設定不具合だった。
- Apacheログを、追記された分だけ読む
収集:増分読みとローテーション追従 - 氷山ビュー — PHPログに残らないもの(この記事)
構造:2つのログの担当範囲 - 500の犯人は「?」ひとつだった
発見:この道具が最初に見つけたもの