サーバー運用者の危機対応

Web シェルが見つかったとき

Web シェルの通知とプロセスの検知を読み、実行の記録とアクセスログで経路を調べ、漏れた鍵を洗い出し、隔離庫から公開フォルダーの外へ取り出して調べるまでの手順です。

  • レベル: 実践
  • 約40分
  • 2026-11-12

#状況

監視に「Web シェル検知」の通知が出ました

運用しているサイトは nginx と php-fpm です。通知には「Webアプリ層の検知ルールが、Webシェルや注入された不正コードを検知しました」とあり、公開フォルダーの uploads/ に PHP ファイルが 1 件、該当したと書かれています。少し前には「Webサービス系プロセスからシェルが起動されました」という別の通知も出ていました。遮断は行われていません(どちらも記録と通知だけです)。

この教材は、Web シェルが見つかったときの、検知の読み方・調べ方・元に戻し方を身につけるための playbook です。攻撃の手口は扱いません。ファイルの検知は Web シェル・不正コードの検知、プロセスの検知は プロセス実行の記録と攻撃手口の検知、時系列は インシデントのタイムライン、アクセスログの調査は アクセスログの診断(AccessScope)、漏れた鍵の確認は API キー・秘密鍵の検出 を使います。

進め方は、他の playbook と同じです。確認 → 調査 → 対応 → 復旧の順に進めます。あわてて消したり、サイトを止めたりする前に、まず何が起きたかを時系列で押さえます。

#ファイルの検知を読む

RoamSwitch は、公開フォルダー(nginx などの設定にある DocumentRoot と、設定で足したフォルダー)に書き込まれた Web ファイルを、閉じた直後に検知ルールで調べます。mv などの移動で置かれたファイルも、移った直後に調べます。該当すると、既定では critical のものだけを隔離庫に退避し、残りは場所を知らせます。

検知を起こして、通知を読む

検知を確かめるため、検知ルールの単体テストと同じ、無害な文字列のファイルを 1 件だけ置きます。害のある動きはしません。置いたあと、書き込みの監視が拾うまで数秒待ちます。

# 検知の確認用。中身は検知ルールのテストと同じ無害な文字列
printf '<?php\n@eval($_POST["x"]);\n' | sudo -u www-data tee /var/www/html/uploads/avatar.php
sleep 6
roamswitch notifications      # 直近 1 週間の通知の履歴
出力の抜粋
  2026-11-12T02:53:18.208982729+00:00
  Webシェル検知: 1 件のファイルに該当
    🚨 Webアプリ層の検知ルールが、Webシェルや注入された不正コードを検知しました
    ホスト: ubuntu (IP: 192.168.64.15)
    該当ファイル: 1 件(隔離: 1 件、そのまま残したもの: 0 件)
    /var/www/html/uploads/avatar.php (php-request-exec, quarantined, owner www-data)
    推奨対応: 該当ファイルの内容と、書き込まれた時刻の前後のアクセスログを確認し、そのサイトの認証情報(管理画面・FTP・DB)を変更してください。隔離したファイルは `/var/lib/roamswitch/quarantine/metadata.json` で確認できます。
    時刻: 2026-11-12 02:53:18
    プロセス: www-data / /usr/bin/tee / コマンド: tee /var/www/html/uploads/avatar.php (pid 7726) ※プロセスは既に終了していたため、実行の記録(2026-11-12 02:53:18)から補いました。
    親プロセス: sudo ← dash ← tart-guest-agent ← systemd / サービス: tart-guest-agent.service

通知は、次のように読みます。

  • 該当ファイルの行に、どのファイルが、どのルールに当たり、隔離されたか残されたかが出ます。php-request-exec がルールの名前、quarantined が隔離済み、owner www-data が所有者です
  • 推奨対応は、内容とアクセスログの確認、サイトの認証情報の変更です。隔離したファイルの退避先は metadata.json にあります
  • プロセスと親プロセスは、そのファイルを書いた処理です。アカウント、実行ファイル、コマンド行が出ます。この演習の tee は、通知の前に終わっていました。そのため、実行の記録から補われ、行の末尾に「※プロセスは既に終了していたため、実行の記録(時刻)から補いました。」と付いています。書き手が残っていれば、この断りは付きません
  • 時刻は UTC(協定世界時)です。日本時間とは 9 時間ずれます。アクセスログと突き合わせるときは注意してください

