タグアーカイブ WP-CLI

WPManageNinjaプラグイン更新で不正コード混入 全亜種を検出するSQLと駆除手順

WPManageNinjaプラグイン更新で不正コード混入 全亜種を検出するSQLと駆除手順

WPManageNinja のプラグインを更新したら不正なコードが紛れ込んだ場合、最も確実な検出方法はデータベースのオプション値を直接検索することだ。apii.observer というドメイン名を探す SQL クエリを実行すれば、影響を受ける全13プラグインの亜種を一網打尽にできる。

なぜ通常のプラグイン更新でバックドアが入り込んだのか

なぜ通常のプラグイン更新でバックドアが入り込んだのか

2026年7月31日、WordPress 用プラグインを多数提供する WPManageNinja 社の旧アップデートサーバーが侵害された。同社は以前に販売プラットフォームを移行しており、本来は停止しているはずの旧サーバーが生き残り、かつプロキシが一部の更新トラフィックをそちらに転送し続けていた。この時間帯に管理画面で「更新」ボタンを押したユーザーは、正規の更新チャネルを通じて悪意あるパッケージを受け取ってしまったのだ。

パスワード突破でも脆弱性攻撃でもなく、ただの更新作業が侵入口になった。このサプライチェーン攻撃の怖さは、更新ボタンを押したこと自体はまったく正常な運用であり、疑いようがない点にある。

影響を受ける13のプラグインを確認する

影響を受ける13のプラグインを確認する

WPManageNinja 社が公開したインシデント対応資料には13のプラグインプロファイルが含まれている。一方、当初の告知では一部しか公表されていなかった。注意すべきなのは以下の全リストだ。

  • azonpress
  • fluent-affiliate-pro
  • fluent-boards-pro
  • fluent-booking-pro
  • fluent-community-pro
  • fluent-player-pro
  • fluent-support-pro
  • fluentcampaign-pro(FluentCRM)
  • fluentform-signature
  • fluentformpro
  • ninja-tables
  • wp-payment-form-pro(Paymattic)
  • wp-social-ninja-pro

これら13種類のいずれかを利用しているサイトは、更新履歴の有無にかかわらず直ちに調査が必要だ。WPManageNinja 社からドメインリストがメールで送られていたとしても、それを鵜呑みにしてはいけない。実際に、リストに載っていないサイトからも感染が確認されている。

侵入されたサイトに見られる具体的な症状と隠蔽の仕組み

侵入されたサイトに見られる具体的な症状と隠蔽の仕組み

バックドアは、正規プラグインのフォルダ内に PHP ファイルを1つ追加し、既存のファイルの末尾に小さなローダーコードを追記する形で設置される。Fluent Forms Pro の例では、以下のようになっていた。

Before(感染状態)
fluentformpro/libs/ に class-license-sync.php(39,743バイト)が存在
fluentformpro.php の22〜24行目に不正な require_once とクラス呼び出しが追記
After(駆除後)
正規のプラグインファイルのみ存在し、不正ファイルは削除済み
fluentformpro.php の行数がクリーンな状態に戻っている
感染状態   駆除後

このデモは Fluent Forms Pro におけるバックドアの有無を視覚化したものだ。

データベースには _wp_update_meta_cache_site_transient_update_meta といったオプション名が書き込まれる。これらの名称は WordPress コアが使う一時データ(Transient)に酷似しており、ひと目見ただけでは異常と気づきにくい。オプションの値には攻撃者のコマンド&コントロールサーバーである apii.observer が含まれ、さらにサイト固有のトークンとログインキーが保存されていた。このログインキーはパスワードなしで WordPress 管理画面にログインできる「万能鍵」であり、ファイルを削除するだけでは不正アクセスのリスクは消えない。

全亜種を一発で検出するデータベースクエリ(WP-CLI)

全亜種を一発で検出するデータベースクエリ(WP-CLI)

