hunting-web-backdoors について
PHP のウェブルートに仕込まれたバックドアを探す、インシデント対応者やサイト管理者向けのセキュリティスキルです。静的解析、段階的な調査、正常なファイルとの比較を重視し、シグネチャや単純な grep だけに頼りません。
主な機能
- 01場所や更新日時から PHP ファイルを絞り込む
- 02難読化やリクエスト起点の実行を検出
- 03正常なバージョンとファイルを比較
- 04ペイロードを実行せず静的にデコード
- 05証拠を保全し、発見後も調査を継続
活用シーン
- 01侵害された WordPress や独自 PHP サイトの調査
- 02デプロイ前のプラグインやテーマの検査
- 03不慣れなウェブルート内の不審なコードの調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ trilwu/ secskills --skill hunting-web-backdoorsインストール後、Claude Code で/hunting-web-backdoorsを入力して読み込まれたか確認します。
詳細
Web バックドアの調査
これは脆弱性監査ではありません。攻撃者がすでに仕込んだコード、つまりウェブシェル、1 行の eval バックドア、偽のプラグイン、正規ファイルに追記された悪意あるコードを探します。侵入を可能にした脆弱性を調べるのは auditing-php-applications の役割です。このスキルでは、攻撃者が残したものを見つけます。
難しい理由は 2 つあります。悪意あるコードは意図的に隠されており、難読化されたり、分割されたり、何千もの正規ファイルの中に埋め込まれたりしているため、単純な grep では見つかりません。また、特徴となるもの(eval、base64_decode、動的な呼び出し)は無害なコードにも数多く含まれるため、単純な grep では誤検知が大量に発生します。重要なのは見分けることです。
使用する場面
- ウェブサーバーが侵害された疑いがある、または侵害されたことが確認されており、すべてのシェルを見つける必要がある場合
- ハッキングされた WordPress、Magento、Joomla、または独自の PHP サイトをクリーンアップする場合
- ダウンロードしたプラグイン、テーマ、またはコードベースをデプロイ前に審査する場合
- ファイルに、デコード済みの入力やリクエスト由来の入力に対する
eval、assert、systemが含まれている場合 - 見慣れない PHP ファイルでいっぱいのウェブルートを調査し、仕込まれた悪意あるコードを見つける場合
使用しない場面
- アップロードを可能にした脆弱性の特定 —
auditing-php-applicationsを使用してください - 単一サンプルの詳細な分析(完全にリバースエンジニアリングしたい特定のシェル、その C2、その機能)—
analyzing-malwareを使用してください - より広範なインシデント(範囲の特定、タイムライン、封じ込め)—
responding-to-incidentsを使用してください。このスキルは、その中でソースツリーを調査するものです - 悪意ある依存関係やパッケージのインストールスクリプト —
auditing-supply-chainを使用してください - ホストレベルの永続化(cron、systemd、LD_PRELOAD)—
analyzing-linux-persistenceを使用してください
難読化解除のために実行しない
決して曲げてはならない唯一のルールは、難読化されたコードそのものがペイロードであるということです。実行してはいけません。「PHP にデコードさせるだけ」も避けてください。eval( を echo( に置き換えてファイルを実行してはいけません。実行するオンラインサンドボックスに貼り付けてもいけません。難読化解除は静的な変換です。
- レイヤーは、手作業か、デコード専用ツール(
base64_decode、gzinflate、gzuncompress、str_rot13、hex/\xNN、strrev)で解読してください。PHP として実行するのではなく、文字列操作として適用します。 - 自動化が必要な場合は、実行せずにデコードする難読化解除ツール(UnPHP のデコーダー、デコードチェーンを Python で再実装したもの)を使用してください。サンプルを PHP インタープリターで実行してはいけません。
- 最終的な
eval(をfile_put_contents('out.php', ...)に置き換えて実行するのも、前のレイヤーに副作用がある場合はペイロードの実行にあたります。静的にデコードしてから内容を読んでください。
「ほとんどのウェブシェルは、10 行ほどのロジックを 3、4 層のエンコードで包んでいる」— ロジックが見えるまで包みを剥がしてください。ラッパーを爆発させてはいけません。
ディレクトリを大規模にトリアージする
すべてのファイルを読むことはできません。安価なシグナルで候補に順位を付けてから、リストの上位を読みます。
# 1. PHP where PHP should not be — uploads, cache, images, media dirs
find wp-content/uploads -name '*.php' -o -name '*.php[0-9]' -o -name '*.phtml' 2>/dev/null
# 2. Recently modified, clustered in time (attackers touch many files at once)
find . -name '*.php' -newermt '2026-07-01' -printf '%T+ %p\n' | sort
# 3. High entropy / long single lines — the shape of packed obfuscation
find . -name '*.php' -exec awk 'length>1000{print FILENAME": "length; nextfile}' {} \;
# 4. The dangerous-sink grep — leads, NOT verdicts (see the FP problem below)
grep -rnE '\b(eval|assert|system|passthru|shell_exec|proc_open|popen|create_function)\s*\(' --include='*.php' .
grep -rnE '(base64_decode|gzinflate|gzuncompress|str_rot13|hex2bin)\s*\(' --include='*.php' .
# 5. Request-driven code execution — the webshell core
grep -rnE '\$_(GET|POST|REQUEST|COOKIE|SERVER)\s*\[[^]]*\]\s*\(' --include='*.php' .
grep -rnE '(eval|assert|system|passthru)\s*\(\s*\$_(GET|POST|REQUEST|COOKIE)' --include='*.php' .
最も信頼性の高い手掛かりは、リクエストデータを変数として実行することです。たとえば $_POST['x']()、ev[a]l($_GET[...])、ass[e]rt($_REQUEST[...])、call_user_func($_GET['f'], ...) です。正規のコードが、スーパーグローバルから関数名を直接取得して呼び出すことはほとんどありません。
grep の結果が空でも、クリーンとは限りません
grep eval で何も見つからないソースツリーにも、バックドアが仕込まれていることがあります。
攻撃者は、明白な grep 検索を意図的に回避します。
- 連結:
$a='ev'.'al'; $a($code);または('e'.'v'.'a'.'l')(...)。 - 可変変数/間接参照:
$f='system'; $f($_GET['c']);、${'_'.'GET'}、$$x。 - 文字/16 進数/8 進数エスケープ:
"\x65\x76\x61\x6c"はevalです。chr(101).chr(118)...も同様です。 - コールバックの実行箇所:
array_map、array_filter、usort、preg_replace_callback、register_shutdown_function、 ユーザー制御のコールバックを指定したob_start。 evalではない文字列評価:assert($str)(8.0 より前)、preg_replace('/./e', $str, ...)(7.0 より前)、create_function('', $str)(8.0 より前)、eを指定したmb_ereg_replace。- 正規表現を使った単純な検索を回避するための、トークンの間へのコメントや空白の挿入。
つまり、grep は 手掛かりを見つける手段 であり、網羅性を保証するものではありません。grep の結果が空でも感染が疑われる場合は、エントロピーや長い行による順位付け、mtime によるクラスタリング、正常なコピー(以下)との差分確認を行ってください。
# Diff the tree against a pristine copy of the same CMS/plugin version
# (download the exact version from wordpress.org / the vendor)
diff -rq ./site /known-good/wordpress-6.x/
# WordPress core/plugin integrity via checksums
wp core verify-checksums; wp plugin verify-checksums --all
# git/composer/npm: what changed vs the committed/locked state
git status --porcelain; git diff
正常なベースラインがない場合は、正常に見えるファイルの閉じ ?> の後にあるコード、設定ファイルの先頭に挿入された単独の eval/base64_decode の行、または周囲のファイルと書式が異なる <?php ... ?> ブロックが手掛かりになります。
バージョンで削除された機能からコードの時期を特定する
削除された言語機能が使われていれば、何らかの手掛かりになります。
/e修飾子を使ったpreg_replace— PHP 7.0 で削除。存在する場合、バックドアが PHP 5.x を対象にしているか、同バージョンより前に作られたことを示しています。- コード実行に使う
create_function()と文字列引数のassert()— PHP 8.0 で削除。古いシェルによく見られます。PHP 8 サーバーではエラーになるため、それ自体がコードの設置時期を推測する手掛かりになります。
既知のファミリーは手掛かりであり、断定材料ではありません
フィンガープリントを使うとトリアージを迅速化できますが、コードを読む代わりにはなりません。
このセクションの IOC 文字列は、角括弧で分断しています。
ev[a]lはeval、ass[e]rtはassertです。文字列をそのまま記述するとBackdoor:PHP/Chopperに一致し、防御目的 のハンティングスキルであるこのファイルは、ダウンロード時に隔離されます。 上記の grep パターンはそのまま使用でき、安全にコピーできます。リポジトリの README にある「ウイルス対策ソフトの誤検知」を参照してください。
- China Chopper — ワンライナーです。
<?php @ev[a]l($_POST['pass']);?>(またはassert)のような形式で、30 バイト未満のこともよくあります。サイズが小さいため見落としやすく、追記も容易です。 - WSO、b374k、c99、r57、indoxploit — 多機能なパネルです。バナー文字列、特徴的な関数名、認証 Cookie 名を手掛かりとして grep してください。
- Weevely — ビルドごとに変化する、難読化されパスワードで保護されたシェルを生成します。シグネチャの照合は有効性が低いため、リクエスト起因の実行や難読化の兆候に注目してください。
シグネチャが一致すれば、シェルが ひとつ あることは確認できます。しかし、すべてを見つけたことにはなりません。攻撃者は複数のシェルを仕込み、2 つ目が 1 つ目と同じファミリーに属することはめったにありません。
ひとつ見つかったら、ほかにもあると考えてください
- 削除する前に保全します。 シェルが確認できた場合、それは証拠です。
responding-to-incidentsの手順に従って、削除する前に保管場所へコピーし、ハッシュを取得してください。削除するだけでは時系列情報が失われます。また、侵入経路を見逃すと、攻撃者は数時間以内に再び仕込みます。 - 侵入経路を特定します。 シェルが置かれた経路があるはずです。脆弱なアップロード機能、既知の CVE を持つプラグイン、盗まれた認証情報などが考えられます。ソースは
auditing-php-applicationsに、ログはresponding-to-incidentsに引き継いでください。侵入口を塞がずにサイトをクリーンアップしても、翌朝には再感染します。 - 調査を続けます。 シェルがひとつ見つかったら、調査を終えるのではなく、ツリー全体への疑いを強めてください。明らかなものを削除した後、もう一度全体を調査します。
却下すべき合理化
- 「
evalを grep して何も見つからなかったから、ツリーはクリーンだ」。連結、16 進エスケープ、コールバックの実行先はすべて検出を回避します。grep の結果が空でも網羅できたわけではありません。エントロピー、mtime、正常な既知の状態との差分に切り替えてください。 - 「デコード結果を確認するために実行してみよう」。それはペイロードを実行することです。必ず静的にデコードしてください。
- 「
evalを使っているから、バックドアだ」。テンプレートエンジン、キャッシュ、フレームワークでも、正当な理由でevalを使います。見分けるポイントはeval自体ではなく、デコードされた入力やリクエスト由来の入力に対するevalです。 - 「ひどく難読化されているから、悪意がある」。商用プラグインも難読化されています(ionCube、Zend Guard、ライセンス関連など)。難読化は疑いを強めますが、悪意の証明にはなりません。デコードしてロジックを確認してください。
- 「シェルを見つけて削除したから、もう終わりだ」。見つけたのは 一つの シェルです。保全し、侵入経路を見つけて、もう一度スキャンしてください。攻撃者は冗長な仕掛けを設置します。
- 「このファイルは CMS のコアファイルだから、安全に違いない」。追加型の感染は、まさにその思い込みを利用して、そうしたファイルに潜みます。チェックサムで検証してください。
- 「既知のシェルのシグネチャには一致しなかった」。Weevely やカスタムシェルは、どのシグネチャにも一致しないことがあります。シグネチャは手がかりであり、証拠となるのは挙動(リクエストに応じた実行、難読化)です。
外部ソースを読む
公開されているアドバイザリー、仕様書、ベンダーレポートを Markdown として取得します。
curl -sL "https://defuddle.md/<url>" # scheme in the path is optional
これによりページの定型部分が取り除かれ、文章中心のページではトークン数が約 78% 減り、要約ではなく全文が返されるため、grep で検索して、該当がない場合もその結果を信頼できます。
この方法が適さないケースは 3 つあります。JSON や API のレスポンスは、生のまま取得してください。可読性抽出によって構造化データが損なわれるためです。認証が必要なページや JavaScript でレンダリングされるページは、直接取得してください。この方法では匿名で取得されるためです。また、攻撃者のインフラ(フィッシングリンク、C2、マルウェアのホスティング先)、クライアント所有のホスト、エンゲージメント URLは、決して経由させないでください。リクエストはお使いのマシンから第三者に送信され、稼働中の攻撃者のインフラの場合は、さらにオペレーターに気づかれることになります。
一部のサイトでは抽出ツールをブロックし、ページの代わりにエラーの内容を返します。たとえば freedesktop.org からは {"error":"Failed to fetch: 418 I'm a teapot"} が返されます。これは取得が拒否されたということであり、ソースがその内容の存在を否定しているわけではありません。結論を出す前に、URL を直接取得し直してください。
参考資料
auditing-php-applications— シェルの侵入を許した脆弱性analyzing-malware— 回収した単一のシェル、その C2 と機能の詳細なリバースエンジニアリングresponding-to-incidents— 証拠の取り扱い、対象範囲の特定、およびこのスキャンが含まれる、より広範な IRanalyzing-linux-persistence— Web ルートを越えたホストレベルの永続化writing-yara-rules— 確認済みのファミリーを再利用可能なシグネチャにする方法