- GPT5.6 Sol Ultraが最新安定版のWordPressを解析し、認証前SQLインジェクションから管理者アカウント作成、リモートコード実行(RCE)に至る攻撃チェーンを10時間あまりで完成させた
- 出発点はWordPress 5.6から提供されたBatch APIの配列インデックス不一致で、再帰的なBatchリクエストを組み合わせてGET制限とパラメータ検証を回避した
- 検証されていない
author_exclude文字列でUNIONインジェクションを起こした後、細工した投稿をメモリキャッシュに入れ、oEmbedキャッシュ・changeset・循環参照・フックを連鎖的に悪用した customize_changesetのuser_id: 1で一時的に管理者権限を取得し、parse_requestフックでBatchリクエストを再実行して新しい管理者を作成した後、バックドアプラグインZIPをアップロードしてコード実行に到達した- 月額200ドルのサブスクリプションの週間使用量50%を按分した費用は約25ドルであり、人間の役割は製品と攻撃面の選定、プロンプト調整のような高レベルな研究指揮へ移る可能性が高まっている
発見プロセスと実験条件
- OpenAIがCycle Double Cover予想の解決に使ったと公開したプロンプトをセキュリティ研究用に修正し、GPT5.6 Sol Ultraに与えた
- 最新安定版のWordPressを
main/に複製して.gitディレクトリを削除し、依存コードを調査できるよう空のthird_party/ディレクトリを用意した - プロンプトでは最大4つのエージェントを並列に使い、少なくとも6時間にわたって多様な攻撃経路を維持するよう指示した
- 入力パース、文字セット、ファイルアップロード、エラー処理、組み込みパス、シリアライズ、キャッシュ、競合状態、暗号、型、マスアサインメントなどを探索した
- アプローチの系統を登録し、特定の戦略にエージェントが集中したら探索の薄い領域へ再配置した
- 具体的なバグは敵対的エージェントが再検証し、失敗した経路も新しいメカニズムが見つかれば再び開くようにした
- 変更履歴やパッチ版との差分、インターネット上の手がかりを使わず、ソースコードそのものから新規脆弱性を見つけるよう制限した
- 非現実的な設定や攻撃者が満たせない前提条件を避けるため、目標を「MySQLを使う一般的な本番環境デプロイにおける認証前RCE」と明示した
- 約6時間後、モデルが認証前SQLインジェクションを見つけ、標準的なWordPressリモートサーバーで管理者メールを数分で抽出して再現した
- RCEへの昇格を追加で求めると約4時間後、パスワードクラッキングやオフライン計算なしで、読み取り専用SQLインジェクションから管理者権限へ上がるチェーンを完成させた
- 総作業時間は10時間あまりで、週間使用量の50%を消費した。月額200ドルのサブスクリプション料金を按分した費用は約25ドルだった
- 公開前の週末に運営者がWordPressをアップグレードする時間を提供し、その間にCalifとHacktronがGitHub上の他のPoCより先に全チェーンを独立に再現した
- 稼働中インスタンスはwp2shell.comで脆弱性の有無を確認できる
Batch APIの検証不一致
- WordPress 5.6で導入されたBatch APIは、1つのリクエストで複数の仮想APIリクエストを処理する。エンドポイント自体には認証なしでアクセスできるが、各サブリクエストには認証情報が引き継がれる
- 通常のRESTリクエストは次の順で処理される
has_valid_params()で必須値と妥当性を検査するsanitize_params()で値をサニタイズする- 権限コールバックを実行する
- エンドポイントコールバックを実行する
- Batch APIは性能のため、検証と実行を2つのループに分離している
- 1つ目のループですべてのリクエストを検証・サニタイズする
- 2つ目のループで検証結果を確認し、権限およびエンドポイントコールバックを実行する
- 実装は、ルートマッチング結果の
$matchesと検証結果の$validationで同じインデックス同士が対応すると仮定している - 不正なリクエストが
is_wp_error($single_request)分岐に入ると、$validationには項目を追加するが、continueのため$matchesには追加しない- その後、すべての
$matches項目が1つずつずれる - あるリクエストのパラメータを別のリクエストのルールで検証した後、本来意図されていないエンドポイントハンドラで実行できる
- その後、すべての
- このインデックス不一致を使うと、パラメータをサニタイズしない別エンドポイントの検証結果をBatch対応エンドポイントに適用できる
author__not_inで発生するSQLインジェクション
GET /wp/v2/postsは、特定の作成者を結果から除外するため内部クエリ変数author__not_inを使う- 値が配列なら各要素に
absintを適用して整数化するが、スカラー値ならそのままimplodeしてSQLのNOT IN句に挿入する - 通常の呼び出しでは公開パラメータ
author_excludeが整数配列でなければならないため、問題は表面化しない - Batchのインデックス不一致を使うと、文字列
author_excludeをそれを認識しないDELETE /wp/v2/posts/1ルールで検証した後、GET /wp/v2/postsへ渡せる - Batch APIがGETサブリクエストを許可しない制約も再帰的なBatch呼び出しで回避する
- 外側のBatchでインデックス不一致を起こし、内側リクエストの
method検証をスキップする - 内側のBatchでも再びインデックス不一致を起こし、
author_exclude検証を回避する
- 外側のBatchでインデックス不一致を起こし、内側リクエストの
0) OR 1=1 --のような値を入れるとすべての投稿行が返り、インジェクションの有無を確認できる- その後、UNIONベースのインジェクションで
wp_postsと同じ形の行を作り、任意のデータベース値を流出させられる - パスワード、リセットトークン、APIキーなどはデータベース内でハッシュ化されるため、管理者パスワードが弱くない限り、データ流出だけでアカウント乗っ取りには至らない
リクエスト内の投稿キャッシュ操作
- WordPressは、1つのリクエスト内で繰り返し参照される
WP_Postオブジェクトをメモリキャッシュに保存し、データベース往復を減らす - UNIONインジェクションで偽の投稿行を返すと、投稿ID、タイプ、状態、親子関係、本文など多数のフィールドを攻撃者が指定した状態でキャッシュに入れられる
- APIレスポンスでも投稿本文を後処理するため、細工した本文で追加のコードパスを実行できる
- このキャッシュはリクエスト終了時に消え、偽の投稿もデータベースには存在しないため、キャッシュ操作だけではリクエスト間の永続性を確保できない
oEmbedキャッシュを使ったデータベース行の作成
- WordPressのembeds機能は、投稿本文の
[embed]...[/embed]構文で対応するリモートコンテンツを埋め込む - 毎回HTTPリクエストを送らないよう、結果を
wp_postsのoembed_cacheタイプ投稿としてデータベースに保存する - 相対パスでローカルWordPress投稿を埋め込むとHTTPリクエストを省略し、参照した投稿IDが実在するかどうかは確認しない
/?p=10のような存在しないローカル投稿を埋め込んでも、そのデータ用のoembed_cache行が作られることがある- 作成された行を再びSQLインジェクションで読み出しつつ、メモリ上の投稿タイプを
postなどに改ざんすると、データベースとキャッシュの表現が食い違う - WordPressは両者を調整する際に
wp_update_post()を呼び出し、明示的に指定したIDとpost_content以外のフィールドでは、攻撃者が作ったメモリ上の値を優先する - その結果、
oembed_cache行を通常投稿に変えられるが、この呼び出しではpost_contentが埋め込み結果で上書きされるため、攻撃者は本文まで制御できない
customize_changesetによる一時的な管理者権限
- テーマのカスタマイズ作業の下書きは、
wp_postsで**customize_changeset**タイプの特殊な投稿として保存される post_contentには、設定キーごとの変更値と型、その変更を行うユーザーIDを含むJSONが入る- changesetを適用するとき、WordPressは各項目の
user_idを読み、wp_set_current_user()で現在ユーザーを一時的に切り替える - 攻撃者が
user_id: 1のchangesetを適用すると、匿名リクエスト中でも一時的に管理者の身元を使える - 前述のoEmbed調整呼び出しでは
post_contentが上書きされるため、悪意あるchangeset JSONを保持する別のwp_update_post()呼び出し経路が必要になる
投稿親の循環を使った本文保持
- WordPressの投稿は1つの親を持てるが、自分自身や子孫投稿を親にする循環構造は許可しない
wp_insert_post_parentフィルタは親階層をたどって循環を検査し、循環を見つけるとその投稿のpost_parentを0に変えるwp_update_post()を呼び出す- この2回目の呼び出しでは
IDとpost_parentだけが指定され、post_contentは上書きされない - SQLインジェクションでメモリ上の投稿を、自分自身を親に持つ
customize_changesetとして偽装すると、循環修復の過程で攻撃者指定の悪意あるchangeset JSONがデータベースに書き込まれる - 日付が過去で状態が
futureのchangesetを作ると、WordPressがそれを適用し、user_id: 1に従って管理者権限で指定された設定変更を実行する - 管理者権限はchangeset処理中だけ維持され、完了後は再びゲスト権限に戻る
動的フックによるリクエスト全体の再実行
- WordPressのフックはアクションとフィルタに分かれ、プラグインがログイン・投稿・スクリプト登録などライフサイクルの各所に介入できるようにする
- 投稿状態が変わると、WordPressは
"{$new_status}_{$post->post_type}"形式の動的アクションを実行する- 通常投稿なら
publish_postのような名前になる - メモリ上の偽投稿では状態とタイプを任意に決められるため、アンダースコアを1つ以上含む任意のアクション名を構成できる
- 通常投稿なら
- フック引数は攻撃者が決めた投稿IDと
WP_Postオブジェクトに限られるため、任意アクションを直接有用に呼び出すのは難しい - 攻撃チェーンでは状態を
parse、タイプをrequestに改ざんし、**parse_request**フックを呼び出す parse_requestはリクエストライフサイクルの初期に実行されるフックであるため、これを再度呼ぶと元のBatch APIリクエスト全体が最初から再処理される- 再処理はchangesetが設定した一時的な管理者の身元が維持されている間に進むため、最初の実行で権限不足により失敗した管理者専用リクエストが2回目の実行では成功する
2つのリクエストで完成するRCEチェーン
- 最終エクスプロイトは2つのHTTPリクエストを使い、実際の投稿と衝突しないほど大きいIDを偽投稿に割り当てる
-
1つ目のリクエスト: 永続行の準備
- SQLインジェクションで3つのローカル埋め込みを持つ偽投稿を返し、
O、C、Dに対応する3つのoembed_cache行を作成する - 3つの埋め込みは同じ投稿
Sを指すが、異なるクエリ文字列を使って別々のoEmbedキャッシュハッシュを作る
- SQLインジェクションで3つのローカル埋め込みを持つ偽投稿を返し、
-
2つ目のリクエスト: 6投稿の組み立て
- メモリキャッシュ内に次の6つの偽投稿を構成する
O: 状態/タイプがpublish/oembed_cacheで親がCの古いキャッシュC: 状態/タイプがfuture/customize_changesetで自分自身を親に持ち、悪意あるchangeset JSONを含むP: 親がDのdraft/pageD: 状態/タイプがparse/requestで自分自身を親に持つS: 埋め込みデータを提供するpublish/postT: 外側の埋め込みを含むpublish/postTの埋め込みがOを参照し、古い更新時刻のためSのキャッシュを更新させるO更新中に親Cの循環を検知すると、Cの親を0に変えつつ、メモリ上のcustomize_changesetと悪意あるJSONをデータベースへ書き込む- 過去日付の
futurechangesetが適用され、user_id: 1の管理者の身元でPを公開する Pの更新では親Dの循環を見つけてDを書き込み、改ざんされた状態とタイプでparse_requestアクションを呼び出す- Batchリクエストには最初から新規管理者作成リクエストが含まれている
- 1回目の処理ではゲスト権限のため失敗する
parse_requestで再実行されたときは一時的な管理者権限が残っているため成功する- 新しい管理者アカウントでログインした後、バックドアプラグインZIPをアップロードすれば、最終的にリモートコード実行に到達する
AIセキュリティ研究で変わる役割
- 全チェーンの中で特に創造的な段階は、再帰的なBatch呼び出しでGET制限を回避した点、キャッシュとchangesetを組み合わせて管理者権限を得た点、偽投稿で
parse_requestを呼び出してリクエストを再実行した点である - 一般的な優位性を断定はできないが、AIなしでセキュリティ研究者が同じチェーンを10時間以内に発見し完成させるのは不可能だっただろうという評価である
- 元のBatchバグを事前に与えられていたとしても、その時間内にRCEまで構成できたかは確信しにくいとしている
- 離れた複数のコードガジェットを見つけ、1つのチェーンに結びつける能力で、GPT5.6 Sol Ultraは以前のGPT5.5より大きく進歩したと判断している
- モデルが技術的なエクスプロイト開発をより多く担うほど、人間は調査対象の製品と攻撃面、投入時間、研究方向を決め、モデルが外れたときに修正する役割へ集中するようになる
- こうしたメタ研究能力はまだAIがうまく扱えず、モデルの技術力が高まるほど、より重要になると見込まれている
1件のコメント
Hacker News の意見
このエクスプロイトに50万ドルが支払われた、または支払われる予定だという根拠はない。記事ではプロンプトを聖書のように慎重に修正していると言っているので、むしろそのプロンプトを50万ドルで売ればよさそうだ。
筆者は自動スキャンAI製品を提供する https://www.assetnote.io/ で働いている
こうした仲介業者は通常、大金を一括で支払うのではなく、国家主体にアクセス権を販売したうえで、バグがパッチされていない間に分割して支払う。転売や早期の使い潰しを防ぐための仕組みなので、似たような脆弱性で実際に全額を受け取ったか確認してくれる人はほとんどいなさそうだ
最も近い例は、Steamゲームにマルウェアを仕込んでアカウントを盗み、捕まったフロリダの少年たちだ。いずれ捕まっていただろうが、ソーシャルメディアで見せびらかしていなければ捜査にははるかに時間がかかったはずだ
https://github.com/WordPress/WordPress/commit/3a640e1c5e39aa... を見ると、2026年にも文字列連結によるSQLインジェクションが登場している
SQLを直接実行する代わりに
dbDeltaを使えと言いながら、フィールドごとに行を分け、PRIMARY KEYの間にはスペースを2つ入れ、INDEXではなくKEYを使うなど、極端に細かいフォーマット規則を要求している。フィールド名には引用符やバッククォートを使ってはならず、データ型は小文字、SQLキーワードは大文字で、長さパラメータもすべて指定しなければならないsprintfでSQLクエリを作っているのか疑問だPOSTとGETリクエストの中に/wp/v2/widgets?author_exclude=1%29+AND+1%3D0+UNION+ALL+SELECT...という形のペイロードが含まれていたPrincipal Software Engineer @ Bluehost,WordPress Core Committerと書かれているが、こういうコードだと本当に「principal」という表現が妙に聞こえるFOMO式の文章にはうんざりだ。25ドルだけで発見したのではなく、どこを見てどう探索すべきかを知る業界の専門知識と、何年も蓄積した資料があったのだ。
ギャンブルのような物語と、皆が機会を逃しているという幻想を広めるのはやめるべきだ
驚くべき部分は、既知の脆弱性に高値が付いていることだが、事実ではない可能性もある。WordPressはしばしばブログ機能付きのリモート root シェルと呼ばれるほどだ
一般ユーザーに GitHub リポジトリへコミットして Hugo でビルドしろと言うより、ドラッグ&ドロップを案内するほうが簡単なのは理解している。だがセキュリティの観点では、コアや何千ものプラグインのどれかに脆弱性が生じ、サービスとしてのリモートコード実行が開くだけを待つ構造だ
興味深い記事であり、LLMベースのエクスプロイト発見と公開は実際に懸念すべき問題だ。Linux のローカル権限昇格脆弱性からコンテナ脱出コードをモデルに比較的短時間で作らせたこともある。
ただし、GPT-5.6 が安全策でプロンプトをブロックしなかった点は驚きだ。GPT-5.5以上は Opus 4.7+/Fable のように攻撃的セキュリティ作業を嫌がる傾向があるため、筆者が OpenAI から安全策を緩和するサイバーセキュリティ承認を受けていた可能性がありそうだ
2020年以前の非AIの**静的アプリケーションセキュリティテスト(SAST)**ツールでも、この種のSQLインジェクションは多く検出できていたし、少なくともコードレビューでは見つけるべきだった。WordPressがコードレビューやSASTを使っていないのか疑問
私のWebサイトの一つがこの脆弱性でハッキングされたが、幸いユーザーのいないサイトだった
攻撃者はデータベースに管理者アカウントを2つ作成し、
wp-content/plugins/wp-coreにwp-core-[任意の12文字].phpというリモートコマンド実行Webシェルを設置した。mu-pluginsにはGET ?sergeiで管理者を作るfirewall.phpバックドアを置き、cache-seo-helper.phpバックドアも追加し、fixer.phpでWordPressのバージョン番号をパッチ適用済みバージョンのように書き換えていた。結局、WordPressの利用をやめることにした記事の最後で投稿を奇妙な名前で呼び始めてから理解しづらくなった。片方のIDを
O、もう片方を0にして、EMBED_01やABCDEFの代わりに単一文字とランダムに見えるOCPDSTを使った理由が気になるOはpublish/oembed_cache、Cはfuture/customize_changeset、Pはdraft/page、Dはparse/request、Sは埋め込みデータを提供するpublish/post、Tは外部埋め込みを含むpublish/postを意味する50万ドルを払う人たちには、GPT-5.6を直接活用する能力がないと仮定しているということなのか疑問
私もLLMでセキュリティ脆弱性を探しているが、結果をそのまま提出して終わりにはできないし、そうしようとする人は多い
GPT-5.6 Solが超人的かは単純なイエス・ノーの質問ではない。コンピューターは数十年前からチェスで人間を上回っていたし、この記事を見ると今やコード理解でも人間を超えたように見える