63個のオプション名や26個の cron フックを個別に調べる必要はない。すべての亜種に共通する特徴は、C2 サーバーアドレス apii.observer をオプションの値として持っていることだ。したがって、次の1行のクエリで全13プラグインの感染を一括検出できる。

PREFIX=$(wp db prefix)
wp db query "SELECT option_name FROM ${PREFIX}options \
WHERE option_value LIKE '%apii.observer%'" --skip-column-names

cron ジョブまで同時に調べたい場合は以下のように拡張する。

wp db query "SELECT option_name FROM ${PREFIX}options \
WHERE option_value LIKE '%apii.observer%' \
OR option_value LIKE '%wp_update_check_schedule%'" --skip-column-names

wp cron event list --fields=hook,next_run_relative \
| grep -E "wp_update_check_schedule|wp_license_verify_schedule"

このアプローチの本質は「マルウェアが自らのサーバーと通信しなければならない」という不変の事実を突く点にある。シグネチャリストは古くなるが、通信先のドメインはそう簡単には変わらない。覚えておいて損はない手法だ。

sFTP しか使えない場合のファイルチェック方法

sFTP しか使えない場合のファイルチェック方法

レンタルサーバーによっては SSH が提供されず、WP-CLI も使えないことがある。その場合は sFTP 経由でファイルを確認する。Python の paramiko ライブラリを使った検出スクリプトが有効だが、重要なのは「確実に読めたと言える状態だけをクリーンと判定する」ことだ。

STEP 1 sFTP で接続し、プラグインフォルダにアクセスできるか確認する
STEP 2 class-license-sync.php や NinjaTableDataSync.php が存在するか確認
STEP 3 ローダーが追記される fluentformpro.php などに不正マーカーが含まれるかテキスト検索
STEP 4 フォルダが存在したのに読み取れなかった場合は「CLEAN」と判定せず、必ず「ERROR」を返す

この判定フローは、読み取り失敗を「感染していない」と誤認させないための安全策だ。

より確実な方法として、販売元のアカウントからクリーンなプラグイン ZIP をダウンロードし、サーバー上のファイル群とファイルサイズを比較する手段もある。手元のクリーンコピーに存在しないファイルや、サイズが異なるファイルがあれば、それが不正コードの証拠になる。

安全かつ完全な駆除手順(ファイルとデータベースの順序が重要)

安全かつ完全な駆除手順(ファイルとデータベースの順序が重要)

駆除で最も失敗しやすいのが「データベースから先に削除する」手順だ。残ったファイルの cron が再度データベースに不正な行を書き込んでしまう。必ず以下の順序で実施する。

1. 感染したプラグインフォルダを削除し、クリーンな ZIP から再インストールする

wp plugin delete fluentformpro
wp plugin install /path/to/fluentformpro-clean.zip --activate
wp plugin get fluentformpro --field=version

バージョン番号を必ず確認し、6.2.8 や 6.2.9 といったアップデータ経由の古い番号が表示されたら、ZIP からの再インストールが正しく行われていない可能性がある。

2. データベースから不正なオプションと cron を削除する

PREFIX=$(wp db prefix)
# まず内容を確認
wp db query "SELECT option_name FROM ${PREFIX}options \
WHERE option_value LIKE '%apii.observer%'"
# 削除実行
wp db query "DELETE FROM ${PREFIX}options \
WHERE option_value LIKE '%apii.observer%'"
# cron イベントも削除
wp cron event delete wp_update_check_schedule
wp cron event delete wp_license_verify_schedule

3. ソルトを変更し、すべてのセッションを無効化する

wp config shuffle-salts で wp-config.php に定義されたソルト(8つのキー)を新しい値に置き換える。これにより、攻撃者が入手したログインキーを含むすべての既存セッションが強制ログアウトされる。

wp config shuffle-salts

その後、管理者パスワードを手動で変更する。ソルトの更新はパスワードそのものを上書きしないからだ。

4. 駆除後は状態を読み取って必ず検証する

