運営者はclaudeに託して寝ました。後で確認するそうです


はじめに

2026年10月21日、PHPに認証不要のRCEが2件出た。CVE-2026-109618とCVE-2026-109619、通称PHP2Shellだ。RCEは、リモートから任意のコードを実行できる脆弱性のこと。

自分のハニーポットにもう来ているのか。それだけを確認するつもりだった。

ところがローカルのELKを起動しようとしたら、Docker Desktopにこんな通知が出ていた。

Security update: eMudhra certificate compromise

先にこちらを片付けることにした。前半(1〜4章)がeMudhraの対応、後半(5章)がPHP2Shellの確認だ。


1. eMudhraの中間CA鍵漏えい

何が起きたか

インドの認証局eMudhraの中間CA証明書8本の秘密鍵が漏えいした。内訳はTLS用が6本、コード署名用が2本。いずれも2025年4月の鍵生成セレモニーで作られたものだ。

しかも原因は、誰かに盗まれたことではない。Mozillaの記録によると、秘密鍵が256通り未満しかありえない極端に弱い値だった。公開されている証明書から、総当たりで一瞬で秘密鍵を求められる。eMudhraは、鍵生成の手順ミスで弱い鍵が使われたと暫定的に説明している。

見つけたのは外部の研究者で、8本すべてについて「秘密鍵を握っている証拠」を添えて報告した。

「漏えい」というより、実質的に最初から鍵が公開されていたのと同じだ。

中間CAの秘密鍵漏えいは、どれぐらいヤバいのか

「秘密鍵が漏れた」と聞いても、パスワード漏えいと同じくらいの話に聞こえるかもしれない。実際は桁が違う。

まず、HTTPSの「鍵マーク」がどう成り立っているかを整理する。

【平常時】

  ルート証明書  : 鍵は金庫の中。PCには最初からコピーが入っている
     │ 署名
     ▼
  中間CA証明書  : 日々の発行はこの鍵で行う
     │ 署名
     ▼
  サーバ証明書  : サイトに接続すると届く(example.com)

  PCの確認:サーバ証明書 → 中間CA → ルート と署名を上にたどり、
            手元の信頼ストアにあるルートに着いたら「本物」→ 鍵マーク

ブラウザやcurlは、受け取った証明書の署名を上にたどって、手元の信頼ストア(百数十本のルートが入っている)にあるルートまで行けるかを確認する。たどり着ければ「本物」と判断する。

逆に言うと、PCが確かめているのは「信頼しているルートの系列の鍵で署名されたか」だけだ。その鍵を使ったのが本当に認証局自身なのかまでは分からない。

そして、ルートや中間CAは「どのドメインの証明書を出してよいか」を原則として制限されていない。インドの認証局でも、google.comの証明書を発行できてしまう。だからこそ認証局は厳しい監査を受け、鍵はHSMという専用機器から出さない決まりになっている。

その中間CAの鍵が漏れた。つまり、誰でも、どのドメインの「本物の」証明書でも作れる状態になった。

【中間CAの鍵が漏れると】

  ルート ── 署名 ──→ 中間CA(鍵が漏れた)
                        │
                        │ 攻撃者が中間CAの鍵で署名
                        ▼
                   偽のサーバ証明書  google.com / 銀行 / 何でも
                        │
                        ▼
  あなたのPC:「ルートまでたどれた。本物だ」→ 鍵マーク

漏えいの範囲ごとに比べると、こうなる。

漏れたもの偽装できる範囲例え
あるサイトのサーバ証明書の鍵そのサイトだけ一軒の家の合鍵
中間CAの鍵(今回)すべてのドメイン合鍵を作る業者の型が盗まれた
ルートの鍵すべてのドメイン、かつ止めるのが最も難しい業者の免許そのもの

実際に起きるとどうなるか

前例がある。2011年、オランダの認証局DigiNotarが侵害され、*.google.comの不正な証明書が発行された。この証明書は、主にイランのGmail利用者を狙った中間者攻撃に使われたと報告されている。DigiNotarはその後、すべてのブラウザから信頼を外され、破綻した。

