はじめに
Redmine 6.1への移行作業がようやく一段落したので、久しぶりにサーバーログを眺めていました。
すると、トップページ (hoge.example.com) に対して、見慣れない長大なペイロードが飛んできていました。
最近の攻撃は「脆弱性を突く」ことだけが目的ではありません。まずは対象がどんな環境なのかを調べ、その結果によって次の手を変えてきます。
今回は、その偵察段階で送られてきたJavaScriptペイロードがなかなか興味深い内容だったので、ModSecurityのログを追いながら見ていきます。
検出された ModSecurity ログ
まずは実際のログです。
例によってIPアドレスはダミーに置き換えています。テロリストに名前を与えていません。
[Wed Sep 02 02:04:07 2026] [security2:error] [client 198.51.100.24:56250] ModSecurity: Warning. Pattern match "(?i)(?:b[\\"'\\\\)\\\\[\\\\x5c]..." at ARGS:0. [file "REQUEST-932-APPLICATION-ATTACK-RCE.conf"] [line "205"] [id "932235"] [msg "Remote Command Execution: Unix Command Injection (command without evasion)"] [data "Matched Data: eval)(global[String['from'+'CharCode'](66,117,102,102,101,114)].from('KGFzeW5jIGZ1bmN0aW9uKCl7Ci8vIGZhc3RfcmVjb25fdjY..."] [hostname "hoge.example.com"] [uri "/"]
[Wed Sep 02 02:04:07 2026] [security2:error] [client 198.51.100.24:56250] ModSecurity: Rule [id "932250"] - Execution error - PCRE limits exceeded (-47): (null). [hostname "hoge.example.com"] [uri "/"]
[Wed Sep 02 02:04:07 2026] [security2:error] [client 198.51.100.24:56250] ModSecurity: Warning. Pattern match "(?i)\\\\b\\\\(?[\\"']*(?:assert|eval|exec|passthru)..." at ARGS:0. [file "REQUEST-933-APPLICATION-ATTACK-PHP.conf"] [line "406"] [id "933160"] [msg "PHP Injection Attack: High-Risk PHP Function Call Found"] [hostname "hoge.example.com"] [uri "/"]
最初に目を引くのは Rule 932235 が反応していることです。
Remote Command Execution:
Unix Command Injection
さらに途中では
PCRE limits exceeded (-47)
というログも出ています。
Base64の中身を見てみる
ログ中に埋め込まれていたBase64文字列をデコードすると、次のようなヘッダーが現れました。
(async function(){
// fast_recon_v6 — signature-rotated recon payload
// Changes from v5:
// - Randomized top-level JSON keys (no fixed schema to fingerprint)
// - Variable output structure per invocation
// - IMDS calls use randomized User-Agent + jittered timeouts
// - File reads are shuffled order (no deterministic sequence)
...
名前からも分かるように、これは偵察を目的としたJavaScriptです。面白いのは「何を調べるか」よりも、「どうやってWAFの検知を避けようとしているか」の方でした。
難読化の目的はコードを隠すことではない
例えば、
global[String['from'+'CharCode'](...)]
という書き方。普通なら
Buffer
と書けば済むものを、わざわざ文字コードから組み立てています。さらに
(0,eval)(...)
という間接呼び出しも使われています。どちらもJavaScriptとしては動作は同じですが、単純な文字列検索やシグネチャ検知を避けるための定番手法です。
コードを読めなくすることより、「機械に見つかりにくくすること」の方が目的と言った方が近いでしょう。
偵察対象も今どきらしい
コードを見ると、次のような情報収集も行おうとしていました。
- クラウドメタデータ(IMDS)へのアクセス
- IAMロールなど認証情報の取得
- ファイル探索順序のランダム化
- User-Agentやタイムアウト値のランダム化
固定パターンをできるだけ作らないよう工夫されており、「同じ攻撃を毎回少しずつ変える」という最近の攻撃らしい作りになっています。
PCRE が限界を迎えても、防御は止まらない
途中で
PCRE limits exceeded (-47)
が記録されています。これは巨大な入力に対して、ある正規表現がバックトラック上限へ達したことを示しています。
しかし、OWASP CRSは一つの巨大なルールだけで防御しているわけではありません。このリクエストでは、
- 932235
- 932260
- 933160
- 933210
と、別方向から検査するルールが続いてヒットしました。
つまり、一つの正規表現が処理を諦めても、他のルールが引き継ぐ設計になっています。
多層防御という言葉はよく聞きますが、今回のログはその動きを非常に分かりやすく見せてくれました。
おわりに
最近は攻撃そのものより、「検知をどう回避するか」の工夫を見る機会が増えてきました。
とはいえ、こうして落ち着いて中身を眺められるのは、防御基盤が先に仕事をしてくれているからです。
「サーバー運用はサファリパークに似ている」ようなものです。猛獣を間近で観察できるのは面白いですが、それは檻や装甲車があるからこそです。
今回も同じでした。難読化されたJavaScriptをじっくり読めたのは、攻撃がアプリケーションまで届かなかったからです。
運用者としては、その「観察できる余裕」こそが、一番ありがたい成果だったのかもしれません。