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_phpmod_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系フリーランスエンジニアとしても、まずは更新、次にログ。この順番で見ておきたいところです。
修正版の情報や、レンタルサーバーの人がやることは、こちらにまとめています。
【追記:2026年10月22日】攻撃はもう始まっています。Amazonの脅威情報チームによると、修正版の公開から数時間で攻撃用のコードが出回り、「すでに広く悪用されている」とのこと。公表より4日前の10月17日にも、攻撃の試みが見つかっていました。22日にはIPAとJPCERT/CCも注意喚起を出...
同じ週には、Linuxカーネルの脆弱性「PwnEscape」もありました。VPSを使っている人は、あわせてチェックしておいてください。
10月16日から、レンタルサーバーやVPSの大手が、相次いで新規の受付を一時停止しています。 さくらインターネット(10月16日〜) KAGOYA(10月19日18時〜) ConoHa(10月19日〜) 原因は、Linuxカーネルの脆弱性「CVE-2026-99999」...
参考にした記事
- PHPにおける複数の脆弱性(CVE-2026-109618、CVE-2026-109619)に関する注意喚起 – JPCERT/CC
- Amazon Threat Intelligence identifies PHP2Shell exploitation prior to public disclosure in MadPot – AWS Security Blog
- Rapid Response: php2shell, Leaked Critical PHP Vulnerabilities – Huntress
- PHP RCE FAQ: PHP2Shell CVE-2026-109618 & CVE-2026-109619 – watchTowr
- Security advisory: Critical remote code execution vulnerabilities, update now – PHP

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