直近にも似た話がある。2026年10月、Googleは国別トップレベルドメインの.gh(ガーナ)・.sl(シエラレオネ)・.as(米領サモア)のレジストリが乗っ取られ、Googleのドメインを含む不正な証明書が発行されたと公表した。

こちらは認証局が侵害されたわけではない。攻撃者は権威DNSを書き換えて証明書を取得した。「このドメインの持ち主は自分だ」という確認(ドメイン認証)を正規の手順で通したとみられる。CTログから、Google以外の大手サービスも被害を受けていたことが分かっている。

事例何が破られたか
DigiNotar(2011)認証局そのものが侵害された
ccTLD乗っ取り(2026)認証局は正常。ドメインの持ち主の確認が騙された
eMudhra(2026)認証局の鍵が計算できる状態だった

経路は違っても、結果は同じだ。ブラウザが鍵マークを出す「本物の」証明書が、本物ではない者の手に渡った。

攻撃の流れはこうなる。

攻撃者が経路上にいる(公衆Wi-Fi・乗っ取られたルータ・国家レベルの通信事業者等)
  → 対象ドメインの「本物の」証明書を提示
  → クライアントは警告を出さない。鍵マークもつく
  → 通信の盗聴・改ざんが成立

今回はTLS用だけでなく、コード署名用の中間CAも2本含まれている。コード署名の証明書は、ソフトウェアが「正規の発行元から来た改ざんされていないもの」であることを示すものだ。これを悪用されると、マルウェアに正規っぽい署名を付けて配れてしまう。Scott Helmeによると、WindowsはeMudhraのコード署名用ルートを信頼しており、失効確認を有効にしていなければ署名を受け入れていたという。

「失効させれば終わり」にならない理由

証明書には失効の仕組みがある。ただ、今回はそれだけでは片付かない。

  • ブラウザ以外の多くのクライアントは、失効確認をデフォルトでしない。curl、OpenSSLを使うプログラム、Goで書かれたツールなどがそうだ
  • 失効確認をしていても、経路上の攻撃者がその通信を遮断すれば「確認できなかったので通す」となる実装が多い
  • 問題の中間CA証明書の有効期限は2039年7月まで残っている。更新されない機器は、それまで信頼し続ける

結局、信頼ストアを更新できる機器だけが守られる。

これはSecure Bootの証明書が2026年6月から期限切れを迎える問題と、構造が似ている。どちらも「信頼の起点を入れ替える」話で、世の中にばらまかれた機器を一台ずつ更新するしかない。そして更新されない機器は、ずっと古い信頼のまま動き続ける。

一方で、今すぐ全員が危ないわけではない

冷静に見ると、悪用には条件がある。

  • 攻撃者が通信経路上にいる必要がある。普通の家庭のネットワークで狙われる可能性は低い
  • Chrome Root Programによると、問題の中間CAから発行されたTLS証明書は、ブロック前の時点でCTログに記録されていなかった
  • ChromeとFirefoxは、報告から数時間でブロックした(ChromeはCRLSet、FirefoxはOneCRLという各ブラウザの緊急ブロックリストを使った)。さらにMozillaは10月20日に、問題の中間CAの上にあるルート(emSign ECC Root CA - G3)を信頼ストアから外した。Chromeも同じルートを外す更新を予定している

危ないのは、ブラウザ以外のクライアントと、更新が止まった機器だ。 自宅ラボで言えば、VPS上のcurlやdockerdがこれに当たる。


2. 確認事項

自宅ラボで確認したのは次の4点。

確認事項結果
ローカルのDocker Desktop通知の通り更新を適用
VPS(Ubuntu 24.04)の信頼ストアに該当ルートがあるかあった
中間CAだけを拒否する手段があるかない
ca-certificatesパッケージは最新か2024年の初期版のまま

該当ルートの有無

Debian/Ubuntuでは、信頼するルートを/etc/ca-certificates.confで管理している。