削除コマンドの成功メッセージを信用してはいけない。cron 削除が正常に受け付けられても、実際には実行されずにスケジュールが残っているケースがある。再度クエリを実行し、返却行数がゼロであることを目視確認する。

wp db query "SELECT option_name FROM ${PREFIX}options \
WHERE option_value LIKE '%apii.observer%'" --skip-column-names
# 出力が完全に空であることを確認
wp cron event list --fields=hook,next_run_relative \
| grep -E "wp_update_check_schedule|wp_license_verify_schedule"
# 同じく出力が空であることを確認

最後に wp option get blogname やサイトのトップページ表示で WordPress が正常に起動していることを確かめる。

WP-CLI が使えない環境でのソルト変更と安全策

WP-CLI が使えない環境でのソルト変更と安全策

レンタルサーバーの制限や独自コネクターの仕様で wp config shuffle-salts が使えない場合は、FTP 経由で wp-config.php を直接編集する。このとき、次の点を徹底する。

  • 現在の wp-config.php を必ずローカルにバックアップする。
  • WordPress.org のソルト生成 API から新しい値を取得し、8つの define 文すべてを置換する。
  • 8つすべてを見つけられなかった場合は作業を中断する。部分的な置換はサイトを破壊する。
  • 置換後のファイルサイズが数百バイト以上変化していないか確認する。
  • アップロード後にサーバーからファイルを読み戻し、意図した内容かをバイト単位で比較する。
  • データベースパスワードを含むため、ファイル内容をログやターミナルに絶対に出力しない。

複数サイトをスクリプトで一括処理する場合は、1サイトごとに別プロセスで実行し、数秒の待機を挟む。連続したリクエストはサーバーのアンチボット機能にブロックされる原因になる。HTTP 202 や 429 が返ったら即座に全処理を停止する。

よくある質問

プラグインを更新していないのに感染する可能性はあるか

今回の経路は更新操作に限られる。ただし、過去に更新したタイミングが問題の時間帯と重なっていれば、更新していないつもりでも感染している場合がある。cron による自動更新が有効なら、手動更新していなくても該当する。

wp-config.php のソルトを変更するとどうなるか

そのサイトにログインしているすべてのユーザーが強制的にログアウトされる。パスワードは変わらないため、同じパスワードで再ログインは可能だ。「Remember Me」で保存されたセッションも無効になる。

感染したかどうか管理画面から判断できるか

見た目にはまったく変化がない。管理画面の表示や動作に異常が出ないよう設計されているため、プラグイン一覧や更新画面から気づくことはほぼ不可能だ。

データベースのバックアップから復元しても大丈夫か

バックアップの中に不正オプションが含まれていれば、復元で再感染する。リストア前に必ずバックアップの SQL を確認し、apii.observer を含む行がないか検索しておく必要がある。

WAF やセキュリティプラグインで防げたか

この攻撃は正規の更新チャネルを経由しているため、一般的な WAF やマルウェアスキャナーでは検知できない。実際に複数のセキュリティプラグインが稼働している状態でも、バックドアファイルを無害と判断した事例が報告されている。今回の経験から、セキュリティプラグインの「異常なし」を鵜呑みにしないことが重要だ。

この記事のポイント

  • WPManageNinja の当該13プラグインを利用しているサイトは、更新の有無にかかわらず即座に調査する。
  • 全亜種の検出は apii.observer を含むオプション値を SQL で LIKE 検索するのが最速かつ確実。
  • 駆除は「プラグインフォルダの再インストール」→「データベース削除」→「ソルト変更とパスワード変更」の順序厳守。
  • 削除後は成功メッセージを信じず、再度クエリを実行してゼロ件を目視確認する。
  • ソルト変更だけではパスワードは変わらない。管理者パスワードも忘れずに変更する。
WP-CLIとREST APIとAbilities API、WordPressインターフェースの選び方

WP-CLIとREST APIとAbilities API、WordPressインターフェースの選び方

3つのインターフェースの全体像

