What's the worst thing a Chrome extension could do to you? Well, it could steal your passwords and cookies, or it could continuously close your tabs. Obviously, these are bad and annoying, but there are limitations to the power of extensions.
Can they run exe files? Not really. They can download files at any time, but opening those downloads requires a special permission and a user gesture.
Can they change your settings? Generally, no. The general design idea for Chrome extensions is that they shouldn't be able to make permanent changes that persist after they're uninstalled.
Alright then, what about editing or reading local files? The rules are a little convoluted when it comes to this:
- Extensions shouldn't be able to read local files
- That is, unless the "allow access to file URLs" switch is turned on in the extension's options
- Chrome apps--like the Text app--are sometimes sometimes able to edit local files, but only when those files are explicitly opened by the user
Of course, these limitations have their occasional bypasses. For example, Google awarded $10,000 to a bug report which showed that extensions could read local files by screenshotting them. But there are more dangerous things than file reads.
It's generally agreed upon that a full "sandbox escape" for an extension is when the extension runs an executable file without user interaction. Then the attack moves out of the browser as the executable starts attacking the user's operating system using other bugs.
The way these attacks almost always work--or at least in theory--is
that the extension abuses a bug to run code on a page more
privileged than itself. Specifically, on
chrome://
URLs: privileged WebUI pages which sometimes
have permission to open downloads and change settings. Normally,
extensions should only be able to run code on http(s) URLs, but
every once in a while, bypasses are found using powerful but rare
permissions like debugger
, devtools
, and
input
.
A good example of a sandbox escape is
this bug reported by David Erceg,
where insufficiently validated functions in
chrome.debugger
were abused to run code on
chrome://downloads
and open an exe on Windows.
The only other option for running code within a privileged page is to
find some kind of XSS bug where it unsafely renders untrusted text as HTML
capable of running JS scripts. This is very rare in chrome://
pages, but it has
happened before; for example, Rob Wu found an
XSS in the downloads page
where the extension's name was unsafely injected as HTML. The
Content-Security-Policy (CSP)
of the page was also bypassed in order to run Javascript that opened
an exe file.
But this was in 2016! The
latest Chrome URL XSS
was in Chrome 65. Nowadays, CSPs are much better at stopping
Javascript execution from HTML injections, and
chrome://
pages don't use
innerHTML
anyway.
In Chrome 102, there was a
very surprising bug
where chrome://settings
displayed extensions' names as
HTML. But even with this, there was nothing to be done except inject
a fake phishing link. Real code execution is simply dead in these
pages because of their strong CSPs and the
introduction of
trusted types.
A quick notice: this blog post talks about ChromeOS 115, before the widespread introduction of Lacros, which aims to seperate ChromeOS from the browser.
So far, we've been talking about Windows. The goal on Windows (and most other operating systems) is to download and run an executable file. But what about ChromeOS, the all-Google operating system?
- There are no executable files
- But you can do more things from the browser
What do I mean by this? In ChromeOS, the browser and the operating
system are coupled. The device settings are hosted on
chrome://os-settings
. All your files are hosted on
chrome://file-manager
. Your device terminal is hosted
on chrome-untrusted://crosh
.
Therefore, an extension running code on a chrome://
URL
in ChromeOS can do literally everything that you can do, which is
insane if you think about it for a second. Depending on the
chrome://
page, it could modify your network settings,
install certificates, edit your files, or run code in your terminal.
But an exploit would be needed for that to happen.
Now, time to talk about the bug I found!
In July, I was poking around in
chrome://file-manager
, ChromeOS's file manager, when I
saw an interesting URL in localStorage
:
Abesntmindedly, I tried to open the URL in my browser, and, surprisingly, it showed my downloads folder. Apparently, every downloaded file can be opened in this way. As it turns out, the two following URLs have exactly the same content:
file:///home/chronos/u-878f28a3486b11359f7db348414fed3b5a15e573/MyFiles/file.txt
filesystem:chrome://file-manager/external/Downloads-878f28a3486b11359f7db348414fed3b5a15e573/file.txt
Wait a minute. file://
URLs have tons of restrictions,
but that second weird-looking URL is hosted on
chrome://file-manager
.
So I decided to try using this new URL to open an
HTML file simply containing the following code:
<script>
alert(1)
</script>
I was almost certain that it wouldn't work. Either the CSP would block it, or it would just display as a text file, or it wouldn't be hosted on the Chrome domain. But, to my surprise:
Wow, it worked! I knew instantly that this had to be a massive bug.
It seemed that this page didn't have any CSP, and the origin
appeared to be
chrome://file-manager
. So I opened up the devtools console
and started to check what would work and what wouldn't.
The first thing I noticed was that Mojo
existed. This
is an inter-process communication (IPC) library that's not normally
exposed to websites because it can
lead to
memory corruption in the browser process.
chrome.send()
--another messaging
function--existed too, although it seemed to be unconfigured.
The second thing I noticed was that I was able to read the source
code of other Chrome pages. For example, I could fetch
chrome://prefs-internals
using
XMLHttpRequest
to get some sensitive info about the
device:
I was also able to use XMLHttpRequest
to read downloaded
files with relative paths like ./file.html
.
Okay, so now we know that an HTML file can be rendered with extra permissions and read sensitive pages, including the user's files. But is this exploitable? Can anything actually set up this attack?
My idea was to have a Chrome extension download a malicious HTML
file and open that file with the special
filesystem:chrome://file-manager
path, leading to XSS.
But there was one issue. If you recall, that URL has a seemingly
random jumble of characters. In my case, that was:
878f28a3486b11359f7db348414fed3b5a15e573
This is actually a ChromeOS user hash; each user has a unique one.
And for this exploit to work, the malicious extension would have to
be able to figure out what it was. Luckily, a number of functions in
the chrome.downloads
extension API return the full path of the file,
including the user hash.
So I started building the exploit:
- Extension downloads a malicious HTML file
-
Extension gets the user hash from the
filename
property -
Extension opens the
filesystem:chrome://file-manager
version of that file - The code in the HTML file is run on the File Manager origin
- The code reads other files and sends the data back to the extension
And it worked. Convinced that I had found a pretty severe infoleak, I submitted the bug.
After submitting, I realized that there might be more to explore
with this bug. Specifically, the real files app at
chrome://file-manager
had access to the
chrome.fileManagerPrivate
API, but the
filesystem:
URL with the vulnerability didn't.
Since the chrome.fileManagerPrivate
API is extremely
cool--and I'll elaborate on this later--I made it my goal to go from
code execution on this URL:
filesystem:chrome://file-manager/external/Downloads...
...to code execution on this URL:
chrome://file-manager
These URLs both have the origin of
chrome://file-manager
, so in theory there should be no
problem with one accessing the other. The only thing that throws a
wrench in those plans is the fact that the File Manager opens as an
app. When we try to open it with window.open()
from the
filesystem:
tab, the browser closes the newly created
tab and instead loads the page in an app-like window, leaving us
with a dangling reference to a nonexistent page:
chrome://
pages can't be embedded in any way, so that
isn't an option. Trying to redirect a tab using JS doesn't work
either. That's why I initially thought it would be impossible to get
a reference to the chrome.fileManagerPrivate
API.
But, as it turns out, it's actually possible--but only with the help of the
malicious extension. First, the filesystem:
page has to
run the following:
let fmWindow = window.open("javascript:0");
The reason a JS URL is used is because it creates an uninitialized renderer. Basically, that tab hasn't commited to displaying anything yet.
Then the extension can use chrome.tabs.update()
to
redirect the new tab to
view-source:chrome://file-manager
, a tricky URL that
has access to the private API and loads as a tab. The extension
doesn't have permission to access this tab, but the
filesystem:
page still has a valid, same-origin reference to it:
fmWindow.chrome.fileManagerPrivate
With this reference, the extension can run scripts under the domain of the real File Manager. Of course, I updated the bug report with this new detail.
Wow! Now our code is running on an actual chrome://
page,
not some weird filesystem:
URL. But what does this
mean?
-
First of all, we have the first
chrome://
URL XSS in 50 versions of Chrome, which is insane! That's 7 years! - We can use the user's camera and microphone without permission: see "chrome:// is too powerful..." in this Chromium doc
Anyway, I've mentioned the fileManagerPrivate
API a few
times. But what can we actually do with it? Well, it has 85 functions,
including ones that allow for:
- Reading downloaded files
- Writing to downloaded files
One possibility that came to mind was ransomware, considering how easy it is for an extension to encrypt a user's files. Of course, the access--and modification--of local files is a huge privacy issue in itself.
But there's one other very interesting capability of the
fileManagerPrivate
API: it can mess with
Crostini, a Linux terminal built into ChromeOS.
In fact, our extension can:
- Set up Crostini
-
Put malicious code into the Crostini
.bashrc
file, which runs every time the terminal is loaded -
Open
chrome-untrusted://terminal
--which is normally forbidden--withfileManagerPrivate.openURL()
, thus running our code
With this, we can run and permanently store and execute code in a container with access to some Android and networking features. Keep in mind that we got here from a Chrome extension, a small add-on designed for tweaking webpages. That's pretty cool!
Three days later, I found something else in the File Manager source code:
Woah, what? A filesystem:
URL on a built-in Chrome extension?
And after a bit of poking around, I managed to open the following URL:
filesystem:chrome-extension://pmfjbimdmchhbnneeidfognadeopoehp/external/Downloads-878f28a3486b11359f7db348414fed3b5a15e573/Downloads/file.html
And it actually loaded my script, again! But what is this URL,
anyway? This is another filesystem:
URL belonging to
the "Image Loader" extension, which is a
component
extension--a privileged Google-made extension built into the browser--only present on
ChromeOS.
This extension also has access to fileManagerPrivate
.
However, it seems like its access to the file system is read-only, and
therefore isn't as dangerous.
It's worth noting that the Image Loader extension is also given
access to
chrome://resources
for the purpose of importing its
scripts, but this inadvertently gives the extension permission to run code on
that origin:
chrome.tabs.create({ url: "chrome://resources/js/cr.m.js" }, (tab) => {
chrome.tabs.executeScript(tab.id, { code: "alert(origin)" })
})
Which gives us our second Chrome XSS! The biggest difference with this one is that downloaded files can't be edited. While somewhat similar to the first bug, I decided to submit it seperately.
As I'll discuss in the final section, the most severe part of the File Manager bug was only in stable ChromeOS for around 25 days. I wasn't able to find a ChromeOS recovery image for any version within that range, and I don't have the resources to build one from scratch, so the only videos I have of that exploit are the ones from the bug report.
Here's a recording of the File Manager bug in action:
And here's the video for the Image Loader one:
And to abuse these bugs, the only permission needed would be
downloads
, which normally only allows an extension to
download and search for user files, not read or write to them. The attack
could also be modified to take place quicker, and the Chrome window
with the XSS could be loaded in the background, hiding it from the
victim. There is no user interaction involved in the exploit.
I've also written some more concise proof-of-concept code in case anyone wants it.
Again, a quick reminder: this issue doesn't allow for extensions to read or write to every file on a Chromebook; the browser just can't do that. Only files in the user directory--like Downloads, Photos, etc--can be accessed with this vulnerability.
So far, I haven't really gone into why these bugs work. What's the
difference between a filesystem:
and
file:
URL? Why do the Image Loader extension and the File
Manager app have these URLs? I'll try to answer some of
these questions in this section.
The filesystem:
protocol is not something you come
across very often on the web. It's a very, very old (2011-ish)
Chrome feature that allows websites to permanently store
File
and Blob
objects in a virtual
filesystem with directories and folders. TL;DR: it's like the
blob:
protocol, but more stable and organized.
As a side effect, every file is also hosted on a URL like the following:
filesystem:https://google.com/temporary/file.html
Back in 2011,
a page could apparently open
its files in the filesystem:
protocol. Spoiler alert:
that is no longer the case, which is why you might've never seen
this type of URL in your life.
You might have noticed that the filesystem:
URL above
contains the /temporary
directory. This path is
actually a required part of the URL; it can be one of
four
possibilities:
/temporary
/persistent
(self-explanatory)-
/isolated
(used to temporarily store uploaded files, can't be rendered) -
/external
In case you were wondering, /external
is not controlled
by the website at all. Rather, it's a ChromeOS-only folder identical
to the user's MyFiles
path. This obscure feature only exists
on
the File Manager and Image Loader origins.
Which means... oh, of course. ChromeOS still uses an outdated and obsolete JavaScript API to power its primary File Manager app. Classic.
When you think about it, it makes some sense. The ChromeOS
developers wanted an easy way for their Javascript-powered app--which
is really just a glorified website--to access files safely.
Therefore, they decided to expand the legacy File System API--which, keep in mind,
was the only good JS option back then--with a new /external
path on
the chrome://file-manager
origin, so that the app could
carry out read and write operations with existing APIs.
And this is, under the hood, how the File Manager app works on
ChromeOS. Whenever you create a file, the app uses the undocumented
webkitResolveLocalFileSystemURL()
function--which is, fun fact, the
longest-named global function in Chrome--to get a
FileSystemEntry
object from a URL like the following:
Then it uses pure Javascript to
create a
writer
on the directory and write a blank Blob
into a new
file. In fact, fileSystemPrivate
doesn't actually have
any functions that directly write to files: it only returns entry
objects which have to be handled with JS.
It's kind of funny, really, that the main files app uses 2011 Javascript APIs to do literally everything. But hey, this is ChromeOS!
Anyway, when this system was designed, I suppose someone forgot that
the filesystem:
URLs they were using could be rendered
in the browser, which meant that an extension could simply open them
for XSS. I think this type of bug is really interesting because it
shows that vulnerabilities don't always come from simple mistakes;
sometimes, decade-long design choices in massive and complex
projects like Chrome/ChromeOS can be exploited in creative ways.
Remember how I said in the last paragraph that bugs don't always
come from simple mistakes? Well, after reporting, I found out that
this one partially did. A Google employee confirmed that
the introduction of
WebUIConfig
for determining the origins of chrome://
pages
partially led to the exploit.
Before this change, blob:chrome://
and
filesystem:chrome://
URLs would not be considered
"real" Chrome URLs; they would have no access to
chrome.send()
or Mojo
, and they couldn't
get a window reference to a page with those permissions. Indeed,
when I tried the window.open()
part of the exploit on
earlier versions of ChromeOS, the browser crashed both pages.
But with the new code, this URL case was never handled.
It's pure chance that I managed to find this bug--the main part of which had been lying undetected for years--only a month or so after the introduction of new code that made it even more critical. In the end, that part of the bug only existed in stable versions of ChromeOS from 115.0.5790.98 to 115.0.5790.170, which was a gap of less than one month. The basic XSS and capability to read downloaded files worked in older versions, though.
The Image Loader bug wasn't new at all, however, and I could verify that it worked on versions as old as ChromeOS 99.
Anyway, both bug reports were marked as fixed on August 8th.
The first fix
updated WebUIConfig
--the new buggy code--to correctly
handle filesystem:chrome://
and
blob:chrome://
URLs.
The second fix
blocked /external
files from being rendered in the
browser, wiping out this type of bug for good.
I was rewarded a total of $10,000 for the two bugs! The File Manager bug was given the CVE number 2023-4369 and was mentioned in the ChromeOS 116.0.5845.120 release notes. Unfortunately, the Image Loader bug report has been dormant since its patch; as of the time of writing, it hasn't been given a CVE number or mentioned in uany update logs.
Looking back at it all, I think this is my favorite find yet. Chrome extensions have always been interesting attack vectors for me, and I'm a big fan of privilege escalation bugs, especially ones that don't require memory corruption. I hope you found this bug as cool as I did; thanks for reading!