php2shellで攻撃されたら、ログに何が残る? サーバー管理者向けの見つけ方とcronの見回りスクリプト

PHPの脆弱性「php2shell」(CVE-2026-109618/CVE-2026-109619)は、修正版の公開から数時間で、悪用が始まっています。Amazonによると、公表の4日前の10月17日には、すでに攻撃の試みがあったとのこと。

となると気になるのが、「更新する前に、もう入られていないか?」ということ。

先に結論を言うと、php2shellはふつうのログには決定的な跡が残りにくい攻撃です。なので、次の2つの方向から探すのが現実的です。

  • 攻撃の前ぶれ:PHPのメモリ上限のエラー、ワーカーのクラッシュ、POSTへの500
  • 入られたあとの動き:Webサーバーが動かすシェル、知らないPHPファイル、cronの書き換え

調べる期間は、10月17日から。この記事では、ログの見方と、cronで数分おきに見回るスクリプトをまとめました。

跡は残りにくい

JPCERT/CCは、攻撃されたかを調べるポイントとして、次の4つを挙げています。

  • 異常に長い、変わった値のPHPSESSID(セッションのクッキー)
  • 怪しいファイル送信(multipart/form-data)のリクエストと、その前後のメモリ上限のエラーやHTTP 500
  • PHPのワーカーが突然落ちたり、何度も再起動したりしていないか
  • 見覚えのないプロセス、外への通信、知らないファイル

ただし、同じ注意喚起の中で「ここにある跡がないからといって、攻撃がなかったとは言えない」とも書いています。ログの設定によっては、跡そのものが残らないこともあるとのこと。

実際、Apacheやnginxのふつうのアクセスログ(combined形式)には、Content-Type、Cookie、POSTの中身は記録されません。今あるログで分かるのは、「POSTで500が返った」ところまでなんです。

ログの見どころ

ここからは、わたくしの見立ても交えて、どこを見ればいいかを整理します。エラーの文言は、PHPやApacheがふだん出すメッセージをもとにしたもので、今回の攻撃でこれが出ると公式に示されたわけではありません。

アクセスログ

POSTで500が返ったリクエストを探します。同じIPから短い時間に集中していたら要注意です。

# POST で 500 を返したリクエスト
grep -h '"POST ' /var/log/nginx/access.log* | awk '$9==500' | less
# IPごとに数える
grep -h '"POST ' /var/log/nginx/access.log* | awk '$9==500{print $1}' | sort | uniq -c | sort -rn | head

エラーログ

いちばん手がかりが出やすいのが、ここ。php2shellは、メモリが足りなくなったときの処理と、片付けたはずのデータの穴です。なので、メモリ上限のエラーや、ワーカーの異常終了として表に出やすいと考えられます。