#プロセスの検知を読む

ファイルの検知とは別に、Web サーバーのプロセスがシェルを起動したことを、実行の記録から検知します。これが EXR-001 です。Web シェルが外部からのコマンドを実行する動きに対応します。遮断はせず、通知だけを出します。

サービスのプロセスからのシェル起動を検知させる

害のない id コマンドを実行するだけの PHP を置き、ローカルから 1 回だけ開きます。php-fpm が子プロセスでシェルを起動する動きを再現します。

printf '<?php system("id");\n' | sudo -u www-data tee /var/www/html/uploads/run.php
sleep 6
curl -s -o /dev/null http://127.0.0.1/uploads/run.php
sleep 5
roamswitch notifications      # 先頭が EXR-001 の通知
roamswitch timeline           # 統合インシデントタイムライン
出力の抜粋
  2026-11-12T02:53:30.242693180+00:00
  🔎 不審な実行パターンを検知
    Webサービス系プロセスからシェルが起動されました (EXR-001)
    プロセス: /usr/bin/dash (PID: 7794) / 重要度: 高
    詳細: dash が、サービスのプロセスからユーザー www-data で起動されました。コマンド: sh -c -- id。親のサービス: /usr/sbin/php-fpm8.3 (pid 7202)
    次の確認コマンド: sudo roamswitch events tree 7794
    (通知のみ・遮断はしていません)
    プロセス: www-data / /usr/bin/dash / コマンド: sh -c -- id (pid 7794)
    親プロセス: php-fpm8.3 ← php-fpm8.3 ← systemd

  2026-11-12T02:53:24.266638774+00:00
  Webシェル検知: 1 件のファイルに該当
    🚨 Webアプリ層の検知ルールが、Webシェルや注入された不正コードを検知しました
    ホスト: ubuntu (IP: 192.168.64.15)
    該当ファイル: 1 件(隔離: 0 件、そのまま残したもの: 1 件)
    /var/www/html/uploads/run.php (php-code-in-uploads, reported, owner www-data)

  🕒 統合インシデントタイムライン

  2026-11-12T02:53:30.264909140+00:00  [Exec Recorder]  記録のみ
    EXR-001: dash が、サービスのプロセスからユーザー www-data で起動されました。コマンド: sh -c -- id。親のサービス: /usr/sbin/php-fpm8.3 (pid 7202)
    プロセス: /usr/bin/dash (pid 7794)
    親プロセス系譜: php-fpm8.3 ← php-fpm8.3 ← systemd
    MITRE ATT&CK: T1505.003 (Web Shell)
    実施した対応: 記録して通知

ここで読み取る点は 3 つです。

  • 親のサービスが php-fpm8.3 です。Web サーバーの実行ユーザー(www-data)が、Web サーバーのプロセスからシェルを起動しています。ふだんの Web リクエストでは起きない動きです
  • タイムラインに T1505.003 (Web Shell) が出ます。ファイルの検知とプロセスの検知が、同じ事象の別の面であることが分かります
  • run.php を置いた時点の通知は php-code-in-uploads ルールです。system のような実行系の関数を、本来データしか置かないフォルダーで使っているためです。critical 未満なので reported(残したまま)です。同じファイルの通知は、この 1 件だけでした(1 分待っても、2 件目は出ません)
ファイルの検知とプロセスの検知は、別々に出ます

ファイルを置いた時点でファイルの検知が、そのファイルが開かれてシェルを起動した時点でプロセスの検知が出ます。片方だけのこともあります。両方を突き合わせると、置かれた時刻と、動かされた時刻が分かります。

同じ当たりの通知は 5 分に 1 回です。5 分のうちにもう一度開かれると、タイムラインにだけ「(記録のみ。同じ当たりの通知は5分に1回、通知の全体は1分に30件までのため、通知していません)」と付いた行が残ります。

#侵入の経路を調べる

次に、誰が、いつ、何をしたかを調べます。読み取りだけのコマンドで、ファイルを書き換えません。

実行の記録とアクセスログを突き合わせる

実行ユーザー www-data の実行の記録と、アクセスログを見ます。アクセスログの POST の時刻と、ファイルの更新時刻を突き合わせると、置かれた経路が分かります。

