From at least 2013 until May 2016 JetBrains’ IDEs were vulnerable to local file leakage, with the Windows (EDIT: and OS X) versions additionally being vulnerable to remote code execution. The only prerequisite for the attack was to have the victim visit an attacker-controlled webpage while the IDE was open.
Affected IDEs included PyCharm, Android Studio, WebStorm, IntelliJ IDEA and several others.
I’ve tracked the core of most of these issues (CORS allowing all origins + always-on webserver) back to the addition of the webserver to WebStorm in 2013. It’s my belief that all JetBrains IDEs with always-on servers since then are vulnerable to variants of these attacks.
The arbitrary code execution vuln affecting Windows and OS X was in all IDE releases since at least July 13, 2015, but was probably exploitable earlier via other means.
All of the issues found were fixed in the patch released May 11th 2016.
To follow along with this you’ll need a copy of PyCharm 5.0.4, or an old build of PyCharm 2016.1 since this has been patched for a while now. Obviously you’ll want to do this in a VM.
- Linux: https://download.jetbrains.com/python/pycharm-community-5.0.4.tar.gz
- OS X: https://download.jetbrains.com/python/pycharm-community-5.0.4.dmg
- Windows: https://download.jetbrains.com/python/pycharm-community-5.0.4.exe
I had just started working on some inter-protocol exploitation research and was looking for some interesting targets. Thinking that I must have some interesting services running on my own device, I ran
lsof -P -ITCP | grep LISTEN to see what programs were listening on a local TCP port. I got back:
1 2 3
Hmm, I’ve used PyCharm as my IDE of choice for a while now, but never noticed that it bound to any ports… Might it be some sort of ad-hoc IPC mechanism? Let’s nmap it to figure out what’s being sent over those ports, and what the protocol is:
1 2 3 4 5 6 7 8
Looks like an HTTP server? Unusual for a local application… Let’s see what CORS headers it serves up with responses:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
Something smells off here. PyCharm’s HTTP server is essentially saying that web pages on any origin (including
http://attacker.com) are allowed to make credentialed requests to it and read the response. What the heck is this HTTP server, though? Does it serve anything sensitive? Do we even care if random pages are able to read its contents?
What’s that HTTP server?
After searching the web for references to that port number, we find that this is related to a WebStorm feature added in early 2013 (WebStorm is another of JetBrains’ IDEs.) The idea was that you wouldn’t need to set up your own web server to preview your pages in a browser. You could just click a “view in browser” button inside WebStorm and it would navigate your browser to
http://localhost:63342/<projectname>/<your_file.html>. Any scripts or subresources that the page tried to include would similarly be served up via URLs like
To verify that PyCharm embeds the same server as WebStorm, let’s create a project named “testing” in PyCharm and place a file named “something.txt” in the root and see if we can fetch it:
1 2 3 4 5 6 7 8 9 10 11 12
Yikes, so any site can read any of your project files so long as they can guess the project name and filename. This would obviously include any in-tree configuration files that contained secrets like AWS keys and the like. Here’s an HTML snippet we could include on
attacker.com that would do the same thing as our cURL command:
1 2 3 4 5 6
This is pretty bad, but as-is this would mostly be useful for targeted attacks. It’s a pain to have to guess at directory structures to get at the interesting files, so what are some ways we can weaponize this?
Escaping from the project directory
Let’s see if we can read files outside of the project directory. There are some files (like SSH keys, etc) that live at standard locations and are interesting for an attacker. Much more interesting than possible credentials for some database that might not even be accessible to us.
The obvious thing to do is see how it handles dot segments in the request URI:
Bah. Per the spec dot segments in paths must be normalized away by either the client or the server. cURL’s behaviour here is the same as you’d see in a browser. Luckily PyCharm’s internal HTTP server treats dot segments with urlencoded
%2F..%2F as semantically equivalent to the unencoded
/../ form, and browsers will not normalize those away.
1 2 3 4 5 6 7 8 9 10
Great success! Our only limitation here is that we must know the name of a project the victim has open. Requesting
/invalidproject/<anything> will always 404.
The obvious choice is to use a dictionary of potential project names a user could have open, and try to request
/<potential_projectname>/.idea/workspace.xml (which is a metadata file automatically added to most JetBrains projects.)
1 2 3 4
We got a 200 for
testing, so we know it’s a valid project.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
There’s no rate-limiting, and I was able to try about 2000 project names a second using this method with Chrome even on my older laptop.
Can we get around having to guess a project name?
At this point, I started looking at the various APIs that PyCharm exposed via the same webserver. Having to guess a valid, open project name to leak files is a big mitigating factor, and the API might give us a way around that.
Eventually I came upon the
/api/internal endpoint, which corresponds to
JetBrainsProtocolHandlerHttpService. Apparently this lets you pass in a JSON blob containing a URL with a
jetbrains: scheme, and the IDE will do something special with it. As far as I can tell, none of the IDEs actually install a systemwide handler for those URLs, and those URLs are undocumented. In any case, let’s look for some interesting
jetbrains: URLs we can pass it.
jetbrains://<project_name>/open/<path> handler seems promising:
1 2 3 4 5 6 7 8 9 10 11 12
This lets us open a project by passing in its absolute path. The
/etc directory exists on most *NIX-like systems, let’s try opening that:
Dang, so the directory needs to actually contain a JetBrains-style project, we can’t just pass any old directory. Lucky for us, PyCharm 2016.1 and above ship with a JetBrains-style project in their system folder! Under OS X this will be in
/Applications/PyCharm.app/Contents/helpers, let’s try that:
Bingo. Now we don’t have to guess at an open project name as we now know the
helpers project is open. There’s no standard location for PyCharm’s root folder under Linux (it’s wherever the user happened to un
tar it to,) but we can determine it by reqesting
/api/about?more=true and looking at the
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Once we’ve opened the
helpers project, we determine the user’s home directory from the
/api/about?more=true response and use that to construct a URL to their SSH keys like
1 2 3 4 5 6 7 8 9 10
Exploitation under Windows is much easier
The above trick with opening the
helpers directory that ships with PyCharm obviously only works if the user has PyCharm 2016.1 installed, everywhere else we still have to guess an open project name. How about something that works reliably with the other JetBrains IDEs like IntelliJ IDEA and Android Studio?
jetbrains://project/open handler lets us pass a completely arbitrary path for the project to open, UNC paths are an obvious choice. UNC paths are a windows-specific path form that allows you to reference files on a network share, and they like
\\servername\sharename\filepath. Many of Windows’ file APIs (and the Java APIs that wrap them) will happily take UNC paths and transparently connect to an SMB share on another computer, allowing you to read and write to the remote files as if they were local. If we can get the IDE to open a project from our SMB share, we won’t need to guess at what projects might be on the victim’s computer.
To test, I set up a remote Samba instance with an unauthenticated SMB share named “anontesting” that contained a JetBrains project, then tried opening it:
Great. Assuming the victim’s ISP doesn’t block outbound SMB traffic (due to the large number of worms that have historically propagated via SMB vulns) we can get them to load an arbitrary project from an SMB share we control.
The impact under Windows is much worse
Wait a second, seems like we can do something a lot more interesting than arbitrary file reads. With one request we can get Windows users to load an attacker-controlled project from our remote SMB share. There’s almost certainly abuse potential here, and we don’t have to look far to find it.
The projects for each of JetBrains’ IDEs have a notion of startup tasks. In PyCharm you can have a Python script run automatically on project load, and similarly on Android Studio and IntelliJ IDEA you can have a
.jar run. Here I’ve made it so that the
hax.py script in the project root will be automatically run when the project opens:
Now we just need to add a
hax.py file to our project root containing:
1 2 3
We then put the project on our anonymous SMB share, and host a page with our payload that will cause the victim to load the malicious project:
1 2 3 4 5
As soon as the victim navigates to that page, our payload will trigger and the calculator will open:
Turns out OS X wasn’t any safer
After this post was initially published comex pointed out that OS X will auto-mount remote NFS shares when you access them via the
autofs mountpoint. That means exploiting the RCE under OS X is pretty similar to Windows, but we create an anonymous NFS share and open
with the HTML PoC looking something like:
1 2 3 4 5
This likely applies to any *NIX-like with an
autofs mountpoint that uses
-hosts, but OS X is the only OS I could find where
autofs is configured like this in the default install.
- Minimal file leaking PoC
- Weaponized file leaking PoC
- No live PoC for the Windows or OS X RCEs ‘cause I don’t want to host public-facing SMB or NFS shares :)
JetBrains took several steps that I’m aware of to remediate this:
- All requests to the local HTTP server now require an unguessable auth token to be included in the request, or the server will return a
- The problematic CORS policies were removed entirely
Hostheader’s value is now validated to prevent similar exploits via DNS rebinding
Interactions with the vendor
I’d like to specifically thank Hadi Hariri and the rest of the JetBrains team for their proactive response to my report. My email requesting a security contact was answered within an hour of my sending it, and the issue was resolved relatively quickly.
They sent me a patchset against
intellij-community and a binary build with their proposed solutions, and were receptive to my feedback when I mentioned potential issues.
Lastly, even though Jetbrains doesn’t have a bug bounty program that I’m aware of, and I definitely wasn’t expecting anything, Jetbrains quite generously awarded a bounty of $50,000 for my report and help reviewing the patch. I’ve asked them to donate the bulk of this to the PyPy project to fund improved Python 3 support, fingers crossed for
await/async support in PyPy :).
- 2016-04-04 - Discovered the local file disclosure issue
- 2016-04-06 - Requested a security contact from the vendor
- 2016-04-06 - Vendor replies with security contact information, requests vulnerability details
- 2016-04-07 - Sent the vendor a PoC for the local file disclosure vulnerability
- 2016-04-10 - Sent the vendor a more detailed report with remediation steps and details about the RCE on Windows
- 2016-04-12 - Vendor responds that they are working on a patch
- 2016-04-14 - Vendor responds with a patch against the open-source
intellij-communityrepo for review
- 2016-04-14 - Sent feedback to the vendor requesting changes to the patch, along with mitigation bypass PoCs
- 2016-04-15 - Vendor responds that they are working on an updated patch that addresses the concerns
- 2016-04-26 - Vendor indicates that they plan to release a patch soon
- 2016-05-11 - Coordinated release of security patches for all JetBrains IDEs, security advisory published.