Derin Eryılmaz
Uncovering a crazy privilege escalation from Chrome extensions
November 14, 2023
A bit of background

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.

What is a sandbox escape?

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.

By the way, XSS is dead (surely)

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.

What about ChromeOS?

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!

CVE-2023-4369

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.

Going further

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.

Doing damage

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--with fileManagerPrivate.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!

Finding a sister bug

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.

Proof-of-concept videos

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.

But why?

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.

What I've learned

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!

Timeline (all 2023)
Other posts My Twitter Email me