ls /usr/share/ca-certificates/mozilla/ | grep -iE 'emsign|aaa|comodo'
grep -nE 'emSign|AAA' /etc/ca-certificates.conf

最初はemudhraで検索して何も出なかった。eMudhraのルートはemSignというブランド名で入っている。名前で探すときは注意が必要だ。

結果、emSignのルートが4本入っていた。今回の8本がぶら下がっているのは、このうちG3の1本だ。(AAAについては後述する)

落とし穴:Sectigoのクロス署名

調べていくと、もう1つ問題があった。

漏えいした8本は、同じ鍵でSectigoのAAA Certificate Servicesルートからもクロス署名されている。eMudhraのルートだけ外しても、AAAを信頼していれば経路が残る。なお、Sectigoはこのクロス署名8本を失効させている。ただ、失効確認をしないクライアントには効かないのは前に書いた通りだ。

【今回の構図:上に道が2本ある】

  emSign ルート              Sectigo AAA ルート
  (eMudhra)                (別の認証局)
       │ 署名                     │ 署名(クロス署名)
       ▼                          ▼
  中間CA証明書 ═══ 同じ鍵 ═══ 中間CA証明書
                    │
                    ▼
            偽のサーバ証明書

  emSignだけ外しても、AAA経由の道が残る

VPSにはAAAのルートも入っていた。ファイル名はComodo_AAA_Services_root.crtだ。中身が本当にAAAかは、subjectで確認した。

openssl x509 -in /usr/share/ca-certificates/mozilla/Comodo_AAA_Services_root.crt -noout -subject

中間CAだけを拒否できるか

OpenSSLやGoは、OSの信頼ストアを使う。そしてDebian/Ubuntuのca-certificatesの仕組みには、特定の中間CAだけを拒否するリストがない。ルートを信頼している限り、その下の中間CAも信頼される。(WindowsやRHEL系には、中間CAを個別に拒否する仕組みがある)

Goは標準ライブラリに今回の中間CAの拒否リストを追加した(1.26.10以降とみられる)。ただ、VPSのdockerdはそれより古いGoでビルドされていた。

つまり現実的な手段はぶら下がっているルートごと外すことになる。

【ブラウザ】中間CAを名指しでブロックできる

  ルート ── 中間CA ✕ ── 偽証明書     → 中間CAで止まる(ルートは生かせる)

【Debian/Ubuntuの信頼ストア】名指しのブロック表がない

  ルート ✕ ── 中間CA ── 偽証明書     → ルートごと外すしかない

パッケージが古かった件

ca-certificatesは2024年の初期版のままだった。unattended-upgradesは有効で、設定も正しく、systemdの上では毎日「成功」している。

aptが残すスタンプファイルを見ると、様子がおかしかった。

/var/lib/apt/periodic/
  update-stamp              10-21   ← 毎日の処理は走っている
  update-success-stamp      08-13   ← 「完全に成功」の印は8月が最後
  unattended-upgrades-stamp 05-07   ← 自動更新の印は5月が最後

毎日の処理は走っているのに、成功の印が付かない。それでもsystemdの上では成功扱いになっていた。security向けの更新は100件を超えて溜まっていた。手動でapt updateを叩くと普通に成功する。自動のときだけうまくいかない原因は、まだ調べている途中だ。

そして、これが今回の件に直結していた。

SectigoのAAAルートは、2025年にMozillaがTLS用の信頼を外している。だからパッケージが更新されていれば、AAA経由の経路は最初から存在しなかったはずだ。さらに10月21日には、G3を無効化したca-certificatesの更新もUbuntuのsecurityに出ている。

AAA経由の道が残っていたのは、自動更新が止まって2024年版のままだったからだ。更新を当てたあとも、/etc/ca-certificates.confで外した設定はそのまま残るので、今回の対処と衝突はしない。


3. 対処

外したのは5本。