sudo roamswitch events search --since 30m --user www-data    # www-data の実行の記録
sudo accessscope scan /var/log/nginx/access.log              # アクセスログの診断
stat -c '%n %U %y' /var/www/html/uploads/*                   # ファイルの所有者と更新時刻
出力の抜粋
2026-11-12T02:53:30.221Z pid=7795 ppid=7794 www-data /usr/bin/id
2026-11-12T02:53:30.221Z pid=7794 ppid=7202 www-data /usr/bin/dash -c -- id
2026-11-12T02:53:24.213Z pid=7765 ppid=7764 www-data /usr/bin/tee /var/www/html/uploads/run.php
2026-11-12T02:53:18.133Z pid=7726 ppid=7725 www-data /usr/bin/tee /var/www/html/uploads/avatar.php

## 最初に確認すること(成功した疑いのあるもの)

| # | 深刻度 | 確度 | 検知 | 件数 | 主な送信元 |
|---|---|---|---|---|---|
| 1 | 中 | 低 | Web シェルらしいアクセス ※1 | 1 (1 IP) | 127.0.0.1(1) |

/var/www/html/uploads/run.php www-data 2026-11-12 02:53:24.212775868 +0000
  • events search は、www-data が動かしたコマンドを時刻つきで並べます。実行の記録は root だけが読めるので、sudo を付けます。dash -c -- id の親が php-fpm(pid 7202)であることが、ここでも確かめられます
  • accessscope scan は、アクセスログを読んで「Web シェルらしいアクセス」を根拠つきで挙げます。確度の「低」は疑いの段階という意味で、応答は返っているがコマンドのパラメータや直後の利用がない状態を指します
  • stat で出る更新時刻を、アクセスログの POST の時刻と並べると、どのリクエストで置かれたかの見当がつきます
時刻の突き合わせ

アクセスログの時刻はサーバーのタイムゾーン、RoamSwitch の通知と events search は UTC です。突き合わせる前に、どちらかにそろえてください。

#漏れた秘密と、隔離からの復旧

Web シェルが置かれたら、その経路で設定ファイルの中身が読まれた可能性があります。サイトが持っている鍵やパスワードを確かめ、入れ替えます。

設定ファイルの秘密を洗い出す

公開フォルダーを audit-secrets で調べます。読むだけで、ファイルは変えません。

sudo roamswitch audit-secrets /var/www/html; echo "終了コード: $?"
出力の抜粋
  3件のファイルを走査しました。

  /var/www/html/config.php
    ⚠️  AWS Access Key ID (AWS) — 行番号 3
        マスク済み: AKIAIO...MPLE
        ↳ 推奨対応: AWS IAMコンソールからアクセスキーを非アクティブ化してください。

  合計 1 件の露出(1 ファイル)。コミット・共有前に対処してください。
終了コード: 1

終了コードは、見つかると 1、何もなければ 0 です。調べるパスが存在しないときは 2 になるので、見張りのスクリプトでも区別できます。ここでは config.php に AWS のアクセスキー ID が見つかりました(この教材では公開されている無効なサンプル値 AKIAIOSFODNN7EXAMPLE を使っています)。見つかった鍵は、漏れた前提で、発行元の管理画面から無効化し、新しい鍵に入れ替えます。鍵やパスワードの入れ替えは、RoamSwitch の外で行う対応です。

隔離庫を確認し、必要なら元に戻す

隔離されたファイルの退避先を確認します。中身を調べ直すときは、公開フォルダーの外へ取り出します(変更を伴う操作です)。

sudo roamswitch quarantine list                  # 隔離庫の一覧(root だけが読めます)
sudo roamswitch web-scan /var/www/html            # 確認のみ(ネットワーク接続なし)
sudo install -d -m 700 /srv/inspect               # 公開フォルダーの外に、調べる場所を作る
sudo roamswitch quarantine restore quar-20261112_025318-a06b811a /srv/inspect/avatar.php
sudo ls -l /srv/inspect
出力の抜粋
  隔離Vault場所: /var/lib/roamswitch/quarantine
  隔離ファイル数: 1 件

  • [quar-20261112_025318-a06b811a] avatar.php (WebShell-PHP: php-request-exec) - 26 bytes
    元パス: /var/www/html/uploads/avatar.php

  ルール 2026-11-11 (20 本)   対象 3 ファイル(Web関連のみ)

  [HIGH] /var/www/html/uploads/run.php
      php-code-in-uploads (WebShell-PHP)
      アップロード・画像などのデータ置き場にあるPHPファイルが、コード実行系やデコード系の関数を呼んでいます。このディレクトリには本来データしか置かれません。
      → 確認のみ(--quarantine を付けるまで何も動かしません)

✓ quar-20261112_025318-a06b811a を復元しました(実行ビットは付けていません)。
total 4
-rw-r--r-- 1 root root 26 Nov 12 02:54 avatar.php
  • quarantine list は、退避したファイルの ID・脅威名・元のパスを出します。退避の実体の場所と SHA-256 は /var/lib/roamswitch/quarantine/metadata.json にあります。sudo を付けないと「隔離 Vault の一覧を読めませんでした」と出て、終了コード 1 で終わります(0 件とは言いません)
  • web-scan は、既定では確認だけで、ファイルを動かしません(--quarantine を付けたときだけ隔離します)。残っている run.php が HIGH として出ています
  • quarantine restore <ID> <パス> は、退避したファイルを指定の場所へ戻し、隔離庫から外します。実行ビットは付けず、同じ名前のファイルがあると上書きしません。/root のようなシステムの場所へは戻せません
  • パスを省くと、元の場所に戻します。元の場所が公開フォルダーなら、戻した直後にまた調べられます。本物の Web シェルなら、すぐに隔離し直されます
隔離するか、残して観察するか

critical のルール(php-request-exec など)に当たったファイルは、既定で隔離されます。隔離すれば、そのファイルは即座に動かなくなります。一方、php-code-in-uploads のような critical 未満の検知は、場所を知らせるだけで残します。残すのは、調査のためにそのままの状態を見たいときに役立ちます。本物の侵入と判断したら、残っているファイルも退避し、経路(受け取り口)を塞いでから復旧します。 誤検知と確かめられたときだけ、隔離から元に戻します。

変更を伴う操作です

quarantine restore、鍵の入れ替えは、状態を変えます。取り出す前に、metadata.json とアクセスログを保管してください。誤検知と確かめて元の場所へ戻したときは、ls -l <元のパス> で、所有者が元のアカウント(この演習では www-data)に戻っていることを確かめます。調べるために取り出したファイルは、終わったら sudo rm -r /srv/inspect で消します。

#見えない部分と確認

この教材で確かめていないこと
  • 検知の確認には、検知ルールのテストと同じ無害な文字列と、id を実行するだけの PHP を使いました。実在のマルウェアや、外部からコマンドを受け取る Web シェルは使っていません
  • 検知するのは、監視するフォルダーに書き込まれ、中身がルールに当たるファイルです。ファイルを作らずに Web サーバーのプロセスの中でコードを実行するものは、サービスのプロセスからシェルを起動したときだけ、EXR-001 で見えます。ルールをすり抜ける難読化や、監視外のフォルダーへの書き込みは、見えません
  • ファイルは 64 MiB までは全体を、それより大きいものは先頭と末尾の一部を読みます。画像の末尾に足されたコードは見えますが、64 MiB を超えるファイルの真ん中のコードは見えません
  • 鍵やパスワードの入れ替え、受け取り口の修正、関係先への連絡は、RoamSwitch の外で行う対応で、この教材の範囲外です
確認問題

uploads/ の PHP ファイルが php-request-exec に当たり、通知に quarantined と出ました。このファイルは、いまどうなっていますか

解説を見る

critical のルールに当たったため、既定で隔離庫へ退避されています。元のパスと退避先は sudo roamswitch quarantine list と metadata.json で分かります。削除ではないので、誤検知なら sudo roamswitch quarantine restore <ID> で元に戻せます。

次は、攻撃トラックの「Web とメール」で、Web シェルの検知の仕組みと、アクセスログから探る動きを止める守りを学べます。

扱う機能

対応する攻撃手法 (ATT&CK): T1505.003 T1059.004 T1027 T1105 T1553.001 T1204.004

確認した内容

2026-11-12に確認しました。対象は Linux 1.17.39 です。公開版 1.17.39 の入った使い捨ての Ubuntu 24.04 の VM に nginx と php-fpm を入れ、検知ルールのテストと同じ無害な文字列と id だけの PHP で、全演習を日本語と英語で別の VM で上から順に実行し、同じファイルの 2 件目の報告が出ないことも 1 分待って確かめました。

コマンドの出力は、製品の版や環境によって変わります。違いが出たら、まず製品の版を確かめてください。