3つのインターフェースの全体像

WordPressには外部からデータをやり取りするための主要なインターフェースが3つ存在する。WP-CLI、REST API、Abilities APIだ。それぞれが異なる距離感でWordPressと向き合い、異なる呼び出し元に対応する。これらを競合関係と捉えるのは誤りで、実際には階層構造をなしている。

WP-CLIはサーバー上で動作し、REST APIはHTTPを介して通信する。そしてAbilities APIは、そのさらに上位に位置し、AIエージェントが何をすべきかを判断する層になる。どのレイヤーがどこに位置するのかを理解すれば、タスクに応じた最適な選択はおのずと見えてくる。

  • WP-CLI:サーバー上で直接PHPを実行(またはSSH経由)。一括操作、移行、デプロイ、メンテナンス向き
  • REST API:wp-jsonへのHTTPリクエスト。ブラウザ、モバイルアプリ、外部サービスからコンテンツの読み書きに使用
  • Abilities API:RESTとMCPで公開される名前付きPHPケイパビリティ。AIエージェントが安全に操作を行えるように設計
Abilities API(AIエージェント層)
AIエージェントが安全にWordPressを操作するためのケイパビリティ定義
「何ができるか」を記述し、許可された操作のみを公開する
REST API(HTTP層)
ブラウザ、アプリ、外部サービスからのHTTP通信
「どんなデータがあるか」を公開し、認証付きで読み書き可能に
WP-CLI(コマンドライン層)
サーバー上での直接PHP実行、SSH経由の一括操作
HTTP往復なし、認証トークン不要、最高速での実行が可能
■ 上位層ほど「自律性」が高く「説明的」 ■ 下位層ほど「高速」で「直接操作」

3つのインターフェースは、下位ほど呼び出し元がサイトに近く、信頼度も高い。上位になるほど、呼び出し元は自律的で遠隔地に位置する。この構造を理解すれば、「どれを使うべきか」の判断はシンプルになる。

WP-CLI:サーバー上のコマンドライン

WP-CLI:サーバー上のコマンドライン

WP-CLIはWordPressのインストール環境に対して直接PHPを実行する。コマンド例としては wp post createwp plugin updatewp search-replacewp db export などがある。実行にはサーバーへのシェルアクセス(SSH)が前提だが、その分HTTPの往復も認証トークンの管理も不要になる。

WP-CLIが最も威力を発揮するのは、サイトを完全に制御できる状況だ。1000件の投稿を移行する、データベース全体でドメインを置換する、定期メンテナンスをスクリプト化する、あるいはデプロイの自動化など、スピードが求められる一括操作では他の追随を許さない。

WP-CLIが適さないケース
ブラウザやモバイルアプリからのアクセス、リモートサービスとの連携には使えない
WP-CLIが最も輝く場面
一括移行、データベース操作、定期メンテナンスの自動化、デプロイスクリプト

WP-CLIはシェルアクセスが前提のため、ブラウザやモバイルアプリ、外部サービスがサイトと通信する手段にはなりえない。しかし開発者がサイト全体を制御できる状況では、WP-CLIは圧倒的な速度と柔軟性を提供する。ターミナルからすべてを操作するワークフローが浸透している開発現場も多く、管理画面(wp-admin)をほとんど開かない運用も可能だ。

REST API:HTTP越しのWordPress

REST API:HTTP越しのWordPress

REST APIはWordPressサイトを、あらゆるHTTPクライアントが読み書きできる状態に変換する。エンドポイントは /wp-json/wp/v2/ 配下に存在し、認証にはアプリケーションパスワード、Cookieとnonce、あるいはOAuthを用いる。ブラウザ、モバイルアプリ、外部サービスがインターネット越しにコンテンツを取得・更新できるようになる。

ヘッドレスCMS構成のWordPressは、このREST APIを基盤に動作する。AstroやNext.jsで構築したフロントエンドがREST経由でコンテンツを取得し、モバイルアプリが投稿を行い、サードパーティ連携がデータを同期する。呼び出し元がサーバー外にいる場合、REST APIがほぼ唯一の通信経路となる。