【対処後】

  emSign ルート(4本)✕       Sectigo AAA ルート ✕
       │                          │
       ▼                          ▼
  中間CA証明書 ═══ 同じ鍵 ═══ 中間CA証明書
                    │
                    ▼
            偽のサーバ証明書
                    │
                    ▼
  VPS:「どのルートにもたどれない」→ 拒否
ルート対処理由
emSign_* ×4外すeMudhraのルート。今回の8本の上にあるのはG3だけだが、このVPSでeMudhraを信頼する理由がないので4本とも外した(自分の判断)
Comodo_AAA_Services_root外すクロス署名したSectigoのルート
COMODO_*_Certification_Authority ×3残す今回とは無関係

/etc/ca-certificates.confで行頭に!を付けると、そのルートを信頼しない設定になる。パッケージを更新してもこの設定は残る。

cp -a /etc/ca-certificates.conf /etc/ca-certificates.conf.bak_YYYYMMDD
sed -i -E 's@^(mozilla/(emSign_[^ ]+|Comodo_AAA_Services_root)\.crt)$@!\1@' /etc/ca-certificates.conf
update-ca-certificates --fresh

検証結果はこうなった。

  • 信頼ストアのルートが146本から141本に減った
  • GitHub・ghcr.io・Dockerのダウンロードサイトへの疎通は問題なし

dockerdは起動時に信頼ストアを読み込む。T-Potは毎日リブートする設定なので、そのあとは新しいストアで動いている。

戻すときは、バックアップを書き戻してupdate-ca-certificates --freshを叩けばいい。

名前が似ているからといってCOMODOをまとめて外すと、関係ないサイトまで繋がらなくなる。外すのは必要な分だけにした。


4. 影響分析

VPS側

対象影響理由
dockerd(イメージのpull)対処済み信頼ストアから該当ルートを外した。そもそもpullは普段走らない設定
curl・OpenSSLを使うもの対処済み同上
apt影響なしパッケージの正しさはGPG署名で検証される。TLSが破られても、改ざんされたパッケージは入らない(httpsのリポジトリでも同じ)
ハニーポットのコンテナ実質影響なしCAバンドルはイメージに焼き込まれていて、ホスト側の変更は効かない。そもそもハニーポット自体が外向きにTLS通信しない
ログの取得影響なしSSHで取得している

ローカル(Windows)側

対象状態
Chrome / Firefoxブラウザ側のブロックで対処済み
Docker Desktop更新で対処済み
Windows標準の証明書ストアを使うアプリ未対処
署名付き実行ファイルの検証未対処

SectigoはMozillaに対して、AAAのルートは「MicrosoftとAppleではまだ信頼されている」と説明している。PowerShellやWindows版のcurl.exeのように、Windowsの証明書ストアを使うアプリは、Microsoftが対応を配布するまで信頼したままになる。コード署名の検証も同じだ。

Windowsには「信頼されていない証明書」ストアがあるので、問題の中間CAを手動で入れて拒否する手もある。

実際のリスク

1章で書いた通り、悪用には経路上の攻撃者が必要になる。自宅ラボが今すぐ狙われる可能性は低いと見ている。

それでも対処したのは、影響の大きさに対して、外すコストが小さかったからだ。このVPSがインドの認証局やAAAのルートを信頼しなければならない場面は、ほぼない。

Windows側はUpdateを待つことにした。


5. 本題:PHP2Shellは着弾していたか

PHP2Shellとは

項目内容
CVECVE-2026-109618 / CVE-2026-109619
深刻度どちらもCVSS v4.0のスコアで9.2。認証不要のリモートコード実行
原因109618はメモリ不足時の処理中断で内部データが中途半端に残る問題。109619は解放済みメモリをリクエストの内部状態が参照し続ける問題
修正版8.5.12 / 8.4.27 / 8.3.36 / 8.2.35(2026-10-21)
暫定策session.upload_progress.enabled = 0(109619向け)、PHP 8.5ではfatal_error_backtraces = 0、Webサーバでのmultipartやリクエストサイズの制限など。いずれも部分的な緩和で、完全な対策はアップデートのみ(公式)