# メモリ上限のエラー
grep -h "Allowed memory size of" /var/log/php*/*error*.log /var/log/nginx/error.log*
# php-fpm のワーカーが落ちた跡
grep -h "exited on signal" /var/log/php*-fpm.log*

ふだんは出ないのに、10月17日以降に急に増えていないか。アクセスログの500と時刻を突き合わせると、怪しいリクエストを絞り込めます。

入られたあとの跡

Huntressは、PHPのワーカーがシェルを起動することを一番の手がかりにしています。

# php-fpm や Apache の子として、シェルなどが動いていないか
ps -ef --forest | grep -A3 -E "php-fpm|apache2|httpd"
# 10/17 以降に増えた PHP ファイル(Webシェル)
find /var/www /tmp /var/tmp -newermt "2026-10-17" -type f \( -name "*.php" -o -name "*.phtml" \) -ls
# Web サーバーのユーザーの cron
crontab -l -u www-data

コマンドの中に/dev/tcp/やmkfifo、whoami、idがあったら、かなり黒に近いと思っていいでしょう。

mod_phpの場合

PHPをApacheに組み込む「mod_php」は、開発チームが「いちばん危ない」と名指ししている方式です。アプリのプログラムが動く前に攻撃が成立することが、実際に確かめられています。

自分のサーバーがmod_phpかどうかは、これで分かります。

apachectl -M 2>/dev/null | grep -i php     # php_module などが出れば mod_php

mod_phpだと、見る場所が少し変わります。

  • php-fpmのログはない。PHPのエラーもクラッシュも、Apacheのerror_logに出る
  • PHPのワーカー=Apacheの子プロセス。落ちると「AH00052: child pid … exit signal Segmentation fault (11)」のような行になる
  • ログイン画面やWordPressのセキュリティプラグインは守りにならない。アプリより前でやられるので、アプリのログにも何も残らない
  • 不審なコマンドは、apache2やhttpdの子として動く
grep -h "exit signal Segmentation fault" /var/log/apache2/error.log* /var/log/httpd/error_log*
# ふだん GET しか来ない .php に POST → 500 が来ていないか
grep -h '"POST ' /var/log/apache2/access.log* | awk '$9==500{print $7}' | sort | uniq -c | sort -rn | head -20

ログの形式を足す

JPCERT/CCが挙げた「レスポンスの中の、異常に長いPHPSESSID」。Apacheなら、レスポンスのSet-Cookieをアクセスログに残せます。

# mod_logio が必要(%I = 受け取ったバイト数)
LogFormat "%h %l %u %t \"%r\" %>s %O %I \"%{Content-Type}i\" \"%{Cookie}i\" \"%{Set-Cookie}o\" %D" php2shell
CustomLog ${APACHE_LOG_DIR}/php2shell.log php2shell

ただし、Cookieをログに残すと、ふつうの利用者のセッションIDまで記録されます。ログを見られる人を絞って、調査が終わったら外すようにしてください。

影響の範囲が広い

ここからは、mod_phpの仕組みからの、わたくしの判断です。

mod_phpでは、そのApacheのすべてのサイトが同じユーザーで動いています。1つのサイトから入られたら、同じサーバーのほかのサイトのファイルや、DBのパスワードも読まれたと考えたほうが安全です。Apacheの子プロセスはTLSの秘密鍵もメモリに持っているので、入られた跡があれば、証明書の作り直しも考えたいところ。

この機会に、サイトごとにユーザーを分けられるPHP-FPMへ移るのもおすすめです。

cronで見回る

数分おきにログを見るのは、人の手では無理。そこで、cronで見回らせるスクリプトを作ってみました。

ポイントは、何か見つかったときだけ出力すること。cronは出力があったときだけMAILTOにメールを送ってくれるので、知らせる仕組みを別に作らなくて済みます。

調べるものcronで拾えるか
メモリ上限のエラー、ワーカーのクラッシュ◎
POSTへの500が急に増える◎
知らないPHPファイル(Webシェル)◎
www-dataのcrontabの書き換え◎
動き続けている不審なシェル○
一瞬で終わるコマンド(id、curlなど)×(auditdが必要)
POSTの中身×(WAFが必要)

※ログの場所やユーザー名は、環境によって違います。まずは手元の検証環境で動かしてから、本番のサーバーに入れてください。

#!/bin/bash
# /usr/local/sbin/php2shell-watch.sh — php2shell の跡を見回る(見つかったときだけ出力)
set -u
STATE=/var/lib/php2shell-watch; mkdir -p "$STATE"
ERRLOG=/var/log/apache2/error.log        # RHEL系は /var/log/httpd/error_log
ACCLOG=/var/log/apache2/access.log       # RHEL系は /var/log/httpd/access_log
WEBROOT=/var/www
WEBUSER=www-data                         # RHEL系は apache
POST500_MAX=20                           # 1回の見回りで、これを超えたら知らせる

# 前回の続きから、増えた行だけを出す(ログが回されて小さくなったら最初から)
new_lines() {
  local f=$1 key; key=$STATE/$(echo "$f" | tr / _).pos
  [ -f "$f" ] || return
  local size prev; size=$(stat -c %s "$f"); prev=$(cat "$key" 2>/dev/null || echo 0)
  [ "$size" -lt "$prev" ] && prev=0
  tail -c +$((prev + 1)) "$f" | head -c $((size - prev))
  echo "$size" > "$key"
}

# 1. エラーログ:メモリ上限のエラーと、ワーカーのクラッシュ
ERR=$(new_lines "$ERRLOG" | grep -E "Allowed memory size of|exit signal Segmentation fault|exited on signal")
[ -n "$ERR" ] && { echo "== エラーログ(メモリ上限・クラッシュ)"; echo "$ERR" | tail -20; }

# 2. アクセスログ:POST で 500 が急に増えていないか
P500=$(new_lines "$ACCLOG" | awk '$6=="\"POST" && $9==500')
N=$(printf "%s" "$P500" | grep -c . || true)
[ "$N" -gt "$POST500_MAX" ] && { echo "== POST→500 が ${N} 件"; echo "$P500" | awk '{print $1, $7}' | sort | uniq -c | sort -rn | head; }

# 3. 前回の見回りのあとに増えた・変わった PHP ファイル
STAMP=$STATE/last_run
if [ -f "$STAMP" ]; then
  NEW=$(find "$WEBROOT" /tmp /var/tmp -newer "$STAMP" -type f \( -name "*.php" -o -name "*.phtml" -o -name "*.phar" \) 2>/dev/null)
  [ -n "$NEW" ] && { echo "== 新しい・変わった PHP ファイル"; echo "$NEW"; }
fi
touch "$STAMP"

# 4. Apache の子として動いているシェルやダウンロードのコマンド
SH=$(ps -eo pid,ppid,user,comm,args --no-headers | awk -v u="$WEBUSER" '$3==u && $4 ~ /^(sh|bash|dash|curl|wget|nc|ncat|python[0-9.]*|perl|socat)$/')
[ -n "$SH" ] && { echo "== $WEBUSER が動かしている不審なプロセス"; echo "$SH"; }

# 5. Web サーバーのユーザーの crontab が変わっていないか
CRON=$(crontab -l -u "$WEBUSER" 2>/dev/null | sha256sum)
[ -f "$STATE/cron.sum" ] && [ "$CRON" != "$(cat "$STATE/cron.sum")" ] && echo "== $WEBUSER の crontab が変わった"
echo "$CRON" > "$STATE/cron.sum"

cronには、こう登録します。

sudo install -m 700 php2shell-watch.sh /usr/local/sbin/
sudo crontab -e
# 以下を足す
MAILTO=you@example.com
*/5 * * * * /usr/local/sbin/php2shell-watch.sh

