スマートフォン・タブレットからインターネットサーバーオペレーション

APPW.jp
 
blind spot / iceberg view

氷山ビュー
PHPログに残らないもの

アプリ側で書いているアクセスログを見て、サイトの全体が分かった気になっていました。 でもそこには、.htaccess で遮断した 403 も、存在しないパスへの 404 も残っていません。 PHP が起動する前に終わっているからです。水面上だけを見ていたと気づいた話です。

Apacheログ 403 / 404 可視化 MongoDB

[ § 0 ] 導入

「うちのログ」は、何を見ていないか

アクセスログを MongoDB に貯めて、そこそこのことができるようになっていました。 セッションを束ねて訪問者を数え(PVからセッションへ)、 パターンから攻撃者を浮かび上がらせ(攻撃者は、パターンで浮かぶ)、 .htaccessAPCu で実際に止める。

ひととおり揃った気でいたのですが、あるとき素朴な疑問が浮かびました。 遮断した相手は、このログに載っているのだろうか。

載っていませんでした。しかもそれは、設計上そうなるべくしてそうなっていたのです。

[ § 1 ] 構造

盲点は、どこにあったか

うちのログは PHP のモジュールが書いています。つまりPHP が動いたリクエストしか記録できません。 ここに3つの穴がありました。

① 攻撃パスは、記録せずに終了している

モジュールは wp-.envadmin といった攻撃的なパスを検知すると、 その場で exit します。処理を軽くするための設計ですが、ログを書く前に終わるので記録が残りません。

② .htaccess で弾くと、PHP は起動すらしない

IP や UA による遮断は Apache 層です。403 を返した時点で完結しており、PHP には一切届きません。 遮断が成功しているほど、ログには何も残らないという逆説的な状態でした。

③ 存在しないパスも、PHP を通らない

静的ファイルとして解決できないパスは Apache が 404 を返します。これも PHP の外です。

事象PHPログApacheログ意味
通常のアクセス問題なく見えていた
botだが通過した攻撃者タブで分類できていた分
攻撃パスへの要求404探索されていること自体が不可視
.htaccess で遮断403遮断の実績が分からない
存在しないパス404リンク切れも探索も見えない
これは不具合ではありません。 アプリ層のログは、アプリに届いたものしか知らない——当たり前の話です。 問題はその当たり前を忘れて「これが全体だ」と思い込んでいたことのほうでした。
[ § 2 ] 設計

足りない側から、補う

穴の形が分かれば、埋め方は決まります。Apache のログを集計して足す。 収集の実装は前回の記事に書きました—— 追記分だけを読み、ローテーションに追従し、時間帯単位の集計だけを保存する仕組みです。

2つのログは、担当範囲がきれいに分かれています。

ログ見えるもの位置づけ
PHPログ(MongoDB)セッション・訪問者・bot分類水面上
Apacheログ(集計)403・404・5xx・静的ファイル水面下

片方だけでは全体になりません。2つを足して、はじめて輪郭が出る。 「氷山ビュー」と呼んでいるのはそのためです。見えていた部分の下に、見えていなかった部分がある。

生ログは足さない

Apache側で保存するのは1時間単位の集計だけです。生ログをDBに入れると容量が破綻しますし、IPが増えるほど扱いも面倒になります。「サーバーは答えだけを返す」という方針はここでも同じです。

[ § 3 ] 成果

何が、見えるようになったか

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 を並べ、合計が総リクエスト数と一致するかを 確認できる形にしました。取りこぼしがあれば自分で気づけます。

「合計が合うか」を画面に出す。 集計ツールで一番怖いのは、静かに数え落とすことです。内訳の和と総数を並べておけば、 ずれた瞬間に分かります。実際これで、どのカテゴリにも入らない7件を見つけました (接続が途中で切れた 000 のような応答でした)。
[ § 4 ] 発見

そして、自分が映った

攻撃の見えない部分を映すために作った道具でした。最初に画面に出たのは、これです。

ステータス件数割合
2xx 正常応答7,55931.1%
3xx リダイレクト10,98545.2%
403 遮断1,0264.2%
404 探索4241.7%
その他 4xx8533.5%
5xx サーバエラー3,44214.2%

7件に1件がサーバエラー。 遮断(1,026)と探索(424)を足した数の、2.4倍です。 攻撃を見るために作った道具が、真っ先に映し出したのは自分のサーバの不具合でした。

原因は、rewrite ルールの ? ひとつでした。 WordPress マルチサイト用の定番ルールが、条件次第で書き換え前後が同一になり、 内部リダイレクトの無限ループを起こしていた。しかも攻撃者のスキャンが引き金で 顕在化していました。切り分けから修正までは 別記事に詳しく書いています。

この道具が無ければ、error.log に流れる AH00124 は 「気になるけれど原因不明」のまま放置されていたと思います。 可視化とは、相手を見ることであると同時に、自分を見ることでもある——そう実感した一件でした。

[ § 5 ] 体感デモ

視界を、切り替えてみる

同じ1日のトラフィックを、2つの視界で見比べます。 PHPログだけの視界と、Apacheログを足した視界。 切り替えた瞬間に、水面下が立ち上がります。

Interactive · what you were not seeing
視界の比較
実測に基づく例です。静的ファイルやリダイレクトも含むため、Apache側の総数はPHP側より大きくなります。

切り替えると、それまで見えていた棒の下に、遮断と探索とサーバエラーが現れます。 注目してほしいのは攻撃のうち何割が見えていたかという数字です。 うちの場合は6割強でした。氷山は、思ったより小さかった—— これも実際に測ってみるまで分からなかったことです。

[ § 6 ] まとめ

ログは、置いた場所しか見ない

当たり前の話に落ち着きました。ログは、それを仕掛けた層のことしか知らない。 アプリ層に置けばアプリに届いたものを、Web サーバー層に置けばそこを通ったものを記録します。 どちらが正しいかではなく、自分のログがどの層にいるのかを把握しているかが問題でした。

この記事のまとめ

  • PHPが書くログには 403・404 が残らない。PHPが起動する前に終わっているため。
  • 遮断が成功しているほど、アプリ層のログには何も残らないという逆説がある。
  • Apacheログの集計を足して、水面上(PHP)と水面下(403/404/5xx)を並べる。
  • 403 は遮断の実績、404 は攻撃者が何を探しているかの目録
  • 内訳の合計と総数を並べて表示すると、数え落としに自分で気づける
  • 攻撃を映すつもりの道具が、最初に映したのは自分の設定不具合だった。
ログ基盤シリーズ
  1. Apacheログを、追記された分だけ読む
    収集:増分読みとローテーション追従
  2. 氷山ビュー — PHPログに残らないもの(この記事)
    構造:2つのログの担当範囲
  3. 500の犯人は「?」ひとつだった
    発見:この道具が最初に見つけたもの

関連記事

『氷山ビュー — PHPログに残らないもの』を公開しました。