公式の説明では、攻撃者が到達できる経路は大きなPOSTボディや不正な形のPOSTボディ、multipartのアップロード、アップロード進捗の処理とされている。mod_phpでは、アプリのコードが動く前のリクエスト処理の段階で攻撃が成立するデモが示されている。

タイムライン

日時(UTC)出来事
10-20CVE番号がSNS等で出回る。本来は10月22日まで非公開の予定だった。PHP公式がセキュリティリリースの事前告知を出す
10-21Cloudflareが初期の攻撃を観測していると発言
10-21 13:40頃修正版がリリースされる(日本時間22:40頃)
10-21 夕方自分のハニーポットのデータはここまで(毎日の定時リブートでログを区切っているため)

悪用の状況は、情報源によって言うことが違う。PHP公式のアドバイザリは悪用に触れていない。悪用できると主張するコードは出回っているが、動くかどうかは確認されていないものも多い。

検知の手がかりについては、PHP公式・Tenable・GMO Flatt Securityのいずれも、ログで見るべき具体的な特徴やIDSのシグネチャを示していない(2026年10月22日時点)。JPCERT/CCの注意喚起も、この時点ではまだ出ていなかった。

パッチの差分から見えること

PHPはオープンソースなので、修正コミットはGitHubでそのまま読める。攻撃者がパッチの差分から弱点を割り出すのも、同じ理由で簡単だ。今回はリリース前にCVE番号と噂が出回っていたので、武器化までの時間はほとんどなかったと考えたほうがいい(推定)。

修正の本体は1つのコミットにまとまっている(506e87fa、作成は10月18日。公開までは非公開で保持されていたと見られる)。テストを含めて400ファイルほどにわたる大きな修正だが、リクエストの解析部分(main/rfc1867.c、main/php_variables.c、main/main.c)を読むと、狙われた箇所がよく分かる。公式アドバイザリにも、ほぼ同じ内容が攻撃経路として書かれている。

修正の内容裏返すと、攻撃に使われたと考えられるもの
multipartの読み込みで、読んだ量がpost_max_sizeを超えたら止めるContent-Lengthを宣言しない(チャンク転送などの)リクエストで、上限を超えてボディを流し込む
各パートのヘッダはContent-DispositionとContent-Typeだけを残し、重複は無視する1つのパートに大量のヘッダを付けて、通信量の割にPHP内部のメモリを膨らませる
a[][][]...のような入れ子のフィールド名で、メモリ不足になったら即座に打ち切る深い入れ子の名前で配列を大量に作らせる
メモリ不足時にlongjmpで処理を投げ出すのをやめ、安全な地点で止めるリクエスト解析の途中でメモリ不足を起こさせ、中途半端な状態を残す
リクエスト終了時に、次のリクエストの初期化前に読まれうる内部状態をリセットする同じワーカーで処理される次のリクエストに、解放済みの状態を参照させる(CVE-2026-109619)

ここから、守る側が見られそうなものも出てくる。ただし、どれも公式の検知指標ではなく、差分から読み取った推定だ。

  • multipartのPOSTで、Content-Lengthがない、もしくは実際のボディが宣言より大きいもの
  • 1つのパートに異常な数のヘッダが付いたもの
  • フィールド名の[]の入れ子が異常に深いもの
  • Webサーバのエラーログで、ワーカーのクラッシュ(Segmentation fault)やメモリ上限エラーが、同じ送信元から短時間に続くもの
  • 修正版を当てたサーバでは、新しく追加された警告「Actual POST length does not match Content-Length, and exceeds … bytes」が出たら、試された痕跡の可能性がある

確認方法

ローカルのELKに取り込んだSuricataのHTTPログを、件数だけ集計した。ペイロードの中身は読んでいない。

見たのは次の4つ。

  • POSTリクエストの件数と、そのうち.php宛ての件数
  • .php宛てPOSTの送信元IPの数
  • Suricataのアラートのうち、シグネチャ名にPHPを含むもの
  • リクエストボディにPHP_SESSION_UPLOAD_PROGRESS(アップロード進捗機能のパラメータ名)を含むもの