使うときの注意

  • 1回目は記録だけ。今あるログの過去分は、上のgrepで別に調べる
  • WordPressの更新でも鳴る。本体やプラグインを更新するとPHPファイルが変わるので、更新した時刻と突き合わせる
  • POST500_MAXは、ふだんの数を1週間ほど見てから決める
  • 入られたら、スクリプト自体を書き換えられるかも。root以外は書けない場所に置いて、知らせはメールなど外へ

一瞬で終わるコマンドまで拾いたいなら、auditdの出番です。Webサーバーのユーザーがコマンドを実行したら、全部記録してくれます。

# UID は id -u www-data で確認
sudo auditctl -a always,exit -F arch=b64 -S execve -F uid=33 -k php2shell
sudo ausearch -k php2shell --start today

入られていたら

JPCERT/CCは、ログを消さずに残し、影響の範囲を調べ、サーバーをネットから切り離すよう勧めています。

気をつけたいのは、更新して再起動すると、メモリの中の跡は消えてしまうこと。怪しいときは、更新の前にログとプロセスの一覧を保存しておきましょう。

まとめ

  • php2shellは、ふつうのログには決定的な跡が残りにくい
  • 見るのはエラーログ(メモリ上限・クラッシュ)、アクセスログ(POSTへの500)、入られたあとの跡(シェル・Webシェル・cron)
  • 調べる期間は10月17日から
  • mod_phpは跡がApacheのログに集まる。アプリより前でやられるので、影響の範囲も広め
  • cronの見回りは「入られたかもしれない」を早く知るためのもの。いちばんの対策は修正版への更新と再起動

IT系フリーランスエンジニアとしても、まずは更新、次にログ。この順番で見ておきたいところです。

修正版の情報や、レンタルサーバーの人がやることは、こちらにまとめています。

同じ週には、Linuxカーネルの脆弱性「PwnEscape」もありました。VPSを使っている人は、あわせてチェックしておいてください。

参考にした記事

関連記事一覧

  1. この記事へのコメントはありません。