2026年7月17日に公開された WordPress Core の認証不要RCE(wp2shell)は、認証不要・プラグイン不要・Core本体という3条件が揃った稀な脆弱性です。ただし本稿の主題はパッチではありません。RCEが通った瞬間、攻撃者はコンテナの中にいます。ホストへ脱出しなくても、環境変数の読み取り、内部ネットワークへの横展開、クラウドメタデータからのIAM権限奪取、KubernetesのServiceAccount悪用は成立します。docker-compose.yml と Kubernetes マニフェストの確認箇所、bind mount の :ro、IMDSv2到達性とIAM最小権限の二段階、RBACの過剰権限まで、侵入後の被害範囲を設計で決めるための確認手順を整理します。
2026年7月17日、WordPress 7.0.2 がリリースされました。Critical 1件、High 1件を修正するセキュリティリリースで、深刻度を理由に WordPress.org 側で強制自動更新が有効化されています。同時に 6.9.5、6.8.6 も公開されました。
Critical の正体は、Searchlight Cyber の Adam Kues 氏が報告した wp2shell — WordPress Core 本体の認証不要RCE(pre-authentication RCE)です。
この脆弱性の異常さは、3つの条件が同時に揃っている点にあります。
| 条件 | 意味 |
|---|---|
| 認証不要(pre-auth / unauthenticated) | ログイン不要。URLを知っていればネット上の誰でも撃てる |
| RCE | サーバー上で任意コード実行。実質サーバーの乗っ取り |
| Core本体・プラグイン不要 | 該当バージョンなら全サイトが例外なく対象。切り分け不要 |
経路は REST API のバッチエンドポイント /wp-json/batch/v1(?rest_route=/batch/v1 からも到達可)。技術詳細と PoC は深刻度を考慮して非公開ですが、修正パッチの差分を解析すれば攻撃コードは作れます。攻撃コードが公開される前が、最も安全に更新できる時間です。
対処: 6.9系は 6.9.5 へ、7.0系は 7.0.2 へ。6.8系にも 6.8.6 が出ています。利用中のブランチの最新セキュリティリリースへ更新してください。強制自動更新が効いているとはいえ、自動更新を無効化している企業サイトは手動対応が必須です。
更新の話はここまでです。本稿で書きたいのは、その先です。
RCEが通った瞬間、攻撃者はあなたのコンテナの中でシェルを持っています。パッチは入口を塞ぐ対策であって、すでに入られた後の横展開を止める対策ではありません。層が違うので、片方だけでは止まりません。
Core本体の脆弱性は自分では防げません。しかし侵入後に何が奪われるかは、自分の設計で決まります。ここが本稿の主張です。
攻撃者が辿るルートは一本道ではありません。ホストへ脱出しなくても横展開は成立します。
RCE
└─ コンテナ内でコード実行
├─ 環境変数・設定ファイル・秘密情報を読む
├─ 内部ネットワークへ横展開する
├─ クラウドやKubernetesの権限を奪う
└─ 危険なマウントや特権設定からホストへ到達する
確認すべきは3つです。
まず設定ファイルを grep します。
grep -nE 'docker\.sock|privileged|cap_add|network_mode|pid:|user:|volumes:' docker-compose.yml
見るべき箇所:
| 危険な記述 | なぜ危ないか | あるべき姿 |
|---|---|---|
/var/run/docker.sock のマウント | コンテナ内からホストのDockerを操作可能。即ホスト奪取 | 公開コンテナには絶対に付けない |
privileged: true | ほぼホストと同等。脱出は容易 | 使わない |
cap_add: [SYS_ADMIN, SYS_PTRACE] | 特権相当のcapability | cap_drop: [ALL] して必要分だけ |
network_mode: host | ネットワーク分離が消え、内部サービスに到達 | 通常のbridge |
pid: host | ホストプロセスが見える。認証情報の窃取 | 使わない |
user: の記述なし | デフォルトroot実行 | user: "1000:1000" |
environment: に秘密の直書き | シェル1本で env すれば全部読める | シークレット管理へ分離 |
- /:/host、- /etc:/etc | ホストFSの過剰マウント | 必要なパスだけ、:ro を付ける |
安全側の型:
services:
app:
user: "1000:1000"
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
:ro — 現場で一番ありがちな穴ここは実運用で必ず引っかかる部分なので、独立して書きます。
「毎回ビルドしたくないから、ソースを bind mount して git pull → restart で回す」。よくある構成で、それ自体は合理的です。bind mount が悪いのではありません。問題は RW であることと、マウント範囲が広いことです。
コードを読み込むだけなら書き込み権限は要りません。
volumes:
- ./src:/app/src:ro
:ro を足すだけで、RCEが通ってもホスト側のコードを書き換えられなくなります。デプロイ手順は1ミリも変わらず、攻撃者の永続化ルートだけが消えます。
より危険なのは - .:/app のようにディレクトリを丸ごとマウントしている場合です。同じ階層の .env と .git が一緒にコンテナへ入ります。
.env — DB認証情報、APIパスワード、Webhook URL がファイルとして読める.git — コミット履歴、削除済みファイル、リモートURL、そして .git/hooks/。構成によっては認証情報の痕跡が残っている場合もあります.git/hooks/post-merge に仕込まれると、運用者が次にホスト上で git pull した瞬間に任意コードが走ります。RWマウントがコンテナからホストへ影響を残す、現実的な経路の一つです。
対策はマウント範囲を絞ること。bind mount は .dockerignore を見ないので、compose 側で指定する必要があります。
volumes:
- ./src:/app/src:ro
- ./config:/app/config:ro
アプリがマウント先に書き込む場合(ログ、__pycache__、build成果物)だけ、別ボリュームに逃がします。
compose ファイルが現状と一致している保証はありません。稼働中コンテナの実態を確認します。
docker inspect $(docker ps -q) --format '{{.Name}} Priv={{.HostConfig.Privileged}} User={{.Config.User}} PID={{.HostConfig.PidMode}} Net={{.HostConfig.NetworkMode}}'
マウントの実態(RW=true と .git/.env に注目):
docker inspect $(docker ps -q) --format '{{.Name}}{{range .Mounts}} {{.Source}}->{{.Destination}} RW={{.RW}}{{end}}'
capability の実効値:
docker inspect $(docker ps -q) --format '{{.Name}} Add={{.HostConfig.CapAdd}} Drop={{.HostConfig.CapDrop}}'
ここは二段階で考えます。
コンテナからメタデータへ到達できる
+
インスタンスロールの権限が強い
↓
AWSアカウント内へ横展開
確認事項は3つです。
| 確認項目 | 意味 |
|---|---|
| IMDSv1が無効か | 単純GETで認証情報が取れる状態でないか |
| コンテナからIMDSv2へ到達できるか | トークン取得経由でも届いてしまわないか |
| 取得できるIAMロールが最小権限か | 奪われた場合に何ができるか |
まず単純GET(IMDSv1の生存確認):
docker exec <container> curl -s -m 2 http://169.254.169.254/latest/meta-data/iam/security-credentials/
ここで応答がなくても安心はできません。 IMDSv2経由で到達できるかを確認します。
docker exec <container> sh -c '
TOKEN=$(curl -sS -m 2 -X PUT \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600" \
http://169.254.169.254/latest/api/token) &&
curl -sS -m 2 \
-H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/
'
ロール名が返った場合、そこで終わりにせず、そのロールで何ができるかまで確認します。
aws iam simulate-principal-policy \
--policy-source-arn <ROLE_ARN> \
--action-names \
s3:ListAllMyBuckets \
s3:GetObject \
secretsmanager:GetSecretValue \
ssm:GetParameter \
ec2:DescribeInstances
EC2側の設定確認:
aws ec2 describe-instances \
--instance-ids i-xxxxxxxx \
--query 'Reservations[].Instances[].MetadataOptions'
まず HttpTokens: required として IMDSv1 を無効化します。
HttpPutResponseHopLimit: 1 にすると、一般的なブリッジ型コンテナから IMDSv2 へ到達しにくくできます。ただし、コンテナ内の正規ワークロードが IMDS を利用する構成では、AWS は hop limit 2 を案内しています。したがって、1 を一律の正解にはできません。
重要なのは、用途に応じて次を組み合わせることです。
実務上、メタデータへの到達制限と、奪われても大事故にならない IAM 権限の両方が必要です。 到達制限だけでは構成変更で崩れる可能性があり、最小権限だけでは不要な認証情報を攻撃者に渡すことになります。
grep -rnE 'privileged|hostPath|hostNetwork|hostPID|automountServiceAccountToken|runAsUser|allowPrivilegeEscalation' ./manifests/
安全側の型:
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
稼働中クラスタの実態:
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"/"}{.metadata.name}{" priv="}{.spec.containers[*].securityContext.privileged}{" hostNet="}{.spec.hostNetwork}{" sa="}{.spec.automountServiceAccountToken}{"\n"}{end}'
コンテナ乗っ取り後の防御は、4層で整理すると明確です。
| 防御層 | 止めるもの |
|---|---|
| Security Context | 特権昇格、root実行、危険なcapability |
| Volume制限 | hostPath、ホストFS、秘密情報へのアクセス |
| RBAC | Secrets、Pod作成、exec、クラスタ操作 |
| NetworkPolicy | DB、Redis、メタデータ、他namespaceへの横展開 |
そして RBACの過剰権限。これがKubernetes版の docker.sock です。
kubectl auth can-i --list --as=system:serviceaccount:<ns>:<sa>
危険なのは secrets の get/list だけではありません。
pods/createpods/execdeployments/patchdaemonsets/createserviceaccounts/tokenroles / rolebindings の作成・変更secretsを読めなくても、Podの作成、既存Podへのexec、RoleBindingの変更が許可されていれば、別の経路で権限を奪われます。 攻撃者はSecretsを直接読めなくても、強いServiceAccountを指定したPodを作れるなら権限を奪えます。RBACは「Secretsが読めるか」だけでなく、**「より強いPodや権限を作れるか」**まで確認します。
NetworkPolicy はデフォルト deny から始め、DB や Redis、他 namespace への到達先を明示的に絞ります。クラウドメタデータへの通信は、利用する CNI がリンクローカルアドレスを制御できるか確認し、必要に応じて CNI 固有のポリシー、ノード側のファイアウォール、クラウド側の Pod 単位 ID を組み合わせます。
| 観点 | Docker | Kubernetes |
|---|---|---|
| ホスト奪取ルート | docker.sock マウント | 過剰なRBAC(secrets、pods/create、pods/exec) |
| 特権 | privileged、cap_add | privileged、allowPrivilegeEscalation |
| ホストFS | bind mount の RW・広範囲 | hostPath |
| 分離の破壊 | network_mode: host、pid: host | hostNetwork、hostPID |
| 認証情報 | env直書き、.env マウント | SAトークン自動マウント |
| クラウド横展開 | メタデータ到達 + 強いIAMロール | ノードメタデータ到達 + 強いIAMロール。CNI、ノード側FW、Pod単位IDで制御 |
WordPress Coreの脆弱性そのものを、利用者が事前に修正することはできません。
しかし、RCEが成立した後の被害範囲は設計で変えられます。
コンテナ内でコードを実行されても、ホストへ出られない。内部のDBや管理系サービスへ届かない。クラウドメタデータから強いIAM権限を奪えない。KubernetesのServiceAccountからクラスタ全体へ進めない。その状態を先に作っておく必要があります。
:ro を一つ足す。docker.sock を外す。rootで動かさない。不要なcapabilityを落とす。メタデータへの通信を制限する。IAMとRBACを最小権限にする。ServiceAccountトークンの自動マウントを切る。NetworkPolicyで公開Podから内部サービスへの到達先を絞る。
どれも、RCEが公表されてから初めて考える対策ではありません。
パッチは侵入口を塞ぐ対策です。コンテナ、IAM、RBAC、ネットワークの分離は、侵入された後に被害を封じ込める対策です。
脆弱性をゼロにすることはできなくても、一つの公開コンテナの侵害を、ホストやクラウドアカウント、クラスタ全体の侵害にしないことはできます。
侵入を防ぐだけではなく、侵入されても広げさせない。今回確認すべきなのは、そこです。
本稿は特定製品の設定を推奨するものではなく、一般的なコンテナ構成の確認手順として整理したものです。
Aikido Community Japan
公開日: 2026/7/18
最終更新: 2026/7/18
セキュリティの最新情報をお届けします。