REST APIの構造と制約
公開するもの
投稿、ユーザー、タクソノミー、設定といった「リソース」
公開しないもの
「誰が何をしたいのか」という意図や操作の文脈
人間の開発者ならドキュメントを読んで適切なリクエストを組み立てられるが、AIエージェントにはハードルが高い

REST APIには重要な限界がある。公開するのは「データの構造」であり、「そのデータで何をしたいのか」という操作の意図までは記述しない。どのエンドポイントが存在し、どうリクエストを組み立てるべきかは、呼び出し元が自ら理解する必要がある。人間の開発者であれば問題ないが、AIエージェントにとっては推論すべき情報が多すぎるという課題が残る。

Abilities API:AIエージェントのためのケイパビリティ層

Abilities API:AIエージェントのためのケイパビリティ層

Abilities APIはWordPress 6.9でコアに導入された最新のインターフェースだ(それ以前のバージョン向けにはプラグインも提供されている)。REST APIが残した「AIエージェントが何を許可されているのかをどう知るか」という課題を解決するために設計された。

Abilities APIでは、生のリソースを公開する代わりに、プラグインやテーマが「名前付きケイパビリティ(能力)」を登録する。各アビリティは、一意のID、人間が読めるラベル、説明文、入力・出力のスキーマ、権限チェックのコールバック、そして実行コールバックを備えた独立した操作単位となる。

add_action( 'wp_abilities_api_init', function () {
    wp_register_ability( 'my-plugin/publish-draft', [
        'label'             => '下書きを公開',
        'description'       => 'IDを指定して既存の下書き投稿を公開する',
        'category'          => 'my-plugin',
        'input_schema'      => [ /* 期待する入力のJSON Schema */ ],
        'output_schema'     => [ /* 結果のJSON Schema */ ],
        'permission_callback' => 'my_plugin_can_publish',
        'execute_callback'  => 'my_plugin_publish_draft',
        'meta'              => [ 'show_in_rest' => true ],
    ] );
} );
REST API
データの構造を公開する
「どんなリソースがあるか」に答える
Abilities API
操作の意図と許可を公開する
「何ができるか」「誰が許可されているか」に答える
アビリティは「これが実行可能な操作であり、必要な入力と許可条件はこれだ」という契約をAIエージェントに提示する

meta.show_in_rest をtrueに設定すると、そのアビリティは wp-json/wp-abilities/v1/abilities で公開され、クライアントが検出できるようになる。JavaScript側では @wordpress/abilities パッケージを介して利用する。

Abilities APIの最大の価値は、エージェントが安全に行動するために必要な「契約」を提供することだ。操作の定義、必要な入力形式、実行許可の条件が明示されるため、AIエージェントがサイトを壊すリスクを最小限に抑えられる。複数のエージェントが共通の語彙で協調動作するマルチエージェント構成でも、Abilities APIが基盤になりつつある。

3つのインターフェースの積み重なり方

3つのインターフェースの積み重なり方

3つのインターフェースは互いに積み重なる関係にある。Abilities APIは多くの場合REST APIの上に構築され、REST APIはWP-CLIが直接駆動するPHPの上で動作する。すべての基盤にあるのは、同じWordPressコア、同じデータベース、同じ関数群だ。

したがって問うべきは「どれが最善か」ではない。「呼び出し元がサイトからどれだけ離れているか」「操作の意図をどこまで明示する必要があるか」という視点で選択することが本質になる。呼び出し元が近く信頼できるほど下位層を、自律的で遠隔にあるほど上位層を使う。

距離:最短 サーバー上(SSH) WP-CLI 一括操作・最高速
距離:中程度 HTTP越し(リモート) REST API データの読み書き
距離:最長 AIエージェント(自律的) Abilities API 安全な操作定義