結果

日付(UTC)POST.php宛てPOST送信元IP数
10-1484463
10-15166474
10-16218726
10-17119452
10-18118534
10-19152452
10-202098313
10-21(途中まで)193947
  • PHP関連のSuricataアラート:0件
  • PHP_SESSION_UPLOAD_PROGRESSを含むリクエスト:0件
  • 10月21日の.php宛てPOSTは少し多いが、前日までの揺れの範囲に収まっている
  • 送信側から1MB以上送ってきた通信(Suricataのflow記録)は1日3〜11件。宛先はほぼすべて445番(SMB)で、HTTPのポート宛ては0件だった

判定:このデータの範囲では、着弾の痕跡は見つからなかった(確信度:低〜中)

「見つからなかった」の中身

確信度を低めにしているのは、見えていないものが多いからだ。

  1. データが修正版リリースのわずか1時間あまり後までしかない。T-Potは毎日の定時リブートでログを区切っている。CVEが出回ってからでも約1日分だ
  2. .php宛てで絞るのは不十分。rewriteやフロントコントローラ(/fooをindex.phpに回す仕組み)があれば、.phpで終わらないURLでもPHPに届く。またアプリのコードより前で攻撃が成立するので、アップロードを受け付けないPHPのページでも狙える。POST全体の件数でも見たが、こちらも普段の範囲だった
  3. PHP_SESSION_UPLOAD_PROGRESSで見えるのは、経路の一部だけ。CVE-2026-109618の巨大なPOSTボディやメモリ不足を狙う経路や、109619のほかの経路は、この方法では見えない
  4. Suricataに専用のシグネチャがまだなかった。ルールは毎日のリブート時に更新されているが、10月21日の更新の時点では入っていなかった。そしてアラートは、その時点のルールでしか出ない。あとからシグネチャが入っても、過去の通信をさかのぼってアラートにはしてくれない
  5. リクエストヘッダが記録されていない。今のSuricataの設定では、リクエストのContent-Typeが残らない。multipartかどうかで絞り込めなかった
  6. リクエストボディが残っているPOSTは一部だけ。確認できた範囲で0件、という意味になる
  7. T-Potに本物のPHPは動いていない。HTTPを模倣しているハニーポットはあるが、攻撃が成功するかどうかは観測できない。見えるのは「試されたかどうか」までだ

おまけ:10月13日の山

集計中、10月13日だけ.php宛てPOSTが3,311件と跳ねていた。04:00〜05:00(UTC)の1時間に集中し、送信元IPは約100個あった。

ただ、この時間帯のアラートにPHP関連のものはなかった。CVE番号が出回った10月20日より7日前でもあり、今回の件と結び付ける根拠はない。何らかのスキャナが一斉に来た、という以上のことは言えない。

次に見ること

  • 10月22日以降のログ(公開後の丸1日以上)
  • Suricataのルールに専用シグネチャが入ったあとのアラート件数
  • Suricataでリクエストヘッダを記録する設定にするかどうか(VPS側の変更になるので、別途検討する)

おわりに

今夜やったことを振り返ると、どれも「表示を信じていいのか」という話だった。

鍵マークは、正しい認証局が署名したことしか保証しない。その認証局の鍵が計算できる状態なら、鍵マークは何も保証しない。

systemdの「成功」は、スクリプトが終わったことしか保証しない。中でapt updateが失敗していても、成功と表示される。

ハニーポットの「0件」も同じだ。「見えている範囲にはなかった」という意味でしかない。

ちゃんと動いているかは、表示ではなく結果の側で確かめるしかない。スタンプファイルの日付、信頼ストアの本数、ログが記録している項目。地味だが、そこを見るのが一番早かった。

PHP2Shellの着弾は、次のログで改めて確認する。


参考

T-Pot: Deutsche Telekom Security GmbH / https://github.com/telekom-security/tpotce