上位層になるほど「記述性」と「安全性」が重視され、下位層ほど「速度」と「直接制御」に優れる。これらは設計上、相補的な関係にあり、実際のプロジェクトではすべてを併用するのが理想的な構成だ。

各インターフェースの使い分け方

各インターフェースの使い分け方

日常的なタスクにおける選択指針を整理する。

  • 自分が制御するサイトに対して、一括かつ高速に操作したい → WP-CLI。移行、デプロイ、定期ジョブ、データベース操作が該当する
  • ブラウザ、アプリ、外部サービスがコンテンツを読み書きする必要がある → REST API。ヘッドレスフロントエンド、モバイルアプリ、外部連携が該当する
  • AIエージェントにサイトを壊さず操作させたい → Abilities API。許可したい操作をスキーマと権限付きで登録し、エージェントに発見させる
STEP 1 呼び出し元の「距離」を確認する(サーバー内か、リモートか、自律エージェントか)
STEP 2 操作に必要な「明示性」を判断する(データ構造だけで足りるか、操作意図の記述が必要か)
STEP 3 最適なレイヤーを選ぶ(多くの場合、複数レイヤーの併用が正解)

実際のプロジェクトでは、この3つを排他的に使うことはまれだ。むしろそれぞれの得意領域を活かして組み合わせるのが、効率的なWordPress運用の鍵になる。

3つを組み合わせた実践的な構成

3つを組み合わせた実践的な構成

WP Mayorの記事では、実際に3つのインターフェースを併用している構成例が紹介されている。まず、公開運用と日常的な運用作業はSSH経由のWP-CLIで実行される。新規投稿、メディアのインポート、プラグイン更新、キャッシュクリアといった操作をターミナルから完結させ、管理画面(wp-admin)をほとんど開かない運用が行われている。

フロントエンドはヘッドレス構成で、REST API越しにコンテンツを取得する。Astroで構築されたサイトが wp-json 経由でWordPressからデータを取得し、高速な静的ページとして配信する。訪問者はWordPressテーマに触れることなく、WordPressはバックエンドのエンジンとして機能し、REST APIがそのパイプ役を担う。

エージェント向けの機能はAbilities APIを通じて提供される。AIエージェントに限定的なタスクを任せたい場合、関連プラグインがその操作をアビリティとして登録する。権限チェックとスキーマを伴うため、シェルアクセスを丸ごと渡したり、大量の生エンドポイントをエージェントに解析させたりする必要がなくなる。

WP-CLI(運用層)
投稿・メディア・プラグイン管理をターミナルから一括実行。シェルアクセス可能なAIアシスタントも直接駆動できる
REST API(配信層)
ヘッドレスフロントエンドがコンテンツを取得。Astroが静的ページとして配信し、訪問者はWordPressテーマに触れない
Abilities API(エージェント層)
AIエージェント向けにスコープ付き操作を登録。権限チェックとスキーマにより安全な自動化を実現
各レイヤーがそれぞれの得意領域を担当し、互いに補完し合う構成

WP-CLIは速度と一括処理能力で、REST APIは外部連携の柔軟性で、Abilities APIはAIエージェントの安全性で優位性を持つ。1つのインターフェースに別の役割を強制しようとするところから問題は始まる。3つのレイヤーを適材適所で使い分けることが、WordPress自動化の効率を最大化する道筋だ。

この記事のポイント

  • WP-CLI、REST API、Abilities APIは競合ではなく、呼び出し元の距離に応じた階層構造をなす
  • WP-CLIはサーバー上の直接操作に最適で、一括処理と速度が求められる場面で選ぶ
  • REST APIはHTTP越しのデータ読み書きを担い、ヘッドレス構成やモバイルアプリ連携の基盤となる
  • Abilities APIはAIエージェントに操作の安全な契約を提供し、マルチエージェント構成でも威力を発揮する
  • 実際のプロジェクトでは3つを組み合わせ、各レイヤーの得意領域を活かすのが理想的な運用だ